Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[web] Remove non-ShadowDom mode #39915

Merged
merged 10 commits into from
Apr 19, 2023
4 changes: 1 addition & 3 deletions lib/web_ui/lib/src/engine/embedder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,7 @@ class FlutterViewEmbedder {

// 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(
final HostNode glassPaneElementHostNode = HostNode(
ditman marked this conversation as resolved.
Show resolved Hide resolved
glassPaneElement,
defaultCssFont,
);
Expand Down
194 changes: 45 additions & 149 deletions lib/web_ui/lib/src/engine/host_node.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,53 @@

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].
/// It is backed by a [DomShadowRoot] and handles the stylesheet that is applied
/// to the ShadowDOM.
class HostNode {
/// Build a HostNode by attaching a [DomShadowRoot] to the `root` element.
///
/// 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);
/// This also calls [applyGlobalCssRulesToSheet], with the [defaultFont]
/// to be used as the default font definition.
HostNode(DomElement root, String defaultFont)
: assert(
root.isConnected ?? true,
'The `root` of a ShadowDomHostNode must be connected to the Document object or a ShadowRoot.',
) {
if (getJsProperty<Object?>(root, 'attachShadow') == null) {
throw UnsupportedError('ShadowDOM is not supported in this browser.');
}

_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;

/// Retrieves the [DomElement] that currently has focus.
///
/// See:
/// * [Document.activeElement](https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement)
DomElement? get activeElement;
DomElement? get activeElement => _shadow.activeElement;

/// Adds a node to the end of the child [nodes] list of this node.
///
Expand All @@ -49,18 +62,22 @@ abstract class HostNode {
///
/// See:
/// * [Node.appendChild](https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild)
DomNode append(DomNode node);
DomNode append(DomNode node) {
return _shadow.appendChild(node);
}

/// Appends all of an [Iterable<DomNode>] to this [HostNode].
void appendAll(Iterable<DomNode> nodes);
void appendAll(Iterable<DomNode> nodes) => nodes.forEach(append);

/// 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);
bool contains(DomNode? other) {
return _shadow.contains(other);
}

/// Returns the currently wrapped [DomNode].
DomNode get node;
DomNode get node => _shadow;

/// Finds the first descendant element of this document that matches the
/// specified group of selectors.
Expand All @@ -77,7 +94,9 @@ abstract class HostNode {
///
/// See:
/// * [Document.querySelector](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
DomElement? querySelector(String selectors);
DomElement? querySelector(String selectors) {
return _shadow.querySelector(selectors);
}

/// Finds all descendant elements of this document that match the specified
/// group of selectors.
Expand All @@ -93,132 +112,9 @@ abstract class HostNode {
///
/// 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.glassPaneTagName,
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`.
Expand Down
10 changes: 2 additions & 8 deletions lib/web_ui/test/embedder_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,13 @@ void testMain() {
expect(domInstanceOfString(hostNode.node, 'ShadowRoot'), isTrue);
});

test('starts without shadowDom available too', () {
test('throws when shadowDom is not available', () {
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'),
);
expect(() => FlutterViewEmbedder(), throwsUnsupportedError);
attachShadow = oldAttachShadow; // Restore ShadowDOM
});

Expand Down
83 changes: 33 additions & 50 deletions lib/web_ui/test/engine/host_node_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ void testMain() {
final DomElement rootNode = domDocument.createElement('div');
domDocument.body!.append(rootNode);

group('ShadowDomHostNode', () {
final HostNode hostNode = ShadowDomHostNode(rootNode, '14px monospace');
group('$HostNode', () {
final HostNode hostNode = HostNode(rootNode, '14px monospace');

test('Initializes and attaches a shadow root', () {
expect(domInstanceOfString(hostNode.node, 'ShadowRoot'), isTrue);
Expand Down Expand Up @@ -137,63 +137,46 @@ void testMain() {
expect(autofillOverlayActive, isTrue);
}, skip: !browserHasAutofillOverlay());

_runDomTests(hostNode);
});
group('DOM operations', () {
final DomElement target = domDocument.createElement('div')..id = 'yep';

group('ElementHostNode', () {
final HostNode hostNode = ElementHostNode(rootNode, '');
setUp(() {
hostNode.appendAll(<DomNode>[
domDocument.createElement('div'),
target,
domDocument.createElement('flt-span'),
domDocument.createElement('div'),
]);
});

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);
});
tearDown(() {
hostNode.node.clearChildren();
});

_runDomTests(hostNode);
});
}
test('querySelector', () {
final DomElement? found = hostNode.querySelector('#yep');

// 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'),
]);
});
expect(found, target);
});

tearDown(() {
hostNode.node.clearChildren();
});
test('.contains and .append', () {
final DomElement another = domDocument.createElement('div')
..id = 'another';

test('querySelector', () {
final DomElement? found = hostNode.querySelector('#yep');
expect(hostNode.contains(target), isTrue);
expect(hostNode.contains(another), isFalse);
expect(hostNode.contains(null), isFalse);

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);
});
hostNode.append(another);
expect(hostNode.contains(another), isTrue);
});

test('querySelectorAll', () {
final List<DomNode> found = hostNode.querySelectorAll('div').toList();
test('querySelectorAll', () {
final List<DomNode> found = hostNode.querySelectorAll('div').toList();

expect(found.length, 3);
expect(found[1], target);
expect(found.length, 3);
expect(found[1], target);
});
});
});
}
Expand Down