[web] Remove non-ShadowDom mode (#39915)
If we still want to do this, here's a quick PR :)
Fixes https://github.com/flutter/flutter/issues/116204
diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index d86db3f..3e80846 100644
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -1908,7 +1908,7 @@
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/font_change_util.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/fonts.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/frame_reference.dart + ../../../flutter/LICENSE
-ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/host_node.dart + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/global_styles.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/html/backdrop_filter.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/html/canvas.dart + ../../../flutter/LICENSE
@@ -4505,7 +4505,7 @@
FILE: ../../../flutter/lib/web_ui/lib/src/engine/font_change_util.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/fonts.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/frame_reference.dart
-FILE: ../../../flutter/lib/web_ui/lib/src/engine/host_node.dart
+FILE: ../../../flutter/lib/web_ui/lib/src/engine/global_styles.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/backdrop_filter.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/canvas.dart
diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart
index 2af3a17..6c152e9 100644
--- a/lib/web_ui/lib/src/engine.dart
+++ b/lib/web_ui/lib/src/engine.dart
@@ -65,7 +65,7 @@
export 'engine/font_change_util.dart';
export 'engine/fonts.dart';
export 'engine/frame_reference.dart';
-export 'engine/host_node.dart';
+export 'engine/global_styles.dart';
export 'engine/html/backdrop_filter.dart';
export 'engine/html/bitmap_canvas.dart';
export 'engine/html/canvas.dart';
diff --git a/lib/web_ui/lib/src/engine/embedder.dart b/lib/web_ui/lib/src/engine/embedder.dart
index 245fd26..5ac02cf 100644
--- a/lib/web_ui/lib/src/engine/embedder.dart
+++ b/lib/web_ui/lib/src/engine/embedder.dart
@@ -4,13 +4,14 @@
import 'dart:async';
+import 'package:ui/src/engine/safe_browser_api.dart';
import 'package:ui/ui.dart' as ui;
import '../engine.dart' show buildMode, renderer, window;
import 'browser_detection.dart';
import 'configuration.dart';
import 'dom.dart';
-import 'host_node.dart';
+import 'global_styles.dart';
import 'keyboard_binding.dart';
import 'platform_dispatcher.dart';
import 'pointer_binding.dart';
@@ -127,9 +128,9 @@
DomElement get glassPaneElement => _glassPaneElement;
late DomElement _glassPaneElement;
- /// The [HostNode] of the [glassPaneElement], which contains the whole Flutter app.
- HostNode get glassPaneShadow => _glassPaneShadow;
- late HostNode _glassPaneShadow;
+ /// The shadow root of the [glassPaneElement], which contains the whole Flutter app.
+ DomShadowRoot get glassPaneShadow => _glassPaneShadow;
+ late DomShadowRoot _glassPaneShadow;
DomElement get textEditingHostNode => _textEditingHostNode;
late DomElement _textEditingHostNode;
@@ -171,15 +172,29 @@
_embeddingStrategy.attachGlassPane(flutterViewElement);
flutterViewElement.appendChild(glassPaneElement);
+ if (getJsProperty<Object?>(glassPaneElement, 'attachShadow') == null) {
+ throw UnsupportedError('ShadowDOM is not supported in this browser.');
+ }
+
// Create a [HostNode] under the glass pane element, and attach everything
// there, instead of directly underneath the glass panel.
- //
- // TODO(dit): clean HostNode, https://github.com/flutter/flutter/issues/116204
- final HostNode glassPaneElementHostNode = HostNode.create(
- glassPaneElement,
- defaultCssFont,
+ final DomShadowRoot shadowRoot = glassPaneElement.attachShadow(<String, dynamic>{
+ 'mode': 'open',
+ // This needs to stay false to prevent issues like this:
+ // - https://github.com/flutter/flutter/issues/85759
+ 'delegatesFocus': false,
+ });
+ _glassPaneShadow = shadowRoot;
+
+ final DomHTMLStyleElement shadowRootStyleElement = createDomHTMLStyleElement();
+ shadowRootStyleElement.id = 'flt-internals-stylesheet';
+ // The shadowRootStyleElement must be appended to the DOM, or its `sheet` will be null later.
+ shadowRoot.appendChild(shadowRootStyleElement);
+ applyGlobalCssRulesToSheet(
+ shadowRootStyleElement,
+ hasAutofillOverlay: browserHasAutofillOverlay(),
+ defaultCssFont: defaultCssFont,
);
- _glassPaneShadow = glassPaneElementHostNode;
_textEditingHostNode =
createTextEditingHostNode(flutterViewElement, defaultCssFont);
@@ -202,10 +217,8 @@
.instance.semanticsHelper
.prepareAccessibilityPlaceholder();
- glassPaneElementHostNode.appendAll(<DomNode>[
- accessibilityPlaceholder,
- _sceneHostElement!,
- ]);
+ shadowRoot.append(accessibilityPlaceholder);
+ shadowRoot.append(_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
@@ -354,8 +367,7 @@
_embeddingStrategy.attachResourcesHost(resourcesHost,
nextTo: flutterViewElement);
} else {
- glassPaneShadow.node
- .insertBefore(resourcesHost, glassPaneShadow.node.firstChild);
+ glassPaneShadow.insertBefore(resourcesHost, glassPaneShadow.firstChild);
}
_resourcesHost = resourcesHost;
}
@@ -420,7 +432,7 @@
styleElement.id = 'flt-text-editing-stylesheet';
root.appendChild(styleElement);
applyGlobalCssRulesToSheet(
- styleElement.sheet! as DomCSSStyleSheet,
+ styleElement,
hasAutofillOverlay: browserHasAutofillOverlay(),
cssSelectorPrefix: FlutterViewEmbedder.flutterViewTagName,
defaultCssFont: defaultFont,
diff --git a/lib/web_ui/lib/src/engine/global_styles.dart b/lib/web_ui/lib/src/engine/global_styles.dart
new file mode 100644
index 0000000..6be6914
--- /dev/null
+++ b/lib/web_ui/lib/src/engine/global_styles.dart
@@ -0,0 +1,151 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'browser_detection.dart';
+import 'dom.dart';
+import 'text_editing/text_editing.dart';
+
+// Applies the required global CSS to an incoming [DomCSSStyleSheet] `sheet`.
+void applyGlobalCssRulesToSheet(
+ DomHTMLStyleElement styleElement, {
+ required bool hasAutofillOverlay,
+ String cssSelectorPrefix = '',
+ required String defaultCssFont,
+}) {
+ // TODO(web): use more efficient CSS selectors; descendant selectors are slow.
+ // More info: https://csswizardry.com/2011/09/writing-efficient-css-selectors
+
+ assert(styleElement.sheet != null);
+ final DomCSSStyleSheet sheet = styleElement.sheet! as DomCSSStyleSheet;
+
+ // These are intentionally outrageous font parameters to make sure that the
+ // apps fully specify their text styles.
+ //
+ // Fixes #115216 by ensuring that our parameters only affect the flt-scene-host children.
+ sheet.insertRule('''
+ $cssSelectorPrefix flt-scene-host {
+ color: red;
+ font: $defaultCssFont;
+ }
+ ''', sheet.cssRules.length);
+
+ // By default on iOS, Safari would highlight the element that's being tapped
+ // on using gray background. This CSS rule disables that.
+ if (isSafari) {
+ sheet.insertRule('''
+ $cssSelectorPrefix * {
+ -webkit-tap-highlight-color: transparent;
+ }
+ ''', sheet.cssRules.length);
+ }
+
+ if (isFirefox) {
+ // For firefox set line-height, otherwise text at same font-size will
+ // measure differently in ruler.
+ //
+ // - See: https://github.com/flutter/flutter/issues/44803
+ sheet.insertRule('''
+ $cssSelectorPrefix flt-paragraph,
+ $cssSelectorPrefix flt-span {
+ line-height: 100%;
+ }
+ ''', sheet.cssRules.length);
+ }
+
+ // This undoes browser's default painting and layout attributes of range
+ // input, which is used in semantics.
+ sheet.insertRule('''
+ $cssSelectorPrefix flt-semantics input[type=range] {
+ appearance: none;
+ -webkit-appearance: none;
+ width: 100%;
+ position: absolute;
+ border: none;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ }
+ ''', sheet.cssRules.length);
+
+ if (isSafari) {
+ sheet.insertRule('''
+ $cssSelectorPrefix flt-semantics input[type=range]::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ }
+ ''', sheet.cssRules.length);
+ }
+
+ // The invisible semantic text field may have a visible cursor and selection
+ // highlight. The following 2 CSS rules force everything to be transparent.
+ sheet.insertRule('''
+ $cssSelectorPrefix input::selection {
+ background-color: transparent;
+ }
+ ''', sheet.cssRules.length);
+ sheet.insertRule('''
+ $cssSelectorPrefix textarea::selection {
+ background-color: transparent;
+ }
+ ''', sheet.cssRules.length);
+
+ sheet.insertRule('''
+ $cssSelectorPrefix flt-semantics input,
+ $cssSelectorPrefix flt-semantics textarea,
+ $cssSelectorPrefix flt-semantics [contentEditable="true"] {
+ caret-color: transparent;
+ }
+ ''', sheet.cssRules.length);
+
+ // Hide placeholder text
+ sheet.insertRule('''
+ $cssSelectorPrefix .flt-text-editing::placeholder {
+ opacity: 0;
+ }
+ ''', sheet.cssRules.length);
+
+ // This CSS makes the autofill overlay transparent in order to prevent it
+ // from overlaying on top of Flutter-rendered text inputs.
+ // See: https://github.com/flutter/flutter/issues/118337.
+ if (browserHasAutofillOverlay()) {
+ sheet.insertRule('''
+ $cssSelectorPrefix .transparentTextEditing:-webkit-autofill,
+ $cssSelectorPrefix .transparentTextEditing:-webkit-autofill:hover,
+ $cssSelectorPrefix .transparentTextEditing:-webkit-autofill:focus,
+ $cssSelectorPrefix .transparentTextEditing:-webkit-autofill:active {
+ opacity: 0 !important;
+ }
+ ''', sheet.cssRules.length);
+ }
+
+ // Removes password reveal icon for text inputs in Edge browsers.
+ // Non-Edge browsers will crash trying to parse -ms-reveal CSS selector,
+ // so we guard it behind an isEdge check.
+ // Fixes: https://github.com/flutter/flutter/issues/83695
+ if (isEdge) {
+ // We try-catch this, because in testing, we fake Edge via the UserAgent,
+ // so the below will throw an exception (because only real Edge understands
+ // the ::-ms-reveal pseudo-selector).
+ try {
+ sheet.insertRule('''
+ $cssSelectorPrefix input::-ms-reveal {
+ display: none;
+ }
+ ''', sheet.cssRules.length);
+ } on DomException catch (e) {
+ // Browsers that don't understand ::-ms-reveal throw a DOMException
+ // of type SyntaxError.
+ domWindow.console.warn(e);
+ // Add a fake rule if our code failed because we're under testing
+ assert(() {
+ sheet.insertRule('''
+ $cssSelectorPrefix input.fallback-for-fakey-browser-in-ci {
+ display: none;
+ }
+ ''', sheet.cssRules.length);
+ return true;
+ }());
+ }
+ }
+}
diff --git a/lib/web_ui/lib/src/engine/host_node.dart b/lib/web_ui/lib/src/engine/host_node.dart
deleted file mode 100644
index dea385d..0000000
--- a/lib/web_ui/lib/src/engine/host_node.dart
+++ /dev/null
@@ -1,361 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import 'browser_detection.dart';
-import 'dom.dart';
-import 'embedder.dart';
-import 'safe_browser_api.dart';
-import 'text_editing/text_editing.dart';
-
-/// The interface required to host a flutter app in the DOM, and its tests.
-///
-/// Consider this as the intersection in functionality between [DomShadowRoot]
-/// (preferred Flutter rendering method) and [DomDocument] (fallback).
-///
-/// Not to be confused with [DomDocumentOrShadowRoot].
-///
-/// This also handles the stylesheet that is applied to the different types of
-/// HostNodes; for ShadowDOM there's not much to do, but for ElementNodes, the
-/// stylesheet is "namespaced" by the `flt-glass-pane` prefix, so it "only"
-/// affects things that Flutter web owns.
-abstract class HostNode {
- /// Returns an appropriate HostNode for the given [root].
- ///
- /// If `attachShadow` is supported, this returns a [ShadowDomHostNode], else
- /// this will fall-back to an [ElementHostNode].
- factory HostNode.create(DomElement root, String defaultFont) {
- if (getJsProperty<Object?>(root, 'attachShadow') != null) {
- return ShadowDomHostNode(root, defaultFont);
- } else {
- // attachShadow not available, fall back to ElementHostNode.
- return ElementHostNode(root, defaultFont);
- }
- }
-
- /// Retrieves the [DomElement] that currently has focus.
- ///
- /// See:
- /// * [Document.activeElement](https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement)
- DomElement? get activeElement;
-
- /// Adds a node to the end of the child [nodes] list of this node.
- ///
- /// If the node already exists in this document, it will be removed from its
- /// current parent node, then added to this node.
- ///
- /// This method is more efficient than `nodes.add`, and is the preferred
- /// way of appending a child node.
- ///
- /// See:
- /// * [Node.appendChild](https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild)
- DomNode append(DomNode node);
-
- /// Appends all of an [Iterable<DomNode>] to this [HostNode].
- void appendAll(Iterable<DomNode> nodes);
-
- /// Returns true if this node contains the specified node.
- /// See:
- /// * [Node.contains](https://developer.mozilla.org/en-US/docs/Web/API/Node.contains)
- bool contains(DomNode? other);
-
- /// Returns the currently wrapped [DomNode].
- DomNode get node;
-
- /// Finds the first descendant element of this document that matches the
- /// specified group of selectors.
- ///
- /// [selectors] should be a string using CSS selector syntax.
- ///
- /// ```dart
- /// var element1 = document.querySelector('.className');
- /// var element2 = document.querySelector('#id');
- /// ```
- ///
- /// For details about CSS selector syntax, see the
- /// [CSS selector specification](http://www.w3.org/TR/css3-selectors/).
- ///
- /// See:
- /// * [Document.querySelector](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
- DomElement? querySelector(String selectors);
-
- /// Finds all descendant elements of this document that match the specified
- /// group of selectors.
- ///
- /// [selectors] should be a string using CSS selector syntax.
- ///
- /// ```dart
- /// var items = document.querySelectorAll('.itemClassName');
- /// ```
- ///
- /// For details about CSS selector syntax, see the
- /// [CSS selector specification](http://www.w3.org/TR/css3-selectors/).
- ///
- /// See:
- /// * [Document.querySelectorAll](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll)
- Iterable<DomElement> querySelectorAll(String selectors);
-}
-
-/// A [HostNode] implementation, backed by a [DomShadowRoot].
-///
-/// This is the preferred flutter implementation, but it might not be supported
-/// by all browsers yet.
-///
-/// The constructor might throw when calling `attachShadow`, if ShadowDOM is not
-/// supported in the current environment. In this case, a fallback [ElementHostNode]
-/// should be created instead.
-class ShadowDomHostNode implements HostNode {
- /// Build a HostNode by attaching a [DomShadowRoot] to the `root` element.
- ///
- /// 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(<String, dynamic>{
- 'mode': 'open',
- // This needs to stay false to prevent issues like this:
- // - https://github.com/flutter/flutter/issues/85759
- 'delegatesFocus': false,
- });
-
- final DomHTMLStyleElement shadowRootStyleElement =
- createDomHTMLStyleElement();
- shadowRootStyleElement.id = 'flt-internals-stylesheet';
- // The shadowRootStyleElement must be appended to the DOM, or its `sheet` will be null later.
- _shadow.appendChild(shadowRootStyleElement);
- applyGlobalCssRulesToSheet(
- shadowRootStyleElement.sheet! as DomCSSStyleSheet,
- hasAutofillOverlay: browserHasAutofillOverlay(),
- defaultCssFont: defaultFont,
- );
- }
-
- late DomShadowRoot _shadow;
-
- @override
- DomElement? get activeElement => _shadow.activeElement;
-
- @override
- DomElement? querySelector(String selectors) {
- return _shadow.querySelector(selectors);
- }
-
- @override
- Iterable<DomElement> querySelectorAll(String selectors) {
- return _shadow.querySelectorAll(selectors);
- }
-
- @override
- DomNode append(DomNode node) {
- return _shadow.appendChild(node);
- }
-
- @override
- bool contains(DomNode? other) {
- return _shadow.contains(other);
- }
-
- @override
- DomNode get node => _shadow;
-
- @override
- void appendAll(Iterable<DomNode> nodes) => nodes.forEach(append);
-}
-
-/// A [HostNode] implementation, backed by a [DomElement].
-///
-/// This is a fallback implementation, in case [ShadowDomHostNode] fails when
-/// being constructed.
-class ElementHostNode implements HostNode {
- /// Build a HostNode by attaching a child [DomElement] to the `root` element.
- ElementHostNode(DomElement root, String defaultFont) {
- // Append the stylesheet here, so this class is completely symmetric to the
- // ShadowDOM version.
- final DomHTMLStyleElement styleElement = createDomHTMLStyleElement();
- styleElement.id = 'flt-internals-stylesheet';
- // The styleElement must be appended to the DOM, or its `sheet` will be null later.
- root.appendChild(styleElement);
- applyGlobalCssRulesToSheet(
- styleElement.sheet! as DomCSSStyleSheet,
- hasAutofillOverlay: browserHasAutofillOverlay(),
- cssSelectorPrefix: FlutterViewEmbedder.flutterViewTagName,
- defaultCssFont: defaultFont,
- );
-
- _element = domDocument.createElement('flt-element-host-node');
- root.appendChild(_element);
- }
-
- late DomElement _element;
-
- @override
- DomElement? get activeElement => _element.ownerDocument?.activeElement;
-
- @override
- DomElement? querySelector(String selectors) {
- return _element.querySelector(selectors);
- }
-
- @override
- Iterable<DomElement> querySelectorAll(String selectors) {
- return _element.querySelectorAll(selectors);
- }
-
- @override
- DomNode append(DomNode node) {
- return _element.appendChild(node);
- }
-
- @override
- bool contains(DomNode? other) {
- return _element.contains(other);
- }
-
- @override
- DomNode get node => _element;
-
- @override
- void appendAll(Iterable<DomNode> nodes) => nodes.forEach(append);
-}
-
-// Applies the required global CSS to an incoming [DomCSSStyleSheet] `sheet`.
-void applyGlobalCssRulesToSheet(
- DomCSSStyleSheet sheet, {
- required bool hasAutofillOverlay,
- String cssSelectorPrefix = '',
- required String defaultCssFont,
-}) {
- // TODO(web): use more efficient CSS selectors; descendant selectors are slow.
- // More info: https://csswizardry.com/2011/09/writing-efficient-css-selectors
-
- // These are intentionally outrageous font parameters to make sure that the
- // apps fully specify their text styles.
- //
- // Fixes #115216 by ensuring that our parameters only affect the flt-scene-host children.
- sheet.insertRule('''
- $cssSelectorPrefix flt-scene-host {
- color: red;
- font: $defaultCssFont;
- }
- ''', sheet.cssRules.length);
-
- // By default on iOS, Safari would highlight the element that's being tapped
- // on using gray background. This CSS rule disables that.
- if (isSafari) {
- sheet.insertRule('''
- $cssSelectorPrefix * {
- -webkit-tap-highlight-color: transparent;
- }
- ''', sheet.cssRules.length);
- }
-
- if (isFirefox) {
- // For firefox set line-height, otherwise text at same font-size will
- // measure differently in ruler.
- //
- // - See: https://github.com/flutter/flutter/issues/44803
- sheet.insertRule('''
- $cssSelectorPrefix flt-paragraph,
- $cssSelectorPrefix flt-span {
- line-height: 100%;
- }
- ''', sheet.cssRules.length);
- }
-
- // This undoes browser's default painting and layout attributes of range
- // input, which is used in semantics.
- sheet.insertRule('''
- $cssSelectorPrefix flt-semantics input[type=range] {
- appearance: none;
- -webkit-appearance: none;
- width: 100%;
- position: absolute;
- border: none;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- }
- ''', sheet.cssRules.length);
-
- if (isSafari) {
- sheet.insertRule('''
- $cssSelectorPrefix flt-semantics input[type=range]::-webkit-slider-thumb {
- -webkit-appearance: none;
- }
- ''', sheet.cssRules.length);
- }
-
- // The invisible semantic text field may have a visible cursor and selection
- // highlight. The following 2 CSS rules force everything to be transparent.
- sheet.insertRule('''
- $cssSelectorPrefix input::selection {
- background-color: transparent;
- }
- ''', sheet.cssRules.length);
- sheet.insertRule('''
- $cssSelectorPrefix textarea::selection {
- background-color: transparent;
- }
- ''', sheet.cssRules.length);
-
- sheet.insertRule('''
- $cssSelectorPrefix flt-semantics input,
- $cssSelectorPrefix flt-semantics textarea,
- $cssSelectorPrefix flt-semantics [contentEditable="true"] {
- caret-color: transparent;
- }
- ''', sheet.cssRules.length);
-
- // Hide placeholder text
- sheet.insertRule('''
- $cssSelectorPrefix .flt-text-editing::placeholder {
- opacity: 0;
- }
- ''', sheet.cssRules.length);
-
- // This CSS makes the autofill overlay transparent in order to prevent it
- // from overlaying on top of Flutter-rendered text inputs.
- // See: https://github.com/flutter/flutter/issues/118337.
- if (browserHasAutofillOverlay()) {
- sheet.insertRule('''
- $cssSelectorPrefix .transparentTextEditing:-webkit-autofill,
- $cssSelectorPrefix .transparentTextEditing:-webkit-autofill:hover,
- $cssSelectorPrefix .transparentTextEditing:-webkit-autofill:focus,
- $cssSelectorPrefix .transparentTextEditing:-webkit-autofill:active {
- opacity: 0 !important;
- }
- ''', sheet.cssRules.length);
- }
-
- // Removes password reveal icon for text inputs in Edge browsers.
- // Non-Edge browsers will crash trying to parse -ms-reveal CSS selector,
- // so we guard it behind an isEdge check.
- // Fixes: https://github.com/flutter/flutter/issues/83695
- if (isEdge) {
- // We try-catch this, because in testing, we fake Edge via the UserAgent,
- // so the below will throw an exception (because only real Edge understands
- // the ::-ms-reveal pseudo-selector).
- try {
- sheet.insertRule('''
- $cssSelectorPrefix input::-ms-reveal {
- display: none;
- }
- ''', sheet.cssRules.length);
- } on DomException catch (e) {
- // Browsers that don't understand ::-ms-reveal throw a DOMException
- // of type SyntaxError.
- domWindow.console.warn(e);
- // Add a fake rule if our code failed because we're under testing
- assert(() {
- sheet.insertRule('''
- $cssSelectorPrefix input.fallback-for-fakey-browser-in-ci {
- display: none;
- }
- ''', sheet.cssRules.length);
- return true;
- }());
- }
- }
-}
diff --git a/lib/web_ui/lib/src/engine/text/measurement.dart b/lib/web_ui/lib/src/engine/text/measurement.dart
index 3a026e2..a1f3bdf 100644
--- a/lib/web_ui/lib/src/engine/text/measurement.dart
+++ b/lib/web_ui/lib/src/engine/text/measurement.dart
@@ -29,7 +29,7 @@
..height = '0';
if (root == null) {
- flutterViewEmbedder.glassPaneShadow.node.appendChild(_rulerHost);
+ flutterViewEmbedder.glassPaneShadow.appendChild(_rulerHost);
} else {
root.appendChild(_rulerHost);
}
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 3e22f3d..2be7350 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
@@ -12,7 +12,6 @@
import '../browser_detection.dart';
import '../dom.dart';
import '../embedder.dart';
-import '../host_node.dart';
import '../platform_dispatcher.dart';
import '../safe_browser_api.dart';
import '../semantics.dart';
diff --git a/lib/web_ui/test/engine/embedder_test.dart b/lib/web_ui/test/engine/embedder_test.dart
index 86a8283..ecba37c 100644
--- a/lib/web_ui/test/engine/embedder_test.dart
+++ b/lib/web_ui/test/engine/embedder_test.dart
@@ -52,28 +52,6 @@
);
});
- test('renders a shadowRoot by default', () {
- final FlutterViewEmbedder embedder = FlutterViewEmbedder();
- final HostNode hostNode = embedder.glassPaneShadow;
- expect(domInstanceOfString(hostNode.node, 'ShadowRoot'), isTrue);
- });
-
- test('starts without shadowDom available too', () {
- final dynamic oldAttachShadow = attachShadow;
- expect(oldAttachShadow, isNotNull);
-
- attachShadow = null; // Break ShadowDOM
-
- final FlutterViewEmbedder embedder = FlutterViewEmbedder();
- final HostNode hostNode = embedder.glassPaneShadow;
- expect(domInstanceOfString(hostNode.node, 'Element'), isTrue);
- expect(
- (hostNode.node as DomElement).tagName,
- equalsIgnoringCase('flt-element-host-node'),
- );
- attachShadow = oldAttachShadow; // Restore ShadowDOM
- });
-
test('should add/remove global resource', () {
final FlutterViewEmbedder embedder = FlutterViewEmbedder();
final DomHTMLDivElement resource = createDomHTMLDivElement();
@@ -110,6 +88,52 @@
expect(style, isNotNull);
expect(style.opacity, '0');
}, skip: browserEngine != BrowserEngine.firefox);
+
+ group('Shadow root', () {
+ late FlutterViewEmbedder embedder;
+
+ setUp(() {
+ embedder = FlutterViewEmbedder();
+ });
+
+ tearDown(() {
+ embedder.glassPaneElement.remove();
+ });
+
+ test('throws when shadowDom is not available', () {
+ final dynamic oldAttachShadow = attachShadow;
+ expect(oldAttachShadow, isNotNull);
+
+ attachShadow = null; // Break ShadowDOM
+
+ expect(() => FlutterViewEmbedder(), throwsUnsupportedError);
+ attachShadow = oldAttachShadow; // Restore ShadowDOM
+ });
+
+ test('Initializes and attaches a shadow root', () {
+ expect(domInstanceOfString(embedder.glassPaneShadow, 'ShadowRoot'), isTrue);
+ expect(embedder.glassPaneShadow.host, embedder.glassPaneElement);
+ expect(embedder.glassPaneShadow, embedder.glassPaneElement.shadowRoot);
+
+ // The shadow root should be initialized with correct parameters.
+ expect(embedder.glassPaneShadow.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(embedder.glassPaneShadow.delegatesFocus, isFalse);
+ }
+ });
+
+ test('Attaches a stylesheet to the shadow root', () {
+ final DomElement? style =
+ embedder.glassPaneShadow.querySelector('#flt-internals-stylesheet');
+
+ expect(style, isNotNull);
+ expect(style!.tagName, equalsIgnoringCase('style'));
+ expect(style.parentNode, embedder.glassPaneShadow);
+ });
+ });
}
@JS('Element.prototype.attachShadow')
diff --git a/lib/web_ui/test/engine/global_styles_test.dart b/lib/web_ui/test/engine/global_styles_test.dart
new file mode 100644
index 0000000..3abeb1f
--- /dev/null
+++ b/lib/web_ui/test/engine/global_styles_test.dart
@@ -0,0 +1,131 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:test/bootstrap/browser.dart';
+import 'package:test/test.dart';
+import 'package:ui/src/engine.dart';
+
+const String _kDefaultCssFont = '14px monospace';
+
+void main() {
+ internalBootstrapBrowserTest(() => testMain);
+}
+
+void testMain() {
+ late DomHTMLStyleElement styleElement;
+
+ setUp(() {
+ styleElement = createDomHTMLStyleElement();
+ domDocument.body!.append(styleElement);
+ applyGlobalCssRulesToSheet(
+ styleElement,
+ hasAutofillOverlay: browserHasAutofillOverlay(),
+ defaultCssFont: _kDefaultCssFont,
+ );
+ });
+ tearDown(() {
+ styleElement.remove();
+ });
+
+ test('(Self-test) hasCssRule can extract rules', () {
+ final bool hasRule = hasCssRule(styleElement,
+ selector: '.flt-text-editing::placeholder', declaration: 'opacity: 0');
+
+ final bool hasFakeRule = hasCssRule(styleElement,
+ selector: 'input::selection', declaration: 'color: #fabada;');
+
+ expect(hasRule, isTrue);
+ expect(hasFakeRule, isFalse);
+ });
+
+ test('Attaches outrageous text styles to flt-scene-host', () {
+ final bool hasColorRed = hasCssRule(styleElement,
+ selector: 'flt-scene-host', declaration: 'color: red');
+
+ bool hasFont = false;
+ if (isSafari) {
+ // Safari expands the shorthand rules, so we check for all we've set (separately).
+ hasFont = hasCssRule(styleElement,
+ selector: 'flt-scene-host',
+ declaration: 'font-family: monospace') &&
+ hasCssRule(styleElement,
+ selector: 'flt-scene-host', declaration: 'font-size: 14px');
+ } else {
+ hasFont = hasCssRule(styleElement,
+ selector: 'flt-scene-host', declaration: 'font: $_kDefaultCssFont');
+ }
+
+ expect(hasColorRed, isTrue,
+ reason: 'Should make foreground color red within scene host.');
+ expect(hasFont, isTrue, reason: 'Should pass default css font.');
+ });
+
+ test('Attaches styling to remove password reveal icons on Edge', () {
+ // Check that style.sheet! contains input::-ms-reveal rule
+ final bool hidesRevealIcons = hasCssRule(styleElement,
+ selector: 'input::-ms-reveal', declaration: 'display: none');
+
+ final bool codeRanInFakeyBrowser = hasCssRule(styleElement,
+ selector: 'input.fallback-for-fakey-browser-in-ci',
+ declaration: 'display: none');
+
+ if (codeRanInFakeyBrowser) {
+ print('Please, fix https://github.com/flutter/flutter/issues/116302');
+ }
+
+ expect(hidesRevealIcons || codeRanInFakeyBrowser, isTrue,
+ reason: 'In Edge, stylesheet must contain "input::-ms-reveal" rule.');
+ }, skip: !isEdge);
+
+ test('Does not attach the Edge-specific style tag on non-Edge browsers', () {
+ // Check that style.sheet! contains input::-ms-reveal rule
+ final bool hidesRevealIcons = hasCssRule(styleElement,
+ selector: 'input::-ms-reveal', declaration: 'display: none');
+
+ expect(hidesRevealIcons, isFalse);
+ }, skip: isEdge);
+
+ test(
+ 'Attaches styles to hide the autofill overlay for browsers that support it',
+ () {
+ final String vendorPrefix = (isSafari || isFirefox) ? '' : '-webkit-';
+ final bool autofillOverlay = hasCssRule(styleElement,
+ selector: '.transparentTextEditing:${vendorPrefix}autofill',
+ declaration: 'opacity: 0 !important');
+ final bool autofillOverlayHovered = hasCssRule(styleElement,
+ selector: '.transparentTextEditing:${vendorPrefix}autofill:hover',
+ declaration: 'opacity: 0 !important');
+ final bool autofillOverlayFocused = hasCssRule(styleElement,
+ selector: '.transparentTextEditing:${vendorPrefix}autofill:focus',
+ declaration: 'opacity: 0 !important');
+ final bool autofillOverlayActive = hasCssRule(styleElement,
+ selector: '.transparentTextEditing:${vendorPrefix}autofill:active',
+ declaration: 'opacity: 0 !important');
+
+ expect(autofillOverlay, isTrue);
+ expect(autofillOverlayHovered, isTrue);
+ expect(autofillOverlayFocused, isTrue);
+ expect(autofillOverlayActive, isTrue);
+ }, skip: !browserHasAutofillOverlay());
+}
+
+/// Finds out whether a given CSS Rule ([selector] { [declaration]; }) exists in a [styleElement].
+bool hasCssRule(
+ DomHTMLStyleElement styleElement, {
+ required String selector,
+ required String declaration,
+}) {
+ assert(styleElement.sheet != null);
+
+ // regexr.com/740ff
+ final RegExp ruleLike =
+ RegExp('[^{]*(?:$selector)[^{]*{[^}]*(?:$declaration)[^}]*}');
+
+ final DomCSSStyleSheet sheet = styleElement.sheet! as DomCSSStyleSheet;
+
+ // Check that the cssText of any rule matches the ruleLike RegExp.
+ return sheet.cssRules
+ .map((DomCSSRule rule) => rule.cssText)
+ .any((String rule) => ruleLike.hasMatch(rule));
+}
diff --git a/lib/web_ui/test/engine/host_node_test.dart b/lib/web_ui/test/engine/host_node_test.dart
deleted file mode 100644
index 205bdaf..0000000
--- a/lib/web_ui/test/engine/host_node_test.dart
+++ /dev/null
@@ -1,221 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import 'package:test/bootstrap/browser.dart';
-import 'package:test/test.dart';
-import 'package:ui/src/engine.dart';
-
-void main() {
- internalBootstrapBrowserTest(() => testMain);
-}
-
-void testMain() {
- final DomElement rootNode = domDocument.createElement('div');
- domDocument.body!.append(rootNode);
-
- group('ShadowDomHostNode', () {
- final HostNode hostNode = ShadowDomHostNode(rootNode, '14px monospace');
-
- 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);
-
- // The shadow root should be initialized with correct parameters.
- 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(rootNode.shadowRoot!.delegatesFocus, isFalse);
- }
- });
-
- test('Attaches a stylesheet to the shadow root', () {
- final DomElement? style =
- hostNode.querySelector('#flt-internals-stylesheet');
-
- expect(style, isNotNull);
- expect(style!.tagName, equalsIgnoringCase('style'));
- });
-
- test('(Self-test) hasCssRule can extract rules', () {
- final DomElement? style =
- hostNode.querySelector('#flt-internals-stylesheet');
-
- final bool hasRule = hasCssRule(style,
- selector: '.flt-text-editing::placeholder',
- declaration: 'opacity: 0');
-
- final bool hasFakeRule = hasCssRule(style,
- selector: 'input::selection', declaration: 'color: #fabada;');
-
- expect(hasRule, isTrue);
- expect(hasFakeRule, isFalse);
- });
-
- test('Attaches outrageous text styles to flt-scene-host', () {
- final DomElement? style =
- hostNode.querySelector('#flt-internals-stylesheet');
-
- final bool hasColorRed = hasCssRule(style,
- selector: 'flt-scene-host', declaration: 'color: red');
-
- bool hasFont = false;
- if (isSafari) {
- // Safari expands the shorthand rules, so we check for all we've set (separately).
- hasFont = hasCssRule(style,
- selector: 'flt-scene-host',
- declaration: 'font-family: monospace') &&
- hasCssRule(style,
- selector: 'flt-scene-host', declaration: 'font-size: 14px');
- } else {
- hasFont = hasCssRule(style,
- selector: 'flt-scene-host', declaration: 'font: 14px monospace');
- }
-
- expect(hasColorRed, isTrue,
- reason: 'Should make foreground color red within scene host.');
- expect(hasFont, isTrue, reason: 'Should pass default css font.');
- });
-
- test('Attaches styling to remove password reveal icons on Edge', () {
- final DomElement? style =
- hostNode.querySelector('#flt-internals-stylesheet');
-
- // Check that style.sheet! contains input::-ms-reveal rule
- final bool hidesRevealIcons = hasCssRule(style,
- selector: 'input::-ms-reveal', declaration: 'display: none');
-
- final bool codeRanInFakeyBrowser = hasCssRule(style,
- selector: 'input.fallback-for-fakey-browser-in-ci',
- declaration: 'display: none');
-
- if (codeRanInFakeyBrowser) {
- print('Please, fix https://github.com/flutter/flutter/issues/116302');
- }
-
- expect(hidesRevealIcons || codeRanInFakeyBrowser, isTrue,
- reason: 'In Edge, stylesheet must contain "input::-ms-reveal" rule.');
- }, skip: !isEdge);
-
- test('Does not attach the Edge-specific style tag on non-Edge browsers',
- () {
- final DomElement? style =
- hostNode.querySelector('#flt-internals-stylesheet');
-
- // Check that style.sheet! contains input::-ms-reveal rule
- final bool hidesRevealIcons = hasCssRule(style,
- selector: 'input::-ms-reveal', declaration: 'display: none');
-
- expect(hidesRevealIcons, isFalse);
- }, skip: isEdge);
-
- test(
- 'Attaches styles to hide the autofill overlay for browsers that support it',
- () {
- final DomElement? style =
- hostNode.querySelector('#flt-internals-stylesheet');
- final String vendorPrefix = (isSafari || isFirefox) ? '' : '-webkit-';
- final bool autofillOverlay = hasCssRule(style,
- selector: '.transparentTextEditing:${vendorPrefix}autofill',
- declaration: 'opacity: 0 !important');
- final bool autofillOverlayHovered = hasCssRule(style,
- selector: '.transparentTextEditing:${vendorPrefix}autofill:hover',
- declaration: 'opacity: 0 !important');
- final bool autofillOverlayFocused = hasCssRule(style,
- selector: '.transparentTextEditing:${vendorPrefix}autofill:focus',
- declaration: 'opacity: 0 !important');
- final bool autofillOverlayActive = hasCssRule(style,
- selector: '.transparentTextEditing:${vendorPrefix}autofill:active',
- declaration: 'opacity: 0 !important');
-
- expect(autofillOverlay, isTrue);
- expect(autofillOverlayHovered, isTrue);
- expect(autofillOverlayFocused, isTrue);
- expect(autofillOverlayActive, isTrue);
- }, skip: !browserHasAutofillOverlay());
-
- _runDomTests(hostNode);
- });
-
- group('ElementHostNode', () {
- final HostNode hostNode = ElementHostNode(rootNode, '');
-
- test('Initializes and attaches a child element', () {
- expect(domInstanceOfString(hostNode.node, 'Element'), isTrue);
- expect((hostNode.node as DomElement).shadowRoot, isNull);
- expect(hostNode.node.parentNode, rootNode);
- });
-
- _runDomTests(hostNode);
- });
-}
-
-// The common test suite that all types of HostNode implementations need to pass.
-void _runDomTests(HostNode hostNode) {
- group('DOM operations', () {
- final DomElement target = domDocument.createElement('div')..id = 'yep';
-
- setUp(() {
- hostNode.appendAll(<DomNode>[
- domDocument.createElement('div'),
- target,
- domDocument.createElement('flt-span'),
- domDocument.createElement('div'),
- ]);
- });
-
- tearDown(() {
- hostNode.node.clearChildren();
- });
-
- test('querySelector', () {
- final DomElement? found = hostNode.querySelector('#yep');
-
- expect(found, target);
- });
-
- test('.contains and .append', () {
- final DomElement another = domDocument.createElement('div')
- ..id = 'another';
-
- expect(hostNode.contains(target), isTrue);
- expect(hostNode.contains(another), isFalse);
- expect(hostNode.contains(null), isFalse);
-
- hostNode.append(another);
- expect(hostNode.contains(another), isTrue);
- });
-
- test('querySelectorAll', () {
- final List<DomNode> found = hostNode.querySelectorAll('div').toList();
-
- expect(found.length, 3);
- expect(found[1], target);
- });
- });
-}
-
-/// Finds out whether a given CSS Rule ([selector] { [declaration]; }) exists in a [styleSheet].
-bool hasCssRule(
- DomElement? styleSheet, {
- required String selector,
- required String declaration,
-}) {
- assert(styleSheet != null);
- assert((styleSheet! as DomHTMLStyleElement).sheet != null);
-
- // regexr.com/740ff
- final RegExp ruleLike =
- RegExp('[^{]*(?:$selector)[^{]*{[^}]*(?:$declaration)[^}]*}');
-
- final DomCSSStyleSheet sheet =
- (styleSheet! as DomHTMLStyleElement).sheet! as DomCSSStyleSheet;
-
- // Check that the cssText of any rule matches the ruleLike RegExp.
- return sheet.cssRules
- .map((DomCSSRule rule) => rule.cssText)
- .any((String rule) => ruleLike.hasMatch(rule));
-}