import React, { useState, useEffect, useRef, useCallback } from 'react'; import { cn } from '@/lib/utils'; import FormLabel from './FormLabel'; import FormError from './FormError'; /** * FormTextarea Component * Textarea with resize options, character counter, auto-resize, and validation states */ export interface FormTextareaProps extends Omit, 'maxLength'> { label?: string; error?: string | string[]; helpText?: string; required?: boolean; showCharCount?: boolean; autoResize?: boolean; maxHeight?: number; minHeight?: number; containerClassName?: string; textareaClassName?: string; maxLength?: number; } export const FormTextarea: React.FC = ({ label, error, helpText, required = false, showCharCount = false, autoResize = false, maxHeight = 300, minHeight = 120, containerClassName, textareaClassName, maxLength, disabled = false, value = '', onChange, rows = 4, ...props }) => { const [isFocused, setIsFocused] = useState(false); const [charCount, setCharCount] = useState(0); const textareaRef = useRef(null); const hasError = !!error; const showError = hasError; // Update character count useEffect(() => { const currentValue = typeof value === 'string' ? value : String(value || ''); setCharCount(currentValue.length); }, [value]); // Auto-resize textarea useEffect(() => { if (!autoResize || !textareaRef.current) return; const textarea = textareaRef.current; // Reset height to calculate new height textarea.style.height = 'auto'; // Calculate new height const newHeight = Math.min( Math.max(textarea.scrollHeight, minHeight), maxHeight ); textarea.style.height = `${newHeight}px`; }, [value, autoResize, minHeight, maxHeight]); const handleFocus = () => setIsFocused(true); const handleBlur = () => setIsFocused(false); const handleChange = useCallback((e: React.ChangeEvent) => { if (maxLength && e.target.value.length > maxLength) { e.target.value = e.target.value.slice(0, maxLength); } onChange?.(e); }, [onChange, maxLength]); const inputId = props.id || (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined); const baseTextareaClasses = cn( 'w-full px-3 py-2 border rounded-md transition-all duration-200 resize-y', 'bg-neutral-light text-text-primary', 'placeholder:text-text-light', 'focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary', 'disabled:opacity-60 disabled:cursor-not-allowed', { 'border-neutral-dark hover:border-neutral-dark': !hasError && !isFocused, 'border-primary ring-2 ring-primary': isFocused && !hasError, 'border-danger ring-2 ring-danger/20': hasError, }, autoResize && 'overflow-hidden', textareaClassName ); const containerClasses = cn( 'flex flex-col gap-1.5', containerClassName ); const charCountClasses = cn( 'text-xs text-right mt-1', { 'text-text-secondary': charCount <= (maxLength || 0) * 0.8, 'text-warning': charCount > (maxLength || 0) * 0.8 && charCount <= (maxLength || 0), 'text-danger': charCount > (maxLength || 0), } ); const showCharCounter = showCharCount || (maxLength && charCount > 0); return (
{label && ( {label} )}