1- import { useBoolean , useDebounceFn } from "ahooks" ;
1+ import { useBoolean } from "ahooks" ;
22import {
3- useRef ,
43 useImperativeHandle ,
54 forwardRef ,
65 KeyboardEvent ,
7- useEffect ,
86 useCallback ,
7+ ChangeEvent ,
8+ useRef ,
9+ useEffect ,
910} from "react" ;
1011import { useTranslation } from "react-i18next" ;
1112
12- const LINE_HEIGHT = 24 ; // 1.5rem
13- const MAX_FIRST_LINE_WIDTH = 470 ; // Width in pixels for first line
14- const MAX_HEIGHT = 240 ; // 15rem
13+ const MAX_HEIGHT = 240 ;
1514
1615interface AutoResizeTextareaProps {
1716 isChatMode : boolean ;
@@ -21,6 +20,7 @@ interface AutoResizeTextareaProps {
2120 chatPlaceholder ?: string ;
2221 lineCount ?: number ;
2322 onLineCountChange ?: ( lineCount : number ) => void ;
23+ firstLineMaxWidth : number ;
2424}
2525
2626// Forward ref to allow parent to interact with this component
@@ -35,87 +35,15 @@ const AutoResizeTextarea = forwardRef<
3535 setInput,
3636 handleKeyDown,
3737 chatPlaceholder,
38- lineCount = 1 ,
3938 onLineCountChange,
39+ firstLineMaxWidth,
4040 } ,
4141 ref
4242 ) => {
4343 const { t } = useTranslation ( ) ;
44- const textareaRef = useRef < HTMLTextAreaElement > ( null ) ;
4544 const [ isComposition , { setTrue, setFalse } ] = useBoolean ( ) ;
46-
47- // Memoize resize logic
48- const { run : debouncedResize } = useDebounceFn (
49- ( ) => {
50- const textarea = textareaRef . current ;
51- if ( ! textarea ) return ;
52- if ( typeof window === "undefined" || typeof document === "undefined" )
53- return ;
54-
55- // Reset height to auto to get the correct scrollHeight
56- textarea . style . height = "auto" ;
57-
58- // Create a hidden span to measure first line width
59- const span = document . createElement ( "span" ) ;
60- span . style . visibility = "hidden" ;
61- span . style . position = "absolute" ;
62- span . style . whiteSpace = "pre" ;
63- span . style . font = window . getComputedStyle ( textarea ) . font ;
64-
65- // Get first line content
66- const content = textarea . value ;
67- const firstLineEnd =
68- content . indexOf ( "\n" ) === - 1 ? content . length : content . indexOf ( "\n" ) ;
69- span . textContent = content . slice ( 0 , firstLineEnd ) ;
70- document . body . appendChild ( span ) ;
71-
72- // Calculate lines based on first line width
73- const firstLineWidth = span . offsetWidth ;
74- document . body . removeChild ( span ) ;
75-
76- // Start with 1 line
77- let lines = 1 ;
78-
79- // Add a line if first line exceeds max width
80- if ( firstLineWidth > MAX_FIRST_LINE_WIDTH ) {
81- lines += 1 ;
82- }
83-
84- // Add lines based on scrollHeight for remaining content
85- const scrollHeight = textarea . scrollHeight ;
86- const remainingLines = Math . floor (
87- ( scrollHeight - LINE_HEIGHT ) / LINE_HEIGHT
88- ) ;
89- lines += Math . max ( 0 , remainingLines ) ;
90-
91- // Calculate final height
92- const newHeight = Math . min ( lines * LINE_HEIGHT , MAX_HEIGHT ) ;
93-
94- // Only update if height actually changed
95- if ( textarea . style . height !== `${ newHeight } px` ) {
96- textarea . style . height = `${ newHeight } px` ;
97- onLineCountChange ?.( lines ) ;
98- }
99- } ,
100- { wait : 100 }
101- ) ;
102-
103- // Handle input changes and initial setup
104- useEffect ( ( ) => {
105- if ( textareaRef . current ) {
106- debouncedResize ( ) ;
107- }
108- } , [ input , debouncedResize ] ) ;
109-
110- useEffect ( ( ) => {
111- if ( textareaRef . current ) {
112- requestAnimationFrame ( ( ) => {
113- // Set cursor position to end
114- const length = textareaRef . current ?. value . length || 0 ;
115- textareaRef . current ?. setSelectionRange ( length , length ) ;
116- } ) ;
117- }
118- } , [ lineCount ] ) ;
45+ const textareaRef = useRef < HTMLTextAreaElement > ( null ) ;
46+ const calcRef = useRef < HTMLDivElement > ( null ) ;
11947
12048 // Expose methods to the parent via ref
12149 useImperativeHandle ( ref , ( ) => ( {
@@ -135,40 +63,67 @@ const AutoResizeTextarea = forwardRef<
13563 handleKeyDown ?.( event ) ;
13664 } ;
13765
66+ useEffect ( ( ) => {
67+ const textarea = textareaRef . current ;
68+
69+ if ( ! textarea || ! calcRef . current ) return ;
70+
71+ if ( ! calcRef . current ) return ;
72+
73+ textarea . style . height = "auto" ;
74+
75+ const computedStyle = getComputedStyle ( textarea ) ;
76+ const lineHeight = parseInt ( computedStyle . lineHeight ) ;
77+ let height = lineHeight ;
78+ let minHeight = lineHeight ;
79+
80+ if ( calcRef . current ?. offsetWidth >= firstLineMaxWidth - 32 ) {
81+ minHeight = lineHeight * 2 ;
82+ height = Math . min (
83+ Math . max ( minHeight , textarea . scrollHeight ) ,
84+ MAX_HEIGHT
85+ ) ;
86+ }
87+
88+ textarea . style . height = `${ height } px` ;
89+ textarea . style . minHeight = `${ minHeight } px` ;
90+
91+ onLineCountChange ?.( height / lineHeight ) ;
92+ } , [ input , firstLineMaxWidth ] ) ;
93+
13894 const handleChange = useCallback (
139- ( e : React . ChangeEvent < HTMLTextAreaElement > ) => {
140- setInput ( e . target . value ) ;
95+ ( event : ChangeEvent < HTMLTextAreaElement > ) => {
96+ setInput ( event . currentTarget . value ) ;
14197 } ,
14298 [ setInput ]
14399 ) ;
144100
145101 return (
146- < textarea
147- ref = { textareaRef }
148- id = { isChatMode ? "chat-textarea" : "search-textarea" }
149- autoFocus
150- autoComplete = "off"
151- autoCapitalize = "none"
152- spellCheck = "false"
153- className = "text-base flex-1 outline-none w-full min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent custom-scrollbar"
154- placeholder = { chatPlaceholder || t ( "search.textarea.placeholder" ) }
155- aria-label = { t ( "search.textarea.ariaLabel" ) }
156- value = { input }
157- onChange = { handleChange }
158- onKeyDown = { handleKeyPress }
159- onCompositionStart = { setTrue }
160- onCompositionEnd = { ( ) => {
161- setTimeout ( setFalse , 0 ) ;
162- } }
163- rows = { 1 }
164- style = { {
165- resize : "none" , // Prevent manual resize
166- overflow : "auto" ,
167- minHeight : "1.5rem" ,
168- maxHeight : "13.5rem" , // Limit height to 9 rows (9 * 1.5 line-height)
169- lineHeight : "1.5rem" , // Line height to match row height
170- } }
171- />
102+ < >
103+ < textarea
104+ ref = { textareaRef }
105+ id = { isChatMode ? "chat-textarea" : "search-textarea" }
106+ autoFocus
107+ autoComplete = "off"
108+ autoCapitalize = "none"
109+ spellCheck = "false"
110+ className = "text-base flex-1 outline-none w-full min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent custom-scrollbar resize-none overflow-y-auto"
111+ placeholder = { chatPlaceholder || t ( "search.textarea.placeholder" ) }
112+ aria-label = { t ( "search.textarea.ariaLabel" ) }
113+ value = { input }
114+ onChange = { handleChange }
115+ onKeyDown = { handleKeyPress }
116+ onCompositionStart = { setTrue }
117+ onCompositionEnd = { ( ) => {
118+ setTimeout ( setFalse , 0 ) ;
119+ } }
120+ rows = { 1 }
121+ />
122+
123+ < div ref = { calcRef } className = "absolute whitespace-nowrap -z-10" >
124+ { input }
125+ </ div >
126+ </ >
172127 ) ;
173128 }
174129) ;
0 commit comments