diff --git a/lib/web_ui/lib/src/engine/embedder.dart b/lib/web_ui/lib/src/engine/embedder.dart index a05b1e0688b7d..d16821e3d9ee0 100644 --- a/lib/web_ui/lib/src/engine/embedder.dart +++ b/lib/web_ui/lib/src/engine/embedder.dart @@ -124,9 +124,6 @@ class FlutterViewEmbedder { HostNode get glassPaneShadow => _glassPaneShadow; late HostNode _glassPaneShadow; - DomElement get textEditingHostNode => _textEditingHostNode; - late DomElement _textEditingHostNode; - static const String defaultFontStyle = 'normal'; static const String defaultFontWeight = 'normal'; static const double defaultFontSize = 14; @@ -171,9 +168,6 @@ 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'; @@ -195,19 +189,19 @@ 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. - glassPaneElement.appendChild(semanticsHostElement); + // 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. + semanticsHostElement, + ]); // When debugging semantics, make the scene semi-transparent so that the // semantics tree is more prominent. @@ -399,24 +393,3 @@ 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 ad7c7cee96017..4b0ca8e13d990 100644 --- a/lib/web_ui/lib/src/engine/host_node.dart +++ b/lib/web_ui/lib/src/engine/host_node.dart @@ -94,8 +94,6 @@ 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 +110,11 @@ 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.') { - root.appendChild(renderHost); - _shadow = renderHost.attachShadow({ + : assert( + root.isConnected ?? true, + 'The `root` of a ShadowDomHostNode must be connected to the Document object or a ShadowRoot.' + ) { + _shadow = root.attachShadow({ 'mode': 'open', // This needs to stay false to prevent issues like this: // - https://github.com/flutter/flutter/issues/85759 @@ -136,9 +135,6 @@ class ShadowDomHostNode implements HostNode { late DomShadowRoot _shadow; - @override - final DomElement renderHost = domDocument.createElement('flt-render-host'); - @override DomElement? get activeElement => _shadow.activeElement; @@ -195,9 +191,6 @@ class ElementHostNode implements HostNode { late DomElement _element; - @override - final DomElement renderHost = domDocument.createElement('flt-render-host'); - @override DomElement? get activeElement => _element.ownerDocument?.activeElement; diff --git a/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/lib/web_ui/lib/src/engine/platform_dispatcher.dart index 4e8697f31f8b7..a0128a0c71383 100644 --- a/lib/web_ui/lib/src/engine/platform_dispatcher.dart +++ b/lib/web_ui/lib/src/engine/platform_dispatcher.dart @@ -587,7 +587,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { _platformViewMessageHandler ??= PlatformViewMessageHandler( contentManager: platformViewManager, contentHandler: (DomElement content) { - flutterViewEmbedder.glassPaneShadow.renderHost.append(content); + flutterViewEmbedder.glassPaneElement.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 28aeb8da0855e..f50d3af3ee670 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,9 +128,8 @@ class PlatformViewManager { } _ensureContentCorrectlySized(content, viewType); - wrapper.append(content); - return wrapper; + return wrapper..append(content); }); } 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 1df8ffbcebda3..6be2b9ccc2bcb 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,22 +19,20 @@ 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); } - - 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 any nodes that don't exist in -/// the shadowDOM such as platform views or text editing nodes. +/// Computes the event offset when hovering over a platformView. /// /// This still uses offsetX/Y, but adds the offset from the top/left corner of the /// platform view to the glass pane (`actualTarget`). @@ -59,7 +57,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 _computeOffsetRelativeToActualTarget(DomMouseEvent event, DomElement actualTarget) { +ui.Offset _computeOffsetOnPlatformView(DomMouseEvent event, DomElement actualTarget) { final DomElement target = event.target! as DomElement; final DomRect targetRect = target.getBoundingClientRect(); final DomRect actualTargetRect = actualTarget.getBoundingClientRect(); 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 4cf1177a84f3f..abf29dd9d6cfd 100644 --- a/lib/web_ui/lib/src/engine/semantics/text_field.dart +++ b/lib/web_ui/lib/src/engine/semantics/text_field.dart @@ -7,6 +7,7 @@ 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'; @@ -421,14 +422,14 @@ class TextField extends RoleManager { ..height = '${semanticsObject.rect!.height}px'; if (semanticsObject.hasFocus) { - if (domDocument.activeElement != + if (flutterViewEmbedder.glassPaneShadow.activeElement != activeEditableElement) { semanticsObject.owner.addOneTimePostUpdateCallback(() { activeEditableElement.focus(); }); } SemanticsTextEditingStrategy._instance?.activate(this); - } else if (domDocument.activeElement == + } else if (flutterViewEmbedder.glassPaneShadow.activeElement == activeEditableElement) { if (!isIosSafari) { SemanticsTextEditingStrategy._instance?.deactivate(this); 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 12e2a72fce210..94e66d8b6e698 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,8 +51,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; +HostNode get defaultTextEditingRoot => flutterViewEmbedder.glassPaneShadow; /// These style attributes are constant throughout the life time of an input /// element. diff --git a/lib/web_ui/test/engine/host_node_test.dart b/lib/web_ui/test/engine/host_node_test.dart index 20c411b47af9c..205bdafa9f4a1 100644 --- a/lib/web_ui/test/engine/host_node_test.dart +++ b/lib/web_ui/test/engine/host_node_test.dart @@ -16,20 +16,19 @@ 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, renderHost); - expect(hostNode.node, renderHost.shadowRoot); + expect((hostNode.node as DomShadowRoot).host, rootNode); + expect(hostNode.node, rootNode.shadowRoot); // The shadow root should be initialized with correct parameters. - expect(renderHost.shadowRoot!.mode, 'open'); + expect(rootNode.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(renderHost.shadowRoot!.delegatesFocus, isFalse); + expect(rootNode.shadowRoot!.delegatesFocus, isFalse); } }); diff --git a/lib/web_ui/test/engine/semantics/semantics_test.dart b/lib/web_ui/test/engine/semantics/semantics_test.dart index e4da65ad22d69..e773cb33dcef3 100644 --- a/lib/web_ui/test/engine/semantics/semantics_test.dart +++ b/lib/web_ui/test/engine/semantics/semantics_test.dart @@ -157,16 +157,18 @@ void _testEngineSemanticsOwner() { expect(semantics().semanticsEnabled, isFalse); // Synthesize a click on the placeholder. - final DomElement placeholder = flutterViewEmbedder.glassPaneShadow - .querySelector('flt-semantics-placeholder')!; + final DomElement placeholder = + appHostNode.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) { @@ -180,8 +182,7 @@ 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 +254,8 @@ void _testEngineSemanticsOwner() { .instance.accessibilityFeatures.accessibleNavigation, isFalse); - final DomElement placeholder = flutterViewEmbedder.glassPaneShadow - .querySelector('flt-semantics-placeholder')!; + final DomElement placeholder = + appHostNode.querySelector('flt-semantics-placeholder')!; expect(placeholder.isConnected, isTrue); @@ -427,8 +428,7 @@ 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'); @@ -439,7 +439,8 @@ void _testEngineSemanticsOwner() { class MockSemanticsEnabler implements SemanticsEnabler { @override - void dispose() {} + void dispose() { + } @override bool get isWaitingToEnableSemantics => throw UnimplementedError(); @@ -715,8 +716,7 @@ 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; @@ -809,9 +809,7 @@ 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; @@ -837,12 +835,10 @@ 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; @@ -1183,8 +1179,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')); @@ -1216,8 +1212,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')); @@ -1303,11 +1299,11 @@ void _testTextField() { final DomElement textField = appHostNode.querySelector('input[data-semantics-role="text-field"]')!; - expect(appHostNode.ownerDocument?.activeElement, isNot(textField)); + expect(appHostNode.activeElement, isNot(textField)); textField.focus(); - expect(appHostNode.ownerDocument?.activeElement, textField); + expect(appHostNode.activeElement, textField); expect(await logger.idLog.first, 0); expect(await logger.actionLog.first, ui.SemanticsAction.tap); @@ -1620,15 +1616,13 @@ void _testTappable() { } updateTappable(enabled: false); - expectSemanticsTree( - ''); + expectSemanticsTree(''); updateTappable(enabled: true); expectSemanticsTree(''); updateTappable(enabled: false); - expectSemanticsTree( - ''); + expectSemanticsTree(''); updateTappable(enabled: true); expectSemanticsTree(''); @@ -1653,7 +1647,7 @@ void _testTappable() { ); tester.apply(); - expect(domDocument.activeElement, tester.getSemanticsObject(0).element); + expect(flutterViewEmbedder.glassPaneShadow.activeElement, tester.getSemanticsObject(0).element); semantics().semanticsEnabled = false; }); } @@ -1948,13 +1942,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, @@ -2015,19 +2009,22 @@ 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); 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 = appHostNode.node as DomShadowRoot; + // Hit test child 1 - expect(domDocument.elementFromPoint(10, 10), child1); + expect(shadowRoot.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(domDocument.elementFromPoint(10, 20), child1); + expect(shadowRoot.elementFromPoint(10, 20), child1); // Hit test child 2 // Clicking at the location of the middle semantics node should allow the @@ -2046,10 +2043,10 @@ void _testPlatformView() { expect(domDocument.elementFromPoint(10, 30), platformViewElement); // Hit test overlap between child 2 and 3 - expect(domDocument.elementFromPoint(10, 40), child3); + expect(shadowRoot.elementFromPoint(10, 40), child3); // Hit test child 3 - expect(domDocument.elementFromPoint(10, 50), child3); + expect(shadowRoot.elementFromPoint(10, 50), child3); semantics().semanticsEnabled = false; }); @@ -2114,11 +2111,9 @@ 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 3ce55da00b13b..0b7b7623c676f 100644 --- a/lib/web_ui/test/engine/semantics/semantics_tester.dart +++ b/lib/web_ui/test/engine/semantics/semantics_tester.dart @@ -8,6 +8,7 @@ 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'; @@ -18,11 +19,10 @@ import '../../common/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 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; +/// 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; /// CSS style applied to the root of the semantics tree. // TODO(yjbanov): this should be handled internally by [expectSemanticsTree]. 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 59ccd3ca56b84..b9ffec7f64067 100644 --- a/lib/web_ui/test/engine/semantics/text_field_test.dart +++ b/lib/web_ui/test/engine/semantics/text_field_test.dart @@ -102,11 +102,11 @@ void testMain() { final DomElement textField = appHostNode .querySelector('input[data-semantics-role="text-field"]')!; - expect(appHostNode.ownerDocument?.activeElement, isNot(textField)); + expect(appHostNode.activeElement, isNot(textField)); textField.focus(); - expect(appHostNode.ownerDocument?.activeElement, textField); + expect(appHostNode.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 @@ -115,7 +115,8 @@ void testMain() { skip: browserEngine != BrowserEngine.blink); test('Syncs semantic state from framework', () { - expect(appHostNode.ownerDocument?.activeElement, domDocument.body); + expect(domDocument.activeElement, domDocument.body); + expect(appHostNode.activeElement, null); int changeCount = 0; int actionCount = 0; @@ -139,7 +140,8 @@ void testMain() { final TextField textField = textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField; - expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); + expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); + expect(appHostNode.activeElement, strategy.domElement); expect(textField.editableElement, strategy.domElement); expect(textField.activeEditableElement.getAttribute('aria-label'), 'greeting'); expect(textField.activeEditableElement.style.width, '10px'); @@ -152,7 +154,8 @@ void testMain() { rect: const ui.Rect.fromLTWH(0, 0, 12, 17), ); - expect(appHostNode.ownerDocument?.activeElement, domDocument.body); + expect(domDocument.activeElement, domDocument.body); + expect(appHostNode.activeElement, null); expect(strategy.domElement, null); expect(textField.activeEditableElement.getAttribute('aria-label'), 'farewell'); expect(textField.activeEditableElement.style.width, '12px'); @@ -198,7 +201,8 @@ void testMain() { test( 'Updates editing state when receiving framework messages from the text input channel', () { - expect(appHostNode.ownerDocument?.activeElement, domDocument.body); + expect(domDocument.activeElement, domDocument.body); + expect(appHostNode.activeElement, null); strategy.enable( singlelineConfig, @@ -242,7 +246,8 @@ void testMain() { }); test('Gives up focus after DOM blur', () { - expect(appHostNode.ownerDocument?.activeElement, domDocument.body); + expect(domDocument.activeElement, domDocument.body); + expect(appHostNode.activeElement, null); strategy.enable( singlelineConfig, @@ -257,11 +262,13 @@ void testMain() { final TextField textField = textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField; expect(textField.editableElement, strategy.domElement); - expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); + expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); + expect(appHostNode.activeElement, strategy.domElement); // The input should not refocus after blur. textField.activeEditableElement.blur(); - expect(appHostNode.ownerDocument?.activeElement, domDocument.body); + expect(domDocument.activeElement, domDocument.body); + expect(appHostNode.activeElement, null); strategy.disable(); }); @@ -281,7 +288,8 @@ void testMain() { isFocused: true, ); expect(strategy.domElement, isNotNull); - expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); + expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); + expect(appHostNode.activeElement, strategy.domElement); strategy.disable(); expect(strategy.domElement, isNull); @@ -292,7 +300,8 @@ void testMain() { expect(appHostNode.contains(textField.editableElement), isTrue); // Editing element is not enabled. expect(strategy.isEnabled, isFalse); - expect(appHostNode.ownerDocument?.activeElement, domDocument.body); + expect(domDocument.activeElement, domDocument.body); + expect(appHostNode.activeElement, null); }); test('Refocuses when setting editing state', () { @@ -307,11 +316,13 @@ void testMain() { isFocused: true, ); expect(strategy.domElement, isNotNull); - expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); + expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); + expect(appHostNode.activeElement, strategy.domElement); // Blur the element without telling the framework. strategy.activeDomElement.blur(); - expect(appHostNode.ownerDocument?.activeElement, domDocument.body); + expect(domDocument.activeElement, domDocument.body); + expect(appHostNode.activeElement, null); // The input will have focus after editing state is set and semantics updated. strategy.setEditingState(EditingState(text: 'foo')); @@ -329,7 +340,8 @@ void testMain() { value: 'hello', isFocused: true, ); - expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); + expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); + expect(appHostNode.activeElement, strategy.domElement); strategy.disable(); }); @@ -349,7 +361,8 @@ void testMain() { final DomHTMLTextAreaElement textArea = strategy.domElement! as DomHTMLTextAreaElement; - expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); + expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); + expect(appHostNode.activeElement, strategy.domElement); strategy.enable( singlelineConfig, @@ -358,7 +371,8 @@ void testMain() { ); textArea.blur(); - expect(appHostNode.ownerDocument?.activeElement, domDocument.body); + expect(domDocument.activeElement, domDocument.body); + expect(appHostNode.activeElement, null); strategy.disable(); // It doesn't remove the textarea from the DOM. @@ -442,14 +456,13 @@ void testMain() { createTwoFieldSemantics(tester, focusFieldId: 1); expect(tester.apply().length, 3); - expect(appHostNode.ownerDocument?.activeElement, - tester.getTextField(1).editableElement); + expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); + expect(appHostNode.activeElement, tester.getTextField(1).editableElement); expect(strategy.domElement, tester.getTextField(1).editableElement); createTwoFieldSemantics(tester, focusFieldId: 2); expect(tester.apply().length, 3); - expect(appHostNode.ownerDocument?.activeElement, - tester.getTextField(2).editableElement); + expect(appHostNode.activeElement, tester.getTextField(2).editableElement); expect(strategy.domElement, tester.getTextField(2).editableElement); } }); @@ -497,7 +510,8 @@ void testMain() { }); test('Syncs semantic state from framework', () { - expect(appHostNode.ownerDocument?.activeElement, domDocument.body); + expect(domDocument.activeElement, domDocument.body); + expect(appHostNode.activeElement, null); int changeCount = 0; int actionCount = 0; @@ -521,7 +535,8 @@ void testMain() { final TextField textField = textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField; - expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); + expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); + expect(appHostNode.activeElement, strategy.domElement); expect(textField.editableElement, strategy.domElement); expect(textField.activeEditableElement.getAttribute('aria-label'), 'greeting'); expect(textField.activeEditableElement.style.width, '10px'); @@ -537,7 +552,8 @@ void testMain() { appHostNode.querySelector('flt-semantics[role="textbox"]')!; expect(strategy.domElement, null); - expect(appHostNode.ownerDocument?.activeElement, textBox); + expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); + expect(appHostNode.activeElement, textBox); expect(textBox.getAttribute('aria-label'), 'farewell'); strategy.disable(); @@ -580,7 +596,8 @@ void testMain() { test( 'Updates editing state when receiving framework messages from the text input channel', () { - expect(appHostNode.ownerDocument?.activeElement, domDocument.body); + expect(domDocument.activeElement, domDocument.body); + expect(appHostNode.activeElement, null); strategy.enable( singlelineConfig, @@ -624,7 +641,8 @@ void testMain() { }); test('Gives up focus after DOM blur', () { - expect(appHostNode.ownerDocument?.activeElement, domDocument.body); + expect(domDocument.activeElement, domDocument.body); + expect(appHostNode.activeElement, null); strategy.enable( singlelineConfig, @@ -639,13 +657,15 @@ void testMain() { textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField; expect(textField.editableElement, strategy.domElement); - expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); + expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); + expect(appHostNode.activeElement, strategy.domElement); // The input should not refocus after blur. textField.activeEditableElement.blur(); final DomElement textBox = appHostNode.querySelector('flt-semantics[role="textbox"]')!; - expect(appHostNode.ownerDocument?.activeElement, textBox); + expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); + expect(appHostNode.activeElement, textBox); strategy.disable(); }); @@ -666,7 +686,8 @@ void testMain() { isFocused: true, ); expect(strategy.domElement, isNotNull); - expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); + expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); + expect(appHostNode.activeElement, strategy.domElement); strategy.disable(); expect(strategy.domElement, isNull); @@ -679,7 +700,8 @@ void testMain() { // Focus is on the semantic object final DomElement textBox = appHostNode.querySelector('flt-semantics[role="textbox"]')!; - expect(appHostNode.ownerDocument?.activeElement, textBox); + expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); + expect(appHostNode.activeElement, textBox); }); test('Refocuses when setting editing state', () { @@ -694,13 +716,15 @@ void testMain() { isFocused: true, ); expect(strategy.domElement, isNotNull); - expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); + expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); + expect(appHostNode.activeElement, strategy.domElement); // Blur the element without telling the framework. strategy.activeDomElement.blur(); final DomElement textBox = appHostNode.querySelector('flt-semantics[role="textbox"]')!; - expect(appHostNode.ownerDocument?.activeElement, textBox); + expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); + expect(appHostNode.activeElement, textBox); // The input will have focus after editing state is set and semantics updated. strategy.setEditingState(EditingState(text: 'foo')); @@ -718,7 +742,8 @@ void testMain() { value: 'hello', isFocused: true, ); - expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); + expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); + expect(appHostNode.activeElement, strategy.domElement); strategy.disable(); }); @@ -735,9 +760,9 @@ void testMain() { isMultiline: true, ); - final DomHTMLTextAreaElement textArea = - strategy.domElement! as DomHTMLTextAreaElement; - expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); + final DomHTMLTextAreaElement textArea = strategy.domElement! as DomHTMLTextAreaElement; + expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); + expect(appHostNode.activeElement, strategy.domElement); strategy.enable( singlelineConfig, @@ -751,7 +776,8 @@ void testMain() { final DomElement textBox = appHostNode.querySelector('flt-semantics[role="textbox"]')!; - expect(appHostNode.ownerDocument?.activeElement, textBox); + expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); + expect(appHostNode.activeElement, textBox); strategy.disable(); // It removes the textarea from the DOM. @@ -814,14 +840,13 @@ void testMain() { createTwoFieldSemanticsForIos(tester, focusFieldId: 1); expect(tester.apply().length, 3); - expect(appHostNode.ownerDocument?.activeElement, - tester.getTextField(1).editableElement); + expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); + expect(appHostNode.activeElement, tester.getTextField(1).editableElement); expect(strategy.domElement, tester.getTextField(1).editableElement); createTwoFieldSemanticsForIos(tester, focusFieldId: 2); expect(tester.apply().length, 3); - expect(appHostNode.ownerDocument?.activeElement, - tester.getTextField(2).editableElement); + expect(appHostNode.activeElement, tester.getTextField(2).editableElement); expect(strategy.domElement, tester.getTextField(2).editableElement); } }); diff --git a/lib/web_ui/test/engine/text_editing_test.dart b/lib/web_ui/test/engine/text_editing_test.dart index b366902458d2d..5c84bbcdaa3e0 100644 --- a/lib/web_ui/test/engine/text_editing_test.dart +++ b/lib/web_ui/test/engine/text_editing_test.dart @@ -91,8 +91,7 @@ Future testMain() async { ); // The focus initially is on the body. expect(domDocument.activeElement, domDocument.body); - expect(defaultTextEditingRoot.ownerDocument?.activeElement, - domDocument.body); + expect(defaultTextEditingRoot.activeElement, null); editingStrategy!.enable( singlelineConfig, @@ -107,8 +106,8 @@ Future testMain() async { final DomElement input = defaultTextEditingRoot.querySelector('input')!; // Now the editing element should have focus. - expect(domDocument.activeElement, input); - expect(defaultTextEditingRoot.ownerDocument?.activeElement, input); + expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); + expect(defaultTextEditingRoot.activeElement, input); expect(editingStrategy!.domElement, input); expect(input.getAttribute('type'), null); @@ -123,8 +122,7 @@ Future testMain() async { ); // The focus is back to the body. expect(domDocument.activeElement, domDocument.body); - expect(defaultTextEditingRoot.ownerDocument?.activeElement, - domDocument.body); + expect(defaultTextEditingRoot.activeElement, null); }); test('Respects read-only config', () { @@ -283,7 +281,7 @@ Future testMain() async { final DomHTMLTextAreaElement textarea = defaultTextEditingRoot.querySelector('textarea')! as DomHTMLTextAreaElement; // Now the textarea should have focus. - expect(defaultTextEditingRoot.ownerDocument?.activeElement, textarea); + expect(defaultTextEditingRoot.activeElement, textarea); expect(editingStrategy!.domElement, textarea); textarea.value = 'foo\nbar'; @@ -305,8 +303,7 @@ Future testMain() async { // 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.activeElement, null); // There should be no input action. expect(lastInputAction, isNull); @@ -623,7 +620,7 @@ Future testMain() async { const MethodCall show = MethodCall('TextInput.show'); sendFrameworkMessage(codec.encodeMethodCall(show)); - expect(defaultTextEditingRoot.ownerDocument?.activeElement, + expect(defaultTextEditingRoot.activeElement, textEditing!.strategy.domElement); }); @@ -683,8 +680,7 @@ Future testMain() async { sendFrameworkMessage(codec.encodeMethodCall(setEditingState)); // Editing shouldn't have started yet. - expect(defaultTextEditingRoot.ownerDocument?.activeElement, - domDocument.body); + expect(defaultTextEditingRoot.activeElement, null); const MethodCall show = MethodCall('TextInput.show'); sendFrameworkMessage(codec.encodeMethodCall(show)); @@ -709,7 +705,7 @@ Future testMain() async { expect(spy.messages, hasLength(0)); await Future.delayed(Duration.zero); // DOM element still keeps the focus. - expect(defaultTextEditingRoot.ownerDocument?.activeElement, + expect(defaultTextEditingRoot.activeElement, textEditing!.strategy.domElement); }); @@ -727,8 +723,7 @@ Future testMain() async { sendFrameworkMessage(codec.encodeMethodCall(setEditingState)); // Editing shouldn't have started yet. - expect(defaultTextEditingRoot.ownerDocument?.activeElement, - domDocument.body); + expect(defaultTextEditingRoot.activeElement, null); const MethodCall show = MethodCall('TextInput.show'); sendFrameworkMessage(codec.encodeMethodCall(show)); @@ -757,8 +752,7 @@ Future testMain() async { spy.messages[0].methodName, 'TextInputClient.onConnectionClosed'); await Future.delayed(Duration.zero); // DOM element loses the focus. - expect(defaultTextEditingRoot.ownerDocument?.activeElement, - domDocument.body); + expect(defaultTextEditingRoot.activeElement, null); }, // Test on ios-safari only. skip: browserEngine != BrowserEngine.webkit || @@ -779,8 +773,7 @@ Future testMain() async { sendFrameworkMessage(codec.encodeMethodCall(setEditingState)); // Editing shouldn't have started yet. - expect(defaultTextEditingRoot.ownerDocument?.activeElement, - domDocument.body); + expect(defaultTextEditingRoot.activeElement, null); const MethodCall show = MethodCall('TextInput.show'); sendFrameworkMessage(codec.encodeMethodCall(show)); @@ -1159,8 +1152,7 @@ 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.ownerDocument?.activeElement, inputElement); + expect(defaultTextEditingRoot.activeElement, inputElement); expect(inputElement.selectionStart, 2); expect(inputElement.selectionEnd, 3); } @@ -1173,7 +1165,7 @@ Future testMain() async { sendFrameworkMessage(codec.encodeMethodCall(updateSizeAndTransform)); // Check the element still has focus. User can keep editing. - expect(defaultTextEditingRoot.ownerDocument?.activeElement, + expect(defaultTextEditingRoot.activeElement, textEditing!.strategy.domElement); // Check the cursor location is the same. @@ -1773,8 +1765,7 @@ Future testMain() async { sendFrameworkMessage(codec.encodeMethodCall(setClient)); // Editing shouldn't have started yet. - expect(defaultTextEditingRoot.ownerDocument?.activeElement, - domDocument.body); + expect(defaultTextEditingRoot.activeElement, null); const MethodCall show = MethodCall('TextInput.show'); sendFrameworkMessage(codec.encodeMethodCall(show)); @@ -2656,7 +2647,7 @@ void checkInputEditingState( expect(element, isNotNull); expect(domInstanceOfString(element, 'HTMLInputElement'), true); final DomHTMLInputElement input = element! as DomHTMLInputElement; - expect(defaultTextEditingRoot.ownerDocument?.activeElement, input); + expect(defaultTextEditingRoot.activeElement, input); expect(input.value, text); expect(input.selectionStart, start); expect(input.selectionEnd, end); @@ -2682,7 +2673,7 @@ void checkTextAreaEditingState( int start, int end, ) { - expect(defaultTextEditingRoot.ownerDocument?.activeElement, textarea); + expect(defaultTextEditingRoot.activeElement, textarea); expect(textarea.value, text); expect(textarea.selectionStart, start); expect(textarea.selectionEnd, end);