| 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 } |
|
|