import { useTheme } from '../../../context/ThemeContext' import FOUNDATION_THEME from '../../../tokens/theme.token' import Block from '../../Primitives/Block/Block' import PrimitiveTextarea from '../../Primitives/PrimitiveTextArea' import InputLabels from '../utils/InputLabels/InputLabels' import InputFooter from '../utils/InputFooter/InputFooter' import type { TextAreaProps } from './types' import type { TextAreaTokensType } from './textarea.token' import { useResponsiveTokens } from '../../../hooks/useResponsiveTokens' import { useState, useId, useRef, useEffect } from 'react' import { useBreakpoints } from '../../../hooks/useBreakPoints' import { BREAKPOINTS } from '../../../breakpoints/breakPoints' import FloatingLabels from '../utils/FloatingLabels/FloatingLabels' import { toPixels } from '../../../global-utils/GlobalUtils' import { useErrorShake } from '../../common/useErrorShake' import { getErrorShakeStyle, errorShakeAnimation, } from '../../common/error.animations' import styled from 'styled-components' const Wrapper = styled(Block)` ${errorShakeAnimation} ` const TextArea = ({ value, placeholder, disabled, autoFocus, onChange, onFocus, onBlur, rows = 3, cols, label, sublabel, hintText, helpIconHintText, required, error, errorMessage, wrap, resize = 'none', name, id: providedId, ...rest }: TextAreaProps) => { const textAreaTokens = useResponsiveTokens('TEXT_AREA') const { theme: blendTheme } = useTheme() // primary[50] reads as a near-white halo on a dark surface const focusRingColor = blendTheme === 'dark' ? FOUNDATION_THEME.colors.primary[800] : FOUNDATION_THEME.colors.primary[50] const [isFocused, setIsFocused] = useState(false) const shouldShake = useErrorShake(error || false) const { breakPointLabel } = useBreakpoints(BREAKPOINTS) const isSmallScreen = breakPointLabel === 'sm' const textAreaRef = useRef(null) // Generate unique IDs for accessibility const generatedId = useId() const textareaId = providedId || generatedId const errorId = `${textareaId}-error` const hintId = `${textareaId}-hint` // Construct aria-describedby to link hint and error messages const ariaDescribedBy = [ hintText && !error ? hintId : null, error && errorMessage ? errorId : null, ] .filter(Boolean) .join(' ') || undefined const inputFocusedOrWithValue = isFocused || value.length > 0 const paddingX = toPixels(textAreaTokens.inputContainer.padding.x) const paddingY = toPixels(textAreaTokens.inputContainer.padding.y) useEffect(() => { if (autoFocus && textAreaRef.current) { textAreaRef.current.focus() } }, [autoFocus]) return ( {!isSmallScreen && ( )} {label && isSmallScreen && ( )} { setIsFocused(true) onFocus?.(e) }} onBlur={(e) => { setIsFocused(false) onBlur?.(e) }} rows={rows} required={required} cols={cols} wrap={wrap} borderRadius={textAreaTokens.inputContainer.borderRadius} resize={resize} paddingX={paddingX} paddingTop={ label && isSmallScreen && inputFocusedOrWithValue ? paddingY + 14 : paddingY } paddingBottom={ label && isSmallScreen && inputFocusedOrWithValue ? 0 : paddingY } border={ textAreaTokens.inputContainer.border[ error ? 'error' : 'default' ] } fontSize={textAreaTokens.inputContainer.fontSize} fontWeight={textAreaTokens.inputContainer.fontWeight} backgroundColor={ textAreaTokens.inputContainer.backgroundColor[ error ? 'error' : 'default' ] } transition="border 200ms ease-in-out, box-shadow 200ms ease-in-out, background-color 200ms ease-in-out" placeholderStyles={{ transition: 'opacity 150ms ease-out', color: textAreaTokens.placeholder?.color, fontWeight: textAreaTokens.placeholder?.fontWeight, }} _hover={{ border: textAreaTokens.inputContainer.border[ error ? 'error' : 'hover' ], backgroundColor: textAreaTokens.inputContainer.backgroundColor[ error ? 'error' : 'hover' ], }} color={ textAreaTokens.inputContainer.color[ disabled ? 'disabled' : 'default' ] } _focus={{ border: textAreaTokens.inputContainer.border[ error ? 'error' : 'focus' ], boxShadow: `0 0 0 3px ${focusRingColor}`, backgroundColor: 'rgba(239, 246, 255, 0.15)', }} disabled={disabled} _disabled={{ backgroundColor: textAreaTokens.inputContainer.backgroundColor .disabled, border: textAreaTokens.inputContainer.border.disabled, cursor: 'not-allowed', }} aria-required={required ? 'true' : undefined} aria-invalid={error ? 'true' : 'false'} aria-describedby={ariaDescribedBy} {...rest} /> ) } export default TextArea