import { useState, forwardRef, useMemo, useEffect, useRef, useImperativeHandle } from 'react'; import { View, TextInput, NativeSyntheticEvent, TextInputContentSizeChangeEventData, TextInput as RNTextInput } from 'react-native'; import { textAreaStyles } from './TextArea.styles'; import Text from '../Text'; import type { TextAreaProps } from './types'; import { getNativeFormAccessibilityProps } from '../utils/accessibility'; import type { TextInputHandle } from '../utils/refTypes'; const TextArea = forwardRef(({ value: controlledValue, defaultValue = '', onChange, onKeyDown: _onKeyDown, // Web-only, no-op on native placeholder, disabled = false, minHeight, maxHeight, autoGrow = false, fill = false, maxLength, rows = 4, label, error, helperText, showCharacterCount = false, intent = 'primary', size = 'md', type = 'outlined', // Spacing variants from FormInputStyleProps margin, marginVertical, marginHorizontal, style, textareaStyle, testID, id, // Accessibility props accessibilityLabel, accessibilityHint, accessibilityDisabled, accessibilityHidden, accessibilityRole, accessibilityLabelledBy, accessibilityDescribedBy, accessibilityRequired, accessibilityInvalid, accessibilityErrorMessage, }, ref) => { const [internalValue, setInternalValue] = useState(defaultValue); const [isFocused, setIsFocused] = useState(false); const [contentHeight, setContentHeight] = useState(undefined); const textInputRef = useRef(null); // Expose focus/blur via imperative handle useImperativeHandle(ref, () => ({ focus: () => textInputRef.current?.focus(), blur: () => textInputRef.current?.blur(), }), []); const value = controlledValue !== undefined ? controlledValue : internalValue; // When value is cleared externally, trigger a layout recalculation via setNativeProps useEffect(() => { if (autoGrow && value === '' && textInputRef.current) { // Force the TextInput to recalculate its layout by setting the text again textInputRef.current.setNativeProps({ text: '' }); } }, [value, autoGrow]); const hasError = Boolean(error); // Generate native accessibility props const nativeA11yProps = useMemo(() => { const computedLabel = accessibilityLabel ?? label ?? placeholder; const isInvalid = accessibilityInvalid ?? hasError; return getNativeFormAccessibilityProps({ accessibilityLabel: computedLabel, accessibilityHint: accessibilityHint ?? (error || helperText), accessibilityDisabled: accessibilityDisabled ?? disabled, accessibilityHidden, accessibilityRole: accessibilityRole ?? 'textbox', accessibilityLabelledBy, accessibilityDescribedBy, accessibilityRequired, accessibilityInvalid: isInvalid, accessibilityErrorMessage: accessibilityErrorMessage ?? error, }); }, [ accessibilityLabel, label, placeholder, accessibilityHint, error, helperText, accessibilityDisabled, disabled, accessibilityHidden, accessibilityRole, accessibilityLabelledBy, accessibilityDescribedBy, accessibilityRequired, accessibilityInvalid, hasError, accessibilityErrorMessage, ]); // Apply variants textAreaStyles.useVariants({ size, intent, type, focused: isFocused, disabled, hasError, autoGrow, resize: 'none', isNearLimit: maxLength ? value.length >= maxLength * 0.9 : false, isAtLimit: maxLength ? value.length >= maxLength : false, margin, marginVertical, marginHorizontal, }); const handleChange = (newValue: string) => { if (maxLength && newValue.length > maxLength) { return; } if (controlledValue === undefined) { setInternalValue(newValue); } onChange?.(newValue); }; const handleFocus = () => { setIsFocused(true); }; const handleBlur = () => { setIsFocused(false); }; const handleContentSizeChange = (e: NativeSyntheticEvent) => { if (!autoGrow) return; let newHeight = e.nativeEvent.contentSize.height; // Apply min/max height constraints if (minHeight && newHeight < minHeight) { newHeight = minHeight; } if (maxHeight && newHeight > maxHeight) { newHeight = maxHeight; } setContentHeight(newHeight); }; const showFooter = (error || helperText) || (showCharacterCount && maxLength); // Get dynamic styles - call as functions for theme reactivity const containerStyleComputed = (textAreaStyles.container as any)({}); const labelStyleComputed = (textAreaStyles.label as any)({ disabled }); const textareaContainerStyleComputed = (textAreaStyles.textareaContainer as any)({ type, focused: isFocused, hasError, disabled }); const textareaStyleComputed = (textAreaStyles.textarea as any)({ autoGrow, disabled }); const footerStyleComputed = (textAreaStyles.footer as any)({}); const helperTextStyleComputed = (textAreaStyles.helperText as any)({ hasError }); const characterCountStyleComputed = (textAreaStyles.characterCount as any)({ isNearLimit: maxLength ? value.length >= maxLength * 0.9 : false, isAtLimit: maxLength ? value.length >= maxLength : false, }); return ( {label && ( {label} )} = maxHeight)} numberOfLines={autoGrow ? undefined : rows} maxLength={maxLength} placeholderTextColor="#999" /> {showFooter && ( {error && ( {error} )} {!error && helperText && ( {helperText} )} {showCharacterCount && maxLength && ( {value.length}/{maxLength} )} )} ); }); TextArea.displayName = 'TextArea'; export default TextArea;