Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(
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