From ae357caefa6c0427c66f0f6c266bca84cbe41778 Mon Sep 17 00:00:00 2001 From: joshualitt Date: Wed, 15 Jun 2022 09:08:48 -0700 Subject: [PATCH] [web] Migrate Flutter Web DOM usage to JS static interop - 31. (#33361) --- lib/web_ui/lib/src/engine/dom.dart | 77 ++++++++++++++++++- lib/web_ui/lib/src/engine/embedder.dart | 2 +- .../lib/src/engine/keyboard_binding.dart | 4 +- .../lib/src/engine/pointer_binding.dart | 7 +- .../lib/src/engine/semantics/checkable.dart | 7 +- .../lib/src/engine/semantics/image.dart | 11 ++- .../src/engine/semantics/incrementable.dart | 10 +-- .../src/engine/semantics/label_and_value.dart | 9 +-- .../lib/src/engine/semantics/live_region.dart | 3 +- .../lib/src/engine/semantics/scrollable.dart | 16 ++-- .../lib/src/engine/semantics/semantics.dart | 27 +++---- .../engine/semantics/semantics_helper.dart | 57 +++++++------- .../lib/src/engine/semantics/tappable.dart | 12 +-- .../lib/src/engine/semantics/text_field.dart | 3 +- .../semantics/semantics_helper_test.dart | 55 ++++++------- .../test/engine/semantics/semantics_test.dart | 16 ++-- 16 files changed, 194 insertions(+), 122 deletions(-) diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart index 589019d023d44..62bc5780bbd04 100644 --- a/lib/web_ui/lib/src/engine/dom.dart +++ b/lib/web_ui/lib/src/engine/dom.dart @@ -99,6 +99,9 @@ extension DomHTMLDocumentExtension on DomHTMLDocument { external DomHTMLHeadElement? get head; external DomHTMLBodyElement? get body; external set title(String? value); + Iterable getElementsByTagName(String tag) => + createDomListWrapper(js_util + .callMethod<_DomList>(this, 'getElementsByTagName', [tag])); } @JS('document') @@ -124,6 +127,8 @@ extension DomEventTargetExtension on DomEventTarget { [type, listener, if (useCapture != null) useCapture]); } } + + external bool dispatchEvent(DomEvent event); } typedef DomEventListener = void Function(DomEvent event); @@ -220,6 +225,23 @@ extension DomElementExtension on DomElement { external void remove(); external void setAttribute(String name, Object value); void appendText(String text) => append(createDomText(text)); + external void removeAttribute(String name); + external set tabIndex(int? value); + external int? get tabIndex; + external void focus(); + + /// [scrollTop] and [scrollLeft] can both return non-integers when using + /// display scaling. + /// + /// The setters have a spurious round just in case the supplied [int] flowed + /// from the non-static interop JS API. When all of Flutter Web has been + /// migrated to static interop we can probably remove the rounds. + int get scrollTop => js_util.getProperty(this, 'scrollTop').round(); + set scrollTop(int value) => + js_util.setProperty(this, 'scrollTop', value.round()); + int get scrollLeft => js_util.getProperty(this, 'scrollLeft').round(); + set scrollLeft(int value) => + js_util.setProperty(this, 'scrollLeft', value.round()); } @JS() @@ -287,6 +309,10 @@ extension DomCSSStyleDeclarationExtension on DomCSSStyleDeclaration { set alignItems(String value) => setProperty('align-items', value); set margin(String value) => setProperty('margin', value); set background(String value) => setProperty('background', value); + set touchAction(String value) => setProperty('touch-action', value); + set overflowY(String value) => setProperty('overflow-y', value); + set overflowX(String value) => setProperty('overflow-x', value); + set outline(String value) => setProperty('outline', value); String get width => getPropertyValue('width'); String get height => getPropertyValue('height'); String get position => getPropertyValue('position'); @@ -342,6 +368,10 @@ extension DomCSSStyleDeclarationExtension on DomCSSStyleDeclaration { String get alignItems => getPropertyValue('align-items'); String get margin => getPropertyValue('margin'); String get background => getPropertyValue('background'); + String get touchAction => getPropertyValue('touch-action'); + String get overflowY => getPropertyValue('overflow-y'); + String get overflowX => getPropertyValue('overflow-x'); + String get outline => getPropertyValue('outline'); external String getPropertyValue(String property); void setProperty(String propertyName, String value, [String? priority]) { @@ -359,7 +389,6 @@ class DomHTMLElement extends DomElement {} extension DomHTMLElementExtension on DomHTMLElement { int get offsetWidth => js_util.getProperty(this, 'offsetWidth') as int; - external void focus(); } @JS() @@ -927,11 +956,19 @@ class DomMouseEvent extends DomUIEvent {} extension DomMouseEventExtension on DomMouseEvent { external num get clientX; external num get clientY; + external num get offsetX; + external num get offsetY; + DomPoint get client => DomPoint(clientX, clientY); + DomPoint get offset => DomPoint(offsetX, offsetY); external int get button; external int? get buttons; external bool getModifierState(String keyArg); } +DomMouseEvent createDomMouseEvent(String type, [Map? init]) => + js_util.callConstructor(domGetConstructor('MouseEvent')!, + [type, if (init != null) js_util.jsify(init)]); + @JS() @staticInterop class DomPointerEvent extends DomMouseEvent {} @@ -947,6 +984,11 @@ extension DomPointerEventExtension on DomPointerEvent { this, 'getCoalescedEvents', []).cast(); } +DomPointerEvent createDomPointerEvent(String type, + [Map? init]) => + js_util.callConstructor(domGetConstructor('PointerEvent')!, + [type, if (init != null) js_util.jsify(init)]); + @JS() @staticInterop class DomWheelEvent extends DomMouseEvent {} @@ -973,8 +1015,37 @@ class DomTouch {} extension DomTouchExtension on DomTouch { external int? get identifier; - external num? get clientX; - external num? get clientY; + external num get clientX; + external num get clientY; + DomPoint get client => DomPoint(clientX, clientY); +} + +DomTouchEvent createDomTouchEvent(String type, [Map? init]) => + js_util.callConstructor(domGetConstructor('TouchEvent')!, + [type, if (init != null) js_util.jsify(init)]); + +@JS() +@staticInterop +class DomHTMLInputElement extends DomHTMLElement {} + +extension DomHTMLInputElementExtension on DomHTMLInputElement { + external set type(String? value); + external set max(String? value); + external set min(String value); + external set value(String? value); + external String? get value; + external bool? get disabled; + external set disabled(bool? value); +} + +DomHTMLInputElement createDomHTMLInputElement() => + domDocument.createElement('input') as DomHTMLInputElement; + +class DomPoint { + final num x; + final num y; + + DomPoint(this.x, this.y); } Object? domGetConstructor(String constructorName) => diff --git a/lib/web_ui/lib/src/engine/embedder.dart b/lib/web_ui/lib/src/engine/embedder.dart index 825cccab9c1e2..3f1ed5dc1de27 100644 --- a/lib/web_ui/lib/src/engine/embedder.dart +++ b/lib/web_ui/lib/src/engine/embedder.dart @@ -311,7 +311,7 @@ class FlutterViewEmbedder { final html.Element _accessibilityPlaceholder = EngineSemanticsOwner .instance.semanticsHelper - .prepareAccessibilityPlaceholder(); + .prepareAccessibilityPlaceholder() as html.Element; glassPaneElementHostNode.nodes.addAll([ _accessibilityPlaceholder, diff --git a/lib/web_ui/lib/src/engine/keyboard_binding.dart b/lib/web_ui/lib/src/engine/keyboard_binding.dart index ff5b5aac1c160..d228d647716f4 100644 --- a/lib/web_ui/lib/src/engine/keyboard_binding.dart +++ b/lib/web_ui/lib/src/engine/keyboard_binding.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:html' as html; - import 'package:ui/ui.dart' as ui; import '../engine.dart' show registerHotRestartListener; @@ -117,7 +115,7 @@ class KeyboardBinding { if (_debugLogKeyEvents) { print(event.type); } - if (EngineSemanticsOwner.instance.receiveGlobalEvent(event as html.Event)) { + if (EngineSemanticsOwner.instance.receiveGlobalEvent(event)) { return handler(event); } return null; diff --git a/lib/web_ui/lib/src/engine/pointer_binding.dart b/lib/web_ui/lib/src/engine/pointer_binding.dart index 57794044debe2..c31af642e0901 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:html' as html; import 'dart:math' as math; import 'package:meta/meta.dart'; @@ -293,7 +292,7 @@ abstract class _BaseAdapter { // Report the event to semantics. This information is used to debounce // browser gestures. Semantics tells us whether it is safe to forward // the event to the framework. - if (EngineSemanticsOwner.instance.receiveGlobalEvent(event as html.Event)) { + if (EngineSemanticsOwner.instance.receiveGlobalEvent(event)) { handler(event); } } @@ -868,8 +867,8 @@ class _TouchAdapter extends _BaseAdapter { kind: ui.PointerDeviceKind.touch, signalKind: ui.PointerSignalKind.none, device: touch.identifier!, - physicalX: touch.clientX!.toDouble() * ui.window.devicePixelRatio, - physicalY: touch.clientY!.toDouble() * ui.window.devicePixelRatio, + physicalX: touch.clientX.toDouble() * ui.window.devicePixelRatio, + physicalY: touch.clientY.toDouble() * ui.window.devicePixelRatio, buttons: pressed ? _kPrimaryMouseButton : 0, pressure: 1.0, pressureMin: 0.0, diff --git a/lib/web_ui/lib/src/engine/semantics/checkable.dart b/lib/web_ui/lib/src/engine/semantics/checkable.dart index 348e31224cca8..2cb442f205095 100644 --- a/lib/web_ui/lib/src/engine/semantics/checkable.dart +++ b/lib/web_ui/lib/src/engine/semantics/checkable.dart @@ -11,10 +11,9 @@ // framework. Currently the framework does not report the // grouping of radio buttons. -import 'dart:html' as html; - import 'package:ui/ui.dart' as ui; +import '../dom.dart'; import 'semantics.dart'; /// The specific type of checkable control. @@ -104,7 +103,7 @@ class Checkable extends RoleManager { void _updateDisabledAttribute() { if (semanticsObject.enabledState() == EnabledState.disabled) { - final html.Element element = semanticsObject.element; + final DomElement element = semanticsObject.element; element ..setAttribute('aria-disabled', 'true') ..setAttribute('disabled', 'true'); @@ -114,7 +113,7 @@ class Checkable extends RoleManager { } void _removeDisabledAttribute() { - final html.Element element = semanticsObject.element; + final DomElement element = semanticsObject.element; element..removeAttribute('aria-disabled')..removeAttribute('disabled'); } } diff --git a/lib/web_ui/lib/src/engine/semantics/image.dart b/lib/web_ui/lib/src/engine/semantics/image.dart index e05787780c2ad..e1a4de392db44 100644 --- a/lib/web_ui/lib/src/engine/semantics/image.dart +++ b/lib/web_ui/lib/src/engine/semantics/image.dart @@ -2,8 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:html' as html; - +import '../dom.dart'; import 'semantics.dart'; /// Represents semantic objects that deliver information in a visual manner. @@ -18,13 +17,13 @@ class ImageRoleManager extends RoleManager { /// The element with role="img" and aria-label could block access to all /// children elements, therefore create an auxiliary element and describe the /// image in that if the semantic object have child nodes. - html.Element? _auxiliaryImageElement; + DomElement? _auxiliaryImageElement; @override void update() { if (semanticsObject.isVisualOnly && semanticsObject.hasChildren) { if (_auxiliaryImageElement == null) { - _auxiliaryImageElement = html.Element.tag('flt-semantics-img'); + _auxiliaryImageElement = domDocument.createElement('flt-semantics-img'); // Absolute positioning and sizing of leaf text elements confuses // VoiceOver. So we let the browser size the value node. The node will // still have a bigger tap area. However, if the node is a parent to @@ -54,7 +53,7 @@ class ImageRoleManager extends RoleManager { } } - void _setLabel(html.Element? element) { + void _setLabel(DomElement? element) { if (semanticsObject.hasLabel) { element!.setAttribute('aria-label', semanticsObject.label!); } @@ -69,7 +68,7 @@ class ImageRoleManager extends RoleManager { void _cleanupElement() { semanticsObject.setAriaRole('img', false); - semanticsObject.element.attributes.remove('aria-label'); + semanticsObject.element.removeAttribute('aria-label'); } @override diff --git a/lib/web_ui/lib/src/engine/semantics/incrementable.dart b/lib/web_ui/lib/src/engine/semantics/incrementable.dart index 71c068cdf8004..fd777014ed575 100644 --- a/lib/web_ui/lib/src/engine/semantics/incrementable.dart +++ b/lib/web_ui/lib/src/engine/semantics/incrementable.dart @@ -2,11 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:html' as html; - import 'package:ui/ui.dart' as ui; +import '../dom.dart'; import '../platform_dispatcher.dart'; +import '../safe_browser_api.dart'; import 'semantics.dart'; /// Adds increment/decrement event handling to a semantics object. @@ -20,7 +20,7 @@ import 'semantics.dart'; /// gestures must be interpreted by the Flutter framework. class Incrementable extends RoleManager { /// The HTML element used to render semantics to the browser. - final html.InputElement _element = html.InputElement(); + final DomHTMLInputElement _element = createDomHTMLInputElement(); /// The value used by the input element. /// @@ -49,7 +49,7 @@ class Incrementable extends RoleManager { _element.type = 'range'; _element.setAttribute('role', 'slider'); - _element.addEventListener('change', (_) { + _element.addEventListener('change', allowInterop((_) { if (_element.disabled!) { return; } @@ -64,7 +64,7 @@ class Incrementable extends RoleManager { EnginePlatformDispatcher.instance.invokeOnSemanticsAction( semanticsObject.id, ui.SemanticsAction.decrease, null); } - }); + })); // Store the callback as a closure because Dart does not guarantee that // tear-offs produce the same function object. diff --git a/lib/web_ui/lib/src/engine/semantics/label_and_value.dart b/lib/web_ui/lib/src/engine/semantics/label_and_value.dart index 80bf443a968cb..83ad32fd6a005 100644 --- a/lib/web_ui/lib/src/engine/semantics/label_and_value.dart +++ b/lib/web_ui/lib/src/engine/semantics/label_and_value.dart @@ -2,11 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:html' as html; - import 'package:ui/ui.dart' as ui; import '../configuration.dart'; +import '../dom.dart'; import 'semantics.dart'; /// Renders [_label] and [_value] to the semantics DOM. @@ -47,7 +46,7 @@ class LabelAndValue extends RoleManager { /// its label is not reachable via accessibility focus. This happens, for /// example in popup dialogs, such as the alert dialog. The text of the /// alert is supplied as a label on the parent node. - html.Element? _auxiliaryValueElement; + DomElement? _auxiliaryValueElement; @override void update() { @@ -90,7 +89,7 @@ class LabelAndValue extends RoleManager { } if (_auxiliaryValueElement == null) { - _auxiliaryValueElement = html.Element.tag('flt-semantics-value'); + _auxiliaryValueElement = domDocument.createElement('flt-semantics-value'); // Absolute positioning and sizing of leaf text elements confuses // VoiceOver. So we let the browser size the value node. The node will // still have a bigger tap area. However, if the node is a parent to other @@ -119,7 +118,7 @@ class LabelAndValue extends RoleManager { _auxiliaryValueElement!.remove(); _auxiliaryValueElement = null; } - semanticsObject.element.attributes.remove('aria-label'); + semanticsObject.element.removeAttribute('aria-label'); semanticsObject.setAriaRole('heading', false); } diff --git a/lib/web_ui/lib/src/engine/semantics/live_region.dart b/lib/web_ui/lib/src/engine/semantics/live_region.dart index bd6353f6f8070..6f51f6ce55290 100644 --- a/lib/web_ui/lib/src/engine/semantics/live_region.dart +++ b/lib/web_ui/lib/src/engine/semantics/live_region.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import '../dom.dart'; import 'semantics.dart'; /// Manages semantics configurations that represent live regions. @@ -31,7 +32,7 @@ class LiveRegion extends RoleManager { } void _cleanupDom() { - semanticsObject.element.attributes.remove('aria-live'); + semanticsObject.element.removeAttribute('aria-live'); } @override diff --git a/lib/web_ui/lib/src/engine/semantics/scrollable.dart b/lib/web_ui/lib/src/engine/semantics/scrollable.dart index 8fe6fb0d9dc9a..3aaea1308d14e 100644 --- a/lib/web_ui/lib/src/engine/semantics/scrollable.dart +++ b/lib/web_ui/lib/src/engine/semantics/scrollable.dart @@ -2,11 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:html' as html; - import 'package:ui/ui.dart' as ui; +import '../dom.dart'; import '../platform_dispatcher.dart'; +import '../safe_browser_api.dart'; import 'semantics.dart'; /// Implements vertical and horizontal scrolling functionality for semantics @@ -37,7 +37,7 @@ class Scrollable extends RoleManager { /// /// This gesture is converted to [ui.SemanticsAction.scrollUp] or /// [ui.SemanticsAction.scrollDown], depending on the direction. - html.EventListener? _scrollListener; + DomEventListener? _scrollListener; /// The value of the "scrollTop" or "scrollLeft" property of this object's /// [element] that has zero offset relative to the [scrollPosition]. @@ -107,9 +107,9 @@ class Scrollable extends RoleManager { }; semanticsObject.owner.addGestureModeListener(_gestureModeListener); - _scrollListener = (_) { + _scrollListener = allowInterop((_) { _recomputeScrollPosition(); - }; + }); semanticsObject.element.addEventListener('scroll', _scrollListener); } } @@ -138,7 +138,7 @@ class Scrollable extends RoleManager { // This value is arbitrary. const int _canonicalNeutralScrollPosition = 10; - final html.Element element = semanticsObject.element; + final DomElement element = semanticsObject.element; if (semanticsObject.isVerticalScrollContainer) { element.scrollTop = _canonicalNeutralScrollPosition; // Read back because the effective value depends on the amount of content. @@ -159,7 +159,7 @@ class Scrollable extends RoleManager { } void _gestureModeDidChange() { - final html.Element element = semanticsObject.element; + final DomElement element = semanticsObject.element; switch (semanticsObject.owner.gestureMode) { case GestureMode.browserGestures: // overflow:scroll will cause the browser report "scroll" events when @@ -190,7 +190,7 @@ class Scrollable extends RoleManager { @override void dispose() { - final html.CssStyleDeclaration style = semanticsObject.element.style; + final DomCSSStyleDeclaration style = semanticsObject.element.style; assert(_gestureModeListener != null); style.removeProperty('overflowY'); style.removeProperty('overflowX'); diff --git a/lib/web_ui/lib/src/engine/semantics/semantics.dart b/lib/web_ui/lib/src/engine/semantics/semantics.dart index accd8b3a1b7df..4ac038f7a017b 100644 --- a/lib/web_ui/lib/src/engine/semantics/semantics.dart +++ b/lib/web_ui/lib/src/engine/semantics/semantics.dart @@ -12,6 +12,7 @@ import '../../engine.dart' show registerHotRestartListener; import '../alarm_clock.dart'; import '../browser_detection.dart'; import '../configuration.dart'; +import '../dom.dart'; import '../embedder.dart'; import '../platform_dispatcher.dart'; import '../util.dart'; @@ -724,7 +725,7 @@ class SemanticsObject { final EngineSemanticsOwner owner; /// The DOM element used to convey semantics information to the browser. - final html.Element element = html.Element.tag('flt-semantics'); + final DomElement element = domDocument.createElement('flt-semantics'); /// Bitfield showing which fields have been updated but have not yet been /// applied to the DOM. @@ -745,9 +746,9 @@ class SemanticsObject { /// is not created. This is necessary for "aria-label" to function correctly. /// The browser will ignore the [label] of HTML element that contain child /// elements. - html.Element? getOrCreateChildContainer() { + DomElement? getOrCreateChildContainer() { if (_childContainerElement == null) { - _childContainerElement = html.Element.tag('flt-semantics-container'); + _childContainerElement = createDomElement('flt-semantics-container'); _childContainerElement!.style ..position = 'absolute' // Ignore pointer events on child container so that platform views @@ -763,7 +764,7 @@ class SemanticsObject { /// /// This element is used to correct for [_rect] offsets. It is only non-`null` /// when there are non-zero children (i.e. when [hasChildren] is `true`). - html.Element? _childContainerElement; + DomElement? _childContainerElement; /// The parent of this semantics object. SemanticsObject? _parent; @@ -1028,7 +1029,7 @@ class SemanticsObject { final Int32List childrenInTraversalOrder = _childrenInTraversalOrder!; final Int32List childrenInHitTestOrder = _childrenInHitTestOrder!; final int childCount = childrenInHitTestOrder.length; - final html.Element? containerElement = getOrCreateChildContainer(); + final DomElement? containerElement = getOrCreateChildContainer(); assert(childrenInTraversalOrder.length == childrenInHitTestOrder.length); @@ -1142,7 +1143,7 @@ class SemanticsObject { } } - html.Element? refNode; + DomElement? refNode; for (int i = childCount - 1; i >= 0; i -= 1) { final SemanticsObject child = childrenInRenderOrder[i]; if (!stationaryIds.contains(child.id)) { @@ -1175,7 +1176,7 @@ class SemanticsObject { if (condition) { element.setAttribute('role', ariaRoleName); } else if (element.getAttribute('role') == ariaRoleName) { - element.attributes.remove('role'); + element.removeAttribute('role'); } } @@ -1265,7 +1266,7 @@ class SemanticsObject { ..width = '${_rect!.width}px' ..height = '${_rect!.height}px'; - final html.Element? containerElement = + final DomElement? containerElement = hasChildren ? getOrCreateChildContainer() : null; final bool hasZeroRectOffset = _rect!.top == 0.0 && _rect!.left == 0.0; @@ -1333,7 +1334,7 @@ class SemanticsObject { /// handle traversal order. /// /// See https://github.com/flutter/flutter/issues/73347. - static void _clearSemanticElementTransform(html.Element element) { + static void _clearSemanticElementTransform(DomElement element) { element.style ..removeProperty('transform-origin') ..removeProperty('transform'); @@ -1479,7 +1480,7 @@ class EngineSemanticsOwner { object.element.remove(); } else { assert(object._parent == parent); - assert(object.element.parent == parent._childContainerElement); + assert(object.element.parentNode == parent._childContainerElement); } } _detachments = []; @@ -1506,7 +1507,7 @@ class EngineSemanticsOwner { } /// The top-level DOM element of the semantics DOM element tree. - html.Element? _rootSemanticsElement; + DomElement? _rootSemanticsElement; // ignore: prefer_function_declarations_over_variables TimestampFunction _now = () => DateTime.now(); @@ -1648,7 +1649,7 @@ class EngineSemanticsOwner { /// is likely that the gesture detected from the pointer even will do the /// right thing. However, if a standalone gesture is received, map it onto a /// [ui.SemanticsAction] to be processed by the framework. - bool receiveGlobalEvent(html.Event event) { + bool receiveGlobalEvent(DomEvent event) { // For pointer event reference see: // // https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events @@ -1786,7 +1787,7 @@ class EngineSemanticsOwner { if (_rootSemanticsElement == null) { final SemanticsObject root = _semanticsTree[0]!; _rootSemanticsElement = root.element; - flutterViewEmbedder.semanticsHostElement!.append(root.element); + flutterViewEmbedder.semanticsHostElement!.append(root.element as html.Node); } _finalizeTree(); diff --git a/lib/web_ui/lib/src/engine/semantics/semantics_helper.dart b/lib/web_ui/lib/src/engine/semantics/semantics_helper.dart index 0d57780b29b12..896cdff26f040 100644 --- a/lib/web_ui/lib/src/engine/semantics/semantics_helper.dart +++ b/lib/web_ui/lib/src/engine/semantics/semantics_helper.dart @@ -3,11 +3,12 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:html' as html; import 'package:meta/meta.dart'; import '../browser_detection.dart'; +import '../dom.dart'; +import '../safe_browser_api.dart'; import 'semantics.dart'; /// The maximum [semanticsActivationAttempts] before we give up waiting for @@ -50,11 +51,11 @@ class SemanticsHelper { _semanticsEnabler = semanticsEnabler; } - bool shouldEnableSemantics(html.Event event) { + bool shouldEnableSemantics(DomEvent event) { return _semanticsEnabler.shouldEnableSemantics(event); } - html.Element prepareAccessibilityPlaceholder() { + DomElement prepareAccessibilityPlaceholder() { return _semanticsEnabler.prepareAccessibilityPlaceholder(); } @@ -75,9 +76,9 @@ abstract class SemanticsEnabler { /// Semantics should be enabled if the web engine is no longer waiting for /// extra signals from the user events. See [isWaitingToEnableSemantics]. /// - /// Or if the received [html.Event] is suitable/enough for enabling the + /// Or if the received [DomEvent] is suitable/enough for enabling the /// semantics. See [tryEnableSemantics]. - bool shouldEnableSemantics(html.Event event) { + bool shouldEnableSemantics(DomEvent event) { if (!isWaitingToEnableSemantics) { // Forward to framework as normal. return true; @@ -90,7 +91,7 @@ abstract class SemanticsEnabler { /// /// Returns true if the `event` is not related to semantics activation and /// should be forwarded to the framework. - bool tryEnableSemantics(html.Event event); + bool tryEnableSemantics(DomEvent event); /// Creates the placeholder for accessibility. /// @@ -98,7 +99,7 @@ abstract class SemanticsEnabler { /// /// On focus the element announces that accessibility can be enabled by /// tapping/clicking. (Announcement depends on the assistive technology) - html.Element prepareAccessibilityPlaceholder(); + DomElement prepareAccessibilityPlaceholder(); /// Whether platform is still considering enabling semantics. /// @@ -123,14 +124,14 @@ abstract class SemanticsEnabler { @visibleForTesting class DesktopSemanticsEnabler extends SemanticsEnabler { /// A temporary placeholder used to capture a request to activate semantics. - html.Element? _semanticsPlaceholder; + DomElement? _semanticsPlaceholder; /// Whether we are waiting for the user to enable semantics. @override bool get isWaitingToEnableSemantics => _semanticsPlaceholder != null; @override - bool tryEnableSemantics(html.Event event) { + bool tryEnableSemantics(DomEvent event) { // Semantics may be enabled programmatically. If there's a race between that // and the DOM event, we may end up here while there's no longer a placeholder // to work with. @@ -173,15 +174,15 @@ class DesktopSemanticsEnabler extends SemanticsEnabler { } @override - html.Element prepareAccessibilityPlaceholder() { - final html.Element placeholder = - _semanticsPlaceholder = html.Element.tag('flt-semantics-placeholder'); + DomElement prepareAccessibilityPlaceholder() { + final DomElement placeholder = + _semanticsPlaceholder = createDomElement('flt-semantics-placeholder'); // Only listen to "click" because other kinds of events are reported via // PointerBinding. - placeholder.addEventListener('click', (html.Event event) { + placeholder.addEventListener('click', allowInterop((DomEvent event) { tryEnableSemantics(event); - }, true); + }), true); // Adding roles to semantics placeholder. 'aria-live' will make sure that // the content is announced to the assistive technology user as soon as the @@ -226,7 +227,7 @@ class MobileSemanticsEnabler extends SemanticsEnabler { Timer? semanticsActivationTimer; /// A temporary placeholder used to capture a request to activate semantics. - html.Element? _semanticsPlaceholder; + DomElement? _semanticsPlaceholder; /// The number of events we processed that could potentially activate /// semantics. @@ -247,7 +248,7 @@ class MobileSemanticsEnabler extends SemanticsEnabler { bool get isWaitingToEnableSemantics => _semanticsPlaceholder != null; @override - bool tryEnableSemantics(html.Event event) { + bool tryEnableSemantics(DomEvent event) { // Semantics may be enabled programmatically. If there's a race between that // and the DOM event, we may end up here while there's no longer a placeholder // to work with. @@ -320,29 +321,29 @@ class MobileSemanticsEnabler extends SemanticsEnabler { // semantics tree is designed to not interfere with Flutter's gesture // detection. bool enableConditionPassed = false; - html.Point activationPoint; + late final DomPoint activationPoint; switch (event.type) { case 'click': - final html.MouseEvent click = event as html.MouseEvent; + final DomMouseEvent click = event as DomMouseEvent; activationPoint = click.offset; break; case 'touchstart': case 'touchend': - final html.TouchEvent touch = event as html.TouchEvent; - activationPoint = touch.changedTouches!.first.client; + final DomTouchEvent touchEvent = event as DomTouchEvent; + activationPoint = touchEvent.changedTouches!.first.client; break; case 'pointerdown': case 'pointerup': - final html.PointerEvent touch = event as html.PointerEvent; - activationPoint = html.Point(touch.client.x, touch.client.y); + final DomPointerEvent touch = event as DomPointerEvent; + activationPoint = touch.client; break; default: // The event is not relevant, forward to framework as normal. return true; } - final html.Rectangle activatingElementRect = + final DomRect activatingElementRect = _semanticsPlaceholder!.getBoundingClientRect(); final double midX = (activatingElementRect.left + (activatingElementRect.right - activatingElementRect.left) / 2) @@ -372,15 +373,15 @@ class MobileSemanticsEnabler extends SemanticsEnabler { } @override - html.Element prepareAccessibilityPlaceholder() { - final html.Element placeholder = - _semanticsPlaceholder = html.Element.tag('flt-semantics-placeholder'); + DomElement prepareAccessibilityPlaceholder() { + final DomElement placeholder = + _semanticsPlaceholder = createDomElement('flt-semantics-placeholder'); // Only listen to "click" because other kinds of events are reported via // PointerBinding. - placeholder.addEventListener('click', (html.Event event) { + placeholder.addEventListener('click', allowInterop((DomEvent event) { tryEnableSemantics(event); - }, true); + }), true); placeholder ..setAttribute('role', 'button') diff --git a/lib/web_ui/lib/src/engine/semantics/tappable.dart b/lib/web_ui/lib/src/engine/semantics/tappable.dart index 77c2923fd2576..2c8ad8a6012df 100644 --- a/lib/web_ui/lib/src/engine/semantics/tappable.dart +++ b/lib/web_ui/lib/src/engine/semantics/tappable.dart @@ -2,11 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:html' as html; - import 'package:ui/ui.dart' as ui; +import '../dom.dart'; import '../platform_dispatcher.dart'; +import '../safe_browser_api.dart'; import 'semantics.dart'; /// Listens to HTML "click" gestures detected by the browser. @@ -19,11 +19,11 @@ class Tappable extends RoleManager { Tappable(SemanticsObject semanticsObject) : super(Role.tappable, semanticsObject); - html.EventListener? _clickListener; + DomEventListener? _clickListener; @override void update() { - final html.Element element = semanticsObject.element; + final DomElement element = semanticsObject.element; // "tab-index=0" is used to allow keyboard traversal of non-form elements. // See also: https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets @@ -44,14 +44,14 @@ class Tappable extends RoleManager { if (semanticsObject.hasAction(ui.SemanticsAction.tap) && !semanticsObject.hasFlag(ui.SemanticsFlag.isTextField)) { if (_clickListener == null) { - _clickListener = (_) { + _clickListener = allowInterop((_) { if (semanticsObject.owner.gestureMode != GestureMode.browserGestures) { return; } EnginePlatformDispatcher.instance.invokeOnSemanticsAction( semanticsObject.id, ui.SemanticsAction.tap, null); - }; + }); element.addEventListener('click', _clickListener); } } else { diff --git a/lib/web_ui/lib/src/engine/semantics/text_field.dart b/lib/web_ui/lib/src/engine/semantics/text_field.dart index a8aaadcf2c8f8..d541b00b11e86 100644 --- a/lib/web_ui/lib/src/engine/semantics/text_field.dart +++ b/lib/web_ui/lib/src/engine/semantics/text_field.dart @@ -7,6 +7,7 @@ import 'dart:html' as html; import 'package:ui/ui.dart' as ui; import '../browser_detection.dart'; +import '../dom.dart'; import '../platform_dispatcher.dart'; import '../text_editing/text_editing.dart'; import 'semantics.dart'; @@ -244,7 +245,7 @@ class TextField extends RoleManager { ..left = '0' ..width = '${semanticsObject.rect!.width}px' ..height = '${semanticsObject.rect!.height}px'; - semanticsObject.element.append(editableElement); + semanticsObject.element.append(editableElement as DomNode); switch (browserEngine) { case BrowserEngine.blink: diff --git a/lib/web_ui/test/engine/semantics/semantics_helper_test.dart b/lib/web_ui/test/engine/semantics/semantics_helper_test.dart index 1b38d0c9d2e66..abb8dd12c8842 100644 --- a/lib/web_ui/test/engine/semantics/semantics_helper_test.dart +++ b/lib/web_ui/test/engine/semantics/semantics_helper_test.dart @@ -2,11 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:html' as html; - import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine/browser_detection.dart'; +import 'package:ui/src/engine/dom.dart'; import 'package:ui/src/engine/pointer_binding.dart'; import 'package:ui/src/engine/semantics.dart'; @@ -19,13 +18,13 @@ void main() { void testMain() { group('$DesktopSemanticsEnabler', () { late DesktopSemanticsEnabler desktopSemanticsEnabler; - late html.Element? _placeholder; + late DomElement? _placeholder; setUp(() { EngineSemanticsOwner.instance.semanticsEnabled = false; desktopSemanticsEnabler = DesktopSemanticsEnabler(); _placeholder = desktopSemanticsEnabler.prepareAccessibilityPlaceholder(); - html.document.body!.append(_placeholder!); + domDocument.body!.append(_placeholder!); }); tearDown(() { @@ -40,9 +39,9 @@ void testMain() { expect(_placeholder!.getAttribute('aria-live'), 'polite'); expect(_placeholder!.getAttribute('tabindex'), '0'); - html.document.body!.append(_placeholder!); + domDocument.body!.append(_placeholder!); - expect(html.document.getElementsByTagName('flt-semantics-placeholder'), + expect(domDocument.getElementsByTagName('flt-semantics-placeholder'), isNotEmpty); expect(_placeholder!.getBoundingClientRect().height, 1); @@ -53,9 +52,9 @@ void testMain() { test('Not relevant events should be forwarded to the framework', () async { // Attach the placeholder to dom. - html.document.body!.append(_placeholder!); + domDocument.body!.append(_placeholder!); - html.Event event = html.MouseEvent('mousemove'); + DomEvent event = createDomEvent('Event', 'mousemove'); bool shouldForwardToFramework = desktopSemanticsEnabler.tryEnableSemantics(event); @@ -63,7 +62,7 @@ void testMain() { // Pointer events are not defined in webkit. if (browserEngine != BrowserEngine.webkit) { - event = html.PointerEvent('pointermove'); + event = createDomEvent('Event', 'pointermove'); shouldForwardToFramework = desktopSemanticsEnabler.tryEnableSemantics(event); @@ -74,7 +73,7 @@ void testMain() { test( 'Relevant events targeting placeholder should not be forwarded to the framework', () async { - final html.Event event = html.MouseEvent('mousedown'); + final DomEvent event = createDomEvent('Event', 'mousedown'); _placeholder!.dispatchEvent(event); final bool shouldForwardToFramework = @@ -84,7 +83,7 @@ void testMain() { }); test('disposes of the placeholder', () { - html.document.body!.append(_placeholder!); + domDocument.body!.append(_placeholder!); expect(_placeholder!.isConnected, isTrue); desktopSemanticsEnabler.dispose(); @@ -96,13 +95,13 @@ void testMain() { '$MobileSemanticsEnabler', () { late MobileSemanticsEnabler mobileSemanticsEnabler; - html.Element? _placeholder; + DomElement? _placeholder; setUp(() { EngineSemanticsOwner.instance.semanticsEnabled = false; mobileSemanticsEnabler = MobileSemanticsEnabler(); _placeholder = mobileSemanticsEnabler.prepareAccessibilityPlaceholder(); - html.document.body!.append(_placeholder!); + domDocument.body!.append(_placeholder!); }); tearDown(() { @@ -114,8 +113,8 @@ void testMain() { expect(_placeholder!.getAttribute('role'), 'button'); // Placeholder should cover all the screen on a mobile device. - final num bodyHeight = html.window.innerHeight!; - final num bodyWidth = html.window.innerWidth!; + final num bodyHeight = domWindow.innerHeight!; + final num bodyWidth = domWindow.innerWidth!; expect(_placeholder!.getBoundingClientRect().height, bodyHeight); expect(_placeholder!.getBoundingClientRect().width, bodyWidth); @@ -123,13 +122,13 @@ void testMain() { test('Non-relevant events should be forwarded to the framework', () async { - html.Event event; + DomEvent event; if (_defaultSupportDetector.hasPointerEvents) { - event = html.PointerEvent('pointermove'); + event = createDomPointerEvent('pointermove'); } else if (_defaultSupportDetector.hasTouchEvents) { - event = html.TouchEvent('touchcancel'); + event = createDomTouchEvent('touchcancel'); } else { - event = html.MouseEvent('mousemove'); + event = createDomMouseEvent('mousemove'); } final bool shouldForwardToFramework = @@ -142,15 +141,17 @@ void testMain() { expect(mobileSemanticsEnabler.semanticsActivationTimer, isNull); // Send a click off center - _placeholder!.dispatchEvent(html.MouseEvent( + _placeholder!.dispatchEvent(createDomMouseEvent( 'click', - clientX: 0, - clientY: 0, + { + 'clientX': 0, + 'clientY': 0, + } )); expect(mobileSemanticsEnabler.semanticsActivationTimer, isNull); // Send a click at center - final html.Rectangle activatingElementRect = + final DomRect activatingElementRect = _placeholder!.getBoundingClientRect(); final int midX = (activatingElementRect.left + (activatingElementRect.right - activatingElementRect.left) / 2) @@ -158,10 +159,12 @@ void testMain() { final int midY = (activatingElementRect.top + (activatingElementRect.bottom - activatingElementRect.top) / 2) .toInt(); - _placeholder!.dispatchEvent(html.MouseEvent( + _placeholder!.dispatchEvent(createDomMouseEvent( 'click', - clientX: midX, - clientY: midY, + { + 'clientX': midX, + 'clientY': midY, + } )); expect(mobileSemanticsEnabler.semanticsActivationTimer, isNotNull); }); diff --git a/lib/web_ui/test/engine/semantics/semantics_test.dart b/lib/web_ui/test/engine/semantics/semantics_test.dart index a888e7a62e44a..d05d0e0a91cf6 100644 --- a/lib/web_ui/test/engine/semantics/semantics_test.dart +++ b/lib/web_ui/test/engine/semantics/semantics_test.dart @@ -356,7 +356,7 @@ void _testEngineSemanticsOwner() { ..debugOverrideTimestampFunction(fakeAsync.getClock(_testTime).now) ..semanticsEnabled = true; expect(semantics().shouldAcceptBrowserGesture('click'), isTrue); - semantics().receiveGlobalEvent(html.Event('pointermove')); + semantics().receiveGlobalEvent(createDomEvent('Event', 'pointermove')); expect(semantics().shouldAcceptBrowserGesture('click'), isFalse); // After 1 second of inactivity a browser gestures counts as standalone. @@ -368,21 +368,21 @@ void _testEngineSemanticsOwner() { test('checks shouldEnableSemantics for every global event', () { final MockSemanticsEnabler mockSemanticsEnabler = MockSemanticsEnabler(); semantics().semanticsHelper.semanticsEnabler = mockSemanticsEnabler; - final html.Event pointerEvent = html.Event('pointermove'); + final DomEvent pointerEvent = createDomEvent('Event', 'pointermove'); semantics().receiveGlobalEvent(pointerEvent); // Verify the interactions. expect( mockSemanticsEnabler.shouldEnableSemanticsEvents, - [pointerEvent], + [pointerEvent], ); }); test('forwards events to framework if shouldEnableSemantics returns true', () { final MockSemanticsEnabler mockSemanticsEnabler = MockSemanticsEnabler(); semantics().semanticsHelper.semanticsEnabler = mockSemanticsEnabler; - final html.Event pointerEvent = html.Event('pointermove'); + final DomEvent pointerEvent = createDomEvent('Event', 'pointermove'); mockSemanticsEnabler.shouldEnableSemanticsReturnValue = true; expect(semantics().receiveGlobalEvent(pointerEvent), isTrue); }); @@ -397,21 +397,21 @@ class MockSemanticsEnabler implements SemanticsEnabler { bool get isWaitingToEnableSemantics => throw UnimplementedError(); @override - html.Element prepareAccessibilityPlaceholder() { + DomElement prepareAccessibilityPlaceholder() { throw UnimplementedError(); } bool shouldEnableSemanticsReturnValue = false; - final List shouldEnableSemanticsEvents = []; + final List shouldEnableSemanticsEvents = []; @override - bool shouldEnableSemantics(html.Event event) { + bool shouldEnableSemantics(DomEvent event) { shouldEnableSemanticsEvents.add(event); return shouldEnableSemanticsReturnValue; } @override - bool tryEnableSemantics(html.Event event) { + bool tryEnableSemantics(DomEvent event) { throw UnimplementedError(); } }