import { type InputEventHandler, type TextareaHTMLAttributes, useLayoutEffect, useRef, } from 'react'; const DEFAULT_MIN_HEIGHT_PX = 40; const DEFAULT_MAX_HEIGHT_PX = 128; function supportsNativeFieldSizing() { return ( typeof CSS !== 'undefined' && typeof CSS.supports === 'function' && CSS.supports('field-sizing', 'content') ); } function parseComputedPixelValue(value: string, fallback: number) { const parsedValue = Number.parseFloat(value); return Number.isFinite(parsedValue) ? parsedValue : fallback; } function resizeFallbackTextArea(textarea: HTMLTextAreaElement) { textarea.style.height = 'auto'; const computedStyle = window.getComputedStyle(textarea); const minHeight = parseComputedPixelValue( computedStyle.minHeight, DEFAULT_MIN_HEIGHT_PX, ); const maxHeight = Math.max( minHeight, parseComputedPixelValue(computedStyle.maxHeight, DEFAULT_MAX_HEIGHT_PX), ); const contentHeight = textarea.scrollHeight; const borderHeight = Math.max( 0, textarea.offsetHeight - textarea.clientHeight, ); const isBorderBox = computedStyle.boxSizing === 'border-box'; const paddingHeight = parseComputedPixelValue(computedStyle.paddingTop, 0) + parseComputedPixelValue(computedStyle.paddingBottom, 0); const measuredHeight = isBorderBox ? contentHeight + borderHeight : Math.max(0, contentHeight - paddingHeight); const nextHeight = Math.min( Math.max(measuredHeight, minHeight), maxHeight, ); textarea.style.height = `${nextHeight}px`; textarea.style.overflowY = measuredHeight > maxHeight ? 'auto' : 'hidden'; } export function AutoGrowTextArea({ className, onInput, rows = 1, value, ...textareaProps }: TextareaHTMLAttributes) { const textareaRef = useRef(null); useLayoutEffect(() => { const textarea = textareaRef.current; if (!textarea || supportsNativeFieldSizing()) { return; } resizeFallbackTextArea(textarea); }, [value]); useLayoutEffect(() => { const textarea = textareaRef.current; if (!textarea || supportsNativeFieldSizing()) { return undefined; } let previousWidth = textarea.getBoundingClientRect().width; const resizeWhenWidthChanges = (nextWidth: number) => { if (Math.abs(nextWidth - previousWidth) < 0.5) { return; } previousWidth = nextWidth; resizeFallbackTextArea(textarea); }; if (typeof ResizeObserver !== 'undefined') { const observer = new ResizeObserver((entries) => { const entry = entries[0]; if (entry) { resizeWhenWidthChanges(entry.contentRect.width); } }); observer.observe(textarea); return () => observer.disconnect(); } const handleWindowResize = () => { resizeWhenWidthChanges(textarea.getBoundingClientRect().width); }; window.addEventListener('resize', handleWindowResize); return () => window.removeEventListener('resize', handleWindowResize); }, []); const resolvedClassName = [ 'auto-grow-text-area max-h-32 min-h-10 resize-none overflow-x-hidden overflow-y-auto overscroll-contain [field-sizing:content] disabled:cursor-not-allowed disabled:opacity-60', className, ] .filter(Boolean) .join(' '); const handleInput: InputEventHandler = (event) => { if (!supportsNativeFieldSizing()) { resizeFallbackTextArea(event.currentTarget); } onInput?.(event); }; return (