From d2e4ccc031102df177918289d53ce4b28b7192e4 Mon Sep 17 00:00:00 2001 From: Juan Tugores Date: Tue, 23 Apr 2024 10:11:07 -0700 Subject: [PATCH 01/29] Make sure the elements can't be reached by keyboard --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 6 ++++-- lib/web_ui/test/engine/text_editing_test.dart | 6 +++++- 2 files changed, 9 insertions(+), 3 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 6f4ebdd7990dd..d96b0406b570f 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 @@ -332,7 +332,8 @@ class EngineAutofillForm { // In order to submit the form when Framework sends a `TextInput.commit` // message, we add a submit button to the form. - final DomHTMLInputElement submitButton = createDomHTMLInputElement(); + // The -1 tab index value makes this element not reachable by keyboard. + final DomHTMLInputElement submitButton = createDomHTMLInputElement()..tabIndex = -1; _styleAutofillElements(submitButton, isOffScreen: true); submitButton.className = 'submitBtn'; submitButton.type = 'submit'; @@ -1285,7 +1286,8 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements }) { assert(!isEnabled); - domElement = inputConfig.inputType.createDomElement(); + // The -1 tab index value makes this element not reachable by keyboard. + domElement = inputConfig.inputType.createDomElement()..tabIndex = -1; applyConfiguration(inputConfig); _setStaticStyleAttributes(activeDomElement); diff --git a/lib/web_ui/test/engine/text_editing_test.dart b/lib/web_ui/test/engine/text_editing_test.dart index ac93b4c0c0657..57d1f8a4ca421 100644 --- a/lib/web_ui/test/engine/text_editing_test.dart +++ b/lib/web_ui/test/engine/text_editing_test.dart @@ -74,6 +74,7 @@ Future testMain() async { cleanTextEditingStrategy(); cleanTestFlags(); clearBackUpDomElementIfExists(); + domDocument.activeElement?.blur(); }); group('$GloballyPositionedTextEditingStrategy', () { @@ -109,6 +110,7 @@ Future testMain() async { final DomElement input = defaultTextEditingRoot.querySelector('input')!; // Now the editing element should have focus. + expect(input.tabIndex, -1, reason: 'The input should not be reachable by keyboard'); expect(domDocument.activeElement, input); expect(defaultTextEditingRoot.ownerDocument?.activeElement, input); @@ -2839,6 +2841,8 @@ Future testMain() async { final DomHTMLInputElement inputElement = form.childNodes.toList()[0] as DomHTMLInputElement; expect(inputElement.type, 'submit'); + expect(inputElement.tabIndex, -1, reason: 'The input should not be reachable by keyboard'); + // The submit button should have class `submitBtn`. expect(inputElement.className, 'submitBtn'); @@ -3503,7 +3507,7 @@ Future testMain() async { } else { expect(input.style.background, contains('transparent')); expect(input.style.outline, contains('none')); - expect(input.style.border, contains('none')); + expect(input.style.border, anyOf(contains('none'), contains('medium'))); } expect(input.style.backgroundColor, contains('transparent')); expect(input.style.caretColor, contains('transparent')); From ea09d6edbdb5c096bec1b904448501565a1aec0a Mon Sep 17 00:00:00 2001 From: Juan Tugores Date: Tue, 23 Apr 2024 10:25:34 -0700 Subject: [PATCH 02/29] Move the focus to the instead of blur. --- .../src/engine/text_editing/text_editing.dart | 23 ++++- lib/web_ui/test/engine/text_editing_test.dart | 98 +++++++++++++------ 2 files changed, 87 insertions(+), 34 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 d96b0406b570f..8653947fed4fb 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 @@ -1249,6 +1249,19 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements return domElement!; } + /// The [FlutterView] in which [activeDomElement] is contained. + EngineFlutterView get activeDomElementView { + final EngineFlutterView? view = EnginePlatformDispatcher + .instance + .viewManager + .findViewForElement(activeDomElement); + assert( + view != null, + 'The DOM element of this text editing strategy is not in a flutter view.', + ); + return view!; + } + late InputConfiguration inputConfiguration; EditingState? lastEditingState; @@ -1418,14 +1431,13 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements } subscriptions.clear(); removeCompositionEventHandlers(activeDomElement); + _moveFocusToFlutterView(); // If focused element is a part of a form, it needs to stay on the DOM // until the autofill context of the form is finalized. // More details on `TextInput.finishAutofillContext` call. if (_appendedToForm && inputConfiguration.autofillGroup?.formElement != null) { - // Subscriptions are removed, listeners won't be triggered. - activeDomElement.blur(); _styleAutofillElements(activeDomElement, isOffScreen: true); inputConfiguration.autofillGroup?.storeForm(); } else { @@ -1574,6 +1586,13 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements event.preventDefault(); })); } + + /// Moves the focus to the parent [EngineFlutterView]. + void _moveFocusToFlutterView() { + if (activeDomElement == domDocument.activeElement) { + activeDomElementView.dom.rootElement.focus(); + } + } } /// IOS/Safari behaviour for text editing. diff --git a/lib/web_ui/test/engine/text_editing_test.dart b/lib/web_ui/test/engine/text_editing_test.dart index 57d1f8a4ca421..8e72a443d91fb 100644 --- a/lib/web_ui/test/engine/text_editing_test.dart +++ b/lib/web_ui/test/engine/text_editing_test.dart @@ -25,6 +25,9 @@ EnginePlatformDispatcher get dispatcher => EnginePlatformDispatcher.instance; DomElement get defaultTextEditingRoot => dispatcher.implicitView!.dom.textEditingHost; +DomElement get implicitViewRootElement => + dispatcher.implicitView!.dom.rootElement; + /// Add unit tests for [FirefoxTextEditingStrategy]. // TODO(mdebbar): https://github.com/flutter/flutter/issues/46891 @@ -67,6 +70,10 @@ Future testMain() async { setUpTestViewDimensions: false ); + setUp(() { + domDocument.activeElement?.blur(); + }); + tearDown(() { lastEditingState = null; editingDeltaState = null; @@ -74,7 +81,6 @@ Future testMain() async { cleanTextEditingStrategy(); cleanTestFlags(); clearBackUpDomElementIfExists(); - domDocument.activeElement?.blur(); }); group('$GloballyPositionedTextEditingStrategy', () { @@ -92,8 +98,11 @@ Future testMain() async { domDocument.getElementsByTagName('input'), hasLength(0), ); - // The focus initially is on the body. - expect(domDocument.activeElement, domDocument.body); + expect( + domDocument.activeElement, + domDocument.body, + reason: 'The focus should initially be on the body', + ); expect(defaultTextEditingRoot.ownerDocument?.activeElement, domDocument.body); @@ -110,12 +119,12 @@ Future testMain() async { final DomElement input = defaultTextEditingRoot.querySelector('input')!; // Now the editing element should have focus. - expect(input.tabIndex, -1, reason: 'The input should not be reachable by keyboard'); expect(domDocument.activeElement, input); expect(defaultTextEditingRoot.ownerDocument?.activeElement, input); expect(editingStrategy!.domElement, input); expect(input.getAttribute('type'), null); + expect(input.tabIndex, -1, reason: 'The input should not be reachable by keyboard'); // Input is appended to the right point of the DOM. expect(defaultTextEditingRoot.contains(editingStrategy!.domElement), isTrue); @@ -125,10 +134,10 @@ Future testMain() async { defaultTextEditingRoot.querySelectorAll('input'), hasLength(0), ); - // The focus is back to the body. - expect(domDocument.activeElement, domDocument.body); + // The focus is back to the flutter view. + expect(domDocument.activeElement, implicitViewRootElement); expect(defaultTextEditingRoot.ownerDocument?.activeElement, - domDocument.body); + implicitViewRootElement); }); test('inserts element in the correct view', () { @@ -339,11 +348,15 @@ Future testMain() async { checkTextAreaEditingState(textarea, 'bar\nbaz', 2, 7); editingStrategy!.disable(); + // The textarea should be cleaned up. expect(defaultTextEditingRoot.querySelectorAll('textarea'), hasLength(0)); - // The focus is back to the body. - expect(defaultTextEditingRoot.ownerDocument?.activeElement, - domDocument.body); + + expect( + defaultTextEditingRoot.ownerDocument?.activeElement, + implicitViewRootElement, + reason: 'The focus should be back to the body', + ); // There should be no input action. expect(lastInputAction, isNull); @@ -725,8 +738,11 @@ Future testMain() async { const MethodCall hide = MethodCall('TextInput.hide'); sendFrameworkMessage(codec.encodeMethodCall(hide)); - // Text editing should've stopped. - expect(domDocument.activeElement, domDocument.body); + expect( + domDocument.activeElement, + implicitViewRootElement, + reason: 'Text editing should have stopped', + ); // Confirm that [HybridTextEditing] didn't send any messages. expect(spy.messages, isEmpty); @@ -745,8 +761,11 @@ Future testMain() async { }); sendFrameworkMessage(codec.encodeMethodCall(setEditingState)); - // Editing shouldn't have started yet. - expect(domDocument.activeElement, domDocument.body); + expect( + domDocument.activeElement, + domDocument.body, + reason: 'Editing should not have started yet', + ); const MethodCall show = MethodCall('TextInput.show'); sendFrameworkMessage(codec.encodeMethodCall(show)); @@ -768,7 +787,11 @@ Future testMain() async { const MethodCall clearClient = MethodCall('TextInput.clearClient'); sendFrameworkMessage(codec.encodeMethodCall(clearClient)); - expect(domDocument.activeElement, domDocument.body); + expect( + domDocument.activeElement, + implicitViewRootElement, + reason: 'Text editing should have stopped', + ); // Confirm that [HybridTextEditing] didn't send any messages. expect(spy.messages, isEmpty); @@ -865,9 +888,11 @@ Future testMain() async { }); sendFrameworkMessage(codec.encodeMethodCall(setEditingState)); - // Editing shouldn't have started yet. - expect(defaultTextEditingRoot.ownerDocument?.activeElement, - domDocument.body); + expect( + defaultTextEditingRoot.ownerDocument?.activeElement, + domDocument.body, + reason: 'Editing should not have started yet', + ); const MethodCall show = MethodCall('TextInput.show'); sendFrameworkMessage(codec.encodeMethodCall(show)); @@ -887,12 +912,10 @@ Future testMain() async { textEditing!.strategy.domElement, 'abcd', 2, 3); expect(textEditing!.isEditing, isTrue); - // DOM element is blurred. - textEditing!.strategy.domElement!.blur(); - // No connection close message sent. expect(spy.messages, hasLength(0)); await Future.delayed(Duration.zero); + // DOM element still keeps the focus. expect(defaultTextEditingRoot.ownerDocument?.activeElement, textEditing!.strategy.domElement); @@ -1234,8 +1257,11 @@ Future testMain() async { }); sendFrameworkMessage(codec.encodeMethodCall(setEditingState)); - // Editing shouldn't have started yet. - expect(domDocument.activeElement, domDocument.body); + expect( + domDocument.activeElement, + domDocument.body, + reason: 'Editing should not have started yet.', + ); const MethodCall show = MethodCall('TextInput.show'); sendFrameworkMessage(codec.encodeMethodCall(show)); @@ -1258,9 +1284,11 @@ Future testMain() async { 'TextInput.setClient', [567, flutterSinglelineConfig]); sendFrameworkMessage(codec.encodeMethodCall(setClient2)); - // Receiving another client via setClient should stop editing, hence - // should remove the previous active element. - expect(domDocument.activeElement, domDocument.body); + expect( + domDocument.activeElement, + implicitViewRootElement, + reason: 'Receiving another client via setClient should stop editing, hence should remove the previous active element.', + ); // Confirm that [HybridTextEditing] didn't send any messages. expect(spy.messages, isEmpty); @@ -2170,9 +2198,11 @@ Future testMain() async { 'TextInput.setClient', [123, flutterMultilineConfig]); sendFrameworkMessage(codec.encodeMethodCall(setClient)); - // Editing shouldn't have started yet. - expect(defaultTextEditingRoot.ownerDocument?.activeElement, - domDocument.body); + expect( + defaultTextEditingRoot.ownerDocument?.activeElement, + domDocument.body, + reason: 'Editing should have not started yet', + ); const MethodCall show = MethodCall('TextInput.show'); sendFrameworkMessage(codec.encodeMethodCall(show)); @@ -2255,8 +2285,11 @@ Future testMain() async { const MethodCall hide = MethodCall('TextInput.hide'); sendFrameworkMessage(codec.encodeMethodCall(hide)); - // Text editing should've stopped. - expect(domDocument.activeElement, domDocument.body); + expect( + domDocument.activeElement, + implicitViewRootElement, + reason: 'Text editing should have stopped', + ); // Confirm that [HybridTextEditing] didn't send any more messages. expect(spy.messages, isEmpty); @@ -2843,7 +2876,6 @@ Future testMain() async { expect(inputElement.type, 'submit'); expect(inputElement.tabIndex, -1, reason: 'The input should not be reachable by keyboard'); - // The submit button should have class `submitBtn`. expect(inputElement.className, 'submitBtn'); }); @@ -3498,6 +3530,8 @@ Future testMain() async { ); final DomHTMLElement input = editingStrategy!.activeDomElement; + expect(domDocument.activeElement, input, reason: 'the input element should be focused'); + expect(input.style.color, contains('transparent')); if (isSafari) { // macOS 13 returns different values than macOS 12. From edab1dc0b59cb60c8699050561ac2ae4d9ce3adc Mon Sep 17 00:00:00 2001 From: Juan Tugores Date: Fri, 10 May 2024 12:09:51 -0700 Subject: [PATCH 03/29] Add listener to the subscription (otherwise never gets cleaned up). --- .../lib/src/engine/text_editing/text_editing.dart | 12 ++++-------- 1 file changed, 4 insertions(+), 8 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 8653947fed4fb..d8817d472f506 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 @@ -1378,8 +1378,7 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements subscriptions.add(DomSubscription(domDocument, 'selectionchange', handleChange)); - activeDomElement.addEventListener('beforeinput', - createDomEventListener(handleBeforeInput)); + subscriptions.add(DomSubscription(activeDomElement, 'beforeinput', handleBeforeInput)); addCompositionEventHandlers(activeDomElement); @@ -1692,8 +1691,7 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy { subscriptions.add(DomSubscription(domDocument, 'selectionchange', handleChange)); - activeDomElement.addEventListener('beforeinput', - createDomEventListener(handleBeforeInput)); + subscriptions.add(DomSubscription(activeDomElement, 'beforeinput', handleBeforeInput)); addCompositionEventHandlers(activeDomElement); @@ -1845,8 +1843,7 @@ class AndroidTextEditingStrategy extends GloballyPositionedTextEditingStrategy { DomSubscription(domDocument, 'selectionchange', handleChange)); - activeDomElement.addEventListener('beforeinput', - createDomEventListener(handleBeforeInput)); + subscriptions.add(DomSubscription(activeDomElement, 'beforeinput', handleBeforeInput)); addCompositionEventHandlers(activeDomElement); @@ -1909,8 +1906,7 @@ class FirefoxTextEditingStrategy extends GloballyPositionedTextEditingStrategy { DomSubscription( activeDomElement, 'keydown', maybeSendAction)); - activeDomElement.addEventListener('beforeinput', - createDomEventListener(handleBeforeInput)); + subscriptions.add(DomSubscription(activeDomElement, 'beforeinput', handleBeforeInput)); addCompositionEventHandlers(activeDomElement); From 8bbc20695d23b438de95430ca1ba4bed21cdba21 Mon Sep 17 00:00:00 2001 From: Juan Tugores Date: Fri, 10 May 2024 13:20:16 -0700 Subject: [PATCH 04/29] Delay the input removal --- .../src/engine/text_editing/text_editing.dart | 34 ++++++++----------- lib/web_ui/test/engine/text_editing_test.dart | 34 ++++++++++++++++--- 2 files changed, 44 insertions(+), 24 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 d8817d472f506..b398ef56f65d5 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 @@ -1250,17 +1250,8 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements } /// The [FlutterView] in which [activeDomElement] is contained. - EngineFlutterView get activeDomElementView { - final EngineFlutterView? view = EnginePlatformDispatcher - .instance - .viewManager - .findViewForElement(activeDomElement); - assert( - view != null, - 'The DOM element of this text editing strategy is not in a flutter view.', - ); - return view!; - } + EngineFlutterView? get _activeDomElementView => + EnginePlatformDispatcher.instance.viewManager.findViewForElement(activeDomElement); late InputConfiguration inputConfiguration; EditingState? lastEditingState; @@ -1430,7 +1421,6 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements } subscriptions.clear(); removeCompositionEventHandlers(activeDomElement); - _moveFocusToFlutterView(); // If focused element is a part of a form, it needs to stay on the DOM // until the autofill context of the form is finalized. @@ -1439,9 +1429,8 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements inputConfiguration.autofillGroup?.formElement != null) { _styleAutofillElements(activeDomElement, isOffScreen: true); inputConfiguration.autofillGroup?.storeForm(); - } else { - activeDomElement.remove(); } + _moveFocusToFlutterView(activeDomElement, _activeDomElementView); domElement = null; } @@ -1586,11 +1575,18 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements })); } - /// Moves the focus to the parent [EngineFlutterView]. - void _moveFocusToFlutterView() { - if (activeDomElement == domDocument.activeElement) { - activeDomElementView.dom.rootElement.focus(); - } + /// Moves the focus to the [EngineFlutterView]. + /// + /// The delay gives the engine the opportunity to focus another element. + /// The delay should help prevent the keyboard from jumping when the focus goes from + /// one text field to another. + static void _moveFocusToFlutterView(DomElement element, EngineFlutterView? view) { + Timer(Duration.zero, () { + if (element == domDocument.activeElement) { + view?.dom.rootElement.focus(); + } + element.remove(); + }); } } diff --git a/lib/web_ui/test/engine/text_editing_test.dart b/lib/web_ui/test/engine/text_editing_test.dart index 8e72a443d91fb..ac48860428a21 100644 --- a/lib/web_ui/test/engine/text_editing_test.dart +++ b/lib/web_ui/test/engine/text_editing_test.dart @@ -74,13 +74,14 @@ Future testMain() async { domDocument.activeElement?.blur(); }); - tearDown(() { + tearDown(() async { lastEditingState = null; editingDeltaState = null; lastInputAction = null; cleanTextEditingStrategy(); cleanTestFlags(); clearBackUpDomElementIfExists(); + await waitForTextStrategyStopPropagation(); }); group('$GloballyPositionedTextEditingStrategy', () { @@ -93,7 +94,7 @@ Future testMain() async { testTextEditing.configuration = singlelineConfig; }); - test('Creates element when enabled and removes it when disabled', () { + test('Creates element when enabled and removes it when disabled', () async { expect( domDocument.getElementsByTagName('input'), hasLength(0), @@ -130,6 +131,7 @@ Future testMain() async { expect(defaultTextEditingRoot.contains(editingStrategy!.domElement), isTrue); editingStrategy!.disable(); + await waitForTextStrategyStopPropagation(); expect( defaultTextEditingRoot.querySelectorAll('input'), hasLength(0), @@ -140,7 +142,7 @@ Future testMain() async { implicitViewRootElement); }); - test('inserts element in the correct view', () { + test('inserts element in the correct view', () async { final DomElement host = createDomElement('div'); domDocument.body!.append(host); final EngineFlutterView view = EngineFlutterView(dispatcher, host); @@ -163,6 +165,7 @@ Future testMain() async { // Cleanup. editingStrategy!.disable(); + await waitForTextStrategyStopPropagation(); expect(textEditingHost.querySelectorAll('input'), hasLength(0)); dispatcher.viewManager.unregisterView(view.viewId); view.dispose(); @@ -316,7 +319,7 @@ Future testMain() async { expect(lastInputAction, isNull); }); - test('Multi-line mode also works', () { + test('Multi-line mode also works', () async { // The textarea element is created lazily. expect(domDocument.getElementsByTagName('textarea'), hasLength(0)); editingStrategy!.enable( @@ -349,6 +352,8 @@ Future testMain() async { editingStrategy!.disable(); + await waitForTextStrategyStopPropagation(); + // The textarea should be cleaned up. expect(defaultTextEditingRoot.querySelectorAll('textarea'), hasLength(0)); @@ -362,7 +367,7 @@ Future testMain() async { expect(lastInputAction, isNull); }); - test('Same instance can be re-enabled with different config', () { + test('Same instance can be re-enabled with different config', () async { // Make sure there's nothing in the DOM yet. expect(domDocument.getElementsByTagName('input'), hasLength(0)); expect(domDocument.getElementsByTagName('textarea'), hasLength(0)); @@ -378,6 +383,7 @@ Future testMain() async { // Disable and check that all DOM elements were removed. editingStrategy!.disable(); + await waitForTextStrategyStopPropagation(); expect(defaultTextEditingRoot.querySelectorAll('input'), hasLength(0)); expect(defaultTextEditingRoot.querySelectorAll('textarea'), hasLength(0)); @@ -387,11 +393,13 @@ Future testMain() async { onChange: trackEditingState, onAction: trackInputAction, ); + await waitForTextStrategyStopPropagation(); expect(defaultTextEditingRoot.querySelectorAll('input'), hasLength(0)); expect(defaultTextEditingRoot.querySelectorAll('textarea'), hasLength(1)); // Disable again and check that all DOM elements were removed. editingStrategy!.disable(); + await waitForTextStrategyStopPropagation(); expect(defaultTextEditingRoot.querySelectorAll('input'), hasLength(0)); expect(defaultTextEditingRoot.querySelectorAll('textarea'), hasLength(0)); @@ -738,6 +746,8 @@ Future testMain() async { const MethodCall hide = MethodCall('TextInput.hide'); sendFrameworkMessage(codec.encodeMethodCall(hide)); + await waitForTextStrategyStopPropagation(); + expect( domDocument.activeElement, implicitViewRootElement, @@ -787,6 +797,8 @@ Future testMain() async { const MethodCall clearClient = MethodCall('TextInput.clearClient'); sendFrameworkMessage(codec.encodeMethodCall(clearClient)); + await waitForTextStrategyStopPropagation(); + expect( domDocument.activeElement, implicitViewRootElement, @@ -1284,6 +1296,8 @@ Future testMain() async { 'TextInput.setClient', [567, flutterSinglelineConfig]); sendFrameworkMessage(codec.encodeMethodCall(setClient2)); + await waitForTextStrategyStopPropagation(); + expect( domDocument.activeElement, implicitViewRootElement, @@ -2285,6 +2299,8 @@ Future testMain() async { const MethodCall hide = MethodCall('TextInput.hide'); sendFrameworkMessage(codec.encodeMethodCall(hide)); + await waitForTextStrategyStopPropagation(); + expect( domDocument.activeElement, implicitViewRootElement, @@ -3754,6 +3770,14 @@ Future waitForDesktopSafariFocus() async { } } +/// After stopped the focus remains on the input element to give the engine +/// an oportunity to move the focus to the right element before sending it back +/// to the flutter view. This helps preventing the keyboard from jumping when the focus +/// goes from one text field to another. +Future waitForTextStrategyStopPropagation() async { + await Future.delayed(Duration.zero); +} + class GlobalTextEditingStrategySpy extends GloballyPositionedTextEditingStrategy { GlobalTextEditingStrategySpy(super.owner); From ed2f851daa1e8041b11317d886d4f9f59dffedd3 Mon Sep 17 00:00:00 2001 From: Juan Tugores Date: Fri, 10 May 2024 13:35:55 -0700 Subject: [PATCH 05/29] Prevent default on pointer down on flutter views --- lib/web_ui/lib/src/engine/pointer_binding.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/web_ui/lib/src/engine/pointer_binding.dart b/lib/web_ui/lib/src/engine/pointer_binding.dart index d198bcfbb7182..f7addea2ac6c8 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding.dart @@ -973,6 +973,21 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { ); _convertEventsToPointerData(data: pointerData, event: event, details: down); _callback(event, pointerData); + + if (event.target == _viewTarget) { + // The event default is prevented so that the element doens't gain focus. + event.preventDefault(); + // We give time to TextFields to move the focus to the right or