@@ -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