@@ -20,16 +20,13 @@ export interface KeyPressEvent {
2020
2121export type OnKeyPressCallback = ( event : KeyPressEvent ) => boolean ;
2222
23- export interface TextInputState {
24- text : string ;
25- selection : {
26- start : number ;
27- end : number ;
28- } ;
23+ export interface TextInputSelection {
24+ start : number ;
25+ end : number ;
2926}
3027
3128export interface MultiTextInputHandle {
32- setTextAndSelection : ( text : string , selection : { start : number ; end : number } ) => void ;
29+ setTextAndSelection : ( text : string , selection : TextInputSelection ) => void ;
3330 focus : ( ) => void ;
3431 blur : ( ) => void ;
3532}
@@ -44,8 +41,8 @@ interface MultiTextInputProps {
4441 paddingLeft ?: number ;
4542 paddingRight ?: number ;
4643 onKeyPress ?: OnKeyPressCallback ;
47- onSelectionChange ?: ( selection : { start : number ; end : number } ) => void ;
48- onStateChange ?: ( state : TextInputState ) => void ;
44+ onSelectionChange ?: ( selection : TextInputSelection ) => void ;
45+ onCompositionStateChange ?: ( isComposing : boolean ) => void ;
4946}
5047
5148export const MultiTextInput = React . forwardRef < MultiTextInputHandle , MultiTextInputProps > (
@@ -57,21 +54,48 @@ export const MultiTextInput = React.forwardRef<MultiTextInputHandle, MultiTextIn
5754 maxHeight = 120 ,
5855 onKeyPress,
5956 onSelectionChange,
60- onStateChange ,
57+ onCompositionStateChange ,
6158 } = props ;
6259
6360 const { theme } = useUnistyles ( ) ;
6461 const textareaRef = React . useRef < HTMLTextAreaElement > ( null ) ;
62+ const isComposingRef = React . useRef ( false ) ;
63+ const selectionRef = React . useRef < TextInputSelection > ( { start : 0 , end : 0 } ) ;
6564
6665 // Convert maxHeight to approximate maxRows (assuming ~24px line height)
6766 const maxRows = Math . floor ( maxHeight / 24 ) ;
6867
68+ React . useEffect ( ( ) => {
69+ const max = value . length ;
70+ const { start, end } = selectionRef . current ;
71+ if ( start > max || end > max ) {
72+ selectionRef . current = { start : max , end : max } ;
73+ }
74+ } , [ value ] ) ;
75+
76+ const emitSelectionChange = React . useCallback (
77+ ( selection : TextInputSelection ) => {
78+ if (
79+ selection . start === selectionRef . current . start &&
80+ selection . end === selectionRef . current . end
81+ ) {
82+ return ;
83+ }
84+ selectionRef . current = selection ;
85+ onSelectionChange ?.( selection ) ;
86+ } ,
87+ [ onSelectionChange ]
88+ ) ;
89+
6990 const handleKeyDown = React . useCallback (
7091 ( e : React . KeyboardEvent < HTMLTextAreaElement > ) => {
7192 if ( ! onKeyPress ) return ;
7293
7394 const isComposing =
74- e . nativeEvent . isComposing || ( e . nativeEvent as any ) . isComposing || e . keyCode === 229 ;
95+ isComposingRef . current ||
96+ e . nativeEvent . isComposing ||
97+ ( e . nativeEvent as any ) . isComposing ||
98+ e . keyCode === 229 ;
7599 if ( isComposing ) {
76100 return ;
77101 }
@@ -129,15 +153,9 @@ export const MultiTextInput = React.forwardRef<MultiTextInputHandle, MultiTextIn
129153 } ;
130154
131155 onChangeText ( text ) ;
132-
133- if ( onStateChange ) {
134- onStateChange ( { text, selection } ) ;
135- }
136- if ( onSelectionChange ) {
137- onSelectionChange ( selection ) ;
138- }
156+ emitSelectionChange ( selection ) ;
139157 } ,
140- [ onChangeText , onStateChange , onSelectionChange ]
158+ [ emitSelectionChange , onChangeText ]
141159 ) ;
142160
143161 const handleSelect = React . useCallback (
@@ -147,43 +165,35 @@ export const MultiTextInput = React.forwardRef<MultiTextInputHandle, MultiTextIn
147165 start : target . selectionStart ,
148166 end : target . selectionEnd ,
149167 } ;
150-
151- if ( onSelectionChange ) {
152- onSelectionChange ( selection ) ;
153- }
154- if ( onStateChange ) {
155- // Read text directly from the DOM element instead of the `value` prop.
156- // A `select` event can fire right after `change` in the same tick;
157- // the closure's `value` may still be the pre-change string, which
158- // would overwrite the correct state that handleChange just set.
159- onStateChange ( { text : target . value , selection } ) ;
160- }
168+ emitSelectionChange ( selection ) ;
161169 } ,
162- [ onSelectionChange , onStateChange ]
170+ [ emitSelectionChange ]
163171 ) ;
164172
173+ const handleCompositionStart = React . useCallback ( ( ) => {
174+ isComposingRef . current = true ;
175+ onCompositionStateChange ?.( true ) ;
176+ } , [ onCompositionStateChange ] ) ;
177+
178+ const handleCompositionEnd = React . useCallback ( ( ) => {
179+ isComposingRef . current = false ;
180+ onCompositionStateChange ?.( false ) ;
181+ } , [ onCompositionStateChange ] ) ;
182+
165183 // Imperative handle for direct control
166184 React . useImperativeHandle (
167185 ref ,
168186 ( ) => ( {
169- setTextAndSelection : ( text : string , selection : { start : number ; end : number } ) => {
170- if ( textareaRef . current ) {
187+ setTextAndSelection : ( text : string , selection : TextInputSelection ) => {
188+ if ( textareaRef . current && ! isComposingRef . current ) {
171189 // Directly set value and selection on DOM element
172190 textareaRef . current . value = text ;
173191 textareaRef . current . setSelectionRange ( selection . start , selection . end ) ;
174192
175193 // Trigger React's onChange by dispatching an input event
176194 const event = new Event ( 'input' , { bubbles : true } ) ;
177195 textareaRef . current . dispatchEvent ( event ) ;
178-
179- // Also call callbacks directly for immediate update
180- onChangeText ( text ) ;
181- if ( onStateChange ) {
182- onStateChange ( { text, selection } ) ;
183- }
184- if ( onSelectionChange ) {
185- onSelectionChange ( selection ) ;
186- }
196+ emitSelectionChange ( selection ) ;
187197 }
188198 } ,
189199 focus : ( ) => {
@@ -193,7 +203,7 @@ export const MultiTextInput = React.forwardRef<MultiTextInputHandle, MultiTextIn
193203 textareaRef . current ?. blur ( ) ;
194204 } ,
195205 } ) ,
196- [ onChangeText , onStateChange , onSelectionChange ]
206+ [ emitSelectionChange ]
197207 ) ;
198208
199209 return (
@@ -222,6 +232,8 @@ export const MultiTextInput = React.forwardRef<MultiTextInputHandle, MultiTextIn
222232 onChange = { handleChange }
223233 onSelect = { handleSelect }
224234 onKeyDown = { handleKeyDown }
235+ onCompositionStart = { handleCompositionStart }
236+ onCompositionEnd = { handleCompositionEnd }
225237 maxRows = { maxRows }
226238 autoCapitalize = "sentences"
227239 autoCorrect = "on"
0 commit comments