@@ -12,9 +12,12 @@ interface UserInputProps {
1212 /** This controls the send button. Not the editor. */
1313 inputEnabled : boolean ;
1414 initialMessage ?: ServerTypes . Message ;
15+ /** Optional controlled height for the editor. */
16+ editorHeight ?: number ;
17+ onEditorHeightChange ?: ( height : number ) => void ;
1518}
1619
17- export function UserInput ( { onUserMessage, inputEnabled, initialMessage } : UserInputProps ) {
20+ export function UserInput ( { onUserMessage, inputEnabled, initialMessage, editorHeight : controlledHeight , onEditorHeightChange } : UserInputProps ) {
1821 /** Text input */
1922 const [ inputValue , setInputValue ] = React . useState (
2023 initialMessage ?. content
@@ -31,11 +34,23 @@ export function UserInput({ onUserMessage, inputEnabled, initialMessage }: UserI
3134 const MIN_HEIGHT = 80 ;
3235 const MAX_HEIGHT = 400 ;
3336
34- const [ editorHeight , setEditorHeight ] = React . useState < number > ( MIN_HEIGHT ) ;
37+ const [ uncontrolledEditorHeight , setUncontrolledEditorHeight ] = React . useState < number > ( MIN_HEIGHT ) ;
3538 const startYRef = React . useRef < number | null > ( null ) ;
3639 const startHeightRef = React . useRef < number > ( 0 ) ;
3740 const draggingRef = React . useRef ( false ) ;
3841 const textAreaRef = React . useRef < HTMLTextAreaElement | null > ( null ) ;
42+ const scrollContainerRef = React . useRef < HTMLDivElement | null > ( null ) ;
43+
44+ const isControlled = controlledHeight !== undefined ;
45+ const editorHeight = isControlled ? controlledHeight as number : uncontrolledEditorHeight ;
46+ const setEditorHeight = React . useCallback ( ( height : number ) => {
47+ const clamped = Math . min ( MAX_HEIGHT , Math . max ( MIN_HEIGHT , height ) ) ;
48+ if ( isControlled ) {
49+ onEditorHeightChange ?.( clamped ) ;
50+ } else {
51+ setUncontrolledEditorHeight ( clamped ) ;
52+ }
53+ } , [ isControlled , onEditorHeightChange ] ) ;
3954
4055 const beginDrag = ( e : React . MouseEvent ) => {
4156 startYRef . current = e . clientY ;
@@ -81,18 +96,30 @@ export function UserInput({ onUserMessage, inputEnabled, initialMessage }: UserI
8196 window . removeEventListener ( "mousemove" , onMove ) ;
8297 window . removeEventListener ( "mouseup" , onUp ) ;
8398 } ;
84- } , [ editorHeight ] ) ;
99+ } , [ editorHeight , setEditorHeight ] ) ;
85100
86- React . useEffect ( ( ) => {
101+ React . useLayoutEffect ( ( ) => {
87102 const ta = textAreaRef . current ;
88- const parent = ta ?. parentElement ;
89- if ( ta && parent ) {
90- const childRect = ta . getBoundingClientRect ( ) ;
91- const parentRect = parent . getBoundingClientRect ( ) ;
92- const maxVisibleHeight = parentRect . height - ( childRect . top - parentRect . top ) - 8 ;
93- ta . style . height = "auto" ;
94- const targetHeight = Math . max ( ta . scrollHeight , maxVisibleHeight ) ;
95- ta . style . height = `${ targetHeight } px` ;
103+ const scrollEl = scrollContainerRef . current ;
104+ if ( ! ta || ! scrollEl ) return ;
105+
106+ const wasAtBottom = scrollEl . scrollHeight - scrollEl . scrollTop - scrollEl . clientHeight < 8 ;
107+ const previousScrollTop = scrollEl . scrollTop ;
108+
109+ ta . style . height = "auto" ;
110+
111+ const parentRect = scrollEl . getBoundingClientRect ( ) ;
112+ const childRect = ta . getBoundingClientRect ( ) ;
113+ const maxVisibleHeight = parentRect . height - ( childRect . top - parentRect . top ) - 8 ;
114+ const targetHeight = Math . max ( ta . scrollHeight , maxVisibleHeight ) ;
115+
116+ ta . style . height = `${ targetHeight } px` ;
117+
118+ // Restore scroll to where the user was, or keep the caret visible at the bottom.
119+ if ( wasAtBottom ) {
120+ scrollEl . scrollTop = scrollEl . scrollHeight ;
121+ } else {
122+ scrollEl . scrollTop = previousScrollTop ;
96123 }
97124 } , [ editorHeight , inputValue , imageUrls ] ) ;
98125
@@ -184,6 +211,7 @@ export function UserInput({ onUserMessage, inputEnabled, initialMessage }: UserI
184211 "absolute inset-0 flex flex-col overflow-y-auto rounded-md border border-input bg-transparent" ,
185212 "scrollbar-thin scrollbar-thumb-muted-foreground/30 scrollbar-track-transparent"
186213 ) }
214+ ref = { scrollContainerRef }
187215 >
188216 { imageUrls . length > 0 && (
189217 < div className = "p-2 flex flex-wrap gap-2" >
0 commit comments