Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 0416103

Browse files
authored
[web] Move text editing nodes outside of shadowDOM (#39688)
[web] Move text editing nodes outside of shadowDOM
1 parent a378da4 commit 0416103

File tree

12 files changed

+192
-165
lines changed

12 files changed

+192
-165
lines changed

lib/web_ui/lib/src/engine/embedder.dart

+39-12
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ class FlutterViewEmbedder {
124124
HostNode get glassPaneShadow => _glassPaneShadow;
125125
late HostNode _glassPaneShadow;
126126

127+
DomElement get textEditingHostNode => _textEditingHostNode;
128+
late DomElement _textEditingHostNode;
129+
127130
static const String defaultFontStyle = 'normal';
128131
static const String defaultFontWeight = 'normal';
129132
static const double defaultFontSize = 14;
@@ -168,6 +171,9 @@ class FlutterViewEmbedder {
168171
);
169172
_glassPaneShadow = glassPaneElementHostNode;
170173

174+
_textEditingHostNode =
175+
createTextEditingHostNode(glassPaneElement, defaultCssFont);
176+
171177
// Don't allow the scene to receive pointer events.
172178
_sceneHostElement = domDocument.createElement('flt-scene-host')
173179
..style.pointerEvents = 'none';
@@ -189,20 +195,20 @@ class FlutterViewEmbedder {
189195
glassPaneElementHostNode.appendAll(<DomNode>[
190196
accessibilityPlaceholder,
191197
_sceneHostElement!,
192-
193-
// The semantic host goes last because hit-test order-wise it must be
194-
// first. If semantics goes under the scene host, platform views will
195-
// obscure semantic elements.
196-
//
197-
// You may be wondering: wouldn't semantics obscure platform views and
198-
// make then not accessible? At least with some careful planning, that
199-
// should not be the case. The semantics tree makes all of its non-leaf
200-
// elements transparent. This way, if a platform view appears among other
201-
// interactive Flutter widgets, as long as those widgets do not intersect
202-
// with the platform view, the platform view will be reachable.
203-
semanticsHostElement,
204198
]);
205199

200+
// The semantic host goes last because hit-test order-wise it must be
201+
// first. If semantics goes under the scene host, platform views will
202+
// obscure semantic elements.
203+
//
204+
// You may be wondering: wouldn't semantics obscure platform views and
205+
// make then not accessible? At least with some careful planning, that
206+
// should not be the case. The semantics tree makes all of its non-leaf
207+
// elements transparent. This way, if a platform view appears among other
208+
// interactive Flutter widgets, as long as those widgets do not intersect
209+
// with the platform view, the platform view will be reachable.
210+
glassPaneElement.appendChild(semanticsHostElement);
211+
206212
// When debugging semantics, make the scene semi-transparent so that the
207213
// semantics tree is more prominent.
208214
if (configuration.debugShowSemanticsNodes) {
@@ -393,3 +399,24 @@ FlutterViewEmbedder? _flutterViewEmbedder;
393399
FlutterViewEmbedder ensureFlutterViewEmbedderInitialized() =>
394400
_flutterViewEmbedder ??=
395401
FlutterViewEmbedder(hostElement: configuration.hostElement);
402+
403+
/// Creates a node to host text editing elements and applies a stylesheet
404+
/// to Flutter nodes that exist outside of the shadowDOM.
405+
DomElement createTextEditingHostNode(DomElement root, String defaultFont) {
406+
final DomElement domElement =
407+
domDocument.createElement('flt-text-editing-host');
408+
final DomHTMLStyleElement styleElement = createDomHTMLStyleElement();
409+
410+
styleElement.id = 'flt-text-editing-stylesheet';
411+
root.appendChild(styleElement);
412+
applyGlobalCssRulesToSheet(
413+
styleElement.sheet! as DomCSSStyleSheet,
414+
hasAutofillOverlay: browserHasAutofillOverlay(),
415+
cssSelectorPrefix: FlutterViewEmbedder.glassPaneTagName,
416+
defaultCssFont: defaultFont,
417+
);
418+
419+
root.appendChild(domElement);
420+
421+
return domElement;
422+
}

lib/web_ui/lib/src/engine/host_node.dart

+12-5
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ abstract class HostNode {
9494
/// See:
9595
/// * [Document.querySelectorAll](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll)
9696
Iterable<DomElement> querySelectorAll(String selectors);
97+
98+
DomElement get renderHost;
9799
}
98100

99101
/// A [HostNode] implementation, backed by a [DomShadowRoot].
@@ -110,11 +112,10 @@ class ShadowDomHostNode implements HostNode {
110112
/// This also calls [applyGlobalCssRulesToSheet], with the [defaultFont]
111113
/// to be used as the default font definition.
112114
ShadowDomHostNode(DomElement root, String defaultFont)
113-
: assert(
114-
root.isConnected ?? true,
115-
'The `root` of a ShadowDomHostNode must be connected to the Document object or a ShadowRoot.'
116-
) {
117-
_shadow = root.attachShadow(<String, dynamic>{
115+
: assert(root.isConnected ?? true,
116+
'The `root` of a ShadowDomHostNode must be connected to the Document object or a ShadowRoot.') {
117+
root.appendChild(renderHost);
118+
_shadow = renderHost.attachShadow(<String, dynamic>{
118119
'mode': 'open',
119120
// This needs to stay false to prevent issues like this:
120121
// - https://github.com/flutter/flutter/issues/85759
@@ -135,6 +136,9 @@ class ShadowDomHostNode implements HostNode {
135136

136137
late DomShadowRoot _shadow;
137138

139+
@override
140+
final DomElement renderHost = domDocument.createElement('flt-render-host');
141+
138142
@override
139143
DomElement? get activeElement => _shadow.activeElement;
140144

@@ -191,6 +195,9 @@ class ElementHostNode implements HostNode {
191195

192196
late DomElement _element;
193197

198+
@override
199+
final DomElement renderHost = domDocument.createElement('flt-render-host');
200+
194201
@override
195202
DomElement? get activeElement => _element.ownerDocument?.activeElement;
196203

lib/web_ui/lib/src/engine/platform_dispatcher.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -587,7 +587,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
587587
_platformViewMessageHandler ??= PlatformViewMessageHandler(
588588
contentManager: platformViewManager,
589589
contentHandler: (DomElement content) {
590-
flutterViewEmbedder.glassPaneElement.append(content);
590+
flutterViewEmbedder.glassPaneShadow.renderHost.append(content);
591591
},
592592
);
593593
_platformViewMessageHandler!.handlePlatformViewCall(data, callback!);

lib/web_ui/lib/src/engine/platform_views/content_manager.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,9 @@ class PlatformViewManager {
128128
}
129129

130130
_ensureContentCorrectlySized(content, viewType);
131+
wrapper.append(content);
131132

132-
return wrapper..append(content);
133+
return wrapper;
133134
});
134135
}
135136

lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart

+8-6
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,22 @@ import '../semantics.dart' show EngineSemanticsOwner;
1919
/// It also takes into account semantics being enabled to fix the case where
2020
/// offsetX, offsetY == 0 (TalkBack events).
2121
ui.Offset computeEventOffsetToTarget(DomMouseEvent event, DomElement actualTarget) {
22-
// On top of a platform view
23-
if (event.target != actualTarget) {
24-
return _computeOffsetOnPlatformView(event, actualTarget);
25-
}
2622
// On a TalkBack event
2723
if (EngineSemanticsOwner.instance.semanticsEnabled && event.offsetX == 0 && event.offsetY == 0) {
2824
return _computeOffsetForTalkbackEvent(event, actualTarget);
2925
}
26+
27+
final bool isTargetOutsideOfShadowDOM = event.target != actualTarget;
28+
if (isTargetOutsideOfShadowDOM) {
29+
return _computeOffsetRelativeToActualTarget(event, actualTarget);
30+
}
3031
// Return the offsetX/Y in the normal case.
3132
// (This works with 3D translations of the parent element.)
3233
return ui.Offset(event.offsetX, event.offsetY);
3334
}
3435

35-
/// Computes the event offset when hovering over a platformView.
36+
/// Computes the event offset when hovering over any nodes that don't exist in
37+
/// the shadowDOM such as platform views or text editing nodes.
3638
///
3739
/// This still uses offsetX/Y, but adds the offset from the top/left corner of the
3840
/// platform view to the glass pane (`actualTarget`).
@@ -57,7 +59,7 @@ ui.Offset computeEventOffsetToTarget(DomMouseEvent event, DomElement actualTarge
5759
///
5860
/// Event offset relative to FlutterView = (offsetX + xP, offsetY + yP)
5961
// TODO(dit): Make this understand 3D transforms, https://github.com/flutter/flutter/issues/117091
60-
ui.Offset _computeOffsetOnPlatformView(DomMouseEvent event, DomElement actualTarget) {
62+
ui.Offset _computeOffsetRelativeToActualTarget(DomMouseEvent event, DomElement actualTarget) {
6163
final DomElement target = event.target! as DomElement;
6264
final DomRect targetRect = target.getBoundingClientRect();
6365
final DomRect actualTargetRect = actualTarget.getBoundingClientRect();

lib/web_ui/lib/src/engine/semantics/text_field.dart

+2-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import 'package:ui/ui.dart' as ui;
77

88
import '../browser_detection.dart';
99
import '../dom.dart';
10-
import '../embedder.dart';
1110
import '../platform_dispatcher.dart';
1211
import '../safe_browser_api.dart';
1312
import '../text_editing/text_editing.dart';
@@ -422,14 +421,14 @@ class TextField extends RoleManager {
422421
..height = '${semanticsObject.rect!.height}px';
423422

424423
if (semanticsObject.hasFocus) {
425-
if (flutterViewEmbedder.glassPaneShadow.activeElement !=
424+
if (domDocument.activeElement !=
426425
activeEditableElement) {
427426
semanticsObject.owner.addOneTimePostUpdateCallback(() {
428427
activeEditableElement.focus();
429428
});
430429
}
431430
SemanticsTextEditingStrategy._instance?.activate(this);
432-
} else if (flutterViewEmbedder.glassPaneShadow.activeElement ==
431+
} else if (domDocument.activeElement ==
433432
activeEditableElement) {
434433
if (!isIosSafari) {
435434
SemanticsTextEditingStrategy._instance?.deactivate(this);

lib/web_ui/lib/src/engine/text_editing/text_editing.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ void _emptyCallback(dynamic _) {}
5151

5252
/// The default [HostNode] that hosts all DOM required for text editing when a11y is not enabled.
5353
@visibleForTesting
54-
HostNode get defaultTextEditingRoot => flutterViewEmbedder.glassPaneShadow;
54+
DomElement get defaultTextEditingRoot =>
55+
flutterViewEmbedder.textEditingHostNode;
5556

5657
/// These style attributes are constant throughout the life time of an input
5758
/// element.

lib/web_ui/test/engine/host_node_test.dart

+5-4
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,20 @@ void testMain() {
1616

1717
group('ShadowDomHostNode', () {
1818
final HostNode hostNode = ShadowDomHostNode(rootNode, '14px monospace');
19+
final DomElement renderHost = domDocument.querySelector('flt-render-host')!;
1920

2021
test('Initializes and attaches a shadow root', () {
2122
expect(domInstanceOfString(hostNode.node, 'ShadowRoot'), isTrue);
22-
expect((hostNode.node as DomShadowRoot).host, rootNode);
23-
expect(hostNode.node, rootNode.shadowRoot);
23+
expect((hostNode.node as DomShadowRoot).host, renderHost);
24+
expect(hostNode.node, renderHost.shadowRoot);
2425

2526
// The shadow root should be initialized with correct parameters.
26-
expect(rootNode.shadowRoot!.mode, 'open');
27+
expect(renderHost.shadowRoot!.mode, 'open');
2728
if (browserEngine != BrowserEngine.firefox &&
2829
browserEngine != BrowserEngine.webkit) {
2930
// Older versions of Safari and Firefox don't support this flag yet.
3031
// See: https://caniuse.com/mdn-api_shadowroot_delegatesfocus
31-
expect(rootNode.shadowRoot!.delegatesFocus, isFalse);
32+
expect(renderHost.shadowRoot!.delegatesFocus, isFalse);
3233
}
3334
});
3435

0 commit comments

Comments
 (0)