import { forwardRef, ReactElement, useEffect, useImperativeHandle, useRef, useState } from 'react'; import { ColorValue, NativeSyntheticEvent, Platform, StyleSheet, TextInput, TextInputFocusEventData, TextInputProps } from 'react-native'; import isUndefined from 'lodash/isUndefined'; import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'; import TextInputMask from 'react-native-text-input-mask'; import { IconButton } from '../buttons/IconButton'; import { AppIcon, IconName } from '../images_and_icons/icon/AppIcon'; import { AppBox } from '../layout/AppBox'; import { AppText } from '../text/AppText'; import { useAppTheme } from '~/view/theme'; export type AppInputRefHanldes = { clear: () => void; setText: (t: string) => void; }; export interface AppInputProps extends Omit { testID?: string; error?: string; tip?: string; label?: string; labelBgColor?: ColorValue; withSecureText?: boolean; disabled?: boolean; withClear?: boolean; leftIcon?: IconName; leftElement?: () => ReactElement; rightElement?: () => ReactElement; mask?: string; fixedLabel?: string; errorHeight?: number; } export const AppInput = forwardRef( (props: AppInputProps, ref) => { const { testID, label, error, tip, disabled, leftIcon, withClear, withSecureText, labelBgColor, leftElement, rightElement, mask, fixedLabel, errorHeight } = props; const { colors, inputSizes, otherSizes, colorAlpha, spacing, textVariants, opacity } = useAppTheme(); const [focused, setFocused] = useState(false); const [secureTextVisible, setSecureTextVisible] = useState(false); const currentValue = useRef(props.defaultValue ?? ''); const textInputRef = useRef(null); useImperativeHandle(ref, () => ({ clear: () => { textInputRef.current?.clear(); }, setText: (text: string) => { textInputRef.current?.setNativeProps({ text }); } })); const showLabel = !!props.value || !!props.defaultValue || !!currentValue.current || focused || !!fixedLabel; const handleFocus = (e: NativeSyntheticEvent) => { setFocused(true); props.onFocus?.(e); }; const handleBlur = (e: NativeSyntheticEvent) => { setFocused(false); props.onBlur?.(e); }; const handleTextChange = (text: string) => { currentValue.current = text; props.onChangeText?.(text); }; const handleMaskedTextChange = (formatted: string, extracted?: string) => { currentValue.current = extracted ?? ''; props.onChangeText?.(extracted ?? ''); }; const toogleSecureText = () => { setSecureTextVisible(s => !s); }; const onTextClear = () => { textInputRef.current?.clear(); handleTextChange(''); }; const labelAnimStyle = useAnimatedStyle(() => ({ opacity: withTiming(showLabel ? 1 : 0, { duration: 150 }) })); const initiallabelAnimStyle = useAnimatedStyle(() => ({ opacity: withTiming(showLabel ? 0 : 1, { duration: 150 }) })); const borderColor = () => { if (error) { return colors.error; } if (focused) { return colors.primary; } return colors.outline; }; const paddingRight = () => { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing if ((withClear && focused) || withSecureText) { return 's'; } if (error) { return 'sm'; } return 'm'; }; const renderLeft = () => { if (!isUndefined(leftElement)) { return leftElement(); } return leftIcon && ; }; const renderRight = () => { if (!isUndefined(rightElement)) { return rightElement(); } if (withClear && focused) { return ; } if (withSecureText) { return ( ); } if (error) { return ; } }; /** * The ugly workaround for forcing to be re-mounted on every * mask prop change. We need it as entered value can't be cleared * and mask can't be aplied, when it changes */ const [showMask, setShowMask] = useState(false); useEffect(() => { if (mask) { setShowMask(false); setTimeout(() => { setShowMask(true); }, 200); handleMaskedTextChange('', ''); } }, [mask]); return ( {renderLeft()} {mask ? ( showMask ? ( ) : null ) : ( )} {label && ( {label} )} {renderRight()} {error ? error : tip ? tip : ''} {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */} {(label || fixedLabel) && ( {label ?? fixedLabel} )} ); } );