Fix IME input when composing in the middle of the textarea

Track the composition range using the textarea selection and avoid re-sending trailing text that existed before composition started (notably in screenReaderMode).

Fixes #5456
This commit is contained in:
Tomoya Nakanishi
2026-02-13 02:43:06 +09:00
parent fafb195f0f
commit c66b0ed168
2 changed files with 49 additions and 6 deletions
+23 -6
View File
@@ -30,6 +30,12 @@ export class CompositionHelper {
*/
private _compositionPosition: IPosition;
/**
* Text that existed after the composing range when composition started.
* This is used to avoid treating existing trailing text as new input.
*/
private _compositionSuffix: string;
/**
* Whether a composition is in the process of being sent, setting this to false will cancel any
* in-progress composition.
@@ -57,6 +63,7 @@ export class CompositionHelper {
this._isComposing = false;
this._isSendingComposition = false;
this._compositionPosition = { start: 0, end: 0 };
this._compositionSuffix = '';
this._dataAlreadySent = '';
}
@@ -65,7 +72,11 @@ export class CompositionHelper {
*/
public compositionstart(): void {
this._isComposing = true;
this._compositionPosition.start = this._textarea.value.length;
const start = this._textarea.selectionStart ?? this._textarea.value.length;
const end = this._textarea.selectionEnd ?? start;
this._compositionPosition.start = Math.min(start, end);
this._compositionPosition.end = Math.max(start, end);
this._compositionSuffix = this._textarea.value.substring(this._compositionPosition.end);
this._compositionView.textContent = '';
this._dataAlreadySent = '';
this._compositionView.classList.add('active');
@@ -79,7 +90,8 @@ export class CompositionHelper {
this._compositionView.textContent = ev.data;
this.updateCompositionElements();
setTimeout(() => {
this._compositionPosition.end = this._textarea.value.length;
const end = this._textarea.selectionEnd ?? this._textarea.value.length;
this._compositionPosition.end = Math.max( this._compositionPosition.start, end);
}, 0);
}
@@ -146,6 +158,7 @@ export class CompositionHelper {
start: this._compositionPosition.start,
end: this._compositionPosition.end
};
const currentCompositionSuffix = this._compositionSuffix;
// Since composition* events happen before the changes take place in the textarea on most
// browsers, use a setTimeout with 0ms time to allow the native compositionend event to
@@ -169,10 +182,14 @@ export class CompositionHelper {
// if a new composition has started.
input = this._textarea.value.substring(currentCompositionPosition.start, this._compositionPosition.start);
} else {
// Don't use the end position here in order to pick up any characters after the
// composition has finished, for example when typing a non-composition character
// (eg. 2) after a composition character.
input = this._textarea.value.substring(currentCompositionPosition.start);
// Keep support for non-composition characters typed immediately after composition end
// while avoiding re-sending the trailing text that was already present
// before composition started.
const value = this._textarea.value;
const valueEnd = currentCompositionSuffix.length > 0 && value.endsWith(currentCompositionSuffix)
? value.length - currentCompositionSuffix.length
: value.length;
input = value.substring(currentCompositionPosition.start, Math.max(currentCompositionPosition.start, valueEnd));
}
if (input.length > 0) {
this._coreService.triggerDataEvent(input, true);