[web] Move text editing nodes outside of shadowDOM (flutter/engine#39688)

[web] Move text editing nodes outside of shadowDOM
This commit is contained in:
htoor3 2023-03-31 18:13:21 -05:00 committed by GitHub
parent c8039e2544
commit c32816a82c
12 changed files with 192 additions and 165 deletions

View File

@ -124,6 +124,9 @@ class FlutterViewEmbedder {
HostNode get glassPaneShadow => _glassPaneShadow;
late HostNode _glassPaneShadow;
DomElement get textEditingHostNode => _textEditingHostNode;
late DomElement _textEditingHostNode;
static const String defaultFontStyle = 'normal';
static const String defaultFontWeight = 'normal';
static const double defaultFontSize = 14;
@ -168,6 +171,9 @@ class FlutterViewEmbedder {
);
_glassPaneShadow = glassPaneElementHostNode;
_textEditingHostNode =
createTextEditingHostNode(glassPaneElement, defaultCssFont);
// Don't allow the scene to receive pointer events.
_sceneHostElement = domDocument.createElement('flt-scene-host')
..style.pointerEvents = 'none';
@ -189,20 +195,20 @@ class FlutterViewEmbedder {
glassPaneElementHostNode.appendAll(<DomNode>[
accessibilityPlaceholder,
_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
// obscure semantic elements.
//
// You may be wondering: wouldn't semantics obscure platform views and
// make then not accessible? At least with some careful planning, that
// should not be the case. The semantics tree makes all of its non-leaf
// elements transparent. This way, if a platform view appears among other
// interactive Flutter widgets, as long as those widgets do not intersect
// with the platform view, the platform view will be reachable.
semanticsHostElement,
]);
// The semantic host goes last because hit-test order-wise it must be
// first. If semantics goes under the scene host, platform views will
// obscure semantic elements.
//
// You may be wondering: wouldn't semantics obscure platform views and
// make then not accessible? At least with some careful planning, that
// should not be the case. The semantics tree makes all of its non-leaf
// elements transparent. This way, if a platform view appears among other
// interactive Flutter widgets, as long as those widgets do not intersect
// with the platform view, the platform view will be reachable.
glassPaneElement.appendChild(semanticsHostElement);
// When debugging semantics, make the scene semi-transparent so that the
// semantics tree is more prominent.
if (configuration.debugShowSemanticsNodes) {
@ -393,3 +399,24 @@ FlutterViewEmbedder? _flutterViewEmbedder;
FlutterViewEmbedder ensureFlutterViewEmbedderInitialized() =>
_flutterViewEmbedder ??=
FlutterViewEmbedder(hostElement: configuration.hostElement);
/// Creates a node to host text editing elements and applies a stylesheet
/// to Flutter nodes that exist outside of the shadowDOM.
DomElement createTextEditingHostNode(DomElement root, String defaultFont) {
final DomElement domElement =
domDocument.createElement('flt-text-editing-host');
final DomHTMLStyleElement styleElement = createDomHTMLStyleElement();
styleElement.id = 'flt-text-editing-stylesheet';
root.appendChild(styleElement);
applyGlobalCssRulesToSheet(
styleElement.sheet! as DomCSSStyleSheet,
hasAutofillOverlay: browserHasAutofillOverlay(),
cssSelectorPrefix: FlutterViewEmbedder.glassPaneTagName,
defaultCssFont: defaultFont,
);
root.appendChild(domElement);
return domElement;
}

View File

@ -94,6 +94,8 @@ abstract class HostNode {
/// See:
/// * [Document.querySelectorAll](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll)
Iterable<DomElement> querySelectorAll(String selectors);
DomElement get renderHost;
}
/// A [HostNode] implementation, backed by a [DomShadowRoot].
@ -110,11 +112,10 @@ class ShadowDomHostNode implements HostNode {
/// 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>{
: assert(root.isConnected ?? true,
'The `root` of a ShadowDomHostNode must be connected to the Document object or a ShadowRoot.') {
root.appendChild(renderHost);
_shadow = renderHost.attachShadow(<String, dynamic>{
'mode': 'open',
// This needs to stay false to prevent issues like this:
// - https://github.com/flutter/flutter/issues/85759
@ -135,6 +136,9 @@ class ShadowDomHostNode implements HostNode {
late DomShadowRoot _shadow;
@override
final DomElement renderHost = domDocument.createElement('flt-render-host');
@override
DomElement? get activeElement => _shadow.activeElement;
@ -191,6 +195,9 @@ class ElementHostNode implements HostNode {
late DomElement _element;
@override
final DomElement renderHost = domDocument.createElement('flt-render-host');
@override
DomElement? get activeElement => _element.ownerDocument?.activeElement;

View File

@ -587,7 +587,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
_platformViewMessageHandler ??= PlatformViewMessageHandler(
contentManager: platformViewManager,
contentHandler: (DomElement content) {
flutterViewEmbedder.glassPaneElement.append(content);
flutterViewEmbedder.glassPaneShadow.renderHost.append(content);
},
);
_platformViewMessageHandler!.handlePlatformViewCall(data, callback!);

View File

@ -128,8 +128,9 @@ class PlatformViewManager {
}
_ensureContentCorrectlySized(content, viewType);
wrapper.append(content);
return wrapper..append(content);
return wrapper;
});
}

View File

@ -19,20 +19,22 @@ import '../semantics.dart' show EngineSemanticsOwner;
/// It also takes into account semantics being enabled to fix the case where
/// offsetX, offsetY == 0 (TalkBack events).
ui.Offset computeEventOffsetToTarget(DomMouseEvent event, DomElement actualTarget) {
// On top of a platform view
if (event.target != actualTarget) {
return _computeOffsetOnPlatformView(event, actualTarget);
}
// On a TalkBack event
if (EngineSemanticsOwner.instance.semanticsEnabled && event.offsetX == 0 && event.offsetY == 0) {
return _computeOffsetForTalkbackEvent(event, actualTarget);
}
final bool isTargetOutsideOfShadowDOM = event.target != actualTarget;
if (isTargetOutsideOfShadowDOM) {
return _computeOffsetRelativeToActualTarget(event, actualTarget);
}
// Return the offsetX/Y in the normal case.
// (This works with 3D translations of the parent element.)
return ui.Offset(event.offsetX, event.offsetY);
}
/// Computes the event offset when hovering over a platformView.
/// Computes the event offset when hovering over any nodes that don't exist in
/// the shadowDOM such as platform views or text editing nodes.
///
/// This still uses offsetX/Y, but adds the offset from the top/left corner of the
/// platform view to the glass pane (`actualTarget`).
@ -57,7 +59,7 @@ ui.Offset computeEventOffsetToTarget(DomMouseEvent event, DomElement actualTarge
///
/// Event offset relative to FlutterView = (offsetX + xP, offsetY + yP)
// TODO(dit): Make this understand 3D transforms, https://github.com/flutter/flutter/issues/117091
ui.Offset _computeOffsetOnPlatformView(DomMouseEvent event, DomElement actualTarget) {
ui.Offset _computeOffsetRelativeToActualTarget(DomMouseEvent event, DomElement actualTarget) {
final DomElement target = event.target! as DomElement;
final DomRect targetRect = target.getBoundingClientRect();
final DomRect actualTargetRect = actualTarget.getBoundingClientRect();

View File

@ -7,7 +7,6 @@ import 'package:ui/ui.dart' as ui;
import '../browser_detection.dart';
import '../dom.dart';
import '../embedder.dart';
import '../platform_dispatcher.dart';
import '../safe_browser_api.dart';
import '../text_editing/text_editing.dart';
@ -422,14 +421,14 @@ class TextField extends RoleManager {
..height = '${semanticsObject.rect!.height}px';
if (semanticsObject.hasFocus) {
if (flutterViewEmbedder.glassPaneShadow.activeElement !=
if (domDocument.activeElement !=
activeEditableElement) {
semanticsObject.owner.addOneTimePostUpdateCallback(() {
activeEditableElement.focus();
});
}
SemanticsTextEditingStrategy._instance?.activate(this);
} else if (flutterViewEmbedder.glassPaneShadow.activeElement ==
} else if (domDocument.activeElement ==
activeEditableElement) {
if (!isIosSafari) {
SemanticsTextEditingStrategy._instance?.deactivate(this);

View File

@ -51,7 +51,8 @@ void _emptyCallback(dynamic _) {}
/// The default [HostNode] that hosts all DOM required for text editing when a11y is not enabled.
@visibleForTesting
HostNode get defaultTextEditingRoot => flutterViewEmbedder.glassPaneShadow;
DomElement get defaultTextEditingRoot =>
flutterViewEmbedder.textEditingHostNode;
/// These style attributes are constant throughout the life time of an input
/// element.

View File

@ -16,19 +16,20 @@ void testMain() {
group('ShadowDomHostNode', () {
final HostNode hostNode = ShadowDomHostNode(rootNode, '14px monospace');
final DomElement renderHost = domDocument.querySelector('flt-render-host')!;
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);
expect((hostNode.node as DomShadowRoot).host, renderHost);
expect(hostNode.node, renderHost.shadowRoot);
// The shadow root should be initialized with correct parameters.
expect(rootNode.shadowRoot!.mode, 'open');
expect(renderHost.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);
expect(renderHost.shadowRoot!.delegatesFocus, isFalse);
}
});

View File

@ -157,18 +157,16 @@ void _testEngineSemanticsOwner() {
expect(semantics().semanticsEnabled, isFalse);
// Synthesize a click on the placeholder.
final DomElement placeholder =
appHostNode.querySelector('flt-semantics-placeholder')!;
final DomElement placeholder = flutterViewEmbedder.glassPaneShadow
.querySelector('flt-semantics-placeholder')!;
expect(placeholder.isConnected, isTrue);
final DomRect rect = placeholder.getBoundingClientRect();
placeholder.dispatchEvent(createDomMouseEvent(
'click', <Object?, Object?>{
'clientX': (rect.left + (rect.right - rect.left) / 2).floor(),
'clientY': (rect.top + (rect.bottom - rect.top) / 2).floor(),
}
));
placeholder.dispatchEvent(createDomMouseEvent('click', <Object?, Object?>{
'clientX': (rect.left + (rect.right - rect.left) / 2).floor(),
'clientY': (rect.top + (rect.bottom - rect.top) / 2).floor(),
}));
// On mobile semantics is enabled asynchronously.
if (isMobile) {
@ -182,7 +180,8 @@ void _testEngineSemanticsOwner() {
test('accessibilityFeatures copyWith function works', () {
const EngineAccessibilityFeatures original = EngineAccessibilityFeatures(0);
EngineAccessibilityFeatures copy = original.copyWith(accessibleNavigation: true);
EngineAccessibilityFeatures copy =
original.copyWith(accessibleNavigation: true);
expect(copy.accessibleNavigation, true);
expect(copy.boldText, false);
expect(copy.disableAnimations, false);
@ -254,8 +253,8 @@ void _testEngineSemanticsOwner() {
.instance.accessibilityFeatures.accessibleNavigation,
isFalse);
final DomElement placeholder =
appHostNode.querySelector('flt-semantics-placeholder')!;
final DomElement placeholder = flutterViewEmbedder.glassPaneShadow
.querySelector('flt-semantics-placeholder')!;
expect(placeholder.isConnected, isTrue);
@ -428,7 +427,8 @@ void _testEngineSemanticsOwner() {
);
});
test('forwards events to framework if shouldEnableSemantics returns true', () {
test('forwards events to framework if shouldEnableSemantics returns true',
() {
final MockSemanticsEnabler mockSemanticsEnabler = MockSemanticsEnabler();
semantics().semanticsHelper.semanticsEnabler = mockSemanticsEnabler;
final DomEvent pointerEvent = createDomEvent('Event', 'pointermove');
@ -439,8 +439,7 @@ void _testEngineSemanticsOwner() {
class MockSemanticsEnabler implements SemanticsEnabler {
@override
void dispose() {
}
void dispose() {}
@override
bool get isWaitingToEnableSemantics => throw UnimplementedError();
@ -716,7 +715,8 @@ void _testContainer() {
semantics().semanticsEnabled = false;
});
test('renders in traversal order, hit-tests in reverse z-index order', () async {
test('renders in traversal order, hit-tests in reverse z-index order',
() async {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
@ -809,7 +809,9 @@ void _testContainer() {
semantics().semanticsEnabled = false;
});
test('container nodes are transparent and leaf children are opaque hit-test wise', () async {
test(
'container nodes are transparent and leaf children are opaque hit-test wise',
() async {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
@ -835,10 +837,12 @@ void _testContainer() {
final DomElement root = appHostNode.querySelector('#flt-semantic-node-0')!;
expect(root.style.pointerEvents, 'none');
final DomElement child1 = appHostNode.querySelector('#flt-semantic-node-1')!;
final DomElement child1 =
appHostNode.querySelector('#flt-semantic-node-1')!;
expect(child1.style.pointerEvents, 'all');
final DomElement child2 = appHostNode.querySelector('#flt-semantic-node-2')!;
final DomElement child2 =
appHostNode.querySelector('#flt-semantic-node-2')!;
expect(child2.style.pointerEvents, 'all');
semantics().semanticsEnabled = false;
@ -1179,8 +1183,8 @@ void _testIncrementables() {
<input aria-valuenow="1" aria-valuetext="d" aria-valuemax="2" aria-valuemin="1">
</sem>''');
final DomHTMLInputElement input = appHostNode.querySelector('input')! as
DomHTMLInputElement;
final DomHTMLInputElement input =
appHostNode.querySelector('input')! as DomHTMLInputElement;
input.value = '2';
input.dispatchEvent(createDomEvent('Event', 'change'));
@ -1212,8 +1216,8 @@ void _testIncrementables() {
<input aria-valuenow="1" aria-valuetext="d" aria-valuemax="1" aria-valuemin="0">
</sem>''');
final DomHTMLInputElement input = appHostNode.querySelector('input')! as
DomHTMLInputElement;
final DomHTMLInputElement input =
appHostNode.querySelector('input')! as DomHTMLInputElement;
input.value = '0';
input.dispatchEvent(createDomEvent('Event', 'change'));
@ -1299,11 +1303,11 @@ void _testTextField() {
final DomElement textField =
appHostNode.querySelector('input[data-semantics-role="text-field"]')!;
expect(appHostNode.activeElement, isNot(textField));
expect(appHostNode.ownerDocument?.activeElement, isNot(textField));
textField.focus();
expect(appHostNode.activeElement, textField);
expect(appHostNode.ownerDocument?.activeElement, textField);
expect(await logger.idLog.first, 0);
expect(await logger.actionLog.first, ui.SemanticsAction.tap);
@ -1616,13 +1620,15 @@ void _testTappable() {
}
updateTappable(enabled: false);
expectSemanticsTree('<sem role="button" aria-disabled="true" style="$rootSemanticStyle"></sem>');
expectSemanticsTree(
'<sem role="button" aria-disabled="true" style="$rootSemanticStyle"></sem>');
updateTappable(enabled: true);
expectSemanticsTree('<sem role="button" style="$rootSemanticStyle"></sem>');
updateTappable(enabled: false);
expectSemanticsTree('<sem role="button" aria-disabled="true" style="$rootSemanticStyle"></sem>');
expectSemanticsTree(
'<sem role="button" aria-disabled="true" style="$rootSemanticStyle"></sem>');
updateTappable(enabled: true);
expectSemanticsTree('<sem role="button" style="$rootSemanticStyle"></sem>');
@ -1647,7 +1653,7 @@ void _testTappable() {
);
tester.apply();
expect(flutterViewEmbedder.glassPaneShadow.activeElement, tester.getSemanticsObject(0).element);
expect(domDocument.activeElement, tester.getSemanticsObject(0).element);
semantics().semanticsEnabled = false;
});
}
@ -1942,13 +1948,13 @@ void _testPlatformView() {
ui.window.render(sceneBuilder.build());
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
updateNode(
builder,
rect: const ui.Rect.fromLTRB(0, 0, 20, 60),
childrenInTraversalOrder: Int32List.fromList(<int>[1, 2, 3]),
childrenInHitTestOrder: Int32List.fromList(<int>[1, 2, 3]),
transform: Float64List.fromList(Matrix4.diagonal3Values(ui.window.devicePixelRatio, ui.window.devicePixelRatio, 1).storage)
);
updateNode(builder,
rect: const ui.Rect.fromLTRB(0, 0, 20, 60),
childrenInTraversalOrder: Int32List.fromList(<int>[1, 2, 3]),
childrenInHitTestOrder: Int32List.fromList(<int>[1, 2, 3]),
transform: Float64List.fromList(Matrix4.diagonal3Values(
ui.window.devicePixelRatio, ui.window.devicePixelRatio, 1)
.storage));
updateNode(
builder,
id: 1,
@ -2009,22 +2015,19 @@ void _testPlatformView() {
final DomElement platformViewElement =
flutterViewEmbedder.glassPaneElement.querySelector('#view-0')!;
final DomRect platformViewRect = platformViewElement.getBoundingClientRect();
final DomRect platformViewRect =
platformViewElement.getBoundingClientRect();
expect(platformViewRect.left, 0);
expect(platformViewRect.top, 15);
expect(platformViewRect.right, 20);
expect(platformViewRect.bottom, 45);
// This test is only relevant for shadow DOM because we only really support
// proper platform view embedding in browsers that support shadow DOM.
final DomShadowRoot shadowRoot = appHostNode.node as DomShadowRoot;
// Hit test child 1
expect(shadowRoot.elementFromPoint(10, 10), child1);
expect(domDocument.elementFromPoint(10, 10), child1);
// Hit test overlap between child 1 and 2
// TODO(yjbanov): this is a known limitation, see https://github.com/flutter/flutter/issues/101439
expect(shadowRoot.elementFromPoint(10, 20), child1);
expect(domDocument.elementFromPoint(10, 20), child1);
// Hit test child 2
// Clicking at the location of the middle semantics node should allow the
@ -2043,10 +2046,10 @@ void _testPlatformView() {
expect(domDocument.elementFromPoint(10, 30), platformViewElement);
// Hit test overlap between child 2 and 3
expect(shadowRoot.elementFromPoint(10, 40), child3);
expect(domDocument.elementFromPoint(10, 40), child3);
// Hit test child 3
expect(shadowRoot.elementFromPoint(10, 50), child3);
expect(domDocument.elementFromPoint(10, 50), child3);
semantics().semanticsEnabled = false;
});
@ -2111,9 +2114,11 @@ void updateNode(
String value = '',
List<ui.StringAttribute> valueAttributes = const <ui.StringAttribute>[],
String increasedValue = '',
List<ui.StringAttribute> increasedValueAttributes = const <ui.StringAttribute>[],
List<ui.StringAttribute> increasedValueAttributes =
const <ui.StringAttribute>[],
String decreasedValue = '',
List<ui.StringAttribute> decreasedValueAttributes = const <ui.StringAttribute>[],
List<ui.StringAttribute> decreasedValueAttributes =
const <ui.StringAttribute>[],
String tooltip = '',
ui.TextDirection textDirection = ui.TextDirection.ltr,
Float64List? transform,

View File

@ -8,7 +8,6 @@ import 'dart:typed_data';
import 'package:test/test.dart';
import 'package:ui/src/engine/dom.dart';
import 'package:ui/src/engine/embedder.dart';
import 'package:ui/src/engine/host_node.dart';
import 'package:ui/src/engine/semantics.dart';
import 'package:ui/src/engine/util.dart';
import 'package:ui/src/engine/vector_math.dart';
@ -19,10 +18,11 @@ import '../../common/matchers.dart';
/// Gets the DOM host where the Flutter app is being rendered.
///
/// This function returns the correct host for the flutter app under testing,
/// so we don't have to hardcode domDocument across the test. (The host of a
/// normal flutter app used to be domDocument, but now that the app is wrapped
/// in a Shadow DOM, that's not the case anymore.)
HostNode get appHostNode => flutterViewEmbedder.glassPaneShadow;
/// so we don't have to hardcode domDocument across the test. The semantics
/// tree has moved outside of the shadowDOM as a workaround for a password
/// autofill bug on Chrome.
/// Ref: https://github.com/flutter/flutter/issues/87735
DomElement get appHostNode => flutterViewEmbedder.glassPaneElement;
/// CSS style applied to the root of the semantics tree.
// TODO(yjbanov): this should be handled internally by [expectSemanticsTree].

View File

@ -102,11 +102,11 @@ void testMain() {
final DomElement textField = appHostNode
.querySelector('input[data-semantics-role="text-field"]')!;
expect(appHostNode.activeElement, isNot(textField));
expect(appHostNode.ownerDocument?.activeElement, isNot(textField));
textField.focus();
expect(appHostNode.activeElement, textField);
expect(appHostNode.ownerDocument?.activeElement, textField);
expect(await logger.idLog.first, 0);
expect(await logger.actionLog.first, ui.SemanticsAction.tap);
}, // TODO(yjbanov): https://github.com/flutter/flutter/issues/46638
@ -115,8 +115,7 @@ void testMain() {
skip: browserEngine != BrowserEngine.blink);
test('Syncs semantic state from framework', () {
expect(domDocument.activeElement, domDocument.body);
expect(appHostNode.activeElement, null);
expect(appHostNode.ownerDocument?.activeElement, domDocument.body);
int changeCount = 0;
int actionCount = 0;
@ -140,8 +139,7 @@ void testMain() {
final TextField textField =
textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField;
expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement);
expect(appHostNode.activeElement, strategy.domElement);
expect(appHostNode.ownerDocument?.activeElement, strategy.domElement);
expect(textField.editableElement, strategy.domElement);
expect(textField.activeEditableElement.getAttribute('aria-label'), 'greeting');
expect(textField.activeEditableElement.style.width, '10px');
@ -154,8 +152,7 @@ void testMain() {
rect: const ui.Rect.fromLTWH(0, 0, 12, 17),
);
expect(domDocument.activeElement, domDocument.body);
expect(appHostNode.activeElement, null);
expect(appHostNode.ownerDocument?.activeElement, domDocument.body);
expect(strategy.domElement, null);
expect(textField.activeEditableElement.getAttribute('aria-label'), 'farewell');
expect(textField.activeEditableElement.style.width, '12px');
@ -201,8 +198,7 @@ void testMain() {
test(
'Updates editing state when receiving framework messages from the text input channel',
() {
expect(domDocument.activeElement, domDocument.body);
expect(appHostNode.activeElement, null);
expect(appHostNode.ownerDocument?.activeElement, domDocument.body);
strategy.enable(
singlelineConfig,
@ -246,8 +242,7 @@ void testMain() {
});
test('Gives up focus after DOM blur', () {
expect(domDocument.activeElement, domDocument.body);
expect(appHostNode.activeElement, null);
expect(appHostNode.ownerDocument?.activeElement, domDocument.body);
strategy.enable(
singlelineConfig,
@ -262,13 +257,11 @@ void testMain() {
final TextField textField =
textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField;
expect(textField.editableElement, strategy.domElement);
expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement);
expect(appHostNode.activeElement, strategy.domElement);
expect(appHostNode.ownerDocument?.activeElement, strategy.domElement);
// The input should not refocus after blur.
textField.activeEditableElement.blur();
expect(domDocument.activeElement, domDocument.body);
expect(appHostNode.activeElement, null);
expect(appHostNode.ownerDocument?.activeElement, domDocument.body);
strategy.disable();
});
@ -288,8 +281,7 @@ void testMain() {
isFocused: true,
);
expect(strategy.domElement, isNotNull);
expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement);
expect(appHostNode.activeElement, strategy.domElement);
expect(appHostNode.ownerDocument?.activeElement, strategy.domElement);
strategy.disable();
expect(strategy.domElement, isNull);
@ -300,8 +292,7 @@ void testMain() {
expect(appHostNode.contains(textField.editableElement), isTrue);
// Editing element is not enabled.
expect(strategy.isEnabled, isFalse);
expect(domDocument.activeElement, domDocument.body);
expect(appHostNode.activeElement, null);
expect(appHostNode.ownerDocument?.activeElement, domDocument.body);
});
test('Refocuses when setting editing state', () {
@ -316,13 +307,11 @@ void testMain() {
isFocused: true,
);
expect(strategy.domElement, isNotNull);
expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement);
expect(appHostNode.activeElement, strategy.domElement);
expect(appHostNode.ownerDocument?.activeElement, strategy.domElement);
// Blur the element without telling the framework.
strategy.activeDomElement.blur();
expect(domDocument.activeElement, domDocument.body);
expect(appHostNode.activeElement, null);
expect(appHostNode.ownerDocument?.activeElement, domDocument.body);
// The input will have focus after editing state is set and semantics updated.
strategy.setEditingState(EditingState(text: 'foo'));
@ -340,8 +329,7 @@ void testMain() {
value: 'hello',
isFocused: true,
);
expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement);
expect(appHostNode.activeElement, strategy.domElement);
expect(appHostNode.ownerDocument?.activeElement, strategy.domElement);
strategy.disable();
});
@ -361,8 +349,7 @@ void testMain() {
final DomHTMLTextAreaElement textArea =
strategy.domElement! as DomHTMLTextAreaElement;
expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement);
expect(appHostNode.activeElement, strategy.domElement);
expect(appHostNode.ownerDocument?.activeElement, strategy.domElement);
strategy.enable(
singlelineConfig,
@ -371,8 +358,7 @@ void testMain() {
);
textArea.blur();
expect(domDocument.activeElement, domDocument.body);
expect(appHostNode.activeElement, null);
expect(appHostNode.ownerDocument?.activeElement, domDocument.body);
strategy.disable();
// It doesn't remove the textarea from the DOM.
@ -456,13 +442,14 @@ void testMain() {
createTwoFieldSemantics(tester, focusFieldId: 1);
expect(tester.apply().length, 3);
expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement);
expect(appHostNode.activeElement, tester.getTextField(1).editableElement);
expect(appHostNode.ownerDocument?.activeElement,
tester.getTextField(1).editableElement);
expect(strategy.domElement, tester.getTextField(1).editableElement);
createTwoFieldSemantics(tester, focusFieldId: 2);
expect(tester.apply().length, 3);
expect(appHostNode.activeElement, tester.getTextField(2).editableElement);
expect(appHostNode.ownerDocument?.activeElement,
tester.getTextField(2).editableElement);
expect(strategy.domElement, tester.getTextField(2).editableElement);
}
});
@ -510,8 +497,7 @@ void testMain() {
});
test('Syncs semantic state from framework', () {
expect(domDocument.activeElement, domDocument.body);
expect(appHostNode.activeElement, null);
expect(appHostNode.ownerDocument?.activeElement, domDocument.body);
int changeCount = 0;
int actionCount = 0;
@ -535,8 +521,7 @@ void testMain() {
final TextField textField =
textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField;
expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement);
expect(appHostNode.activeElement, strategy.domElement);
expect(appHostNode.ownerDocument?.activeElement, strategy.domElement);
expect(textField.editableElement, strategy.domElement);
expect(textField.activeEditableElement.getAttribute('aria-label'), 'greeting');
expect(textField.activeEditableElement.style.width, '10px');
@ -552,8 +537,7 @@ void testMain() {
appHostNode.querySelector('flt-semantics[role="textbox"]')!;
expect(strategy.domElement, null);
expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement);
expect(appHostNode.activeElement, textBox);
expect(appHostNode.ownerDocument?.activeElement, textBox);
expect(textBox.getAttribute('aria-label'), 'farewell');
strategy.disable();
@ -596,8 +580,7 @@ void testMain() {
test(
'Updates editing state when receiving framework messages from the text input channel',
() {
expect(domDocument.activeElement, domDocument.body);
expect(appHostNode.activeElement, null);
expect(appHostNode.ownerDocument?.activeElement, domDocument.body);
strategy.enable(
singlelineConfig,
@ -641,8 +624,7 @@ void testMain() {
});
test('Gives up focus after DOM blur', () {
expect(domDocument.activeElement, domDocument.body);
expect(appHostNode.activeElement, null);
expect(appHostNode.ownerDocument?.activeElement, domDocument.body);
strategy.enable(
singlelineConfig,
@ -657,15 +639,13 @@ void testMain() {
textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField;
expect(textField.editableElement, strategy.domElement);
expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement);
expect(appHostNode.activeElement, strategy.domElement);
expect(appHostNode.ownerDocument?.activeElement, strategy.domElement);
// The input should not refocus after blur.
textField.activeEditableElement.blur();
final DomElement textBox =
appHostNode.querySelector('flt-semantics[role="textbox"]')!;
expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement);
expect(appHostNode.activeElement, textBox);
expect(appHostNode.ownerDocument?.activeElement, textBox);
strategy.disable();
});
@ -686,8 +666,7 @@ void testMain() {
isFocused: true,
);
expect(strategy.domElement, isNotNull);
expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement);
expect(appHostNode.activeElement, strategy.domElement);
expect(appHostNode.ownerDocument?.activeElement, strategy.domElement);
strategy.disable();
expect(strategy.domElement, isNull);
@ -700,8 +679,7 @@ void testMain() {
// Focus is on the semantic object
final DomElement textBox =
appHostNode.querySelector('flt-semantics[role="textbox"]')!;
expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement);
expect(appHostNode.activeElement, textBox);
expect(appHostNode.ownerDocument?.activeElement, textBox);
});
test('Refocuses when setting editing state', () {
@ -716,15 +694,13 @@ void testMain() {
isFocused: true,
);
expect(strategy.domElement, isNotNull);
expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement);
expect(appHostNode.activeElement, strategy.domElement);
expect(appHostNode.ownerDocument?.activeElement, strategy.domElement);
// Blur the element without telling the framework.
strategy.activeDomElement.blur();
final DomElement textBox =
appHostNode.querySelector('flt-semantics[role="textbox"]')!;
expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement);
expect(appHostNode.activeElement, textBox);
expect(appHostNode.ownerDocument?.activeElement, textBox);
// The input will have focus after editing state is set and semantics updated.
strategy.setEditingState(EditingState(text: 'foo'));
@ -742,8 +718,7 @@ void testMain() {
value: 'hello',
isFocused: true,
);
expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement);
expect(appHostNode.activeElement, strategy.domElement);
expect(appHostNode.ownerDocument?.activeElement, strategy.domElement);
strategy.disable();
});
@ -760,9 +735,9 @@ void testMain() {
isMultiline: true,
);
final DomHTMLTextAreaElement textArea = strategy.domElement! as DomHTMLTextAreaElement;
expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement);
expect(appHostNode.activeElement, strategy.domElement);
final DomHTMLTextAreaElement textArea =
strategy.domElement! as DomHTMLTextAreaElement;
expect(appHostNode.ownerDocument?.activeElement, strategy.domElement);
strategy.enable(
singlelineConfig,
@ -776,8 +751,7 @@ void testMain() {
final DomElement textBox =
appHostNode.querySelector('flt-semantics[role="textbox"]')!;
expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement);
expect(appHostNode.activeElement, textBox);
expect(appHostNode.ownerDocument?.activeElement, textBox);
strategy.disable();
// It removes the textarea from the DOM.
@ -840,13 +814,14 @@ void testMain() {
createTwoFieldSemanticsForIos(tester, focusFieldId: 1);
expect(tester.apply().length, 3);
expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement);
expect(appHostNode.activeElement, tester.getTextField(1).editableElement);
expect(appHostNode.ownerDocument?.activeElement,
tester.getTextField(1).editableElement);
expect(strategy.domElement, tester.getTextField(1).editableElement);
createTwoFieldSemanticsForIos(tester, focusFieldId: 2);
expect(tester.apply().length, 3);
expect(appHostNode.activeElement, tester.getTextField(2).editableElement);
expect(appHostNode.ownerDocument?.activeElement,
tester.getTextField(2).editableElement);
expect(strategy.domElement, tester.getTextField(2).editableElement);
}
});

View File

@ -91,7 +91,8 @@ Future<void> testMain() async {
);
// The focus initially is on the body.
expect(domDocument.activeElement, domDocument.body);
expect(defaultTextEditingRoot.activeElement, null);
expect(defaultTextEditingRoot.ownerDocument?.activeElement,
domDocument.body);
editingStrategy!.enable(
singlelineConfig,
@ -106,8 +107,8 @@ Future<void> testMain() async {
final DomElement input = defaultTextEditingRoot.querySelector('input')!;
// Now the editing element should have focus.
expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement);
expect(defaultTextEditingRoot.activeElement, input);
expect(domDocument.activeElement, input);
expect(defaultTextEditingRoot.ownerDocument?.activeElement, input);
expect(editingStrategy!.domElement, input);
expect(input.getAttribute('type'), null);
@ -122,7 +123,8 @@ Future<void> testMain() async {
);
// The focus is back to the body.
expect(domDocument.activeElement, domDocument.body);
expect(defaultTextEditingRoot.activeElement, null);
expect(defaultTextEditingRoot.ownerDocument?.activeElement,
domDocument.body);
});
test('Respects read-only config', () {
@ -281,7 +283,7 @@ Future<void> testMain() async {
final DomHTMLTextAreaElement textarea =
defaultTextEditingRoot.querySelector('textarea')! as DomHTMLTextAreaElement;
// Now the textarea should have focus.
expect(defaultTextEditingRoot.activeElement, textarea);
expect(defaultTextEditingRoot.ownerDocument?.activeElement, textarea);
expect(editingStrategy!.domElement, textarea);
textarea.value = 'foo\nbar';
@ -303,7 +305,8 @@ Future<void> testMain() async {
// The textarea should be cleaned up.
expect(defaultTextEditingRoot.querySelectorAll('textarea'), hasLength(0));
// The focus is back to the body.
expect(defaultTextEditingRoot.activeElement, null);
expect(defaultTextEditingRoot.ownerDocument?.activeElement,
domDocument.body);
// There should be no input action.
expect(lastInputAction, isNull);
@ -620,7 +623,7 @@ Future<void> testMain() async {
const MethodCall show = MethodCall('TextInput.show');
sendFrameworkMessage(codec.encodeMethodCall(show));
expect(defaultTextEditingRoot.activeElement,
expect(defaultTextEditingRoot.ownerDocument?.activeElement,
textEditing!.strategy.domElement);
});
@ -680,7 +683,8 @@ Future<void> testMain() async {
sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
// Editing shouldn't have started yet.
expect(defaultTextEditingRoot.activeElement, null);
expect(defaultTextEditingRoot.ownerDocument?.activeElement,
domDocument.body);
const MethodCall show = MethodCall('TextInput.show');
sendFrameworkMessage(codec.encodeMethodCall(show));
@ -705,7 +709,7 @@ Future<void> testMain() async {
expect(spy.messages, hasLength(0));
await Future<void>.delayed(Duration.zero);
// DOM element still keeps the focus.
expect(defaultTextEditingRoot.activeElement,
expect(defaultTextEditingRoot.ownerDocument?.activeElement,
textEditing!.strategy.domElement);
});
@ -723,7 +727,8 @@ Future<void> testMain() async {
sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
// Editing shouldn't have started yet.
expect(defaultTextEditingRoot.activeElement, null);
expect(defaultTextEditingRoot.ownerDocument?.activeElement,
domDocument.body);
const MethodCall show = MethodCall('TextInput.show');
sendFrameworkMessage(codec.encodeMethodCall(show));
@ -752,7 +757,8 @@ Future<void> testMain() async {
spy.messages[0].methodName, 'TextInputClient.onConnectionClosed');
await Future<void>.delayed(Duration.zero);
// DOM element loses the focus.
expect(defaultTextEditingRoot.activeElement, null);
expect(defaultTextEditingRoot.ownerDocument?.activeElement,
domDocument.body);
},
// Test on ios-safari only.
skip: browserEngine != BrowserEngine.webkit ||
@ -773,7 +779,8 @@ Future<void> testMain() async {
sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
// Editing shouldn't have started yet.
expect(defaultTextEditingRoot.activeElement, null);
expect(defaultTextEditingRoot.ownerDocument?.activeElement,
domDocument.body);
const MethodCall show = MethodCall('TextInput.show');
sendFrameworkMessage(codec.encodeMethodCall(show));
@ -1152,7 +1159,8 @@ Future<void> testMain() async {
// In Safari Desktop Autofill menu appears as soon as an element is
// focused, therefore the input element is only focused after the
// location is received.
expect(defaultTextEditingRoot.activeElement, inputElement);
expect(
defaultTextEditingRoot.ownerDocument?.activeElement, inputElement);
expect(inputElement.selectionStart, 2);
expect(inputElement.selectionEnd, 3);
}
@ -1165,7 +1173,7 @@ Future<void> testMain() async {
sendFrameworkMessage(codec.encodeMethodCall(updateSizeAndTransform));
// Check the element still has focus. User can keep editing.
expect(defaultTextEditingRoot.activeElement,
expect(defaultTextEditingRoot.ownerDocument?.activeElement,
textEditing!.strategy.domElement);
// Check the cursor location is the same.
@ -1765,7 +1773,8 @@ Future<void> testMain() async {
sendFrameworkMessage(codec.encodeMethodCall(setClient));
// Editing shouldn't have started yet.
expect(defaultTextEditingRoot.activeElement, null);
expect(defaultTextEditingRoot.ownerDocument?.activeElement,
domDocument.body);
const MethodCall show = MethodCall('TextInput.show');
sendFrameworkMessage(codec.encodeMethodCall(show));
@ -2647,7 +2656,7 @@ void checkInputEditingState(
expect(element, isNotNull);
expect(domInstanceOfString(element, 'HTMLInputElement'), true);
final DomHTMLInputElement input = element! as DomHTMLInputElement;
expect(defaultTextEditingRoot.activeElement, input);
expect(defaultTextEditingRoot.ownerDocument?.activeElement, input);
expect(input.value, text);
expect(input.selectionStart, start);
expect(input.selectionEnd, end);
@ -2673,7 +2682,7 @@ void checkTextAreaEditingState(
int start,
int end,
) {
expect(defaultTextEditingRoot.activeElement, textarea);
expect(defaultTextEditingRoot.ownerDocument?.activeElement, textarea);
expect(textarea.value, text);
expect(textarea.selectionStart, start);
expect(textarea.selectionEnd, end);