From b7f7f221d04af3a68019d41e0648952d8c35c5c6 Mon Sep 17 00:00:00 2001 From: Hassan Toor Date: Thu, 16 Feb 2023 13:31:38 -0600 Subject: [PATCH 01/15] Add a text editing host node, attach all text editing nodes to it --- lib/web_ui/lib/src/engine/embedder.dart | 11 +++++-- lib/web_ui/lib/src/engine/host_node.dart | 30 +++++++++++++++---- .../src/engine/text_editing/text_editing.dart | 9 ++++-- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/lib/web_ui/lib/src/engine/embedder.dart b/lib/web_ui/lib/src/engine/embedder.dart index c2d507de65044..ab3e9025d4169 100644 --- a/lib/web_ui/lib/src/engine/embedder.dart +++ b/lib/web_ui/lib/src/engine/embedder.dart @@ -124,6 +124,9 @@ class FlutterViewEmbedder { HostNode? get glassPaneShadow => _glassPaneShadow; HostNode? _glassPaneShadow; + DomElement? get textEditingHostNode => _textEditingHostNode; + DomElement? _textEditingHostNode; + static const String defaultFontStyle = 'normal'; static const String defaultFontWeight = 'normal'; static const double defaultFontSize = 14; @@ -170,6 +173,9 @@ class FlutterViewEmbedder { ); _glassPaneShadow = glassPaneElementHostNode; + _textEditingHostNode = + createTextEditingHostNode(glassPaneElement, defaultCssFont); + // Don't allow the scene to receive pointer events. _sceneHostElement = domDocument.createElement('flt-scene-host') ..style.pointerEvents = 'none'; @@ -189,7 +195,6 @@ class FlutterViewEmbedder { .prepareAccessibilityPlaceholder(); glassPaneElementHostNode.appendAll([ - accessibilityPlaceholder, _sceneHostElement!, // The semantic host goes last because hit-test order-wise it must be @@ -202,9 +207,11 @@ class FlutterViewEmbedder { // elements transparent. This way, if a platform view appears among other // interactive Flutter widgets, as long as those widgets do not intersect // with the platform view, the platform view will be reachable. - semanticsHostElement, ]); + _textEditingHostNode?.appendChild(accessibilityPlaceholder); + _textEditingHostNode?.appendChild(semanticsHostElement); + // When debugging semantics, make the scene semi-transparent so that the // semantics tree is more prominent. if (configuration.debugShowSemanticsNodes) { diff --git a/lib/web_ui/lib/src/engine/host_node.dart b/lib/web_ui/lib/src/engine/host_node.dart index 4b0ca8e13d990..77143e19a2096 100644 --- a/lib/web_ui/lib/src/engine/host_node.dart +++ b/lib/web_ui/lib/src/engine/host_node.dart @@ -110,11 +110,12 @@ class ShadowDomHostNode implements HostNode { /// This also calls [applyGlobalCssRulesToSheet], with the [defaultFont] /// to be used as the default font definition. ShadowDomHostNode(DomElement root, String defaultFont) - : assert( - root.isConnected ?? true, - 'The `root` of a ShadowDomHostNode must be connected to the Document object or a ShadowRoot.' - ) { - _shadow = root.attachShadow({ + : assert(root.isConnected ?? true, + 'The `root` of a ShadowDomHostNode must be connected to the Document object or a ShadowRoot.') { + final DomElement element = + domDocument.createElement('flt-shadow-host-node'); + root.appendChild(element); + _shadow = element.attachShadow({ 'mode': 'open', // This needs to stay false to prevent issues like this: // - https://github.com/flutter/flutter/issues/85759 @@ -221,6 +222,25 @@ class ElementHostNode implements HostNode { void appendAll(Iterable nodes) => nodes.forEach(append); } +DomElement createTextEditingHostNode(DomElement root, String defaultFont) { + const String hostTagName = 'flt-text-editing-host-node'; + final DomElement domElement = domDocument.createElement(hostTagName); + final DomHTMLStyleElement styleElement = createDomHTMLStyleElement(); + + styleElement.id = 'flt-text-editing-stylesheet'; + root.appendChild(styleElement); + applyGlobalCssRulesToSheet( + styleElement.sheet! as DomCSSStyleSheet, + hasAutofillOverlay: browserHasAutofillOverlay(), + cssSelectorPrefix: hostTagName, + defaultCssFont: defaultFont, + ); + + root.appendChild(domElement); + + return domElement; +} + // Applies the required global CSS to an incoming [DomCSSStyleSheet] `sheet`. void applyGlobalCssRulesToSheet( DomCSSStyleSheet sheet, { 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 27b574808228e..c1fc576db632d 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 @@ -53,6 +53,9 @@ void _emptyCallback(dynamic _) {} @visibleForTesting HostNode get defaultTextEditingRoot => flutterViewEmbedder.glassPaneShadow!; +DomElement get nonShadowTextEditingRoot => + flutterViewEmbedder.textEditingHostNode!; + /// These style attributes are constant throughout the life time of an input /// element. /// @@ -278,7 +281,7 @@ class EngineAutofillForm { void placeForm(DomHTMLElement mainTextEditingElement) { formElement.append(mainTextEditingElement); - defaultTextEditingRoot.append(formElement); + nonShadowTextEditingRoot.append(formElement); } void storeForm() { @@ -1175,7 +1178,7 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements // DOM later, when the first location information arrived. // Otherwise, on Blink based Desktop browsers, the autofill menu appears // on top left of the screen. - defaultTextEditingRoot.append(activeDomElement); + nonShadowTextEditingRoot.append(activeDomElement); _appendedToForm = false; } @@ -1661,7 +1664,7 @@ class AndroidTextEditingStrategy extends GloballyPositionedTextEditingStrategy { if (hasAutofillGroup) { placeForm(); } else { - defaultTextEditingRoot.append(activeDomElement); + nonShadowTextEditingRoot.append(activeDomElement); } inputConfig.textCapitalization.setAutocapitalizeAttribute( activeDomElement); From 1aa89a2ad565188f0d353b711cd2516358519185 Mon Sep 17 00:00:00 2001 From: Hassan Toor Date: Thu, 16 Feb 2023 13:34:39 -0600 Subject: [PATCH 02/15] whitespace --- lib/web_ui/lib/src/engine/embedder.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/embedder.dart b/lib/web_ui/lib/src/engine/embedder.dart index ab3e9025d4169..f2d495bf60de6 100644 --- a/lib/web_ui/lib/src/engine/embedder.dart +++ b/lib/web_ui/lib/src/engine/embedder.dart @@ -211,7 +211,6 @@ class FlutterViewEmbedder { _textEditingHostNode?.appendChild(accessibilityPlaceholder); _textEditingHostNode?.appendChild(semanticsHostElement); - // When debugging semantics, make the scene semi-transparent so that the // semantics tree is more prominent. if (configuration.debugShowSemanticsNodes) { From d9f1dd1fcbd5d7e4059629877d5f5f0a9aaef970 Mon Sep 17 00:00:00 2001 From: Hassan Toor Date: Mon, 27 Feb 2023 12:08:32 -0600 Subject: [PATCH 03/15] Move semantics tree outside of text editing host, change naming --- lib/web_ui/lib/src/engine/embedder.dart | 5 ++--- lib/web_ui/lib/src/engine/host_node.dart | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/web_ui/lib/src/engine/embedder.dart b/lib/web_ui/lib/src/engine/embedder.dart index 36dd436573a3f..63ebec90ded7f 100644 --- a/lib/web_ui/lib/src/engine/embedder.dart +++ b/lib/web_ui/lib/src/engine/embedder.dart @@ -193,8 +193,8 @@ class FlutterViewEmbedder { .prepareAccessibilityPlaceholder(); glassPaneElementHostNode.appendAll([ + accessibilityPlaceholder, _sceneHostElement!, - // The semantic host goes last because hit-test order-wise it must be // first. If semantics goes under the scene host, platform views will // obscure semantic elements. @@ -207,8 +207,7 @@ class FlutterViewEmbedder { // with the platform view, the platform view will be reachable. ]); - _textEditingHostNode?.appendChild(accessibilityPlaceholder); - _textEditingHostNode?.appendChild(semanticsHostElement); + glassPaneElement.appendChild(semanticsHostElement); // When debugging semantics, make the scene semi-transparent so that the // semantics tree is more prominent. if (configuration.debugShowSemanticsNodes) { diff --git a/lib/web_ui/lib/src/engine/host_node.dart b/lib/web_ui/lib/src/engine/host_node.dart index 77143e19a2096..f821b2036173b 100644 --- a/lib/web_ui/lib/src/engine/host_node.dart +++ b/lib/web_ui/lib/src/engine/host_node.dart @@ -113,7 +113,7 @@ class ShadowDomHostNode implements HostNode { : assert(root.isConnected ?? true, 'The `root` of a ShadowDomHostNode must be connected to the Document object or a ShadowRoot.') { final DomElement element = - domDocument.createElement('flt-shadow-host-node'); + domDocument.createElement('flt-render-host'); root.appendChild(element); _shadow = element.attachShadow({ 'mode': 'open', @@ -223,8 +223,8 @@ class ElementHostNode implements HostNode { } DomElement createTextEditingHostNode(DomElement root, String defaultFont) { - const String hostTagName = 'flt-text-editing-host-node'; - final DomElement domElement = domDocument.createElement(hostTagName); + final DomElement domElement = + domDocument.createElement('flt-text-editing-host'); final DomHTMLStyleElement styleElement = createDomHTMLStyleElement(); styleElement.id = 'flt-text-editing-stylesheet'; @@ -232,7 +232,7 @@ DomElement createTextEditingHostNode(DomElement root, String defaultFont) { applyGlobalCssRulesToSheet( styleElement.sheet! as DomCSSStyleSheet, hasAutofillOverlay: browserHasAutofillOverlay(), - cssSelectorPrefix: hostTagName, + cssSelectorPrefix: FlutterViewEmbedder.glassPaneTagName, defaultCssFont: defaultFont, ); From dfc1babc04907e4d30ef2e972f2567cf9d52d64c Mon Sep 17 00:00:00 2001 From: Hassan Toor Date: Mon, 27 Feb 2023 12:10:49 -0600 Subject: [PATCH 04/15] Refactor --- .../lib/src/engine/text_editing/text_editing.dart | 10 ++++------ 1 file changed, 4 insertions(+), 6 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 6102e7a0d99cd..efd9e93bf037c 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 @@ -51,9 +51,7 @@ void _emptyCallback(dynamic _) {} /// The default [HostNode] that hosts all DOM required for text editing when a11y is not enabled. @visibleForTesting -HostNode get defaultTextEditingRoot => flutterViewEmbedder.glassPaneShadow; - -DomElement get nonShadowTextEditingRoot => +DomElement get defaultTextEditingRoot => flutterViewEmbedder.textEditingHostNode!; /// These style attributes are constant throughout the life time of an input @@ -281,7 +279,7 @@ class EngineAutofillForm { void placeForm(DomHTMLElement mainTextEditingElement) { formElement.append(mainTextEditingElement); - nonShadowTextEditingRoot.append(formElement); + defaultTextEditingRoot.append(formElement); } void storeForm() { @@ -1178,7 +1176,7 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements // DOM later, when the first location information arrived. // Otherwise, on Blink based Desktop browsers, the autofill menu appears // on top left of the screen. - nonShadowTextEditingRoot.append(activeDomElement); + defaultTextEditingRoot.append(activeDomElement); _appendedToForm = false; } @@ -1664,7 +1662,7 @@ class AndroidTextEditingStrategy extends GloballyPositionedTextEditingStrategy { if (hasAutofillGroup) { placeForm(); } else { - nonShadowTextEditingRoot.append(activeDomElement); + defaultTextEditingRoot.append(activeDomElement); } inputConfig.textCapitalization.setAutocapitalizeAttribute( activeDomElement); From 23b5b6c6a41f8eee73d9483a9565b6adf13f97e9 Mon Sep 17 00:00:00 2001 From: Hassan Toor Date: Wed, 1 Mar 2023 12:28:52 -0600 Subject: [PATCH 05/15] fix text editing tests --- lib/web_ui/test/text_editing_test.dart | 43 ++++++++++++++++---------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index 30d669bf5801c..69b0fa32fab48 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -91,7 +91,8 @@ Future testMain() async { ); // The focus initially is on the body. expect(domDocument.activeElement, domDocument.body); - expect(defaultTextEditingRoot.activeElement, null); + expect(defaultTextEditingRoot.ownerDocument?.activeElement, + domDocument.body); editingStrategy!.enable( singlelineConfig, @@ -106,8 +107,8 @@ Future testMain() async { final DomElement input = defaultTextEditingRoot.querySelector('input')!; // Now the editing element should have focus. - expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); - expect(defaultTextEditingRoot.activeElement, input); + expect(domDocument.activeElement, input); + expect(defaultTextEditingRoot.ownerDocument?.activeElement, input); expect(editingStrategy!.domElement, input); expect(input.getAttribute('type'), null); @@ -122,7 +123,8 @@ Future testMain() async { ); // The focus is back to the body. expect(domDocument.activeElement, domDocument.body); - expect(defaultTextEditingRoot.activeElement, null); + expect(defaultTextEditingRoot.ownerDocument?.activeElement, + domDocument.body); }); test('Respects read-only config', () { @@ -281,7 +283,7 @@ Future testMain() async { final DomHTMLTextAreaElement textarea = defaultTextEditingRoot.querySelector('textarea')! as DomHTMLTextAreaElement; // Now the textarea should have focus. - expect(defaultTextEditingRoot.activeElement, textarea); + expect(defaultTextEditingRoot.ownerDocument?.activeElement, textarea); expect(editingStrategy!.domElement, textarea); textarea.value = 'foo\nbar'; @@ -303,7 +305,8 @@ Future testMain() async { // The textarea should be cleaned up. expect(defaultTextEditingRoot.querySelectorAll('textarea'), hasLength(0)); // The focus is back to the body. - expect(defaultTextEditingRoot.activeElement, null); + expect(defaultTextEditingRoot.ownerDocument?.activeElement, + domDocument.body); // There should be no input action. expect(lastInputAction, isNull); @@ -620,7 +623,7 @@ Future testMain() async { const MethodCall show = MethodCall('TextInput.show'); sendFrameworkMessage(codec.encodeMethodCall(show)); - expect(defaultTextEditingRoot.activeElement, + expect(defaultTextEditingRoot.ownerDocument?.activeElement, textEditing!.strategy.domElement); }); @@ -680,7 +683,8 @@ Future testMain() async { sendFrameworkMessage(codec.encodeMethodCall(setEditingState)); // Editing shouldn't have started yet. - expect(defaultTextEditingRoot.activeElement, null); + expect(defaultTextEditingRoot.ownerDocument?.activeElement, + domDocument.body); const MethodCall show = MethodCall('TextInput.show'); sendFrameworkMessage(codec.encodeMethodCall(show)); @@ -705,7 +709,7 @@ Future testMain() async { expect(spy.messages, hasLength(0)); await Future.delayed(Duration.zero); // DOM element still keeps the focus. - expect(defaultTextEditingRoot.activeElement, + expect(defaultTextEditingRoot.ownerDocument?.activeElement, textEditing!.strategy.domElement); }); @@ -723,7 +727,8 @@ Future testMain() async { sendFrameworkMessage(codec.encodeMethodCall(setEditingState)); // Editing shouldn't have started yet. - expect(defaultTextEditingRoot.activeElement, null); + expect(defaultTextEditingRoot.ownerDocument?.activeElement, + domDocument.body); const MethodCall show = MethodCall('TextInput.show'); sendFrameworkMessage(codec.encodeMethodCall(show)); @@ -752,7 +757,8 @@ Future testMain() async { spy.messages[0].methodName, 'TextInputClient.onConnectionClosed'); await Future.delayed(Duration.zero); // DOM element loses the focus. - expect(defaultTextEditingRoot.activeElement, null); + expect(defaultTextEditingRoot.ownerDocument?.activeElement, + domDocument.body); }, // Test on ios-safari only. skip: browserEngine != BrowserEngine.webkit || @@ -773,7 +779,8 @@ Future testMain() async { sendFrameworkMessage(codec.encodeMethodCall(setEditingState)); // Editing shouldn't have started yet. - expect(defaultTextEditingRoot.activeElement, null); + expect(defaultTextEditingRoot.ownerDocument?.activeElement, + domDocument.body); const MethodCall show = MethodCall('TextInput.show'); sendFrameworkMessage(codec.encodeMethodCall(show)); @@ -1152,7 +1159,8 @@ Future testMain() async { // In Safari Desktop Autofill menu appears as soon as an element is // focused, therefore the input element is only focused after the // location is received. - expect(defaultTextEditingRoot.activeElement, inputElement); + expect( + defaultTextEditingRoot.ownerDocument?.activeElement, inputElement); expect(inputElement.selectionStart, 2); expect(inputElement.selectionEnd, 3); } @@ -1165,7 +1173,7 @@ Future testMain() async { sendFrameworkMessage(codec.encodeMethodCall(updateSizeAndTransform)); // Check the element still has focus. User can keep editing. - expect(defaultTextEditingRoot.activeElement, + expect(defaultTextEditingRoot.ownerDocument?.activeElement, textEditing!.strategy.domElement); // Check the cursor location is the same. @@ -1765,7 +1773,8 @@ Future testMain() async { sendFrameworkMessage(codec.encodeMethodCall(setClient)); // Editing shouldn't have started yet. - expect(defaultTextEditingRoot.activeElement, null); + expect(defaultTextEditingRoot.ownerDocument?.activeElement, + domDocument.body); const MethodCall show = MethodCall('TextInput.show'); sendFrameworkMessage(codec.encodeMethodCall(show)); @@ -2647,7 +2656,7 @@ void checkInputEditingState( expect(element, isNotNull); expect(domInstanceOfString(element, 'HTMLInputElement'), true); final DomHTMLInputElement input = element! as DomHTMLInputElement; - expect(defaultTextEditingRoot.activeElement, input); + expect(defaultTextEditingRoot.ownerDocument?.activeElement, input); expect(input.value, text); expect(input.selectionStart, start); expect(input.selectionEnd, end); @@ -2673,7 +2682,7 @@ void checkTextAreaEditingState( int start, int end, ) { - expect(defaultTextEditingRoot.activeElement, textarea); + expect(defaultTextEditingRoot.ownerDocument?.activeElement, textarea); expect(textarea.value, text); expect(textarea.selectionStart, start); expect(textarea.selectionEnd, end); From 9586c58e619ae8cd6040ecce6b7d850402bd841b Mon Sep 17 00:00:00 2001 From: Hassan Toor Date: Thu, 2 Mar 2023 18:06:26 -0600 Subject: [PATCH 06/15] Fix issues on semantic mode, fix tests, move platform views to new render host --- lib/web_ui/lib/src/engine/embedder.dart | 21 +++++ lib/web_ui/lib/src/engine/host_node.dart | 33 ++----- .../lib/src/engine/platform_dispatcher.dart | 2 +- .../platform_views/content_manager.dart | 3 +- .../lib/src/engine/semantics/text_field.dart | 4 +- .../test/engine/semantics/semantics_test.dart | 94 ++++++++++--------- .../engine/semantics/semantics_tester.dart | 9 +- 7 files changed, 93 insertions(+), 73 deletions(-) diff --git a/lib/web_ui/lib/src/engine/embedder.dart b/lib/web_ui/lib/src/engine/embedder.dart index 63ebec90ded7f..831388a1a77e2 100644 --- a/lib/web_ui/lib/src/engine/embedder.dart +++ b/lib/web_ui/lib/src/engine/embedder.dart @@ -398,3 +398,24 @@ FlutterViewEmbedder? _flutterViewEmbedder; FlutterViewEmbedder ensureFlutterViewEmbedderInitialized() => _flutterViewEmbedder ??= FlutterViewEmbedder(hostElement: configuration.hostElement); + +/// Creates a node to host text editing elements and applies a stylesheet +/// to Flutter nodes that exist outside of the shadowDOM. +DomElement createTextEditingHostNode(DomElement root, String defaultFont) { + final DomElement domElement = + domDocument.createElement('flt-text-editing-host'); + final DomHTMLStyleElement styleElement = createDomHTMLStyleElement(); + + styleElement.id = 'flt-text-editing-stylesheet'; + root.appendChild(styleElement); + applyGlobalCssRulesToSheet( + styleElement.sheet! as DomCSSStyleSheet, + hasAutofillOverlay: browserHasAutofillOverlay(), + cssSelectorPrefix: FlutterViewEmbedder.glassPaneTagName, + defaultCssFont: defaultFont, + ); + + root.appendChild(domElement); + + return domElement; +} diff --git a/lib/web_ui/lib/src/engine/host_node.dart b/lib/web_ui/lib/src/engine/host_node.dart index f821b2036173b..ad7c7cee96017 100644 --- a/lib/web_ui/lib/src/engine/host_node.dart +++ b/lib/web_ui/lib/src/engine/host_node.dart @@ -94,6 +94,8 @@ abstract class HostNode { /// See: /// * [Document.querySelectorAll](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll) Iterable querySelectorAll(String selectors); + + DomElement get renderHost; } /// A [HostNode] implementation, backed by a [DomShadowRoot]. @@ -112,10 +114,8 @@ class ShadowDomHostNode implements HostNode { ShadowDomHostNode(DomElement root, String defaultFont) : assert(root.isConnected ?? true, 'The `root` of a ShadowDomHostNode must be connected to the Document object or a ShadowRoot.') { - final DomElement element = - domDocument.createElement('flt-render-host'); - root.appendChild(element); - _shadow = element.attachShadow({ + root.appendChild(renderHost); + _shadow = renderHost.attachShadow({ 'mode': 'open', // This needs to stay false to prevent issues like this: // - https://github.com/flutter/flutter/issues/85759 @@ -136,6 +136,9 @@ class ShadowDomHostNode implements HostNode { late DomShadowRoot _shadow; + @override + final DomElement renderHost = domDocument.createElement('flt-render-host'); + @override DomElement? get activeElement => _shadow.activeElement; @@ -192,6 +195,9 @@ class ElementHostNode implements HostNode { late DomElement _element; + @override + final DomElement renderHost = domDocument.createElement('flt-render-host'); + @override DomElement? get activeElement => _element.ownerDocument?.activeElement; @@ -222,25 +228,6 @@ class ElementHostNode implements HostNode { void appendAll(Iterable nodes) => nodes.forEach(append); } -DomElement createTextEditingHostNode(DomElement root, String defaultFont) { - final DomElement domElement = - domDocument.createElement('flt-text-editing-host'); - final DomHTMLStyleElement styleElement = createDomHTMLStyleElement(); - - styleElement.id = 'flt-text-editing-stylesheet'; - root.appendChild(styleElement); - applyGlobalCssRulesToSheet( - styleElement.sheet! as DomCSSStyleSheet, - hasAutofillOverlay: browserHasAutofillOverlay(), - cssSelectorPrefix: FlutterViewEmbedder.glassPaneTagName, - defaultCssFont: defaultFont, - ); - - root.appendChild(domElement); - - return domElement; -} - // Applies the required global CSS to an incoming [DomCSSStyleSheet] `sheet`. void applyGlobalCssRulesToSheet( DomCSSStyleSheet sheet, { diff --git a/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/lib/web_ui/lib/src/engine/platform_dispatcher.dart index 1c8c384bbbe5f..9194e8eda1e82 100644 --- a/lib/web_ui/lib/src/engine/platform_dispatcher.dart +++ b/lib/web_ui/lib/src/engine/platform_dispatcher.dart @@ -590,7 +590,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { _platformViewMessageHandler ??= PlatformViewMessageHandler( contentManager: platformViewManager, contentHandler: (DomElement content) { - flutterViewEmbedder.glassPaneElement.append(content); + flutterViewEmbedder.glassPaneShadow.renderHost.append(content); }, ); _platformViewMessageHandler!.handlePlatformViewCall(data, callback!); diff --git a/lib/web_ui/lib/src/engine/platform_views/content_manager.dart b/lib/web_ui/lib/src/engine/platform_views/content_manager.dart index f50d3af3ee670..28aeb8da0855e 100644 --- a/lib/web_ui/lib/src/engine/platform_views/content_manager.dart +++ b/lib/web_ui/lib/src/engine/platform_views/content_manager.dart @@ -128,8 +128,9 @@ class PlatformViewManager { } _ensureContentCorrectlySized(content, viewType); + wrapper.append(content); - return wrapper..append(content); + return wrapper; }); } diff --git a/lib/web_ui/lib/src/engine/semantics/text_field.dart b/lib/web_ui/lib/src/engine/semantics/text_field.dart index 9552707bcb6a0..1e5c08a5e6e93 100644 --- a/lib/web_ui/lib/src/engine/semantics/text_field.dart +++ b/lib/web_ui/lib/src/engine/semantics/text_field.dart @@ -424,14 +424,14 @@ class TextField extends RoleManager { ..height = '${semanticsObject.rect!.height}px'; if (semanticsObject.hasFocus) { - if (flutterViewEmbedder.glassPaneShadow.activeElement != + if (domDocument.activeElement != activeEditableElement) { semanticsObject.owner.addOneTimePostUpdateCallback(() { activeEditableElement.focus(); }); } SemanticsTextEditingStrategy.instance.activate(this); - } else if (flutterViewEmbedder.glassPaneShadow.activeElement == + } else if (domDocument.activeElement == activeEditableElement) { if (!isIosSafari) { SemanticsTextEditingStrategy.instance.deactivate(this); diff --git a/lib/web_ui/test/engine/semantics/semantics_test.dart b/lib/web_ui/test/engine/semantics/semantics_test.dart index 63f70a6a06d40..b828b7302210f 100644 --- a/lib/web_ui/test/engine/semantics/semantics_test.dart +++ b/lib/web_ui/test/engine/semantics/semantics_test.dart @@ -156,18 +156,16 @@ void _testEngineSemanticsOwner() { expect(semantics().semanticsEnabled, isFalse); // Synthesize a click on the placeholder. - final DomElement placeholder = - appHostNode.querySelector('flt-semantics-placeholder')!; + final DomElement placeholder = flutterViewEmbedder.glassPaneShadow + .querySelector('flt-semantics-placeholder')!; expect(placeholder.isConnected, isTrue); final DomRect rect = placeholder.getBoundingClientRect(); - placeholder.dispatchEvent(createDomMouseEvent( - 'click', { - 'clientX': (rect.left + (rect.right - rect.left) / 2).floor(), - 'clientY': (rect.top + (rect.bottom - rect.top) / 2).floor(), - } - )); + placeholder.dispatchEvent(createDomMouseEvent('click', { + 'clientX': (rect.left + (rect.right - rect.left) / 2).floor(), + 'clientY': (rect.top + (rect.bottom - rect.top) / 2).floor(), + })); // On mobile semantics is enabled asynchronously. if (isMobile) { @@ -181,7 +179,8 @@ void _testEngineSemanticsOwner() { test('accessibilityFeatures copyWith function works', () { const EngineAccessibilityFeatures original = EngineAccessibilityFeatures(0); - EngineAccessibilityFeatures copy = original.copyWith(accessibleNavigation: true); + EngineAccessibilityFeatures copy = + original.copyWith(accessibleNavigation: true); expect(copy.accessibleNavigation, true); expect(copy.boldText, false); expect(copy.disableAnimations, false); @@ -253,8 +252,8 @@ void _testEngineSemanticsOwner() { .instance.configuration.accessibilityFeatures.accessibleNavigation, isFalse); - final DomElement placeholder = - appHostNode.querySelector('flt-semantics-placeholder')!; + final DomElement placeholder = flutterViewEmbedder.glassPaneShadow + .querySelector('flt-semantics-placeholder')!; expect(placeholder.isConnected, isTrue); @@ -427,7 +426,8 @@ void _testEngineSemanticsOwner() { ); }); - test('forwards events to framework if shouldEnableSemantics returns true', () { + test('forwards events to framework if shouldEnableSemantics returns true', + () { final MockSemanticsEnabler mockSemanticsEnabler = MockSemanticsEnabler(); semantics().semanticsHelper.semanticsEnabler = mockSemanticsEnabler; final DomEvent pointerEvent = createDomEvent('Event', 'pointermove'); @@ -438,8 +438,7 @@ void _testEngineSemanticsOwner() { class MockSemanticsEnabler implements SemanticsEnabler { @override - void dispose() { - } + void dispose() {} @override bool get isWaitingToEnableSemantics => throw UnimplementedError(); @@ -715,7 +714,8 @@ void _testContainer() { semantics().semanticsEnabled = false; }); - test('renders in traversal order, hit-tests in reverse z-index order', () async { + test('renders in traversal order, hit-tests in reverse z-index order', + () async { semantics() ..debugOverrideTimestampFunction(() => _testTime) ..semanticsEnabled = true; @@ -808,7 +808,9 @@ void _testContainer() { semantics().semanticsEnabled = false; }); - test('container nodes are transparent and leaf children are opaque hit-test wise', () async { + test( + 'container nodes are transparent and leaf children are opaque hit-test wise', + () async { semantics() ..debugOverrideTimestampFunction(() => _testTime) ..semanticsEnabled = true; @@ -834,10 +836,12 @@ void _testContainer() { final DomElement root = appHostNode.querySelector('#flt-semantic-node-0')!; expect(root.style.pointerEvents, 'none'); - final DomElement child1 = appHostNode.querySelector('#flt-semantic-node-1')!; + final DomElement child1 = + appHostNode.querySelector('#flt-semantic-node-1')!; expect(child1.style.pointerEvents, 'all'); - final DomElement child2 = appHostNode.querySelector('#flt-semantic-node-2')!; + final DomElement child2 = + appHostNode.querySelector('#flt-semantic-node-2')!; expect(child2.style.pointerEvents, 'all'); semantics().semanticsEnabled = false; @@ -1178,8 +1182,8 @@ void _testIncrementables() { '''); - final DomHTMLInputElement input = appHostNode.querySelector('input')! as - DomHTMLInputElement; + final DomHTMLInputElement input = + appHostNode.querySelector('input')! as DomHTMLInputElement; input.value = '2'; input.dispatchEvent(createDomEvent('Event', 'change')); @@ -1211,8 +1215,8 @@ void _testIncrementables() { '''); - final DomHTMLInputElement input = appHostNode.querySelector('input')! as - DomHTMLInputElement; + final DomHTMLInputElement input = + appHostNode.querySelector('input')! as DomHTMLInputElement; input.value = '0'; input.dispatchEvent(createDomEvent('Event', 'change')); @@ -1298,11 +1302,11 @@ void _testTextField() { final DomElement textField = appHostNode.querySelector('input[data-semantics-role="text-field"]')!; - expect(appHostNode.activeElement, isNot(textField)); + expect(appHostNode.ownerDocument?.activeElement, isNot(textField)); textField.focus(); - expect(appHostNode.activeElement, textField); + expect(appHostNode.ownerDocument?.activeElement, textField); expect(await logger.idLog.first, 0); expect(await logger.actionLog.first, ui.SemanticsAction.tap); @@ -1615,13 +1619,15 @@ void _testTappable() { } updateTappable(enabled: false); - expectSemanticsTree(''); + expectSemanticsTree( + ''); updateTappable(enabled: true); expectSemanticsTree(''); updateTappable(enabled: false); - expectSemanticsTree(''); + expectSemanticsTree( + ''); updateTappable(enabled: true); expectSemanticsTree(''); @@ -1646,7 +1652,7 @@ void _testTappable() { ); tester.apply(); - expect(flutterViewEmbedder.glassPaneShadow.activeElement, tester.getSemanticsObject(0).element); + expect(domDocument.activeElement, tester.getSemanticsObject(0).element); semantics().semanticsEnabled = false; }); } @@ -1941,13 +1947,13 @@ void _testPlatformView() { ui.window.render(sceneBuilder.build()); final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder(); - updateNode( - builder, - rect: const ui.Rect.fromLTRB(0, 0, 20, 60), - childrenInTraversalOrder: Int32List.fromList([1, 2, 3]), - childrenInHitTestOrder: Int32List.fromList([1, 2, 3]), - transform: Float64List.fromList(Matrix4.diagonal3Values(ui.window.devicePixelRatio, ui.window.devicePixelRatio, 1).storage) - ); + updateNode(builder, + rect: const ui.Rect.fromLTRB(0, 0, 20, 60), + childrenInTraversalOrder: Int32List.fromList([1, 2, 3]), + childrenInHitTestOrder: Int32List.fromList([1, 2, 3]), + transform: Float64List.fromList(Matrix4.diagonal3Values( + ui.window.devicePixelRatio, ui.window.devicePixelRatio, 1) + .storage)); updateNode( builder, id: 1, @@ -2008,7 +2014,8 @@ void _testPlatformView() { final DomElement platformViewElement = flutterViewEmbedder.glassPaneElement.querySelector('#view-0')!; - final DomRect platformViewRect = platformViewElement.getBoundingClientRect(); + final DomRect platformViewRect = + platformViewElement.getBoundingClientRect(); expect(platformViewRect.left, 0); expect(platformViewRect.top, 15); expect(platformViewRect.right, 20); @@ -2016,14 +2023,15 @@ void _testPlatformView() { // This test is only relevant for shadow DOM because we only really support // proper platform view embedding in browsers that support shadow DOM. - final DomShadowRoot shadowRoot = appHostNode.node as DomShadowRoot; + // final DomShadowRoot shadowRoot = + // flutterViewEmbedder.glassPaneShadow.node as DomShadowRoot; // Hit test child 1 - expect(shadowRoot.elementFromPoint(10, 10), child1); + expect(domDocument.elementFromPoint(10, 10), child1); // Hit test overlap between child 1 and 2 // TODO(yjbanov): this is a known limitation, see https://github.com/flutter/flutter/issues/101439 - expect(shadowRoot.elementFromPoint(10, 20), child1); + expect(domDocument.elementFromPoint(10, 20), child1); // Hit test child 2 // Clicking at the location of the middle semantics node should allow the @@ -2042,10 +2050,10 @@ void _testPlatformView() { expect(domDocument.elementFromPoint(10, 30), platformViewElement); // Hit test overlap between child 2 and 3 - expect(shadowRoot.elementFromPoint(10, 40), child3); + expect(domDocument.elementFromPoint(10, 40), child3); // Hit test child 3 - expect(shadowRoot.elementFromPoint(10, 50), child3); + expect(domDocument.elementFromPoint(10, 50), child3); semantics().semanticsEnabled = false; }); @@ -2110,9 +2118,11 @@ void updateNode( String value = '', List valueAttributes = const [], String increasedValue = '', - List increasedValueAttributes = const [], + List increasedValueAttributes = + const [], String decreasedValue = '', - List decreasedValueAttributes = const [], + List decreasedValueAttributes = + const [], String tooltip = '', ui.TextDirection textDirection = ui.TextDirection.ltr, Float64List? transform, diff --git a/lib/web_ui/test/engine/semantics/semantics_tester.dart b/lib/web_ui/test/engine/semantics/semantics_tester.dart index cee9eead3d3b0..e65d97ad48349 100644 --- a/lib/web_ui/test/engine/semantics/semantics_tester.dart +++ b/lib/web_ui/test/engine/semantics/semantics_tester.dart @@ -19,10 +19,11 @@ import '../../matchers.dart'; /// Gets the DOM host where the Flutter app is being rendered. /// /// This function returns the correct host for the flutter app under testing, -/// so we don't have to hardcode domDocument across the test. (The host of a -/// normal flutter app used to be domDocument, but now that the app is wrapped -/// in a Shadow DOM, that's not the case anymore.) -HostNode get appHostNode => flutterViewEmbedder.glassPaneShadow; +/// so we don't have to hardcode domDocument across the test. The semantics +/// tree has moved outside of the shadowDOM as a workaround for a password +/// autofill bug on Chrome. +/// Ref: https://github.com/flutter/flutter/issues/87735 +DomElement get appHostNode => flutterViewEmbedder.glassPaneElement; /// CSS style applied to the root of the semantics tree. // TODO(yjbanov): this should be handled internally by [expectSemanticsTree]. From ae00e409af92bc5d3c4d7ba7fa930839b42e2db7 Mon Sep 17 00:00:00 2001 From: Hassan Toor Date: Thu, 2 Mar 2023 18:23:15 -0600 Subject: [PATCH 07/15] Fix text field tests --- .../engine/semantics/text_field_test.dart | 87 ++++++++----------- 1 file changed, 38 insertions(+), 49 deletions(-) diff --git a/lib/web_ui/test/engine/semantics/text_field_test.dart b/lib/web_ui/test/engine/semantics/text_field_test.dart index c7dd2805cbdfa..67a2322ac2ed8 100644 --- a/lib/web_ui/test/engine/semantics/text_field_test.dart +++ b/lib/web_ui/test/engine/semantics/text_field_test.dart @@ -73,11 +73,11 @@ void testMain() { final DomElement textField = appHostNode .querySelector('input[data-semantics-role="text-field"]')!; - expect(appHostNode.activeElement, isNot(textField)); + expect(appHostNode.ownerDocument?.activeElement, isNot(textField)); textField.focus(); - expect(appHostNode.activeElement, textField); + expect(appHostNode.ownerDocument?.activeElement, textField); expect(await logger.idLog.first, 0); expect(await logger.actionLog.first, ui.SemanticsAction.tap); }, // TODO(yjbanov): https://github.com/flutter/flutter/issues/46638 @@ -86,8 +86,7 @@ void testMain() { skip: browserEngine != BrowserEngine.blink); test('Syncs semantic state from framework', () { - expect(domDocument.activeElement, domDocument.body); - expect(appHostNode.activeElement, null); + expect(appHostNode.ownerDocument?.activeElement, domDocument.body); int changeCount = 0; int actionCount = 0; @@ -111,8 +110,7 @@ void testMain() { final TextField textField = textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField; - expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); - expect(appHostNode.activeElement, strategy.domElement); + expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); expect(textField.editableElement, strategy.domElement); expect(textField.activeEditableElement.getAttribute('aria-label'), 'greeting'); expect(textField.activeEditableElement.style.width, '10px'); @@ -125,8 +123,7 @@ void testMain() { rect: const ui.Rect.fromLTWH(0, 0, 12, 17), ); - expect(domDocument.activeElement, domDocument.body); - expect(appHostNode.activeElement, null); + expect(appHostNode.ownerDocument?.activeElement, domDocument.body); expect(strategy.domElement, null); expect(textField.activeEditableElement.getAttribute('aria-label'), 'farewell'); expect(textField.activeEditableElement.style.width, '12px'); @@ -172,8 +169,7 @@ void testMain() { test( 'Updates editing state when receiving framework messages from the text input channel', () { - expect(domDocument.activeElement, domDocument.body); - expect(appHostNode.activeElement, null); + expect(appHostNode.ownerDocument?.activeElement, domDocument.body); strategy.enable( singlelineConfig, @@ -217,8 +213,7 @@ void testMain() { }); test('Gives up focus after DOM blur', () { - expect(domDocument.activeElement, domDocument.body); - expect(appHostNode.activeElement, null); + expect(appHostNode.ownerDocument?.activeElement, domDocument.body); strategy.enable( singlelineConfig, @@ -233,13 +228,11 @@ void testMain() { final TextField textField = textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField; expect(textField.editableElement, strategy.domElement); - expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); - expect(appHostNode.activeElement, strategy.domElement); + expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); // The input should not refocus after blur. textField.activeEditableElement.blur(); - expect(domDocument.activeElement, domDocument.body); - expect(appHostNode.activeElement, null); + expect(appHostNode.ownerDocument?.activeElement, domDocument.body); strategy.disable(); }); @@ -259,8 +252,7 @@ void testMain() { isFocused: true, ); expect(strategy.domElement, isNotNull); - expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); - expect(appHostNode.activeElement, strategy.domElement); + expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); strategy.disable(); expect(strategy.domElement, isNull); @@ -271,8 +263,7 @@ void testMain() { expect(appHostNode.contains(textField.editableElement), isTrue); // Editing element is not enabled. expect(strategy.isEnabled, isFalse); - expect(domDocument.activeElement, domDocument.body); - expect(appHostNode.activeElement, null); + expect(appHostNode.ownerDocument?.activeElement, domDocument.body); }); test('Refocuses when setting editing state', () { @@ -287,13 +278,11 @@ void testMain() { isFocused: true, ); expect(strategy.domElement, isNotNull); - expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); - expect(appHostNode.activeElement, strategy.domElement); + expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); // Blur the element without telling the framework. strategy.activeDomElement.blur(); - expect(domDocument.activeElement, domDocument.body); - expect(appHostNode.activeElement, null); + expect(appHostNode.ownerDocument?.activeElement, domDocument.body); // The input will have focus after editing state is set and semantics updated. strategy.setEditingState(EditingState(text: 'foo')); @@ -311,8 +300,7 @@ void testMain() { value: 'hello', isFocused: true, ); - expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); - expect(appHostNode.activeElement, strategy.domElement); + expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); strategy.disable(); }); @@ -332,8 +320,7 @@ void testMain() { final DomHTMLTextAreaElement textArea = strategy.domElement! as DomHTMLTextAreaElement; - expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); - expect(appHostNode.activeElement, strategy.domElement); + expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); strategy.enable( singlelineConfig, @@ -342,8 +329,7 @@ void testMain() { ); textArea.blur(); - expect(domDocument.activeElement, domDocument.body); - expect(appHostNode.activeElement, null); + expect(appHostNode.ownerDocument?.activeElement, domDocument.body); strategy.disable(); // It doesn't remove the textarea from the DOM. @@ -427,13 +413,14 @@ void testMain() { createTwoFieldSemantics(tester, focusFieldId: 1); expect(tester.apply().length, 3); - expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); - expect(appHostNode.activeElement, tester.getTextField(1).editableElement); + expect(appHostNode.ownerDocument?.activeElement, + tester.getTextField(1).editableElement); expect(strategy.domElement, tester.getTextField(1).editableElement); createTwoFieldSemantics(tester, focusFieldId: 2); expect(tester.apply().length, 3); - expect(appHostNode.activeElement, tester.getTextField(2).editableElement); + expect(appHostNode.ownerDocument?.activeElement, + tester.getTextField(2).editableElement); expect(strategy.domElement, tester.getTextField(2).editableElement); } }); @@ -482,7 +469,7 @@ void testMain() { test('Syncs semantic state from framework', () { expect(domDocument.activeElement, domDocument.body); - expect(appHostNode.activeElement, null); + expect(appHostNode.ownerDocument?.activeElement, null); int changeCount = 0; int actionCount = 0; @@ -507,7 +494,7 @@ void testMain() { textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField; expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); - expect(appHostNode.activeElement, strategy.domElement); + expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); expect(textField.editableElement, strategy.domElement); expect(textField.activeEditableElement.getAttribute('aria-label'), 'greeting'); expect(textField.activeEditableElement.style.width, '10px'); @@ -524,7 +511,7 @@ void testMain() { expect(strategy.domElement, null); expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); - expect(appHostNode.activeElement, textBox); + expect(appHostNode.ownerDocument?.activeElement, textBox); expect(textBox.getAttribute('aria-label'), 'farewell'); strategy.disable(); @@ -568,7 +555,7 @@ void testMain() { 'Updates editing state when receiving framework messages from the text input channel', () { expect(domDocument.activeElement, domDocument.body); - expect(appHostNode.activeElement, null); + expect(appHostNode.ownerDocument?.activeElement, null); strategy.enable( singlelineConfig, @@ -613,7 +600,7 @@ void testMain() { test('Gives up focus after DOM blur', () { expect(domDocument.activeElement, domDocument.body); - expect(appHostNode.activeElement, null); + expect(appHostNode.ownerDocument?.activeElement, null); strategy.enable( singlelineConfig, @@ -629,14 +616,14 @@ void testMain() { expect(textField.editableElement, strategy.domElement); expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); - expect(appHostNode.activeElement, strategy.domElement); + expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); // The input should not refocus after blur. textField.activeEditableElement.blur(); final DomElement textBox = appHostNode.querySelector('flt-semantics[role="textbox"]')!; expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); - expect(appHostNode.activeElement, textBox); + expect(appHostNode.ownerDocument?.activeElement, textBox); strategy.disable(); }); @@ -658,7 +645,7 @@ void testMain() { ); expect(strategy.domElement, isNotNull); expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); - expect(appHostNode.activeElement, strategy.domElement); + expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); strategy.disable(); expect(strategy.domElement, isNull); @@ -672,7 +659,7 @@ void testMain() { final DomElement textBox = appHostNode.querySelector('flt-semantics[role="textbox"]')!; expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); - expect(appHostNode.activeElement, textBox); + expect(appHostNode.ownerDocument?.activeElement, textBox); }); test('Refocuses when setting editing state', () { @@ -688,14 +675,14 @@ void testMain() { ); expect(strategy.domElement, isNotNull); expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); - expect(appHostNode.activeElement, strategy.domElement); + expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); // Blur the element without telling the framework. strategy.activeDomElement.blur(); final DomElement textBox = appHostNode.querySelector('flt-semantics[role="textbox"]')!; expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); - expect(appHostNode.activeElement, textBox); + expect(appHostNode.ownerDocument?.activeElement, textBox); // The input will have focus after editing state is set and semantics updated. strategy.setEditingState(EditingState(text: 'foo')); @@ -714,7 +701,7 @@ void testMain() { isFocused: true, ); expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); - expect(appHostNode.activeElement, strategy.domElement); + expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); strategy.disable(); }); @@ -733,7 +720,7 @@ void testMain() { final DomHTMLTextAreaElement textArea = strategy.domElement! as DomHTMLTextAreaElement; expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); - expect(appHostNode.activeElement, strategy.domElement); + expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); strategy.enable( singlelineConfig, @@ -748,7 +735,7 @@ void testMain() { appHostNode.querySelector('flt-semantics[role="textbox"]')!; expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); - expect(appHostNode.activeElement, textBox); + expect(appHostNode.ownerDocument?.activeElement, textBox); strategy.disable(); // It removes the textarea from the DOM. @@ -812,12 +799,14 @@ void testMain() { expect(tester.apply().length, 3); expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); - expect(appHostNode.activeElement, tester.getTextField(1).editableElement); + expect(appHostNode.ownerDocument?.activeElement, + tester.getTextField(1).editableElement); expect(strategy.domElement, tester.getTextField(1).editableElement); createTwoFieldSemanticsForIos(tester, focusFieldId: 2); expect(tester.apply().length, 3); - expect(appHostNode.activeElement, tester.getTextField(2).editableElement); + expect(appHostNode.ownerDocument?.activeElement, + tester.getTextField(2).editableElement); expect(strategy.domElement, tester.getTextField(2).editableElement); } }); From 3b6f9eb2e35178dd6f35f3711e3930d0a2aa9478 Mon Sep 17 00:00:00 2001 From: Hassan Toor Date: Fri, 3 Mar 2023 10:51:14 -0600 Subject: [PATCH 08/15] Remove unused imports --- lib/web_ui/lib/src/engine/semantics/text_field.dart | 1 - lib/web_ui/test/engine/semantics/semantics_tester.dart | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/web_ui/lib/src/engine/semantics/text_field.dart b/lib/web_ui/lib/src/engine/semantics/text_field.dart index 1e5c08a5e6e93..f16b41525937b 100644 --- a/lib/web_ui/lib/src/engine/semantics/text_field.dart +++ b/lib/web_ui/lib/src/engine/semantics/text_field.dart @@ -7,7 +7,6 @@ import 'package:ui/ui.dart' as ui; import '../browser_detection.dart'; import '../dom.dart'; -import '../embedder.dart'; import '../platform_dispatcher.dart'; import '../safe_browser_api.dart'; import '../text_editing/text_editing.dart'; diff --git a/lib/web_ui/test/engine/semantics/semantics_tester.dart b/lib/web_ui/test/engine/semantics/semantics_tester.dart index e65d97ad48349..73f079240dfed 100644 --- a/lib/web_ui/test/engine/semantics/semantics_tester.dart +++ b/lib/web_ui/test/engine/semantics/semantics_tester.dart @@ -8,7 +8,6 @@ import 'dart:typed_data'; import 'package:test/test.dart'; import 'package:ui/src/engine/dom.dart'; import 'package:ui/src/engine/embedder.dart'; -import 'package:ui/src/engine/host_node.dart'; import 'package:ui/src/engine/semantics.dart'; import 'package:ui/src/engine/util.dart'; import 'package:ui/src/engine/vector_math.dart'; From 4f75b6b29a4afc22d7baea1cabae13585d485fe0 Mon Sep 17 00:00:00 2001 From: Hassan Toor Date: Fri, 3 Mar 2023 12:28:09 -0600 Subject: [PATCH 09/15] Fix host node tests --- lib/web_ui/test/engine/host_node_test.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/web_ui/test/engine/host_node_test.dart b/lib/web_ui/test/engine/host_node_test.dart index 205bdafa9f4a1..20c411b47af9c 100644 --- a/lib/web_ui/test/engine/host_node_test.dart +++ b/lib/web_ui/test/engine/host_node_test.dart @@ -16,19 +16,20 @@ void testMain() { group('ShadowDomHostNode', () { final HostNode hostNode = ShadowDomHostNode(rootNode, '14px monospace'); + final DomElement renderHost = domDocument.querySelector('flt-render-host')!; test('Initializes and attaches a shadow root', () { expect(domInstanceOfString(hostNode.node, 'ShadowRoot'), isTrue); - expect((hostNode.node as DomShadowRoot).host, rootNode); - expect(hostNode.node, rootNode.shadowRoot); + expect((hostNode.node as DomShadowRoot).host, renderHost); + expect(hostNode.node, renderHost.shadowRoot); // The shadow root should be initialized with correct parameters. - expect(rootNode.shadowRoot!.mode, 'open'); + expect(renderHost.shadowRoot!.mode, 'open'); if (browserEngine != BrowserEngine.firefox && browserEngine != BrowserEngine.webkit) { // Older versions of Safari and Firefox don't support this flag yet. // See: https://caniuse.com/mdn-api_shadowroot_delegatesfocus - expect(rootNode.shadowRoot!.delegatesFocus, isFalse); + expect(renderHost.shadowRoot!.delegatesFocus, isFalse); } }); From df91ed52e94cd168504f1e15b863894592a49220 Mon Sep 17 00:00:00 2001 From: Hassan Toor Date: Fri, 3 Mar 2023 13:03:46 -0600 Subject: [PATCH 10/15] Refactor, fix safari text field tests --- lib/web_ui/lib/src/engine/embedder.dart | 4 ++-- .../src/engine/text_editing/text_editing.dart | 2 +- .../engine/semantics/text_field_test.dart | 24 ++++--------------- 3 files changed, 8 insertions(+), 22 deletions(-) diff --git a/lib/web_ui/lib/src/engine/embedder.dart b/lib/web_ui/lib/src/engine/embedder.dart index 831388a1a77e2..8e67d1f044897 100644 --- a/lib/web_ui/lib/src/engine/embedder.dart +++ b/lib/web_ui/lib/src/engine/embedder.dart @@ -124,8 +124,8 @@ class FlutterViewEmbedder { HostNode get glassPaneShadow => _glassPaneShadow; late HostNode _glassPaneShadow; - DomElement? get textEditingHostNode => _textEditingHostNode; - DomElement? _textEditingHostNode; + DomElement get textEditingHostNode => _textEditingHostNode; + late DomElement _textEditingHostNode; static const String defaultFontStyle = 'normal'; static const String defaultFontWeight = 'normal'; 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 efd9e93bf037c..7256907a45c2d 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 @@ -52,7 +52,7 @@ void _emptyCallback(dynamic _) {} /// The default [HostNode] that hosts all DOM required for text editing when a11y is not enabled. @visibleForTesting DomElement get defaultTextEditingRoot => - flutterViewEmbedder.textEditingHostNode!; + flutterViewEmbedder.textEditingHostNode; /// These style attributes are constant throughout the life time of an input /// element. diff --git a/lib/web_ui/test/engine/semantics/text_field_test.dart b/lib/web_ui/test/engine/semantics/text_field_test.dart index 67a2322ac2ed8..2c6d1031f7702 100644 --- a/lib/web_ui/test/engine/semantics/text_field_test.dart +++ b/lib/web_ui/test/engine/semantics/text_field_test.dart @@ -468,8 +468,7 @@ void testMain() { }); test('Syncs semantic state from framework', () { - expect(domDocument.activeElement, domDocument.body); - expect(appHostNode.ownerDocument?.activeElement, null); + expect(appHostNode.ownerDocument?.activeElement, domDocument.body); int changeCount = 0; int actionCount = 0; @@ -493,7 +492,6 @@ void testMain() { final TextField textField = textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField; - expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); expect(textField.editableElement, strategy.domElement); expect(textField.activeEditableElement.getAttribute('aria-label'), 'greeting'); @@ -510,7 +508,6 @@ void testMain() { appHostNode.querySelector('flt-semantics[role="textbox"]')!; expect(strategy.domElement, null); - expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); expect(appHostNode.ownerDocument?.activeElement, textBox); expect(textBox.getAttribute('aria-label'), 'farewell'); @@ -554,8 +551,7 @@ void testMain() { test( 'Updates editing state when receiving framework messages from the text input channel', () { - expect(domDocument.activeElement, domDocument.body); - expect(appHostNode.ownerDocument?.activeElement, null); + expect(appHostNode.ownerDocument?.activeElement, domDocument.body); strategy.enable( singlelineConfig, @@ -599,8 +595,7 @@ void testMain() { }); test('Gives up focus after DOM blur', () { - expect(domDocument.activeElement, domDocument.body); - expect(appHostNode.ownerDocument?.activeElement, null); + expect(appHostNode.ownerDocument?.activeElement, domDocument.body); strategy.enable( singlelineConfig, @@ -615,14 +610,12 @@ void testMain() { textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField; expect(textField.editableElement, strategy.domElement); - expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); // The input should not refocus after blur. textField.activeEditableElement.blur(); final DomElement textBox = appHostNode.querySelector('flt-semantics[role="textbox"]')!; - expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); expect(appHostNode.ownerDocument?.activeElement, textBox); strategy.disable(); @@ -644,7 +637,6 @@ void testMain() { isFocused: true, ); expect(strategy.domElement, isNotNull); - expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); strategy.disable(); @@ -658,7 +650,6 @@ void testMain() { // Focus is on the semantic object final DomElement textBox = appHostNode.querySelector('flt-semantics[role="textbox"]')!; - expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); expect(appHostNode.ownerDocument?.activeElement, textBox); }); @@ -674,14 +665,12 @@ void testMain() { isFocused: true, ); expect(strategy.domElement, isNotNull); - expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); // Blur the element without telling the framework. strategy.activeDomElement.blur(); final DomElement textBox = appHostNode.querySelector('flt-semantics[role="textbox"]')!; - expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); expect(appHostNode.ownerDocument?.activeElement, textBox); // The input will have focus after editing state is set and semantics updated. @@ -700,7 +689,6 @@ void testMain() { value: 'hello', isFocused: true, ); - expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); strategy.disable(); @@ -718,8 +706,8 @@ void testMain() { isMultiline: true, ); - final DomHTMLTextAreaElement textArea = strategy.domElement! as DomHTMLTextAreaElement; - expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); + final DomHTMLTextAreaElement textArea = + strategy.domElement! as DomHTMLTextAreaElement; expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); strategy.enable( @@ -734,7 +722,6 @@ void testMain() { final DomElement textBox = appHostNode.querySelector('flt-semantics[role="textbox"]')!; - expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); expect(appHostNode.ownerDocument?.activeElement, textBox); strategy.disable(); @@ -798,7 +785,6 @@ void testMain() { createTwoFieldSemanticsForIos(tester, focusFieldId: 1); expect(tester.apply().length, 3); - expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); expect(appHostNode.ownerDocument?.activeElement, tester.getTextField(1).editableElement); expect(strategy.domElement, tester.getTextField(1).editableElement); From d8f96cde5d92d9b1d1b0ce6252d85efedbad69cf Mon Sep 17 00:00:00 2001 From: Hassan Toor Date: Tue, 7 Mar 2023 10:01:29 -0600 Subject: [PATCH 11/15] Refactor, fix indentation, remove comments --- lib/web_ui/lib/src/engine/embedder.dart | 21 ++++++++++--------- .../test/engine/semantics/semantics_test.dart | 5 ----- .../engine/semantics/text_field_test.dart | 4 ++-- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/lib/web_ui/lib/src/engine/embedder.dart b/lib/web_ui/lib/src/engine/embedder.dart index 8e67d1f044897..16ad2ee196d25 100644 --- a/lib/web_ui/lib/src/engine/embedder.dart +++ b/lib/web_ui/lib/src/engine/embedder.dart @@ -195,19 +195,20 @@ class FlutterViewEmbedder { glassPaneElementHostNode.appendAll([ accessibilityPlaceholder, _sceneHostElement!, - // The semantic host goes last because hit-test order-wise it must be - // first. If semantics goes under the scene host, platform views will - // obscure semantic elements. - // - // You may be wondering: wouldn't semantics obscure platform views and - // make then not accessible? At least with some careful planning, that - // should not be the case. The semantics tree makes all of its non-leaf - // elements transparent. This way, if a platform view appears among other - // interactive Flutter widgets, as long as those widgets do not intersect - // with the platform view, the platform view will be reachable. ]); + // The semantic host goes last because hit-test order-wise it must be + // first. If semantics goes under the scene host, platform views will + // obscure semantic elements. + // + // You may be wondering: wouldn't semantics obscure platform views and + // make then not accessible? At least with some careful planning, that + // should not be the case. The semantics tree makes all of its non-leaf + // elements transparent. This way, if a platform view appears among other + // interactive Flutter widgets, as long as those widgets do not intersect + // with the platform view, the platform view will be reachable. glassPaneElement.appendChild(semanticsHostElement); + // When debugging semantics, make the scene semi-transparent so that the // semantics tree is more prominent. if (configuration.debugShowSemanticsNodes) { diff --git a/lib/web_ui/test/engine/semantics/semantics_test.dart b/lib/web_ui/test/engine/semantics/semantics_test.dart index 42a6df2b392bf..544ffe72dc3e3 100644 --- a/lib/web_ui/test/engine/semantics/semantics_test.dart +++ b/lib/web_ui/test/engine/semantics/semantics_test.dart @@ -2021,11 +2021,6 @@ void _testPlatformView() { expect(platformViewRect.right, 20); expect(platformViewRect.bottom, 45); - // This test is only relevant for shadow DOM because we only really support - // proper platform view embedding in browsers that support shadow DOM. - // final DomShadowRoot shadowRoot = - // flutterViewEmbedder.glassPaneShadow.node as DomShadowRoot; - // Hit test child 1 expect(domDocument.elementFromPoint(10, 10), child1); diff --git a/lib/web_ui/test/engine/semantics/text_field_test.dart b/lib/web_ui/test/engine/semantics/text_field_test.dart index 2c6d1031f7702..c7393c953accc 100644 --- a/lib/web_ui/test/engine/semantics/text_field_test.dart +++ b/lib/web_ui/test/engine/semantics/text_field_test.dart @@ -73,11 +73,11 @@ void testMain() { final DomElement textField = appHostNode .querySelector('input[data-semantics-role="text-field"]')!; - expect(appHostNode.ownerDocument?.activeElement, isNot(textField)); + expect(appHostNode.ownerDocument?.activeElement, isNot(textField)); textField.focus(); - expect(appHostNode.ownerDocument?.activeElement, textField); + expect(appHostNode.ownerDocument?.activeElement, textField); expect(await logger.idLog.first, 0); expect(await logger.actionLog.first, ui.SemanticsAction.tap); }, // TODO(yjbanov): https://github.com/flutter/flutter/issues/46638 From cc4ed2381c2104662da617166715b226be2dfe16 Mon Sep 17 00:00:00 2001 From: Hassan Toor Date: Tue, 7 Mar 2023 10:02:55 -0600 Subject: [PATCH 12/15] Trailing whitespace --- lib/web_ui/lib/src/engine/embedder.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/embedder.dart b/lib/web_ui/lib/src/engine/embedder.dart index 16ad2ee196d25..a05b1e0688b7d 100644 --- a/lib/web_ui/lib/src/engine/embedder.dart +++ b/lib/web_ui/lib/src/engine/embedder.dart @@ -208,7 +208,7 @@ class FlutterViewEmbedder { // interactive Flutter widgets, as long as those widgets do not intersect // with the platform view, the platform view will be reachable. glassPaneElement.appendChild(semanticsHostElement); - + // When debugging semantics, make the scene semi-transparent so that the // semantics tree is more prominent. if (configuration.debugShowSemanticsNodes) { From 97555e5c6566f86c240e0f2841bc6b2b7caff345 Mon Sep 17 00:00:00 2001 From: Hassan Toor Date: Mon, 20 Mar 2023 11:14:35 -0500 Subject: [PATCH 13/15] Move conditional to compute talkback events when semantics enabled --- .../src/engine/pointer_binding/event_position_helper.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart b/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart index 6be2b9ccc2bcb..9ae0b5acad440 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart @@ -19,14 +19,14 @@ import '../semantics.dart' show EngineSemanticsOwner; /// It also takes into account semantics being enabled to fix the case where /// offsetX, offsetY == 0 (TalkBack events). ui.Offset computeEventOffsetToTarget(DomMouseEvent event, DomElement actualTarget) { - // On top of a platform view - if (event.target != actualTarget) { - return _computeOffsetOnPlatformView(event, actualTarget); - } // On a TalkBack event if (EngineSemanticsOwner.instance.semanticsEnabled && event.offsetX == 0 && event.offsetY == 0) { return _computeOffsetForTalkbackEvent(event, actualTarget); } + // On top of a platform view + if (event.target != actualTarget) { + return _computeOffsetOnPlatformView(event, actualTarget); + } // Return the offsetX/Y in the normal case. // (This works with 3D translations of the parent element.) return ui.Offset(event.offsetX, event.offsetY); From 401bb1c367e0b1de7eaa436c7931a9cf106de29e Mon Sep 17 00:00:00 2001 From: Hassan Toor Date: Tue, 28 Mar 2023 17:08:09 -0500 Subject: [PATCH 14/15] Change function naming and comments for offset computations --- .../pointer_binding/event_position_helper.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart b/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart index 9ae0b5acad440..7bb15f9a07027 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart @@ -23,16 +23,18 @@ ui.Offset computeEventOffsetToTarget(DomMouseEvent event, DomElement actualTarge if (EngineSemanticsOwner.instance.semanticsEnabled && event.offsetX == 0 && event.offsetY == 0) { return _computeOffsetForTalkbackEvent(event, actualTarget); } - // On top of a platform view - if (event.target != actualTarget) { - return _computeOffsetOnPlatformView(event, actualTarget); + + final bool isTargetOutsideOfShadowDOM = event.target != actualTarget; + if (isTargetOutsideOfShadowDOM) { + return _computeOffsetRelativeToActualTarget(event, actualTarget); } // Return the offsetX/Y in the normal case. // (This works with 3D translations of the parent element.) return ui.Offset(event.offsetX, event.offsetY); } -/// Computes the event offset when hovering over a platformView. +/// Computes the event offset when hovering over any nodes that don't exist in +/// the shadowDOM such as platform views or text editing nodes. /// /// This still uses offsetX/Y, but adds the offset from the top/left corner of the /// platform view to the glass pane (`actualTarget`). @@ -57,7 +59,7 @@ ui.Offset computeEventOffsetToTarget(DomMouseEvent event, DomElement actualTarge /// /// Event offset relative to FlutterView = (offsetX + xP, offsetY + yP) // TODO(dit): Make this understand 3D transforms, https://github.com/flutter/flutter/issues/117091 -ui.Offset _computeOffsetOnPlatformView(DomMouseEvent event, DomElement actualTarget) { +ui.Offset _computeOffsetRelativeToActualTarget(DomMouseEvent event, DomElement actualTarget) { final DomElement target = event.target! as DomElement; final DomRect targetRect = target.getBoundingClientRect(); final DomRect actualTargetRect = actualTarget.getBoundingClientRect(); From 3815cea6843a70499b116a0f24c092f765e8f0d2 Mon Sep 17 00:00:00 2001 From: Hassan Toor Date: Tue, 28 Mar 2023 17:09:49 -0500 Subject: [PATCH 15/15] Whitespace --- .../lib/src/engine/pointer_binding/event_position_helper.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart b/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart index 7bb15f9a07027..1df8ffbcebda3 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart @@ -33,7 +33,7 @@ ui.Offset computeEventOffsetToTarget(DomMouseEvent event, DomElement actualTarge return ui.Offset(event.offsetX, event.offsetY); } -/// Computes the event offset when hovering over any nodes that don't exist in +/// Computes the event offset when hovering over any nodes that don't exist in /// the shadowDOM such as platform views or text editing nodes. /// /// This still uses offsetX/Y, but adds the offset from the top/left corner of the