From 71dfa104e02cef3b8877171cff985c0d164800f0 Mon Sep 17 00:00:00 2001 From: Hassan Toor <110993981+htoor3@users.noreply.github.com> Date: Thu, 14 Mar 2024 11:58:45 -0500 Subject: [PATCH] [web] - fix text editing IME composition interruption on geometry updates (#51323) This change fixes an issue where IME composition gets interrupted when the `setEditableSizeAndTransform` platform message is received mid-composition. This occurs when a multiline input expands and needs to inform the underlying `textarea` to update its size. Fixes https://github.com/flutter/flutter/issues/134797 Fixes https://github.com/flutter/flutter/issues/98817 --- .../src/engine/text_editing/text_editing.dart | 11 ++- lib/web_ui/test/engine/text_editing_test.dart | 70 +++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 813885329be1d..672a5ad3be61d 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -1338,7 +1338,16 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements void updateElementPlacement(EditableTextGeometry textGeometry) { geometry = textGeometry; if (isEnabled) { - placeElement(); + // On updates, we shouldn't go through the entire placeElement() flow if + // we are in the middle of IME composition, otherwise we risk interrupting it. + // Geometry updates occur when a multiline input expands or contracts. If + // we are in the middle of composition, we should just update the geometry. + // See: https://github.com/flutter/flutter/issues/98817 + if (composingText != null) { + geometry?.applyToDomElement(activeDomElement); + } else { + placeElement(); + } } } diff --git a/lib/web_ui/test/engine/text_editing_test.dart b/lib/web_ui/test/engine/text_editing_test.dart index 09a99a242d964..a9fa99443eb5b 100644 --- a/lib/web_ui/test/engine/text_editing_test.dart +++ b/lib/web_ui/test/engine/text_editing_test.dart @@ -513,6 +513,64 @@ Future testMain() async { expect(editingStrategy!.domElement!.style.width, '13px'); expect(editingStrategy!.domElement!.style.height, '12px'); }); + + test('updateElementPlacement() should not call placeElement() when in mid-composition', () { + final HybridTextEditing testTextEditing = HybridTextEditing(); + final GlobalTextEditingStrategySpy editingStrategy = GlobalTextEditingStrategySpy(testTextEditing); + testTextEditing.debugTextEditingStrategyOverride = editingStrategy; + testTextEditing.configuration = singlelineConfig; + + editingStrategy.enable( + singlelineConfig, + onChange: trackEditingState, + onAction: trackInputAction, + ); + expect(editingStrategy.isEnabled, isTrue); + + // placeElement() called from enable() + expect(editingStrategy.placeElementCount, 1); + + // No geometry should be set until setEditableSizeAndTransform is called. + expect(editingStrategy.domElement!.style.transform, ''); + expect(editingStrategy.domElement!.style.width, ''); + expect(editingStrategy.domElement!.style.height, ''); + + // set some composing text. + editingStrategy.composingText = '뮤'; + + testTextEditing.acceptCommand(TextInputSetEditableSizeAndTransform(geometry: EditableTextGeometry( + width: 13, + height: 12, + globalTransform: Matrix4.translationValues(14, 15, 0).storage, + )), () {}); + + // placeElement() should not be called again. + expect(editingStrategy.placeElementCount, 1); + + // geometry should be applied. + expect(editingStrategy.domElement!.style.transform, + 'matrix(1, 0, 0, 1, 14, 15)'); + expect(editingStrategy.domElement!.style.width, '13px'); + expect(editingStrategy.domElement!.style.height, '12px'); + + // set composing text to null. + editingStrategy.composingText = null; + + testTextEditing.acceptCommand(TextInputSetEditableSizeAndTransform(geometry: EditableTextGeometry( + width: 10, + height: 10, + globalTransform: Matrix4.translationValues(11, 12, 0).storage, + )), () {}); + + // placeElement() should be called again. + expect(editingStrategy.placeElementCount, 2); + + // geometry should be updated. + expect(editingStrategy.domElement!.style.transform, + 'matrix(1, 0, 0, 1, 11, 12)'); + expect(editingStrategy.domElement!.style.width, '10px'); + expect(editingStrategy.domElement!.style.height, '10px'); + }); }); group('$HybridTextEditing', () { @@ -3408,3 +3466,15 @@ Future waitForDesktopSafariFocus() async { await Future.delayed(Duration.zero); } } + +class GlobalTextEditingStrategySpy extends GloballyPositionedTextEditingStrategy { + GlobalTextEditingStrategySpy(super.owner); + + int placeElementCount = 0; + + @override + void placeElement() { + placeElementCount++; + super.placeElement(); + } +}