| import * as React from 'react' | |
| import { cn } from '@/lib/utils' | |
| type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> & { | |
| className?: string | |
| value?: string | |
| onChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void | |
| } | |
| const MIN_HEIGHT = 40 | |
| const MAX_HEIGHT = 96 | |
| const TextArea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( | |
| ({ className, value, onChange, ...props }, forwardedRef) => { | |
| const [showScroll, setShowScroll] = React.useState(false) | |
| const textareaRef = React.useRef<HTMLTextAreaElement | null>(null) | |
| const adjustHeight = React.useCallback(() => { | |
| const textarea = textareaRef.current | |
| if (!textarea) return | |
| textarea.style.height = `${MIN_HEIGHT}px` | |
| const { scrollHeight } = textarea | |
| const newHeight = Math.min(Math.max(scrollHeight, MIN_HEIGHT), MAX_HEIGHT) | |
| textarea.style.height = `${newHeight}px` | |
| setShowScroll(scrollHeight > MAX_HEIGHT) | |
| }, []) | |
| const handleChange = React.useCallback( | |
| (e: React.ChangeEvent<HTMLTextAreaElement>) => { | |
| const cursorPosition = e.target.selectionStart | |
| onChange?.(e) | |
| requestAnimationFrame(() => { | |
| adjustHeight() | |
| if (textareaRef.current) { | |
| textareaRef.current.setSelectionRange( | |
| cursorPosition, | |
| cursorPosition | |
| ) | |
| } | |
| }) | |
| }, | |
| [onChange, adjustHeight] | |
| ) | |
| const handleRef = React.useCallback( | |
| (node: HTMLTextAreaElement | null) => { | |
| const ref = forwardedRef as | |
| | React.MutableRefObject<HTMLTextAreaElement | null> | |
| | ((instance: HTMLTextAreaElement | null) => void) | |
| | null | |
| if (typeof ref === 'function') { | |
| ref(node) | |
| } else if (ref) { | |
| ref.current = node | |
| } | |
| textareaRef.current = node | |
| }, | |
| [forwardedRef] | |
| ) | |
| React.useEffect(() => { | |
| if (textareaRef.current) { | |
| adjustHeight() | |
| } | |
| }, [value, adjustHeight]) | |
| return ( | |
| <textarea | |
| className={cn( | |
| 'w-full resize-none bg-transparent shadow-sm', | |
| 'rounded-xl border border-border', | |
| 'px-3 py-2', | |
| 'text-sm leading-5', | |
| 'placeholder:text-muted-foreground', | |
| 'focus-visible:ring-0.5 focus-visible:ring-ring focus-visible:border-primary/50 focus-visible:outline-none', | |
| 'disabled:cursor-not-allowed disabled:opacity-50', | |
| showScroll ? 'overflow-y-auto' : 'overflow-hidden', | |
| className | |
| )} | |
| style={{ | |
| minHeight: `${MIN_HEIGHT}px`, | |
| height: `${MIN_HEIGHT}px`, | |
| maxHeight: `${MAX_HEIGHT}px` | |
| }} | |
| ref={handleRef} | |
| value={value} | |
| onChange={handleChange} | |
| {...props} | |
| /> | |
| ) | |
| } | |
| ) | |
| TextArea.displayName = 'TextArea' | |
| export type { TextareaProps } | |
| export { TextArea } | |