Skip to content

Commit 26ce9bd

Browse files
authored
Merge pull request #5698 from ntomoya/fix/ime-middle-composition-suffix
Fix IME input when composing in the middle of the textarea
2 parents fd1e53c + 1c1aac0 commit 26ce9bd

File tree

2 files changed

+51
-6
lines changed

2 files changed

+51
-6
lines changed

src/browser/input/CompositionHelper.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,5 +233,31 @@ describe('CompositionHelper', () => {
233233
}, 0);
234234
}, 0);
235235
});
236+
237+
it('Should insert middle composition and subsequent input without appending existing trailing text', (done) => {
238+
textarea.value = '一二';
239+
// screenReaderMode keeps textarea content/selection for assistive technologies (eg. screen
240+
// readers), so the caret can be moved within the textarea (eg. via arrow keys) before
241+
// starting composition.
242+
textarea.selectionStart = 1;
243+
textarea.selectionEnd = 1;
244+
245+
compositionHelper.compositionstart();
246+
compositionHelper.compositionupdate({ data: '一' });
247+
textarea.value = '一一二';
248+
// After the composed text is inserted, the caret typically moves to after it.
249+
textarea.selectionStart = 2;
250+
textarea.selectionEnd = 2;
251+
252+
setTimeout(() => { // wait for any textarea updates
253+
compositionHelper.compositionend();
254+
// Second character '1' (a non-composition character)
255+
textarea.value = '一一1二';
256+
setTimeout(() => { // wait for any textarea updates
257+
assert.equal(handledText, '一1');
258+
done();
259+
}, 0);
260+
}, 0);
261+
});
236262
});
237263
});

src/browser/input/CompositionHelper.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ export class CompositionHelper {
3030
*/
3131
private _compositionPosition: IPosition;
3232

33+
/**
34+
* Text that existed after the composing range when composition started.
35+
* This is used to avoid treating existing trailing text as new input.
36+
*/
37+
private _compositionSuffix: string;
38+
3339
/**
3440
* Whether a composition is in the process of being sent, setting this to false will cancel any
3541
* in-progress composition.
@@ -57,6 +63,7 @@ export class CompositionHelper {
5763
this._isComposing = false;
5864
this._isSendingComposition = false;
5965
this._compositionPosition = { start: 0, end: 0 };
66+
this._compositionSuffix = '';
6067
this._dataAlreadySent = '';
6168
}
6269

@@ -65,7 +72,13 @@ export class CompositionHelper {
6572
*/
6673
public compositionstart(): void {
6774
this._isComposing = true;
68-
this._compositionPosition.start = this._textarea.value.length;
75+
// It's important to use the selection here instead of textarea length to avoid conflicts with
76+
// screen reader mode
77+
const start = this._textarea.selectionStart ?? this._textarea.value.length;
78+
const end = this._textarea.selectionEnd ?? start;
79+
this._compositionPosition.start = Math.min(start, end);
80+
this._compositionPosition.end = Math.max(start, end);
81+
this._compositionSuffix = this._textarea.value.substring(this._compositionPosition.end);
6982
this._compositionView.textContent = '';
7083
this._dataAlreadySent = '';
7184
this._compositionView.classList.add('active');
@@ -81,7 +94,8 @@ export class CompositionHelper {
8194
this._compositionView.textContent = `\u200E${ev.data}\u200E`;
8295
this.updateCompositionElements();
8396
setTimeout(() => {
84-
this._compositionPosition.end = this._textarea.value.length;
97+
const end = this._textarea.selectionEnd ?? this._textarea.value.length;
98+
this._compositionPosition.end = Math.max( this._compositionPosition.start, end);
8599
}, 0);
86100
}
87101

@@ -148,6 +162,7 @@ export class CompositionHelper {
148162
start: this._compositionPosition.start,
149163
end: this._compositionPosition.end
150164
};
165+
const currentCompositionSuffix = this._compositionSuffix;
151166

152167
// Since composition* events happen before the changes take place in the textarea on most
153168
// browsers, use a setTimeout with 0ms time to allow the native compositionend event to
@@ -171,10 +186,14 @@ export class CompositionHelper {
171186
// if a new composition has started.
172187
input = this._textarea.value.substring(currentCompositionPosition.start, this._compositionPosition.start);
173188
} else {
174-
// Don't use the end position here in order to pick up any characters after the
175-
// composition has finished, for example when typing a non-composition character
176-
// (eg. 2) after a composition character.
177-
input = this._textarea.value.substring(currentCompositionPosition.start);
189+
// Keep support for non-composition characters typed immediately after composition end
190+
// while avoiding re-sending the trailing text that was already present
191+
// before composition started.
192+
const value = this._textarea.value;
193+
const valueEnd = currentCompositionSuffix.length > 0 && value.endsWith(currentCompositionSuffix)
194+
? value.length - currentCompositionSuffix.length
195+
: value.length;
196+
input = value.substring(currentCompositionPosition.start, Math.max(currentCompositionPosition.start, valueEnd));
178197
}
179198
if (input.length > 0) {
180199
this._coreService.triggerDataEvent(input, true);

0 commit comments

Comments
 (0)