From d4836707e01b6df7be70c00ed6d9cf89b6a2ed70 Mon Sep 17 00:00:00 2001 From: Hassan Toor Date: Tue, 13 Jun 2023 19:19:24 -0500 Subject: [PATCH 1/5] Wrap autofill placement + focus logic within zero-duration Timer --- .../src/engine/text_editing/text_editing.dart | 52 ++++++++++++------- 1 file changed, 33 insertions(+), 19 deletions(-) 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 49746c9245e90..ffd3ff20ff67c 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 @@ -1077,24 +1077,35 @@ class SafariDesktopTextEditingStrategy extends DefaultTextEditingStrategy { void placeElement() { geometry?.applyToDomElement(activeDomElement); if (hasAutofillGroup) { - placeForm(); - // On Safari Desktop, when a form is focused, it opens an autofill menu - // immediately. - // Flutter framework sends `setEditableSizeAndTransform` for informing - // the engine about the location of the text field. This call may arrive - // after the first `show` call, depending on the text input widget's - // implementation. Therefore form is placed, when - // `setEditableSizeAndTransform` method is called and focus called on the - // form only after placing it to the correct position and only once after - // that. Calling focus multiple times causes flickering. - focusedFormElement!.focus(); - - // 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 domElement is not focused cursor location will not be correct. - activeDomElement.focus(); - lastEditingState?.applyToDomElement(activeDomElement); + // We listen to pointerdown events on the Flutter View element and programatically + // focus our inputs. However, these inputs are focused before the pointerdown + // events conclude. Thus, the browser triggers a blur event immediately after + // focusing these inputs. This causes issues with Safari Desktop's autofill + // dialog (ref: https://github.com/flutter/flutter/issues/127960). + // In order to guarantee that we only focus after the pointerdown event concludes, + // we wrap the form autofill placement and focus logic in a zero-duration Timer. + // This ensures that our input doesn't have instantaneous focus/blur events + // occur on it and fixes the autofill dialog bug as a result. + Timer(Duration.zero, () { + placeForm(); + // On Safari Desktop, when a form is focused, it opens an autofill menu + // immediately. + // Flutter framework sends `setEditableSizeAndTransform` for informing + // the engine about the location of the text field. This call may arrive + // after the first `show` call, depending on the text input widget's + // implementation. Therefore form is placed, when + // `setEditableSizeAndTransform` method is called and focus called on the + // form only after placing it to the correct position and only once after + // that. Calling focus multiple times causes flickering. + focusedFormElement!.focus(); + + // 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 domElement is not focused cursor location will not be correct. + activeDomElement.focus(); + lastEditingState?.applyToDomElement(activeDomElement); + }); } } @@ -1270,7 +1281,10 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements // Refocus on the activeDomElement after blur, so that user can keep editing the // text field. subscriptions.add(DomSubscription(activeDomElement, 'blur', - (_) { activeDomElement.focus(); })); + (_) { + print('blurred'); + activeDomElement.focus(); + })); preventDefaultForMouseEvents(); } From 1a4a429bdbcd3197ce7d5cf9fa6490bd8bed269f Mon Sep 17 00:00:00 2001 From: Hassan Toor Date: Tue, 13 Jun 2023 19:21:54 -0500 Subject: [PATCH 2/5] Remove print statement --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 ffd3ff20ff67c..f599c56b88b0a 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 @@ -1281,10 +1281,7 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements // Refocus on the activeDomElement after blur, so that user can keep editing the // text field. subscriptions.add(DomSubscription(activeDomElement, 'blur', - (_) { - print('blurred'); - activeDomElement.focus(); - })); + (_) { activeDomElement.focus(); })); preventDefaultForMouseEvents(); } From 4857ca35945997fa687093c0cb39a89b104f54fb Mon Sep 17 00:00:00 2001 From: Hassan Toor Date: Thu, 15 Jun 2023 19:31:23 -0500 Subject: [PATCH 3/5] Fix tests --- lib/web_ui/test/engine/text_editing_test.dart | 132 +++++++++++++++--- 1 file changed, 111 insertions(+), 21 deletions(-) diff --git a/lib/web_ui/test/engine/text_editing_test.dart b/lib/web_ui/test/engine/text_editing_test.dart index 0b651e6c677a9..c8fe781968798 100644 --- a/lib/web_ui/test/engine/text_editing_test.dart +++ b/lib/web_ui/test/engine/text_editing_test.dart @@ -512,7 +512,7 @@ Future testMain() async { //No-op and without crashing. }); - test('setClient, show, setEditingState, hide', () { + test('setClient, show, setEditingState, hide', () async { final MethodCall setClient = MethodCall( 'TextInput.setClient', [123, flutterSinglelineConfig]); sendFrameworkMessage(codec.encodeMethodCall(setClient)); @@ -532,6 +532,12 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); + // This timer exists because Desktop Safari element is focused after + // zero-duration timer to prevent autofill dialog + if(isSafari){ + await Future.delayed(Duration.zero); + } + checkInputEditingState(textEditing!.strategy.domElement, '', 0, 0); const MethodCall setEditingState = @@ -555,7 +561,7 @@ Future testMain() async { expect(spy.messages, isEmpty); }); - test('setClient, setEditingState, show, clearClient', () { + test('setClient, setEditingState, show, clearClient', () async { final MethodCall setClient = MethodCall( 'TextInput.setClient', [123, flutterSinglelineConfig]); sendFrameworkMessage(codec.encodeMethodCall(setClient)); @@ -583,6 +589,12 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); + // This timer exists because Desktop Safari element is focused after + // zero-duration timer to prevent autofill dialog + if(isSafari){ + await Future.delayed(Duration.zero); + } + checkInputEditingState( textEditing!.strategy.domElement, 'abcd', 2, 3); @@ -595,20 +607,15 @@ Future testMain() async { expect(spy.messages, isEmpty); }); - test('setClient, setEditingState, setSizeAndTransform, show - input element is put into the DOM', () { + test('setClient, setEditingState, setSizeAndTransform, show - input element is put into the DOM Safari Desktop', () async { editingStrategy = SafariDesktopTextEditingStrategy(textEditing!); textEditing!.debugTextEditingStrategyOverride = editingStrategy; final MethodCall setClient = MethodCall( 'TextInput.setClient', [123, flutterSinglelineConfig]); sendFrameworkMessage(codec.encodeMethodCall(setClient)); - const MethodCall setEditingState = - MethodCall('TextInput.setEditingState', { - 'text': 'abcd', - 'selectionBase': 2, - 'selectionExtent': 3, - }); - sendFrameworkMessage(codec.encodeMethodCall(setEditingState)); + const MethodCall show = MethodCall('TextInput.show'); + sendFrameworkMessage(codec.encodeMethodCall(show)); // Editing shouldn't have started yet. expect(domDocument.activeElement, domDocument.body); @@ -622,12 +629,21 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); - const MethodCall show = MethodCall('TextInput.show'); - sendFrameworkMessage(codec.encodeMethodCall(show)); + // This timer exists because Desktop Safari element is focused after + // zero-duration timer to prevent autofill dialog + await Future.delayed(Duration.zero); + + const MethodCall setEditingState = + MethodCall('TextInput.setEditingState', { + 'text': 'abcd', + 'selectionBase': 2, + 'selectionExtent': 3, + }); + sendFrameworkMessage(codec.encodeMethodCall(setEditingState)); expect(defaultTextEditingRoot.ownerDocument?.activeElement, textEditing!.strategy.domElement); - }); + }, skip: !isSafari); test('setClient, setEditingState, show, updateConfig, clearClient', () { final MethodCall setClient = MethodCall('TextInput.setClient', [ @@ -700,6 +716,12 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); + // This timer exists because Desktop Safari element is focused after + // zero-duration timer to prevent autofill dialog + if(isSafari){ + await Future.delayed(Duration.zero); + } + checkInputEditingState( textEditing!.strategy.domElement, 'abcd', 2, 3); expect(textEditing!.isEditing, isTrue); @@ -796,6 +818,12 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); + // This timer exists because Desktop Safari element is focused after + // zero-duration timer to prevent autofill dialog + if(isSafari){ + await Future.delayed(Duration.zero); + } + checkInputEditingState( textEditing!.strategy.domElement, 'abcd', 2, 3); @@ -852,6 +880,12 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); + // This timer exists because Desktop Safari element is focused after + // zero-duration timer to prevent autofill dialog + if(isSafari){ + await Future.delayed(Duration.zero); + } + // Form is added to DOM. expect(defaultTextEditingRoot.querySelectorAll('form'), isNotEmpty); @@ -906,6 +940,12 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); + // This timer exists because Desktop Safari element is focused after + // zero-duration timer to prevent autofill dialog + if(isSafari){ + await Future.delayed(Duration.zero); + } + // Form is added to DOM. expect(defaultTextEditingRoot.querySelectorAll('form'), isNotEmpty); final DomHTMLFormElement formElement = @@ -959,6 +999,12 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); + // This timer exists because Desktop Safari element is focused after + // zero-duration timer to prevent autofill dialog + if(isSafari){ + await Future.delayed(Duration.zero); + } + // Form is added to DOM. expect(defaultTextEditingRoot.querySelectorAll('form'), isNotEmpty); final DomHTMLFormElement formElement = @@ -985,7 +1031,7 @@ Future testMain() async { expect(formsOnTheDom, hasLength(0)); }); - test('setClient, setEditingState, show, setClient', () { + test('setClient, setEditingState, show, setClient', () async { final MethodCall setClient = MethodCall( 'TextInput.setClient', [123, flutterSinglelineConfig]); sendFrameworkMessage(codec.encodeMethodCall(setClient)); @@ -1013,6 +1059,12 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); + // This timer exists because Desktop Safari element is focused after + // zero-duration timer to prevent autofill dialog + if(isSafari){ + await Future.delayed(Duration.zero); + } + checkInputEditingState( textEditing!.strategy.domElement, 'abcd', 2, 3); @@ -1030,7 +1082,7 @@ Future testMain() async { hideKeyboard(); }); - test('setClient, setEditingState, show, setEditingState, clearClient', () { + test('setClient, setEditingState, show, setEditingState, clearClient', () async { final MethodCall setClient = MethodCall( 'TextInput.setClient', [123, flutterSinglelineConfig]); sendFrameworkMessage(codec.encodeMethodCall(setClient)); @@ -1063,6 +1115,11 @@ Future testMain() async { }); sendFrameworkMessage(codec.encodeMethodCall(setEditingState2)); + // This timer exists because Desktop Safari element is focused after + // zero-duration timer to prevent autofill dialog + if(isSafari){ + await Future.delayed(Duration.zero); + } // The second [setEditingState] should override the first one. checkInputEditingState( textEditing!.strategy.domElement, 'xyz', 0, 2); @@ -1076,7 +1133,7 @@ Future testMain() async { test( 'singleTextField Autofill: setClient, setEditingState, show, ' - 'setSizeAndTransform, setEditingState, clearClient', () { + 'setSizeAndTransform, setEditingState, clearClient', () async { // Create a configuration with focused element has autofil hint. final Map flutterSingleAutofillElementConfig = createFlutterConfig('text', autofillHint: 'username'); @@ -1104,6 +1161,11 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); + // This timer exists because Desktop Safari element is focused after + // zero-duration timer to prevent autofill dialog + if(isSafari){ + await Future.delayed(Duration.zero); + } // The second [setEditingState] should override the first one. checkInputEditingState( textEditing!.strategy.domElement, 'abcd', 2, 3); @@ -1125,7 +1187,7 @@ Future testMain() async { test( 'singleTextField Autofill setEditableSizeAndTransform preserves' - 'editing state', () { + 'editing state', () async { // Create a configuration with focused element has autofil hint. final Map flutterSingleAutofillElementConfig = createFlutterConfig('text', autofillHint: 'username'); @@ -1174,6 +1236,11 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(updateSizeAndTransform)); + // This timer exists because Desktop Safari element is focused after + // zero-duration timer to prevent autofill dialog + if(isSafari){ + await Future.delayed(Duration.zero); + } // Check the element still has focus. User can keep editing. expect(defaultTextEditingRoot.ownerDocument?.activeElement, textEditing!.strategy.domElement); @@ -1194,7 +1261,7 @@ Future testMain() async { test( 'multiTextField Autofill: setClient, setEditingState, show, ' - 'setSizeAndTransform setEditingState, clearClient', () { + 'setSizeAndTransform setEditingState, clearClient', () async { // Create a configuration with an AutofillGroup of four text fields. final Map flutterMultiAutofillElementConfig = createFlutterConfig('text', @@ -1229,6 +1296,12 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); + // This timer exists because Desktop Safari element is focused after + // zero-duration timer to prevent autofill dialog + if(isSafari){ + await Future.delayed(Duration.zero); + } + // The second [setEditingState] should override the first one. checkInputEditingState( textEditing!.strategy.domElement, 'abcd', 2, 3); @@ -1514,7 +1587,7 @@ Future testMain() async { test( 'negative base offset and selection extent values in editing state is handled', - () { + () async { final MethodCall setClient = MethodCall( 'TextInput.setClient', [123, flutterSinglelineConfig]); sendFrameworkMessage(codec.encodeMethodCall(setClient)); @@ -1539,6 +1612,12 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); + // This timer exists because Desktop Safari element is focused after + // zero-duration timer to prevent autofill dialog + if(isSafari){ + await Future.delayed(Duration.zero); + } + // Check if the selection range is correct. checkInputEditingState( textEditing!.strategy.domElement, 'xyz', 1, 2); @@ -1687,7 +1766,7 @@ Future testMain() async { hideKeyboard(); }); - test('multiTextField Autofill sync updates back to Flutter', () { + test('multiTextField Autofill sync updates back to Flutter', () async { // Create a configuration with an AutofillGroup of four text fields. const String hintForFirstElement = 'familyName'; final Map flutterMultiAutofillElementConfig = @@ -1723,6 +1802,11 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); + // This timer exists because Desktop Safari element is focused after + // zero-duration timer to prevent autofill dialog + if(isSafari){ + await Future.delayed(Duration.zero); + } // The second [setEditingState] should override the first one. checkInputEditingState( textEditing!.strategy.domElement, 'abcd', 2, 3); @@ -1769,7 +1853,7 @@ Future testMain() async { hideKeyboard(); }); - test('Multi-line mode also works', () { + test('Multi-line mode also works', () async { final MethodCall setClient = MethodCall( 'TextInput.setClient', [123, flutterMultilineConfig]); sendFrameworkMessage(codec.encodeMethodCall(setClient)); @@ -1790,6 +1874,12 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); + // This timer exists because Desktop Safari element is focused after + // zero-duration timer to prevent autofill dialog + if(isSafari){ + await Future.delayed(Duration.zero); + } + final DomHTMLTextAreaElement textarea = textEditing!.strategy.domElement! as DomHTMLTextAreaElement; checkTextAreaEditingState(textarea, '', 0, 0); From db91a0d16cb40d7a2de27dafb85a67ad8ae4ceb3 Mon Sep 17 00:00:00 2001 From: Hassan Toor Date: Fri, 16 Jun 2023 11:25:06 -0500 Subject: [PATCH 4/5] Add tests --- lib/web_ui/test/engine/text_editing_test.dart | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/lib/web_ui/test/engine/text_editing_test.dart b/lib/web_ui/test/engine/text_editing_test.dart index c8fe781968798..17200940a32b3 100644 --- a/lib/web_ui/test/engine/text_editing_test.dart +++ b/lib/web_ui/test/engine/text_editing_test.dart @@ -1031,6 +1031,53 @@ Future testMain() async { expect(formsOnTheDom, hasLength(0)); }); + test('form is not placed and input is not focused until after tick on Desktop Safari', () async { + // Create a configuration with an AutofillGroup of four text fields. + final Map flutterMultiAutofillElementConfig = + createFlutterConfig('text', + autofillHint: 'username', + autofillHintsForFields: [ + 'username', + 'email', + 'name', + 'telephoneNumber' + ]); + final MethodCall setClient = MethodCall('TextInput.setClient', + [123, flutterMultiAutofillElementConfig]); + 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)); + + final MethodCall setSizeAndTransform = + configureSetSizeAndTransformMethodCall(150, 50, + Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); + sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); + + // Prior to tick, form should not exist and no elements should be focused. + expect(defaultTextEditingRoot.querySelectorAll('form'), isEmpty); + expect(domDocument.activeElement, domDocument.body); + + // This timer exists because Desktop Safari element is focused after + // zero-duration timer to prevent autofill dialog + await Future.delayed(Duration.zero); + + // Form is added to DOM. + expect(defaultTextEditingRoot.querySelectorAll('form'), isNotEmpty); + + final DomHTMLInputElement inputElement = + textEditing!.strategy.domElement! as DomHTMLInputElement; + expect(domDocument.activeElement, inputElement); + }, skip: !isSafari); + test('setClient, setEditingState, show, setClient', () async { final MethodCall setClient = MethodCall( 'TextInput.setClient', [123, flutterSinglelineConfig]); From 7171cfb30731e615d8ac6383fbf3b76ed3b2f0a7 Mon Sep 17 00:00:00 2001 From: Hassan Toor Date: Fri, 16 Jun 2023 16:06:02 -0500 Subject: [PATCH 5/5] Refactor --- lib/web_ui/test/engine/text_editing_test.dart | 107 +++++------------- 1 file changed, 26 insertions(+), 81 deletions(-) diff --git a/lib/web_ui/test/engine/text_editing_test.dart b/lib/web_ui/test/engine/text_editing_test.dart index 17200940a32b3..07e68c3e746ee 100644 --- a/lib/web_ui/test/engine/text_editing_test.dart +++ b/lib/web_ui/test/engine/text_editing_test.dart @@ -532,11 +532,7 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); - // This timer exists because Desktop Safari element is focused after - // zero-duration timer to prevent autofill dialog - if(isSafari){ - await Future.delayed(Duration.zero); - } + await waitForDesktopSafariFocus(); checkInputEditingState(textEditing!.strategy.domElement, '', 0, 0); @@ -589,11 +585,7 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); - // This timer exists because Desktop Safari element is focused after - // zero-duration timer to prevent autofill dialog - if(isSafari){ - await Future.delayed(Duration.zero); - } + await waitForDesktopSafariFocus(); checkInputEditingState( textEditing!.strategy.domElement, 'abcd', 2, 3); @@ -629,9 +621,7 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); - // This timer exists because Desktop Safari element is focused after - // zero-duration timer to prevent autofill dialog - await Future.delayed(Duration.zero); + await waitForDesktopSafariFocus(); const MethodCall setEditingState = MethodCall('TextInput.setEditingState', { @@ -716,11 +706,7 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); - // This timer exists because Desktop Safari element is focused after - // zero-duration timer to prevent autofill dialog - if(isSafari){ - await Future.delayed(Duration.zero); - } + await waitForDesktopSafariFocus(); checkInputEditingState( textEditing!.strategy.domElement, 'abcd', 2, 3); @@ -818,11 +804,7 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); - // This timer exists because Desktop Safari element is focused after - // zero-duration timer to prevent autofill dialog - if(isSafari){ - await Future.delayed(Duration.zero); - } + await waitForDesktopSafariFocus(); checkInputEditingState( textEditing!.strategy.domElement, 'abcd', 2, 3); @@ -880,11 +862,7 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); - // This timer exists because Desktop Safari element is focused after - // zero-duration timer to prevent autofill dialog - if(isSafari){ - await Future.delayed(Duration.zero); - } + await waitForDesktopSafariFocus(); // Form is added to DOM. expect(defaultTextEditingRoot.querySelectorAll('form'), isNotEmpty); @@ -940,11 +918,7 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); - // This timer exists because Desktop Safari element is focused after - // zero-duration timer to prevent autofill dialog - if(isSafari){ - await Future.delayed(Duration.zero); - } + await waitForDesktopSafariFocus(); // Form is added to DOM. expect(defaultTextEditingRoot.querySelectorAll('form'), isNotEmpty); @@ -999,11 +973,7 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); - // This timer exists because Desktop Safari element is focused after - // zero-duration timer to prevent autofill dialog - if(isSafari){ - await Future.delayed(Duration.zero); - } + await waitForDesktopSafariFocus(); // Form is added to DOM. expect(defaultTextEditingRoot.querySelectorAll('form'), isNotEmpty); @@ -1066,9 +1036,7 @@ Future testMain() async { expect(defaultTextEditingRoot.querySelectorAll('form'), isEmpty); expect(domDocument.activeElement, domDocument.body); - // This timer exists because Desktop Safari element is focused after - // zero-duration timer to prevent autofill dialog - await Future.delayed(Duration.zero); + await waitForDesktopSafariFocus(); // Form is added to DOM. expect(defaultTextEditingRoot.querySelectorAll('form'), isNotEmpty); @@ -1106,11 +1074,7 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); - // This timer exists because Desktop Safari element is focused after - // zero-duration timer to prevent autofill dialog - if(isSafari){ - await Future.delayed(Duration.zero); - } + await waitForDesktopSafariFocus(); checkInputEditingState( textEditing!.strategy.domElement, 'abcd', 2, 3); @@ -1162,11 +1126,7 @@ Future testMain() async { }); sendFrameworkMessage(codec.encodeMethodCall(setEditingState2)); - // This timer exists because Desktop Safari element is focused after - // zero-duration timer to prevent autofill dialog - if(isSafari){ - await Future.delayed(Duration.zero); - } + await waitForDesktopSafariFocus(); // The second [setEditingState] should override the first one. checkInputEditingState( textEditing!.strategy.domElement, 'xyz', 0, 2); @@ -1208,11 +1168,7 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); - // This timer exists because Desktop Safari element is focused after - // zero-duration timer to prevent autofill dialog - if(isSafari){ - await Future.delayed(Duration.zero); - } + await waitForDesktopSafariFocus(); // The second [setEditingState] should override the first one. checkInputEditingState( textEditing!.strategy.domElement, 'abcd', 2, 3); @@ -1283,11 +1239,7 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(updateSizeAndTransform)); - // This timer exists because Desktop Safari element is focused after - // zero-duration timer to prevent autofill dialog - if(isSafari){ - await Future.delayed(Duration.zero); - } + await waitForDesktopSafariFocus(); // Check the element still has focus. User can keep editing. expect(defaultTextEditingRoot.ownerDocument?.activeElement, textEditing!.strategy.domElement); @@ -1343,11 +1295,7 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); - // This timer exists because Desktop Safari element is focused after - // zero-duration timer to prevent autofill dialog - if(isSafari){ - await Future.delayed(Duration.zero); - } + await waitForDesktopSafariFocus(); // The second [setEditingState] should override the first one. checkInputEditingState( @@ -1659,11 +1607,7 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); - // This timer exists because Desktop Safari element is focused after - // zero-duration timer to prevent autofill dialog - if(isSafari){ - await Future.delayed(Duration.zero); - } + await waitForDesktopSafariFocus(); // Check if the selection range is correct. checkInputEditingState( @@ -1849,11 +1793,7 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); - // This timer exists because Desktop Safari element is focused after - // zero-duration timer to prevent autofill dialog - if(isSafari){ - await Future.delayed(Duration.zero); - } + await waitForDesktopSafariFocus(); // The second [setEditingState] should override the first one. checkInputEditingState( textEditing!.strategy.domElement, 'abcd', 2, 3); @@ -1921,11 +1861,7 @@ Future testMain() async { Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList()); sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); - // This timer exists because Desktop Safari element is focused after - // zero-duration timer to prevent autofill dialog - if(isSafari){ - await Future.delayed(Duration.zero); - } + await waitForDesktopSafariFocus(); final DomHTMLTextAreaElement textarea = textEditing!.strategy.domElement! as DomHTMLTextAreaElement; @@ -3005,3 +2941,12 @@ void clearForms() { } formsOnTheDom.clear(); } + +/// On Desktop Safari, the editing element is focused after a zero-duration timer +/// to prevent autofill popup flickering. We must wait a tick for this placement +/// before referencing these elements. +Future waitForDesktopSafariFocus() async { + if (textEditing.strategy is SafariDesktopTextEditingStrategy) { + await Future.delayed(Duration.zero); + } +}