"use client"; import * as React from "react"; import { useLayoutEffect } from "react"; import { debounce } from "../util"; type State = { outerHeightStyle: number; overflow?: boolean | undefined; }; function getStyleValue(value: string) { return parseInt(value, 10) || 0; } export const TextareaAutosize = React.forwardRef(function TextareaAutosize( props: TextareaAutosizeProps, ref: React.ForwardedRef ) { const { onChange, onScroll, onResize, maxRows, minRows = 1, style, value, onFocus, onBlur, sizeRef, ignoreBoxSizing, ...other } = props; const { current: isControlled } = React.useRef(value != null); const inputRef = React.useRef(null); const handleRef = useForkRef(ref, inputRef); const syncHeight = React.useCallback(() => { const el = inputRef.current; if (!el || typeof window === "undefined") return; if (el.offsetWidth === 0) return; const cs = window.getComputedStyle(el); const paddingY = getStyleValue(cs.paddingTop) + getStyleValue(cs.paddingBottom); const borderY = getStyleValue(cs.borderTopWidth) + getStyleValue(cs.borderBottomWidth); const boxSizing = cs.boxSizing; // ── measure by temporarily collapsing the real element ── const prevHeight = el.style.height; const prevOverflow = el.style.overflowY; el.style.overflowY = "hidden"; el.style.height = "0px"; // scrollHeight = content + padding (always, regardless of box-sizing) const scrollH = el.scrollHeight; // Measure single-row height for minRows / maxRows // Save cursor position — directly setting el.value resets it. const selStart = el.selectionStart; const selEnd = el.selectionEnd; const prevValue = el.value; el.value = "x"; const singleRowScrollH = el.scrollHeight; el.value = prevValue; // Restore immediately — all of this happens before paint (useLayoutEffect) el.style.height = prevHeight; el.style.overflowY = prevOverflow; // Restore cursor position after value manipulation if (document.activeElement === el) { el.selectionStart = selStart; el.selectionEnd = selEnd; } const lineHeight = singleRowScrollH - paddingY; let targetHeight = scrollH; // includes padding if (minRows) { targetHeight = Math.max( Number(minRows) * lineHeight + paddingY, targetHeight ); } const unclampedHeight = targetHeight; if (maxRows) { targetHeight = Math.min( Number(maxRows) * lineHeight + paddingY, targetHeight ); } // For border-box, height CSS prop = content + padding + border. // scrollHeight already includes padding, so only add border. const extra = !ignoreBoxSizing && boxSizing === "border-box" ? borderY : 0; const finalHeight = Math.ceil(targetHeight + extra); const shouldScroll = Math.abs(unclampedHeight - targetHeight) > 1; el.style.height = `${finalHeight}px`; el.style.overflowY = shouldScroll ? "auto" : "hidden"; if (onResize) { onResize({ outerHeightStyle: finalHeight, overflow: !shouldScroll }); } }, [maxRows, minRows, ignoreBoxSizing, onResize]); // ── sync on every layout ── useLayoutEffect(() => { syncHeight(); }); // ── sync on window resize / element resize ── React.useEffect(() => { const handleResize = debounce(() => { if (inputRef.current) { syncHeight(); } }); const input = inputRef.current!; if (typeof window === "undefined") return; window.addEventListener("resize", handleResize); let resizeObserver: ResizeObserver | undefined; if (typeof ResizeObserver !== "undefined") { resizeObserver = new ResizeObserver(handleResize); resizeObserver.observe(input); } return () => { handleResize.clear(); window.removeEventListener("resize", handleResize); resizeObserver?.disconnect(); }; }, [syncHeight]); const handleChange = (event: React.ChangeEvent) => { if (!isControlled) { syncHeight(); } if (onChange) { onChange(event); } }; return (