diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 66fdc6c94173b..0ab2484e9431d 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -1902,7 +1902,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/engine_canvas.dart + ../../.. 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 @@ -4489,7 +4489,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/engine_canvas.dart 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 07e54ca03c127..1c3021224e98b 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -66,7 +66,7 @@ export 'engine/engine_canvas.dart'; 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 245fd26e00b51..5ac02cf7b6141 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 @@ class FlutterViewEmbedder { 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 @@ class FlutterViewEmbedder { _embeddingStrategy.attachGlassPane(flutterViewElement); flutterViewElement.appendChild(glassPaneElement); + if (getJsProperty(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({ + '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 @@ class FlutterViewEmbedder { .instance.semanticsHelper .prepareAccessibilityPlaceholder(); - glassPaneElementHostNode.appendAll([ - 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 @@ class FlutterViewEmbedder { _embeddingStrategy.attachResourcesHost(resourcesHost, nextTo: flutterViewElement); } else { - glassPaneShadow.node - .insertBefore(resourcesHost, glassPaneShadow.node.firstChild); + glassPaneShadow.insertBefore(resourcesHost, glassPaneShadow.firstChild); } _resourcesHost = resourcesHost; } @@ -420,7 +432,7 @@ DomElement createTextEditingHostNode(DomElement root, String defaultFont) { 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 0000000000000..6be6914ceaf22 --- /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 dea385d0a4dac..0000000000000 --- 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(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] to this [HostNode]. - void appendAll(Iterable 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 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({ - '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 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 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 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 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 3a026e2c5d235..a1f3bdf6ebce2 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 @@ class RulerHost { ..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 3e22f3dd10adb..2be73503270a9 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 'package:ui/ui.dart' as ui; 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 86a8283870934..ecba37c945ba0 100644 --- a/lib/web_ui/test/engine/embedder_test.dart +++ b/lib/web_ui/test/engine/embedder_test.dart @@ -52,28 +52,6 @@ void testMain() { ); }); - 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 @@ void testMain() { 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 0000000000000..3abeb1f9fe7ca --- /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 205bdafa9f4a1..0000000000000 --- 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([ - 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 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)); -}