11import { SendIcon } from '@shared/assets/icons' ;
2- import { useState , type ChangeEventHandler , type KeyboardEventHandler } from 'react' ;
2+ import { useLayoutEffect , useRef , useState , type ChangeEventHandler , type KeyboardEventHandler } from 'react' ;
33import { chatInputVariants } from './ChatInput.variants' ;
44
5+ const TEXTAREA_LINE_HEIGHT = 24 ;
6+ const TEXTAREA_MAX_LINES = 4 ;
7+
58export interface ChatInputProps {
69 placeholder ?: string ;
710 onSend : ( message : string ) => void ;
@@ -12,6 +15,7 @@ export interface ChatInputProps {
1215export const ChatInput = ( { placeholder, onSend, value, onChange } : ChatInputProps ) => {
1316 const [ internalMessage , setInternalMessage ] = useState ( '' ) ;
1417 const [ isFocused , setFocused ] = useState ( false ) ;
18+ const textareaRef = useRef < HTMLTextAreaElement | null > ( null ) ;
1519
1620 const isControlled = value !== undefined ;
1721 const message = isControlled ? value : internalMessage ;
@@ -23,7 +27,7 @@ export const ChatInput = ({ placeholder, onSend, value, onChange }: ChatInputPro
2327 sendState : isFilled ? 'active' : 'inactive' ,
2428 } ) ;
2529
26- const handleChange : ChangeEventHandler < HTMLInputElement > = ( event ) => {
30+ const handleChange : ChangeEventHandler < HTMLTextAreaElement > = ( event ) => {
2731 const nextValue = event . target . value ;
2832 if ( isControlled ) {
2933 onChange ?.( nextValue ) ;
@@ -46,18 +50,31 @@ export const ChatInput = ({ placeholder, onSend, value, onChange }: ChatInputPro
4650 }
4751 } ;
4852
49- const handleKeyDown : KeyboardEventHandler < HTMLInputElement > = ( event ) => {
50- if ( event . key === 'Enter' && ! event . nativeEvent . isComposing ) {
53+ const handleKeyDown : KeyboardEventHandler < HTMLTextAreaElement > = ( event ) => {
54+ if ( event . key === 'Enter' && ! event . shiftKey && ! event . nativeEvent . isComposing ) {
5155 event . preventDefault ( ) ;
5256 handleSend ( ) ;
5357 }
5458 } ;
5559
60+ useLayoutEffect ( ( ) => {
61+ const element = textareaRef . current ;
62+ if ( ! element ) {
63+ return ;
64+ }
65+
66+ element . style . height = 'auto' ;
67+ const maxHeight = TEXTAREA_LINE_HEIGHT * TEXTAREA_MAX_LINES ;
68+ const nextHeight = Math . min ( element . scrollHeight , maxHeight ) ;
69+ element . style . height = `${ nextHeight } px` ;
70+ element . style . overflowY = element . scrollHeight > maxHeight ? 'auto' : 'hidden' ;
71+ } , [ message ] ) ;
72+
5673 return (
5774 < div className = { styles . root ( ) } >
5875 < div className = { styles . wrapper ( ) } >
59- < input
60- type = "text"
76+ < textarea
77+ ref = { textareaRef }
6178 value = { message }
6279 onFocus = { ( ) => setFocused ( true ) }
6380 onBlur = { ( ) => setFocused ( false ) }
@@ -66,6 +83,7 @@ export const ChatInput = ({ placeholder, onSend, value, onChange }: ChatInputPro
6683 placeholder = { placeholder }
6784 aria-label = { placeholder ?? 'Chat input' }
6885 onKeyDown = { handleKeyDown }
86+ rows = { 1 }
6987 />
7088 < button type = "button" className = { styles . button ( ) } onClick = { handleSend } disabled = { ! isFilled } aria-label = "Send" >
7189 < SendIcon className = { styles . icon ( ) } aria-hidden = "true" />
0 commit comments