diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index e5640764ffa..c46e15c22d3 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -614,10 +614,15 @@ class GloballyPositionedTextEditingStrategy extends DefaultTextEditingStrategy { @override void placeElement() { - super.placeElement(); if (hasAutofillGroup) { _geometry?.applyToDomElement(focusedFormElement!); placeForm(); + // Set the last editing state if it exists, this is critical for a + // users ongoing work to continue uninterrupted when there is an update to + // the transform. + if (_lastEditingState != null) { + _lastEditingState!.applyToDomElement(domElement); + } // On Chrome, when a form is focused, it opens an autofill menu // immediately. // Flutter framework sends `setEditableSizeAndTransform` for informing @@ -627,7 +632,9 @@ class GloballyPositionedTextEditingStrategy extends DefaultTextEditingStrategy { // `setEditableSizeAndTransform` method is called and focus on the form // only after placing it to the correct position. Hence autofill menu // does not appear on top-left of the page. + // Refocus on the elements after applying the geometry. focusedFormElement!.focus(); + domElement.focus(); } else { _geometry?.applyToDomElement(domElement); } @@ -663,6 +670,12 @@ class SafariDesktopTextEditingStrategy extends DefaultTextEditingStrategy { _geometry?.applyToDomElement(domElement); if (hasAutofillGroup) { placeForm(); + // Set the last editing state if it exists, this is critical for a + // users ongoing work to continue uninterrupted when there is an update to + // the transform. + if (_lastEditingState != null) { + _lastEditingState!.applyToDomElement(domElement); + } // On Safari Desktop, when a form is focused, it opens an autofill menu // immediately. // Flutter framework sends `setEditableSizeAndTransform` for informing @@ -1236,6 +1249,12 @@ class FirefoxTextEditingStrategy extends GloballyPositionedTextEditingStrategy { void placeElement() { domElement.focus(); _geometry?.applyToDomElement(domElement); + // Set the last editing state if it exists, this is critical for a + // users ongoing work to continue uninterrupted when there is an update to + // the transform. + if (_lastEditingState != null) { + _lastEditingState!.applyToDomElement(domElement); + } } } diff --git a/engine/src/flutter/lib/web_ui/test/text_editing_test.dart b/engine/src/flutter/lib/web_ui/test/text_editing_test.dart index 9b2137d386f..ec511cf4f98 100644 --- a/engine/src/flutter/lib/web_ui/test/text_editing_test.dart +++ b/engine/src/flutter/lib/web_ui/test/text_editing_test.dart @@ -1013,6 +1013,55 @@ void testMain() { expect(formsOnTheDom, hasLength(1)); }); + test( + 'singleTextField Autofill setEditableSizeAndTransform preserves' + 'editing state', () { + // Create a configuration with focused element has autofil hint. + final Map flutterSingleAutofillElementConfig = + createFlutterConfig('text', autofillHint: 'username'); + final MethodCall setClient = MethodCall('TextInput.setClient', + [123, flutterSingleAutofillElementConfig]); + sendFrameworkMessage(codec.encodeMethodCall(setClient)); + + const MethodCall setEditingState1 = + MethodCall('TextInput.setEditingState', { + 'text': 'abcd', + 'selectionBase': 2, + 'selectionExtent': 3, + }); + sendFrameworkMessage(codec.encodeMethodCall(setEditingState1)); + + const MethodCall show = MethodCall('TextInput.show'); + sendFrameworkMessage(codec.encodeMethodCall(show)); + + // The second [setEditingState] should override the first one. + checkInputEditingState( + textEditing.editingElement.domElement, 'abcd', 2, 3); + + // The transform is changed. For example after a validation error, red + // line appeared under the input field. + final MethodCall setSizeAndTransform = + configureSetSizeAndTransformMethodCall(150, 50, + Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); + sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); + + // Check the element still has focus. User can keep editing. + expect(document.activeElement, textEditing.editingElement.domElement); + + // Check the cursor location is the same. + checkInputEditingState( + textEditing.editingElement.domElement, 'abcd', 2, 3); + + const MethodCall clearClient = MethodCall('TextInput.clearClient'); + sendFrameworkMessage(codec.encodeMethodCall(clearClient)); + + // Confirm that [HybridTextEditing] didn't send any messages. + expect(spy.messages, isEmpty); + // Form stays on the DOM until autofill context is finalized. + expect(document.getElementsByTagName('form'), isNotEmpty); + expect(formsOnTheDom, hasLength(1)); + }); + test( 'multiTextField Autofill: setClient, setEditingState, show, ' 'setEditingState, clearClient', () {