index.tsx56 lines · main
| 1 | 'use client' |
| 2 | |
| 3 | import React, { forwardRef, useImperativeHandle, useLayoutEffect, useRef } from 'react' |
| 4 | |
| 5 | import { cn } from '../../lib/utils' |
| 6 | import { Textarea } from '../shadcn/ui/textarea' |
| 7 | |
| 8 | export interface ExpandingTextAreaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> { |
| 9 | /* The value of the textarea. Required to calculate the height of the textarea. */ |
| 10 | value: string |
| 11 | } |
| 12 | |
| 13 | /** |
| 14 | * This is a custom TextArea component that expands based on the content. |
| 15 | */ |
| 16 | const ExpandingTextArea = forwardRef<HTMLTextAreaElement | null, ExpandingTextAreaProps>( |
| 17 | ({ className, value, ...props }, ref) => { |
| 18 | const internalRef = useRef<HTMLTextAreaElement | null>(null) |
| 19 | |
| 20 | useImperativeHandle(ref, () => internalRef.current as HTMLTextAreaElement, []) |
| 21 | |
| 22 | const updateTextAreaHeight = (element: HTMLTextAreaElement | null) => { |
| 23 | if (!element) return |
| 24 | |
| 25 | // Match single-line input height (h-10 = 40px) so we don't shrink when typing; grow only when content wraps |
| 26 | const singleLineHeightPx = 40 |
| 27 | element.style.height = 'auto' |
| 28 | const contentHeight = element.scrollHeight |
| 29 | element.style.height = Math.max(singleLineHeightPx, contentHeight) + 'px' |
| 30 | } |
| 31 | |
| 32 | useLayoutEffect(() => { |
| 33 | updateTextAreaHeight(internalRef.current) |
| 34 | }, [value]) |
| 35 | |
| 36 | return ( |
| 37 | <Textarea |
| 38 | ref={(element) => { |
| 39 | if (element) { |
| 40 | internalRef.current = element |
| 41 | updateTextAreaHeight(element) |
| 42 | } |
| 43 | }} |
| 44 | rows={1} |
| 45 | aria-expanded={false} |
| 46 | className={cn('h-auto resize-none box-border', className)} |
| 47 | value={value} |
| 48 | {...props} |
| 49 | /> |
| 50 | ) |
| 51 | } |
| 52 | ) |
| 53 | |
| 54 | ExpandingTextArea.displayName = 'ExpandingTextArea' |
| 55 | |
| 56 | export { ExpandingTextArea } |