From 95cea2c6320d18028ef7a4b0f5899e1266f94e46 Mon Sep 17 00:00:00 2001 From: Bartek Bak Date: Fri, 10 Jul 2026 10:55:28 +0100 Subject: [PATCH 1/4] feat(input-area): add auto resize support --- .changeset/tall-pandas-grow.md | 5 ++ .../src/components/demos/InputAreaDemo.tsx | 14 +++++ .../src/pages/components/input-area.mdx | 11 ++++ .../kumo/src/components/input/input-area.tsx | 55 ++++++++++++++++-- .../kumo/src/components/input/input.test.tsx | 58 +++++++++++++++++-- 5 files changed, 134 insertions(+), 9 deletions(-) create mode 100644 .changeset/tall-pandas-grow.md diff --git a/.changeset/tall-pandas-grow.md b/.changeset/tall-pandas-grow.md new file mode 100644 index 0000000000..af256497f7 --- /dev/null +++ b/.changeset/tall-pandas-grow.md @@ -0,0 +1,5 @@ +--- +"@cloudflare/kumo": minor +--- + +Add auto-resize support to InputArea and Textarea. diff --git a/packages/kumo-docs-astro/src/components/demos/InputAreaDemo.tsx b/packages/kumo-docs-astro/src/components/demos/InputAreaDemo.tsx index 8b4afdc6fe..ce9311d9c7 100644 --- a/packages/kumo-docs-astro/src/components/demos/InputAreaDemo.tsx +++ b/packages/kumo-docs-astro/src/components/demos/InputAreaDemo.tsx @@ -80,6 +80,20 @@ export function InputAreaRowsDemo() { ); } +export function InputAreaAutoResizeDemo() { + return ( + + ); +} + export function InputAreaOptionalFieldDemo() { return ( +### Auto Resize + +

+ Use `autoResize` to let the textarea grow vertically as users type or paste + multi-line content. +

+ + + + ### Bare InputArea

diff --git a/packages/kumo/src/components/input/input-area.tsx b/packages/kumo/src/components/input/input-area.tsx index be0c14a32a..4d7e5ad570 100644 --- a/packages/kumo/src/components/input/input-area.tsx +++ b/packages/kumo/src/components/input/input-area.tsx @@ -1,9 +1,13 @@ import { inputVariants } from "./input"; import { cn } from "../../utils/cn"; -import { useCallback, type ReactNode } from "react"; +import { useCallback, useLayoutEffect, useRef, type ReactNode } from "react"; import * as React from "react"; import { Field as FieldBase } from "@base-ui/react/field"; -import { Field as KumoField, normalizeFieldError, type FieldErrorMatch } from "../field/field"; +import { + Field as KumoField, + normalizeFieldError, + type FieldErrorMatch, +} from "../field/field"; export const InputArea = React.forwardRef( (props, ref) => { @@ -17,6 +21,7 @@ export const InputArea = React.forwardRef( labelTooltip, description, error, + autoResize = false, ...inputProps } = props; @@ -35,17 +40,55 @@ export const InputArea = React.forwardRef( // Extract required from inputProps to pass to Field for label decoration const { required } = inputProps; + const textareaRef = useRef(null); + + const resizeTextarea = useCallback( + (textarea = textareaRef.current) => { + if (!textarea) return; + + if (!autoResize) { + textarea.style.height = ""; + return; + } + + textarea.style.height = "auto"; + textarea.style.height = `${textarea.scrollHeight}px`; + }, + [autoResize], + ); + + const setTextareaRef = useCallback( + (node: HTMLTextAreaElement | null) => { + textareaRef.current = node; + + if (typeof ref === "function") { + ref(node); + } else if (ref) { + ref.current = node; + } + + resizeTextarea(node); + }, + [ref, resizeTextarea], + ); + + useLayoutEffect(() => { + resizeTextarea(); + }, [resizeTextarea, inputProps.value, inputProps.defaultValue]); + const handleChange = useCallback( (event: React.ChangeEvent) => { onChange?.(event); onValueChange?.(event.target.value); + resizeTextarea(event.currentTarget); }, - [onChange, onValueChange], + [onChange, onValueChange, resizeTextarea], ); const textareaClassName = cn( inputVariants({ size, variant, focusIndicator: true }), "h-auto py-2", // Input variant always comes with size, but it does not apply for textarea + autoResize && "resize-none overflow-hidden", className, ); @@ -65,7 +108,7 @@ export const InputArea = React.forwardRef( render={(controlProps) => (