Skip to content

Commit a60f7bd

Browse files
committed
fix: add IME input support to the composition input
1 parent 94c9fef commit a60f7bd

File tree

1 file changed

+68
-1
lines changed

1 file changed

+68
-1
lines changed

ts/components/conversation/composition/CompositionInput.tsx

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,12 @@ export type ContentEditableProps = Omit<React.HTMLAttributes<HTMLDivElement>, 'o
273273
/** If true, disables editing (e.g., remove contentEditable or make read-only). */
274274
disabled?: boolean;
275275
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;
276282
};
277283

278284
const UnstyledCompositionInput = forwardRef<CompositionInputRef, ContentEditableProps>(
@@ -287,6 +293,9 @@ const UnstyledCompositionInput = forwardRef<CompositionInputRef, ContentEditable
287293
onKeyUp,
288294
onKeyDown,
289295
onClick,
296+
onCompositionStart,
297+
onCompositionUpdate,
298+
onCompositionEnd,
290299
children,
291300
...rest
292301
} = props;
@@ -297,6 +306,11 @@ const UnstyledCompositionInput = forwardRef<CompositionInputRef, ContentEditable
297306
const lastPosition = useRef<number | null>(null);
298307
const lastHtmlIndex = useRef<number>(0);
299308

309+
// IME composition state
310+
const isComposing = useRef(false);
311+
const compositionStartIndex = useRef<number>(0);
312+
const compositionData = useRef<string>('');
313+
300314
useDebouncedSpellcheck({
301315
elementRef: elRef,
302316
});
@@ -447,6 +461,11 @@ const UnstyledCompositionInput = forwardRef<CompositionInputRef, ContentEditable
447461
return;
448462
}
449463

464+
// Don't update selection during IME composition
465+
if (isComposing.current) {
466+
return;
467+
}
468+
450469
lastPosition.current = getHtmlIndexFromSelection(el);
451470
lastHtmlIndex.current = lastPosition.current;
452471
};
@@ -652,6 +671,11 @@ const UnstyledCompositionInput = forwardRef<CompositionInputRef, ContentEditable
652671

653672
const onInput = useCallback(
654673
(e: Omit<ContentEditableEvent, 'target'>) => {
674+
// Skip input handling during IME composition
675+
if (isComposing.current) {
676+
return;
677+
}
678+
655679
const hasChanged = handleChange();
656680
if (hasChanged) {
657681
emitChangeEvent(e);
@@ -769,6 +793,45 @@ const UnstyledCompositionInput = forwardRef<CompositionInputRef, ContentEditable
769793
[createSyntheticEvent, handleHistory]
770794
);
771795

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+
772835
useEffect(() => {
773836
const el = elRef.current;
774837
if (!el) {
@@ -802,7 +865,8 @@ const UnstyledCompositionInput = forwardRef<CompositionInputRef, ContentEditable
802865
el.innerHTML = normalizedHtml;
803866
lastHtml.current = normalizedHtml;
804867
isMount.current = false;
805-
} else if (normalizedHtml !== normalizedCurrentHtml) {
868+
} else if (normalizedHtml !== normalizedCurrentHtml && !isComposing.current) {
869+
// Don't update DOM during IME composition
806870
el.innerHTML = normalizedHtml;
807871
lastHtml.current = normalizedHtml;
808872
handleChange();
@@ -828,6 +892,9 @@ const UnstyledCompositionInput = forwardRef<CompositionInputRef, ContentEditable
828892
onKeyDown={_onKeyDown}
829893
onCopy={onCopy}
830894
onClick={_onClick}
895+
onCompositionStart={handleCompositionStart}
896+
onCompositionUpdate={handleCompositionUpdate}
897+
onCompositionEnd={handleCompositionEnd}
831898
>
832899
{children}
833900
</div>

0 commit comments

Comments
 (0)