index.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. import {
  2. useImperativeHandle,
  3. forwardRef,
  4. useState,
  5. useRef
  6. } from "react";
  7. import {
  8. TextInput as NativeTextInput,
  9. TouchableOpacity,
  10. Keyboard,
  11. Platform,
  12. View
  13. } from "react-native";
  14. import type ITextInputProps from "./type";
  15. import {
  16. type TextInputInstance,
  17. type ITextInputRef,
  18. type TextInputType
  19. } from "./type";
  20. import stylesheet, {
  21. getTextInputType,
  22. useStyles
  23. } from "./stylesheet";
  24. import {
  25. NCoreUIKitLocalize,
  26. NCoreUIKitTheme
  27. } from "../../core/hooks";
  28. import {
  29. type RefForwardingComponent
  30. } from "../../types";
  31. import type ITextProps from "../text/type";
  32. import {
  33. BadgeQuestionMark as BadgeQuestionMarkIcon,
  34. BadgeAlert as BadgeAlertIcon,
  35. BadgeCheck as BadgeCheckIcon,
  36. BadgeInfo as BadgeInfoIcon,
  37. EyeClosed as EyeClosedIcon,
  38. CircleX as CircleXIcon,
  39. BadgeX as BadgeXIcon,
  40. type LucideIcon,
  41. Eye as EyeIcon
  42. } from "lucide-react-native";
  43. import Text from "../text";
  44. const TextInputTypeIcon: Record<Exclude<TextInputType, "default">, LucideIcon> = {
  45. "question": BadgeQuestionMarkIcon,
  46. "success": BadgeCheckIcon,
  47. "warning": BadgeAlertIcon,
  48. "info": BadgeInfoIcon,
  49. "danger": BadgeXIcon
  50. };
  51. const TextInput: RefForwardingComponent<ITextInputRef, ITextInputProps> = ({
  52. isAutoKeyboardDismissOnBlur = true,
  53. rightIcon: RightIconComponentProp,
  54. hintTextIcon: HintTextIconProp,
  55. spreadBehaviour = "baseline",
  56. isShowHideTextButton = true,
  57. isShowHintTextIcon = false,
  58. icon: IconComponentProp,
  59. hintTextContainerStyle,
  60. isCleanEnabled = false,
  61. contentContainerStyle,
  62. subTitle = "Optional",
  63. onFocus: onFocusProp,
  64. isDisabled = false,
  65. isRequired = false,
  66. onBlur: onBlurProp,
  67. customBorderColor,
  68. hideTextIconStyle,
  69. customTitleColor,
  70. rightIconOnPress,
  71. type = "default",
  72. variant = "text",
  73. customIconColor,
  74. cleanIconStyle,
  75. customLocalize,
  76. isShowSubTitle,
  77. rightIconStyle,
  78. initialValue,
  79. onChangeText,
  80. customColor,
  81. customTheme,
  82. iconOnPress,
  83. placeholder,
  84. inputStyle,
  85. isOptional,
  86. validation,
  87. iconStyle,
  88. hintText,
  89. style,
  90. title,
  91. ...props
  92. }, ref) => {
  93. const {
  94. inlineSpaces,
  95. typography,
  96. radiuses,
  97. borders,
  98. spaces,
  99. colors
  100. } = NCoreUIKitTheme.useContext(customTheme);
  101. const {
  102. localize
  103. } = NCoreUIKitLocalize.useContext(customLocalize);
  104. const currentType = getTextInputType({
  105. type
  106. });
  107. const inputRef = useRef<TextInputInstance | null>(null);
  108. const styleType = type === "default" ? "neutral" : type === "question" ? "neutral" : type === "danger" ? "error" : type;
  109. const [
  110. value,
  111. setValue
  112. ] = useState(initialValue ? initialValue : "");
  113. const [
  114. hideValue,
  115. setHideValue
  116. ] = useState(true);
  117. const [
  118. isFocused,
  119. setIsFocused
  120. ] = useState(false);
  121. const {
  122. hideTextIconContainer: hideTextIconContainerDynamicStyle,
  123. titleContainer: titleContainerDynamicStyle,
  124. hintTextIcon: hintTextIconDynamicStyle,
  125. cleanButton: cleanButtonDynamicStyle,
  126. container: containerDynamicStyle,
  127. rightIcon: rightIconDynamicStyle,
  128. hintText: hintTextDynamicStyle,
  129. required: requiredDynamicStyle,
  130. subTitle: subTitleDynamicStyle,
  131. content: contentDynamicStyle,
  132. overlay: overlayDynamicStyle,
  133. input: inputDynamicStyle,
  134. title: titleDynamicStyle,
  135. icon: iconDynamicStyle
  136. } = useStyles({
  137. icon: IconComponentProp ? true : false,
  138. customBorderColor,
  139. spreadBehaviour,
  140. inlineSpaces,
  141. currentType,
  142. customColor,
  143. isDisabled,
  144. typography,
  145. isFocused,
  146. radiuses,
  147. borders,
  148. colors,
  149. spaces,
  150. title,
  151. type
  152. });
  153. useImperativeHandle(
  154. ref,
  155. () => ({
  156. updateValue,
  157. cleanText,
  158. focus,
  159. blur
  160. }),
  161. []
  162. );
  163. const titleProps: ITextProps = {
  164. color: currentType.titleColor,
  165. variant: "bodyLargeSize"
  166. };
  167. const iconProps: NCoreUIKit.IconCallbackProps = {
  168. size: Number(typography.labelLargeSize.fontSize) + 6,
  169. color: currentType.iconColor
  170. };
  171. if(isDisabled) {
  172. iconProps.color = "disabled";
  173. }
  174. const blur = () => {
  175. inputRef.current?.blur();
  176. };
  177. const focus = () => {
  178. inputRef.current?.focus();
  179. };
  180. const cleanText = () => {
  181. setValue("");
  182. inputRef.current?.clear();
  183. if(onChangeText) {
  184. onChangeText("");
  185. }
  186. };
  187. const updateValue = (text: string) => {
  188. setValue(text);
  189. if (Platform.OS !== "web") {
  190. inputRef.current?.setNativeProps({
  191. text: text
  192. });
  193. } else if (inputRef.current) {
  194. (inputRef.current as unknown as { value: string }).value = text;
  195. }
  196. };
  197. const onFocus = () => {
  198. setIsFocused(true);
  199. if(onFocusProp) onFocusProp();
  200. };
  201. const onBlur = () => {
  202. setIsFocused(false);
  203. inputRef.current?.blur();
  204. if(isAutoKeyboardDismissOnBlur) {
  205. Keyboard.dismiss();
  206. }
  207. if(onBlurProp) onBlurProp();
  208. };
  209. const toggleValueVisibility = () => {
  210. setHideValue(prevState => !prevState);
  211. };
  212. const renderCleanButton = () => {
  213. if(isDisabled) {
  214. return null;
  215. }
  216. if(variant !== "text") {
  217. return null;
  218. }
  219. if(!isCleanEnabled || !value.length) {
  220. return null;
  221. }
  222. return <TouchableOpacity
  223. onPress={() => {
  224. if(inputRef.current) inputRef.current.clear();
  225. if(onChangeText) {
  226. onChangeText("");
  227. }
  228. setValue("");
  229. }}
  230. style={[
  231. cleanIconStyle,
  232. stylesheet.cleanButton,
  233. cleanButtonDynamicStyle
  234. ]}
  235. >
  236. <CircleXIcon
  237. color={colors.content.icon[currentType.iconColor]}
  238. size={20}
  239. />
  240. </TouchableOpacity>;
  241. };
  242. const renderIcon = () => {
  243. if (!IconComponentProp) {
  244. return null;
  245. }
  246. return <TouchableOpacity
  247. onPress={iconOnPress}
  248. style={[
  249. iconStyle,
  250. stylesheet.icon,
  251. iconDynamicStyle
  252. ]}
  253. >
  254. <IconComponentProp
  255. customColor={customIconColor}
  256. color={iconProps.color}
  257. size={iconProps.size}
  258. />
  259. </TouchableOpacity>;
  260. };
  261. const renderRightIcon = () => {
  262. if (!RightIconComponentProp) {
  263. return null;
  264. }
  265. if(isShowHideTextButton && variant === "hidden") {
  266. return null;
  267. }
  268. if(isCleanEnabled && value.length > 0 && variant === "text") {
  269. return null;
  270. }
  271. return <TouchableOpacity
  272. onPress={rightIconOnPress}
  273. style={[
  274. rightIconStyle,
  275. stylesheet.rightIcon,
  276. rightIconDynamicStyle
  277. ]}
  278. >
  279. <RightIconComponentProp
  280. color={iconProps.color}
  281. size={iconProps.size}
  282. />
  283. </TouchableOpacity>;
  284. };
  285. const renderHideTextIcon = () => {
  286. if(!isShowHideTextButton) {
  287. return null;
  288. }
  289. if (variant !== "hidden") {
  290. return null;
  291. }
  292. return <TouchableOpacity
  293. style={[
  294. hideTextIconStyle,
  295. stylesheet.hideTextIconContainer,
  296. hideTextIconContainerDynamicStyle
  297. ]}
  298. onPress={toggleValueVisibility}
  299. >
  300. {
  301. hideValue ?
  302. <EyeIcon
  303. color={colors.content.icon[iconProps.color]}
  304. size={20}
  305. />
  306. :
  307. <EyeClosedIcon
  308. color={colors.content.icon[iconProps.color]}
  309. size={20}
  310. />
  311. }
  312. </TouchableOpacity>;
  313. };
  314. const renderHintIcon = () => {
  315. if(!isShowHintTextIcon) {
  316. return null;
  317. }
  318. if(HintTextIconProp) {
  319. return <HintTextIconProp
  320. color={isDisabled ? "disabled" : currentType.hintTextIconColor}
  321. style={[
  322. stylesheet.hintTextIcon,
  323. hintTextIconDynamicStyle
  324. ]}
  325. size={20}
  326. />;
  327. }
  328. const CurrentHintIcon = TextInputTypeIcon[type === "default" ? "question" : type];
  329. let hintIconColor = colors.content.icon[currentType.hintTextIconColor];
  330. if(isDisabled) {
  331. hintIconColor = colors.system.state.content.disabled[styleType];
  332. }
  333. return <CurrentHintIcon
  334. style={[
  335. stylesheet.hintTextIcon,
  336. hintTextIconDynamicStyle
  337. ]}
  338. color={hintIconColor}
  339. size={20}
  340. />;
  341. };
  342. const renderHintText = () => {
  343. if (!hintText) {
  344. return null;
  345. }
  346. return <View
  347. style={[
  348. hintTextContainerStyle,
  349. stylesheet.hintText,
  350. hintTextDynamicStyle
  351. ]}
  352. >
  353. {renderHintIcon()}
  354. <Text
  355. customColor={isDisabled ? colors.system.state.content.disabled[styleType] : undefined}
  356. color={currentType.hintTextColor}
  357. variant="labelSmallSize"
  358. >
  359. {hintText}
  360. </Text>
  361. </View>;
  362. };
  363. const renderRequired = () => {
  364. if(!isRequired) {
  365. return null;
  366. }
  367. return <Text
  368. style={[
  369. stylesheet.required,
  370. requiredDynamicStyle
  371. ]}
  372. color="danger"
  373. >
  374. *
  375. </Text>;
  376. };
  377. const renderSubtitle = () => {
  378. if(!isShowSubTitle && !isOptional) {
  379. return null;
  380. }
  381. return <Text
  382. customColor={customTitleColor}
  383. style={[
  384. stylesheet.subTitle,
  385. subTitleDynamicStyle
  386. ]}
  387. variant="labelLargeSize"
  388. color={titleProps.color}
  389. >
  390. ( {isOptional ? localize("is-optional") : subTitle} )
  391. </Text>;
  392. };
  393. const renderTitle = () => {
  394. if (!title) {
  395. return null;
  396. }
  397. return <View
  398. style={[
  399. stylesheet.titleContainer,
  400. titleContainerDynamicStyle
  401. ]}
  402. >
  403. {renderRequired()}
  404. <Text
  405. {...titleProps}
  406. variant={titleProps.variant}
  407. color={titleProps.color}
  408. style={[
  409. stylesheet.title,
  410. titleDynamicStyle
  411. ]}
  412. >
  413. {title}
  414. </Text>
  415. {renderSubtitle()}
  416. </View>;
  417. };
  418. const renderInput = () => {
  419. return <NativeTextInput
  420. {...props}
  421. placeholderTextColor={colors.content.text[currentType.placeholderColor]}
  422. onChangeText={(text) => {
  423. const inputValue = text.replace(/\n/g, "");
  424. if (inputValue === "") {
  425. setValue("");
  426. if(onChangeText) onChangeText("");
  427. return;
  428. }
  429. if (validation) {
  430. if (validation(inputValue)) {
  431. setValue(inputValue);
  432. if(onChangeText) onChangeText(inputValue);
  433. }
  434. } else {
  435. setValue(inputValue);
  436. if(onChangeText) onChangeText(inputValue);
  437. }
  438. }}
  439. secureTextEntry={variant === "hidden" && hideValue}
  440. underlineColorAndroid="rgba(255,255,255,0)"
  441. placeholder={placeholder}
  442. allowFontScaling={false}
  443. editable={!isDisabled}
  444. style={[
  445. inputStyle,
  446. stylesheet.input,
  447. inputDynamicStyle
  448. ]}
  449. multiline={false}
  450. onFocus={onFocus}
  451. onBlur={onBlur}
  452. ref={inputRef}
  453. />;
  454. };
  455. const renderOverlay = () => {
  456. return <View
  457. style={[
  458. stylesheet.overlay,
  459. overlayDynamicStyle
  460. ]}
  461. />;
  462. };
  463. return <TouchableOpacity
  464. onPress={() => {
  465. if(!isDisabled) inputRef.current?.focus();
  466. }}
  467. style={[
  468. style,
  469. stylesheet.container,
  470. containerDynamicStyle
  471. ]}
  472. disabled={isDisabled}
  473. {...Platform.select({
  474. web: {
  475. dataSet: {
  476. disablePressableDownEffect: "true"
  477. }
  478. },
  479. default: {}
  480. })}
  481. >
  482. {renderTitle()}
  483. <View
  484. style={[
  485. contentContainerStyle,
  486. stylesheet.content,
  487. contentDynamicStyle
  488. ]}
  489. >
  490. {renderIcon()}
  491. {renderInput()}
  492. {renderCleanButton()}
  493. {renderHideTextIcon()}
  494. {renderRightIcon()}
  495. {renderOverlay()}
  496. </View>
  497. {renderHintText()}
  498. </TouchableOpacity>;
  499. };
  500. export default forwardRef(TextInput);