index.tsx 13 KB

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