import { forwardRef, useMemo } from 'react'; import { TextInput, View, type BlurEvent, type FocusEvent, type TextInput as TextInputType, } from 'react-native'; import Animated from 'react-native-reanimated'; import { HeroText } from '../../helpers/components'; import { AnimationSettingsProvider } from '../../helpers/contexts/animation-settings-context'; import { useThemeColor } from '../../helpers/theme'; import type { TextRef, ViewRef } from '../../helpers/types/primitives'; import { createContext, getElementByDisplayName } from '../../helpers/utils'; import { ErrorView } from '../error-view'; import { useTextFieldDescriptionAnimation, useTextFieldInputAnimation, useTextFieldLabelAnimation, useTextFieldRootAnimation, } from './text-field.animation'; import { DISPLAY_NAME } from './text-field.constants'; import textFieldStyles, { styleSheet } from './text-field.styles'; import type { TextFieldContextValue, TextFieldDescriptionProps, TextFieldErrorMessageProps, TextFieldInputEndContentProps, TextFieldInputProps, TextFieldInputStartContentProps, TextFieldLabelProps, TextFieldRootProps, } from './text-field.types'; const [TextFieldProvider, useTextField] = createContext({ name: 'TextFieldContext', }); const AnimatedText = Animated.createAnimatedComponent(HeroText); // -------------------------------------------------- const TextFieldRoot = forwardRef((props, ref) => { const { children, className, isDisabled = false, isInvalid = false, isRequired = false, animation, ...restProps } = props; const tvStyles = textFieldStyles.root({ isDisabled, className }); const { isAllAnimationsDisabled } = useTextFieldRootAnimation({ animation }); const contextValue = useMemo( () => ({ isDisabled, isInvalid, isRequired }), [isDisabled, isInvalid, isRequired] ); const animationSettingsContextValue = useMemo( () => ({ isAllAnimationsDisabled, }), [isAllAnimationsDisabled] ); return ( {children} ); }); // -------------------------------------------------- const TextFieldLabel = forwardRef( (props, ref) => { const { children, className, classNames, isInvalid: localIsInvalid, animation, ...restProps } = props; const { isDisabled, isInvalid: contextIsInvalid, isRequired, } = useTextField(); const isInvalid = localIsInvalid !== undefined ? localIsInvalid : contextIsInvalid; const tvStyles = textFieldStyles.label({ isDisabled, isInvalid }); const textStyles = tvStyles.text({ className: [className, classNames?.text], }); const asteriskStyles = tvStyles.asterisk({ className: classNames?.asterisk, }); const { entering, exiting } = useTextFieldLabelAnimation({ animation }); return ( {children} {isRequired && *} ); } ); // -------------------------------------------------- const TextFieldInput = forwardRef( (props, ref) => { const { children, isInvalid: localIsInvalid, className, classNames, style, styles, animation, isAnimatedStyleActive = true, onFocus, onBlur, ...restProps } = props; const { isInvalid: contextIsInvalid } = useTextField(); const isInvalid = localIsInvalid !== undefined ? localIsInvalid : contextIsInvalid; const startContent = getElementByDisplayName( children, DISPLAY_NAME.INPUT_START_CONTENT ); const endContent = getElementByDisplayName( children, DISPLAY_NAME.INPUT_END_CONTENT ); const [themeColorFieldPlaceholder, themeColorMuted] = useThemeColor([ 'field-placeholder', 'muted', ]); const tvStyles = textFieldStyles.input({ isMultiline: Boolean(restProps.multiline), }); const containerClassName = tvStyles.container({ className: [className, classNames?.container], }); const inputStyles = tvStyles.input({ className: classNames?.input }); const { animatedContainerStyle, handleFocusAnimation, handleBlurAnimation, } = useTextFieldInputAnimation({ animation, isInvalid, }); const containerStyle = isAnimatedStyleActive ? [animatedContainerStyle, styleSheet.borderCurve, style] : [styleSheet.borderCurve, style]; const handleFocus = (e: FocusEvent) => { handleFocusAnimation(); onFocus?.(e); }; const handleBlur = (e: BlurEvent) => { handleBlurAnimation(); onBlur?.(e); }; return ( {startContent} {endContent} ); } ); // -------------------------------------------------- const TextFieldInputStartContent = forwardRef< ViewRef, TextFieldInputStartContentProps >((props, ref) => { const { children, className, ...restProps } = props; const tvStyles = textFieldStyles.inputStartContent({ className }); return ( {children} ); }); // -------------------------------------------------- const TextFieldInputEndContent = forwardRef< ViewRef, TextFieldInputEndContentProps >((props, ref) => { const { children, className, ...restProps } = props; const tvStyles = textFieldStyles.inputEndContent({ className }); return ( {children} ); }); // -------------------------------------------------- const TextFieldDescription = forwardRef( (props, ref) => { const { isInvalid: localIsInvalid, children, className, animation, ...restProps } = props; const { isInvalid: contextIsInvalid } = useTextField(); const isInvalid = localIsInvalid !== undefined ? localIsInvalid : contextIsInvalid; const tvStyles = textFieldStyles.description({ className, }); const { entering, exiting } = useTextFieldDescriptionAnimation({ animation, }); if (isInvalid) return null; return ( {children} ); } ); // -------------------------------------------------- const TextFieldErrorMessage = forwardRef( (props, ref) => { const { isInvalid: contextIsInvalid } = useTextField(); const { className, isInvalid: localIsInvalid, ...restProps } = props; const isInvalid = localIsInvalid !== undefined ? localIsInvalid : contextIsInvalid; const tvStyles = textFieldStyles.errorMessage({ className, }); return ( ); } ); // -------------------------------------------------- TextFieldRoot.displayName = DISPLAY_NAME.ROOT; TextFieldLabel.displayName = DISPLAY_NAME.LABEL; TextFieldInput.displayName = DISPLAY_NAME.INPUT; TextFieldInputStartContent.displayName = DISPLAY_NAME.INPUT_START_CONTENT; TextFieldInputEndContent.displayName = DISPLAY_NAME.INPUT_END_CONTENT; TextFieldDescription.displayName = DISPLAY_NAME.DESCRIPTION; TextFieldErrorMessage.displayName = DISPLAY_NAME.ERROR_MESSAGE; /** * Compound TextField component with sub-components * * @component TextField - Main container that provides gap-1 spacing between children. * Handles disabled state and validation state for the entire field. * * @component TextField.Label - Label with optional asterisk for required fields. * Changes to danger color when field is invalid. * * @component TextField.Input - Input container with animated border and background. * Supports start/end content slots and handles focus state animations. * Border turns danger color when field is invalid. * * @component TextField.InputStartContent - Optional content at the start of the input. * Use for icons or prefixes. * * @component TextField.InputEndContent - Optional content at the end of the input. * Use for icons, suffixes, or action buttons. * * @component TextField.Description - Description text with muted styling. * Hidden when field is invalid and error message is shown. * * @component TextField.ErrorMessage - Error message with danger styling. * Shown with animation when field is invalid. Automatically populated from errorMessage prop. * * @see Full documentation: https://heroui.com/components/text-field */ const CompoundTextField = Object.assign(TextFieldRoot, { /** @optional Label with asterisk support */ Label: TextFieldLabel, /** @required Input container with focus animations */ Input: TextFieldInput, /** @optional Start content for input */ InputStartContent: TextFieldInputStartContent, /** @optional End content for input */ InputEndContent: TextFieldInputEndContent, /** @optional Description or helper text */ Description: TextFieldDescription, /** @optional Error message displayed when field is invalid */ ErrorMessage: TextFieldErrorMessage, }); export default CompoundTextField; export { useTextField };