diff --git a/lib/web_ui/lib/src/engine/embedder.dart b/lib/web_ui/lib/src/engine/embedder.dart index d16821e3d9ee0..a05b1e0688b7d 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; 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; @@ -168,6 +171,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,20 +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. - 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. + glassPaneElement.appendChild(semanticsHostElement); + // When debugging semantics, make the scene semi-transparent so that the // semantics tree is more prominent. if (configuration.debugShowSemanticsNodes) { @@ -393,3 +399,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 4b0ca8e13d990..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]. @@ -110,11 +112,10 @@ 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.') { + 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 @@ -135,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; @@ -191,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; diff --git a/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/lib/web_ui/lib/src/engine/platform_dispatcher.dart index a0128a0c71383..4e8697f31f8b7 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.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/pointer_binding/event_position_helper.dart b/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart index 6be2b9ccc2bcb..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 @@ -19,20 +19,22 @@ 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 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(); 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 abf29dd9d6cfd..4cf1177a84f3f 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'; @@ -422,14 +421,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/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 94e66d8b6e698..12e2a72fce210 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,7 +51,8 @@ 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 defaultTextEditingRoot => + flutterViewEmbedder.textEditingHostNode; /// 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 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); } }); diff --git a/lib/web_ui/test/engine/semantics/semantics_test.dart b/lib/web_ui/test/engine/semantics/semantics_test.dart index e773cb33dcef3..e4da65ad22d69 100644 --- a/lib/web_ui/test/engine/semantics/semantics_test.dart +++ b/lib/web_ui/test/engine/semantics/semantics_test.dart @@ -157,18 +157,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) { @@ -182,7 +180,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); @@ -254,8 +253,8 @@ void _testEngineSemanticsOwner() { .instance.accessibilityFeatures.accessibleNavigation, isFalse); - final DomElement placeholder = - appHostNode.querySelector('flt-semantics-placeholder')!; + final DomElement placeholder = flutterViewEmbedder.glassPaneShadow + .querySelector('flt-semantics-placeholder')!; expect(placeholder.isConnected, isTrue); @@ -428,7 +427,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'); @@ -439,8 +439,7 @@ void _testEngineSemanticsOwner() { class MockSemanticsEnabler implements SemanticsEnabler { @override - void dispose() { - } + void dispose() {} @override bool get isWaitingToEnableSemantics => throw UnimplementedError(); @@ -716,7 +715,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; @@ -809,7 +809,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; @@ -835,10 +837,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; @@ -1179,8 +1183,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')); @@ -1212,8 +1216,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')); @@ -1299,11 +1303,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); @@ -1616,13 +1620,15 @@ void _testTappable() { } updateTappable(enabled: false); - expectSemanticsTree(''); + expectSemanticsTree( + ''); updateTappable(enabled: true); expectSemanticsTree(''); updateTappable(enabled: false); - expectSemanticsTree(''); + expectSemanticsTree( + ''); updateTappable(enabled: true); expectSemanticsTree(''); @@ -1647,7 +1653,7 @@ void _testTappable() { ); tester.apply(); - expect(flutterViewEmbedder.glassPaneShadow.activeElement, tester.getSemanticsObject(0).element); + expect(domDocument.activeElement, tester.getSemanticsObject(0).element); semantics().semanticsEnabled = false; }); } @@ -1942,13 +1948,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, @@ -2009,22 +2015,19 @@ 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(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 @@ -2043,10 +2046,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; }); @@ -2111,9 +2114,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 0b7b7623c676f..3ce55da00b13b 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'; @@ -19,10 +18,11 @@ 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 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]. 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 b9ffec7f64067..59ccd3ca56b84 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.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 @@ -115,8 +115,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; @@ -140,8 +139,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'); @@ -154,8 +152,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'); @@ -201,8 +198,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, @@ -246,8 +242,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, @@ -262,13 +257,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(); }); @@ -288,8 +281,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); @@ -300,8 +292,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', () { @@ -316,13 +307,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')); @@ -340,8 +329,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(); }); @@ -361,8 +349,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, @@ -371,8 +358,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. @@ -456,13 +442,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); } }); @@ -510,8 +497,7 @@ void testMain() { }); 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; @@ -535,8 +521,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'); @@ -552,8 +537,7 @@ void testMain() { appHostNode.querySelector('flt-semantics[role="textbox"]')!; 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(); @@ -596,8 +580,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, @@ -641,8 +624,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, @@ -657,15 +639,13 @@ void testMain() { 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(); 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(); }); @@ -686,8 +666,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); @@ -700,8 +679,7 @@ void testMain() { // Focus is on the semantic object 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', () { @@ -716,15 +694,13 @@ 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(); 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')); @@ -742,8 +718,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(); }); @@ -760,9 +735,9 @@ void testMain() { isMultiline: true, ); - final DomHTMLTextAreaElement textArea = strategy.domElement! as DomHTMLTextAreaElement; - expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement); - expect(appHostNode.activeElement, strategy.domElement); + final DomHTMLTextAreaElement textArea = + strategy.domElement! as DomHTMLTextAreaElement; + expect(appHostNode.ownerDocument?.activeElement, strategy.domElement); strategy.enable( singlelineConfig, @@ -776,8 +751,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); strategy.disable(); // It removes the textarea from the DOM. @@ -840,13 +814,14 @@ void testMain() { createTwoFieldSemanticsForIos(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); 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); } }); diff --git a/lib/web_ui/test/engine/text_editing_test.dart b/lib/web_ui/test/engine/text_editing_test.dart index 5c84bbcdaa3e0..b366902458d2d 100644 --- a/lib/web_ui/test/engine/text_editing_test.dart +++ b/lib/web_ui/test/engine/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);