@@ -273,6 +273,12 @@ export type ContentEditableProps = Omit<React.HTMLAttributes<HTMLDivElement>, 'o
273
273
/** If true, disables editing (e.g., remove contentEditable or make read-only). */
274
274
disabled ?: boolean ;
275
275
innerRef ?: React . Ref < HTMLDivElement > ;
276
+ /** Called when IME composition starts */
277
+ onCompositionStart ?: ( e : React . CompositionEvent < HTMLDivElement > ) => void ;
278
+ /** Called when IME composition updates */
279
+ onCompositionUpdate ?: ( e : React . CompositionEvent < HTMLDivElement > ) => void ;
280
+ /** Called when IME composition ends */
281
+ onCompositionEnd ?: ( e : React . CompositionEvent < HTMLDivElement > ) => void ;
276
282
} ;
277
283
278
284
const UnstyledCompositionInput = forwardRef < CompositionInputRef , ContentEditableProps > (
@@ -287,6 +293,9 @@ const UnstyledCompositionInput = forwardRef<CompositionInputRef, ContentEditable
287
293
onKeyUp,
288
294
onKeyDown,
289
295
onClick,
296
+ onCompositionStart,
297
+ onCompositionUpdate,
298
+ onCompositionEnd,
290
299
children,
291
300
...rest
292
301
} = props ;
@@ -297,6 +306,11 @@ const UnstyledCompositionInput = forwardRef<CompositionInputRef, ContentEditable
297
306
const lastPosition = useRef < number | null > ( null ) ;
298
307
const lastHtmlIndex = useRef < number > ( 0 ) ;
299
308
309
+ // IME composition state
310
+ const isComposing = useRef ( false ) ;
311
+ const compositionStartIndex = useRef < number > ( 0 ) ;
312
+ const compositionData = useRef < string > ( '' ) ;
313
+
300
314
useDebouncedSpellcheck ( {
301
315
elementRef : elRef ,
302
316
} ) ;
@@ -447,6 +461,11 @@ const UnstyledCompositionInput = forwardRef<CompositionInputRef, ContentEditable
447
461
return ;
448
462
}
449
463
464
+ // Don't update selection during IME composition
465
+ if ( isComposing . current ) {
466
+ return ;
467
+ }
468
+
450
469
lastPosition . current = getHtmlIndexFromSelection ( el ) ;
451
470
lastHtmlIndex . current = lastPosition . current ;
452
471
} ;
@@ -652,6 +671,11 @@ const UnstyledCompositionInput = forwardRef<CompositionInputRef, ContentEditable
652
671
653
672
const onInput = useCallback (
654
673
( e : Omit < ContentEditableEvent , 'target' > ) => {
674
+ // Skip input handling during IME composition
675
+ if ( isComposing . current ) {
676
+ return ;
677
+ }
678
+
655
679
const hasChanged = handleChange ( ) ;
656
680
if ( hasChanged ) {
657
681
emitChangeEvent ( e ) ;
@@ -769,6 +793,45 @@ const UnstyledCompositionInput = forwardRef<CompositionInputRef, ContentEditable
769
793
[ createSyntheticEvent , handleHistory ]
770
794
) ;
771
795
796
+ // IME Composition Event Handlers
797
+ const handleCompositionStart = useCallback (
798
+ ( e : React . CompositionEvent < HTMLDivElement > ) => {
799
+ isComposing . current = true ;
800
+ compositionStartIndex . current = lastHtmlIndex . current ;
801
+ compositionData . current = '' ;
802
+
803
+ onCompositionStart ?.( e ) ;
804
+ } ,
805
+ [ onCompositionStart ]
806
+ ) ;
807
+
808
+ const handleCompositionUpdate = useCallback (
809
+ ( e : React . CompositionEvent < HTMLDivElement > ) => {
810
+ if ( isComposing . current ) {
811
+ compositionData . current = e . data ;
812
+ }
813
+
814
+ onCompositionUpdate ?.( e ) ;
815
+ } ,
816
+ [ onCompositionUpdate ]
817
+ ) ;
818
+
819
+ const handleCompositionEnd = useCallback (
820
+ ( e : React . CompositionEvent < HTMLDivElement > ) => {
821
+ isComposing . current = false ;
822
+
823
+ // Process the final composition result
824
+ const hasChanged = handleChange ( ) ;
825
+ if ( hasChanged ) {
826
+ emitChangeEvent ( e as any ) ;
827
+ }
828
+
829
+ compositionData . current = '' ;
830
+ onCompositionEnd ?.( e ) ;
831
+ } ,
832
+ [ handleChange , emitChangeEvent , onCompositionEnd ]
833
+ ) ;
834
+
772
835
useEffect ( ( ) => {
773
836
const el = elRef . current ;
774
837
if ( ! el ) {
@@ -802,7 +865,8 @@ const UnstyledCompositionInput = forwardRef<CompositionInputRef, ContentEditable
802
865
el . innerHTML = normalizedHtml ;
803
866
lastHtml . current = normalizedHtml ;
804
867
isMount . current = false ;
805
- } else if ( normalizedHtml !== normalizedCurrentHtml ) {
868
+ } else if ( normalizedHtml !== normalizedCurrentHtml && ! isComposing . current ) {
869
+ // Don't update DOM during IME composition
806
870
el . innerHTML = normalizedHtml ;
807
871
lastHtml . current = normalizedHtml ;
808
872
handleChange ( ) ;
@@ -828,6 +892,9 @@ const UnstyledCompositionInput = forwardRef<CompositionInputRef, ContentEditable
828
892
onKeyDown = { _onKeyDown }
829
893
onCopy = { onCopy }
830
894
onClick = { _onClick }
895
+ onCompositionStart = { handleCompositionStart }
896
+ onCompositionUpdate = { handleCompositionUpdate }
897
+ onCompositionEnd = { handleCompositionEnd }
831
898
>
832
899
{ children }
833
900
</ div >
0 commit comments