index.tsx 13 KB

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