Revert "[web] fix clicks on merged semantic nodes (#43620)" (flutter/engine#46067)
This reverts commit 6dee3dd14e.
The commit caused https://github.com/flutter/flutter/issues/134842. I'm going to try again, this time accounting for nested clickables/tappables.
This commit is contained in:
@@ -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:async';
|
||||
import 'dart:js_interop';
|
||||
import 'dart:math' as math;
|
||||
|
||||
@@ -26,7 +25,7 @@ const bool _debugLogPointerEvents = false;
|
||||
const bool _debugLogFlutterEvents = false;
|
||||
|
||||
/// The signature of a callback that handles pointer events.
|
||||
typedef _PointerDataCallback = void Function(DomEvent event, List<ui.PointerData>);
|
||||
typedef _PointerDataCallback = void Function(Iterable<ui.PointerData>);
|
||||
|
||||
// The mask for the bitfield of event buttons. Buttons not contained in this
|
||||
// mask are cut off.
|
||||
@@ -104,14 +103,11 @@ class PointerBinding {
|
||||
}
|
||||
}
|
||||
|
||||
final ClickDebouncer clickDebouncer = ClickDebouncer();
|
||||
|
||||
/// Performs necessary clean up for PointerBinding including removing event listeners
|
||||
/// and clearing the existing pointer state
|
||||
void dispose() {
|
||||
_adapter.clearListeners();
|
||||
_pointerDataConverter.clearPointerState();
|
||||
clickDebouncer.reset();
|
||||
}
|
||||
|
||||
final DomElement flutterViewElement;
|
||||
@@ -160,247 +156,20 @@ class PointerBinding {
|
||||
// TODO(dit): remove old API fallbacks, https://github.com/flutter/flutter/issues/116141
|
||||
_BaseAdapter _createAdapter() {
|
||||
if (_detector.hasPointerEvents) {
|
||||
return _PointerAdapter(clickDebouncer.onPointerData, flutterViewElement, _pointerDataConverter, _keyboardConverter);
|
||||
return _PointerAdapter(_onPointerData, flutterViewElement, _pointerDataConverter, _keyboardConverter);
|
||||
}
|
||||
// Fallback for Safari Mobile < 13. To be removed.
|
||||
if (_detector.hasTouchEvents) {
|
||||
return _TouchAdapter(clickDebouncer.onPointerData, flutterViewElement, _pointerDataConverter, _keyboardConverter);
|
||||
return _TouchAdapter(_onPointerData, flutterViewElement, _pointerDataConverter, _keyboardConverter);
|
||||
}
|
||||
// Fallback for Safari Desktop < 13. To be removed.
|
||||
if (_detector.hasMouseEvents) {
|
||||
return _MouseAdapter(clickDebouncer.onPointerData, flutterViewElement, _pointerDataConverter, _keyboardConverter);
|
||||
return _MouseAdapter(_onPointerData, flutterViewElement, _pointerDataConverter, _keyboardConverter);
|
||||
}
|
||||
throw UnsupportedError('This browser does not support pointer, touch, or mouse events.');
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
typedef QueuedEvent = ({ DomEvent event, Duration timeStamp, List<ui.PointerData> data });
|
||||
|
||||
@visibleForTesting
|
||||
typedef DebounceState = ({
|
||||
DomElement target,
|
||||
Timer timer,
|
||||
List<QueuedEvent> queue,
|
||||
});
|
||||
|
||||
/// Disambiguates taps and clicks that are produced both by the framework from
|
||||
/// `pointerdown`/`pointerup` events and those detected as DOM "click" events by
|
||||
/// the browser.
|
||||
///
|
||||
/// The implementation is waiting for a `pointerdown`, and as soon as it sees
|
||||
/// one stops forwarding pointer events to the framework, and instead queues
|
||||
/// them in a list. The queuing process stops as soon as one of the following
|
||||
/// two conditions happens first:
|
||||
///
|
||||
/// * 200ms passes after the `pointerdown` event. Most clicks, even slow ones,
|
||||
/// are typically done by then. Importantly, screen readers simulate clicks
|
||||
/// much faster than 200ms. So if the timer expires, it is likely the user is
|
||||
/// not interested in producing a click, so the debouncing process stops and
|
||||
/// all queued events are forwarded to the framework. If, for example, a
|
||||
/// tappable node is inside a scrollable viewport, the events can be
|
||||
/// intrepreted by the framework to initiate scrolling.
|
||||
/// * A `click` event arrives. If the event queue has not been flushed to the
|
||||
/// framework, the event is forwarded to the framework as a
|
||||
/// `SemanticsAction.tap`, and all the pointer events are dropped. If, by the
|
||||
/// time the click event arrives, the queue was flushed (but no more than 50ms
|
||||
/// ago), then the click event is dropped instead under the assumption that
|
||||
/// the flushed pointer events are interpreted by the framework as the desired
|
||||
/// gesture.
|
||||
///
|
||||
/// This mechanism is in place to deal with https://github.com/flutter/flutter/issues/130162.
|
||||
class ClickDebouncer {
|
||||
DebounceState? _state;
|
||||
|
||||
@visibleForTesting
|
||||
DebounceState? get debugState => _state;
|
||||
|
||||
// The timestamp of the last "pointerup" DOM event that was flushed.
|
||||
//
|
||||
// Not to be confused with the time when it was flushed. The two may be far
|
||||
// apart because the flushing can happen after a delay due to timer, or events
|
||||
// that happen after the said "pointerup".
|
||||
Duration? _lastFlushedPointerUpTimeStamp;
|
||||
|
||||
/// Returns true if the debouncer has a non-empty queue of pointer events that
|
||||
/// were withheld from the framework.
|
||||
///
|
||||
/// This value is normally false, and it flips to true when the first
|
||||
/// pointerdown is observed that lands on a tappable semantics node, denoted
|
||||
/// by the presence of the `flt-tappable` attribute.
|
||||
bool get isDebouncing => _state != null;
|
||||
|
||||
/// Processes a pointer event.
|
||||
///
|
||||
/// If semantics are off, simply forwards the event to the framework.
|
||||
///
|
||||
/// If currently debouncing events (see [isDebouncing]), adds the event to
|
||||
/// the debounce queue, unless the target of the event is different from the
|
||||
/// target that initiated the debouncing process, in which case stops
|
||||
/// debouncing and flushes pointer events to the framework.
|
||||
///
|
||||
/// If the event is a `pointerdown` and the target is `flt-tappable`, begins
|
||||
/// debouncing events.
|
||||
///
|
||||
/// In all other situations forwards the event to the framework.
|
||||
void onPointerData(DomEvent event, List<ui.PointerData> data) {
|
||||
if (!EnginePlatformDispatcher.instance.semanticsEnabled) {
|
||||
_sendToFramework(event, data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDebouncing) {
|
||||
_debounce(event, data);
|
||||
} else if (event.type == 'pointerdown') {
|
||||
_startDebouncing(event, data);
|
||||
} else {
|
||||
_sendToFramework(event, data);
|
||||
}
|
||||
}
|
||||
|
||||
/// Notifies the debouncer of the browser-detected "click" DOM event.
|
||||
///
|
||||
/// Forwards the event to the framework, unless it is deduplicated because
|
||||
/// the corresponding pointer down/up events were recently flushed to the
|
||||
/// framework already.
|
||||
void onClick(DomEvent click, int semanticsNodeId, bool isListening) {
|
||||
assert(click.type == 'click');
|
||||
|
||||
if (!isDebouncing) {
|
||||
// There's no pending queue of pointer events that are being debounced. It
|
||||
// is a standalone click event. Unless pointer down/up were flushed
|
||||
// recently and if the node is currently listening to event, forward to
|
||||
// the framework.
|
||||
if (isListening && _shouldSendClickEventToFramework(click)) {
|
||||
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
|
||||
semanticsNodeId, ui.SemanticsAction.tap, null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isListening) {
|
||||
// There's a pending queue of pointer events. Prefer sending the tap action
|
||||
// instead of pointer events, because the pointer events may not land on the
|
||||
// combined semantic node and miss the click/tap.
|
||||
final DebounceState state = _state!;
|
||||
_state = null;
|
||||
state.timer.cancel();
|
||||
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
|
||||
semanticsNodeId, ui.SemanticsAction.tap, null);
|
||||
} else {
|
||||
// The semantic node is not listening to taps. Flush the pointer events
|
||||
// for the framework to figure out what to do with them. It's possible
|
||||
// the framework is interested in gestures other than taps.
|
||||
_flush();
|
||||
}
|
||||
}
|
||||
|
||||
void _startDebouncing(DomEvent event, List<ui.PointerData> data) {
|
||||
assert(
|
||||
_state == null,
|
||||
'Cannot start debouncing. Already debouncing.'
|
||||
);
|
||||
assert(
|
||||
event.type == 'pointerdown',
|
||||
'Click debouncing must begin with a pointerdown'
|
||||
);
|
||||
|
||||
final DomEventTarget? target = event.target;
|
||||
if (target is DomElement && target.hasAttribute('flt-tappable')) {
|
||||
_state = (
|
||||
target: target,
|
||||
// The 200ms duration was chosen empirically by testing tapping, mouse
|
||||
// clicking, trackpad tapping and clicking, as well as the following
|
||||
// screen readers: TalkBack on Android, VoiceOver on macOS, Narrator/
|
||||
// NVDA/JAWS on Windows. 200ms seemed to hit the sweet spot by
|
||||
// satisfying the following:
|
||||
// * It was short enough that delaying the `pointerdown` still allowed
|
||||
// drag gestures to begin reasonably soon (e.g. scrolling).
|
||||
// * It was long enough to register taps and clicks.
|
||||
// * It was successful at detecting taps generated by all tested
|
||||
// screen readers.
|
||||
timer: Timer(const Duration(milliseconds: 200), _onTimerExpired),
|
||||
queue: <QueuedEvent>[(
|
||||
event: event,
|
||||
timeStamp: _BaseAdapter._eventTimeStampToDuration(event.timeStamp!),
|
||||
data: data,
|
||||
)],
|
||||
);
|
||||
} else {
|
||||
// The event landed on an non-tappable target. Assume this won't lead to
|
||||
// double clicks and forward the event to the framework.
|
||||
_sendToFramework(event, data);
|
||||
}
|
||||
}
|
||||
|
||||
void _debounce(DomEvent event, List<ui.PointerData> data) {
|
||||
assert(
|
||||
_state != null,
|
||||
'Cannot debounce event. Debouncing state not established by _startDebouncing.'
|
||||
);
|
||||
|
||||
final DebounceState state = _state!;
|
||||
state.queue.add((
|
||||
event: event,
|
||||
timeStamp: _BaseAdapter._eventTimeStampToDuration(event.timeStamp!),
|
||||
data: data,
|
||||
));
|
||||
|
||||
// It's only interesting to debounce clicks when both `pointerdown` and
|
||||
// `pointerup` land on the same element.
|
||||
if (event.type == 'pointerup') {
|
||||
// TODO(yjbanov): this is a bit mouthful, but see https://github.com/dart-lang/sdk/issues/53070
|
||||
final DomEventTarget? eventTarget = event.target;
|
||||
final DomElement stateTarget = state.target;
|
||||
final bool targetChanged = eventTarget != stateTarget;
|
||||
if (targetChanged) {
|
||||
_flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onTimerExpired() {
|
||||
if (!isDebouncing) {
|
||||
return;
|
||||
}
|
||||
_flush();
|
||||
}
|
||||
|
||||
// If the click event happens soon after the last `pointerup` event that was
|
||||
// already flushed to the framework, the click event is dropped to avoid
|
||||
// double click.
|
||||
bool _shouldSendClickEventToFramework(DomEvent click) {
|
||||
final Duration? lastFlushedPointerUpTimeStamp = _lastFlushedPointerUpTimeStamp;
|
||||
|
||||
if (lastFlushedPointerUpTimeStamp == null) {
|
||||
// We haven't seen a pointerup. It's standalone click event. Let it through.
|
||||
return true;
|
||||
}
|
||||
|
||||
final Duration clickTimeStamp = _BaseAdapter._eventTimeStampToDuration(click.timeStamp!);
|
||||
final Duration delta = clickTimeStamp - lastFlushedPointerUpTimeStamp;
|
||||
return delta >= const Duration(milliseconds: 50);
|
||||
}
|
||||
|
||||
void _flush() {
|
||||
assert(_state != null);
|
||||
|
||||
final DebounceState state = _state!;
|
||||
state.timer.cancel();
|
||||
|
||||
final List<ui.PointerData> aggregateData = <ui.PointerData>[];
|
||||
for (final QueuedEvent queuedEvent in state.queue) {
|
||||
if (queuedEvent.event.type == 'pointerup') {
|
||||
_lastFlushedPointerUpTimeStamp = queuedEvent.timeStamp;
|
||||
}
|
||||
aggregateData.addAll(queuedEvent.data);
|
||||
}
|
||||
|
||||
_sendToFramework(null, aggregateData);
|
||||
_state = null;
|
||||
}
|
||||
|
||||
void _sendToFramework(DomEvent? event, List<ui.PointerData> data) {
|
||||
void _onPointerData(Iterable<ui.PointerData> data) {
|
||||
final ui.PointerDataPacket packet = ui.PointerDataPacket(data: data.toList());
|
||||
if (_debugLogFlutterEvents) {
|
||||
for(final ui.PointerData datum in data) {
|
||||
@@ -409,16 +178,6 @@ class ClickDebouncer {
|
||||
}
|
||||
EnginePlatformDispatcher.instance.invokeOnPointerDataPacket(packet);
|
||||
}
|
||||
|
||||
/// Cancels any pending debounce process and forgets anything that happened so
|
||||
/// far.
|
||||
///
|
||||
/// This object can be used as if it was just initialized.
|
||||
void reset() {
|
||||
_state?.timer.cancel();
|
||||
_state = null;
|
||||
_lastFlushedPointerUpTimeStamp = null;
|
||||
}
|
||||
}
|
||||
|
||||
class PointerSupportDetector {
|
||||
@@ -439,50 +198,65 @@ class _Listener {
|
||||
required this.target,
|
||||
required this.handler,
|
||||
required this.useCapture,
|
||||
required this.isNative,
|
||||
});
|
||||
|
||||
/// Registers a listener for the given `event` on a `target`.
|
||||
///
|
||||
/// If `passive` is null uses the default behavior determined by the event
|
||||
/// type. If `passive` is true, marks the handler as non-blocking for the
|
||||
/// built-in browser behavior. This means the browser will not wait for the
|
||||
/// handler to finish execution before performing the default action
|
||||
/// associated with this event. If `passive` is false, the browser will wait
|
||||
/// for the handler to finish execution before performing the respective
|
||||
/// action.
|
||||
/// Registers a listener for the given [event] on [target] using the Dart-to-JS API.
|
||||
factory _Listener.register({
|
||||
required String event,
|
||||
required DomEventTarget target,
|
||||
required DartDomEventListener handler,
|
||||
bool capture = false,
|
||||
bool? passive,
|
||||
}) {
|
||||
final DomEventListener jsHandler = createDomEventListener(handler);
|
||||
if (passive == null) {
|
||||
target.addEventListener(event, jsHandler, capture);
|
||||
} else {
|
||||
final Map<String, Object> eventOptions = <String, Object>{
|
||||
'capture': capture,
|
||||
'passive': passive,
|
||||
};
|
||||
target.addEventListenerWithOptions(event, jsHandler, eventOptions);
|
||||
}
|
||||
|
||||
return _Listener._(
|
||||
final _Listener listener = _Listener._(
|
||||
event: event,
|
||||
target: target,
|
||||
handler: jsHandler,
|
||||
useCapture: capture,
|
||||
isNative: false,
|
||||
);
|
||||
target.addEventListener(event, jsHandler, capture);
|
||||
return listener;
|
||||
}
|
||||
|
||||
/// Registers a listener for the given [event] on [target] using the native JS API.
|
||||
factory _Listener.registerNative({
|
||||
required String event,
|
||||
required DomEventTarget target,
|
||||
required DomEventListener jsHandler,
|
||||
bool capture = false,
|
||||
bool passive = false,
|
||||
}) {
|
||||
final Map<String, Object> eventOptions = <String, Object>{
|
||||
'capture': capture,
|
||||
'passive': passive,
|
||||
};
|
||||
final _Listener listener = _Listener._(
|
||||
event: event,
|
||||
target: target,
|
||||
handler: jsHandler,
|
||||
useCapture: capture,
|
||||
isNative: true,
|
||||
);
|
||||
target.addEventListenerWithOptions(event, jsHandler, eventOptions);
|
||||
return listener;
|
||||
}
|
||||
|
||||
final String event;
|
||||
|
||||
final DomEventTarget target;
|
||||
final DomEventListener handler;
|
||||
|
||||
final bool useCapture;
|
||||
final bool isNative;
|
||||
|
||||
void unregister() {
|
||||
target.removeEventListener(event, handler, useCapture);
|
||||
if (isNative) {
|
||||
target.removeEventListener(event, handler, useCapture);
|
||||
} else {
|
||||
target.removeEventListener(event, handler, useCapture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -722,11 +496,10 @@ mixin _WheelEventListenerMixin on _BaseAdapter {
|
||||
}
|
||||
|
||||
void _addWheelEventListener(DartDomEventListener handler) {
|
||||
_listeners.add(_Listener.register(
|
||||
_listeners.add(_Listener.registerNative(
|
||||
event: 'wheel',
|
||||
target: flutterViewElement,
|
||||
handler: handler,
|
||||
passive: false,
|
||||
jsHandler: createDomEventListener(handler),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -736,7 +509,7 @@ mixin _WheelEventListenerMixin on _BaseAdapter {
|
||||
if (_debugLogPointerEvents) {
|
||||
print(event.type);
|
||||
}
|
||||
_callback(e, _convertWheelEventToPointerData(event));
|
||||
_callback(_convertWheelEventToPointerData(event));
|
||||
// Prevent default so mouse wheel event doesn't get converted to
|
||||
// a scroll event that semantic nodes would process.
|
||||
//
|
||||
@@ -986,7 +759,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
|
||||
buttons: event.buttons!.toInt(),
|
||||
);
|
||||
_convertEventsToPointerData(data: pointerData, event: event, details: down);
|
||||
_callback(event, pointerData);
|
||||
_callback(pointerData);
|
||||
});
|
||||
|
||||
// Why `domWindow` you ask? See this fiddle: https://jsfiddle.net/ditman/7towxaqp
|
||||
@@ -1003,7 +776,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
|
||||
final _SanitizedDetails move = sanitizer.sanitizeMoveEvent(buttons: event.buttons!.toInt());
|
||||
_convertEventsToPointerData(data: pointerData, event: event, details: move);
|
||||
}
|
||||
_callback(event, pointerData);
|
||||
_callback(pointerData);
|
||||
});
|
||||
|
||||
_addPointerEventListener(flutterViewElement, 'pointerleave', (DomPointerEvent event) {
|
||||
@@ -1013,7 +786,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
|
||||
final _SanitizedDetails? details = sanitizer.sanitizeLeaveEvent(buttons: event.buttons!.toInt());
|
||||
if (details != null) {
|
||||
_convertEventsToPointerData(data: pointerData, event: event, details: details);
|
||||
_callback(event, pointerData);
|
||||
_callback(pointerData);
|
||||
}
|
||||
}, useCapture: false, checkModifiers: false);
|
||||
|
||||
@@ -1026,7 +799,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
|
||||
_removePointerIfUnhoverable(event);
|
||||
if (details != null) {
|
||||
_convertEventsToPointerData(data: pointerData, event: event, details: details);
|
||||
_callback(event, pointerData);
|
||||
_callback(pointerData);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1042,7 +815,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
|
||||
final _SanitizedDetails details = _getSanitizer(device).sanitizeCancelEvent();
|
||||
_removePointerIfUnhoverable(event);
|
||||
_convertEventsToPointerData(data: pointerData, event: event, details: details);
|
||||
_callback(event, pointerData);
|
||||
_callback(pointerData);
|
||||
}
|
||||
}, checkModifiers: false);
|
||||
|
||||
@@ -1181,7 +954,7 @@ class _TouchAdapter extends _BaseAdapter {
|
||||
);
|
||||
}
|
||||
}
|
||||
_callback(event, pointerData);
|
||||
_callback(pointerData);
|
||||
});
|
||||
|
||||
_addTouchEventListener(flutterViewElement, 'touchmove', (DomTouchEvent event) {
|
||||
@@ -1200,7 +973,7 @@ class _TouchAdapter extends _BaseAdapter {
|
||||
);
|
||||
}
|
||||
}
|
||||
_callback(event, pointerData);
|
||||
_callback(pointerData);
|
||||
});
|
||||
|
||||
_addTouchEventListener(flutterViewElement, 'touchend', (DomTouchEvent event) {
|
||||
@@ -1222,7 +995,7 @@ class _TouchAdapter extends _BaseAdapter {
|
||||
);
|
||||
}
|
||||
}
|
||||
_callback(event, pointerData);
|
||||
_callback(pointerData);
|
||||
});
|
||||
|
||||
_addTouchEventListener(flutterViewElement, 'touchcancel', (DomTouchEvent event) {
|
||||
@@ -1241,7 +1014,7 @@ class _TouchAdapter extends _BaseAdapter {
|
||||
);
|
||||
}
|
||||
}
|
||||
_callback(event, pointerData);
|
||||
_callback(pointerData);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1339,7 +1112,7 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin {
|
||||
buttons: event.buttons!.toInt(),
|
||||
);
|
||||
_convertEventsToPointerData(data: pointerData, event: event, details: sanitizedDetails);
|
||||
_callback(event, pointerData);
|
||||
_callback(pointerData);
|
||||
});
|
||||
|
||||
// Why `domWindow` you ask? See this fiddle: https://jsfiddle.net/ditman/7towxaqp
|
||||
@@ -1351,7 +1124,7 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin {
|
||||
}
|
||||
final _SanitizedDetails move = _sanitizer.sanitizeMoveEvent(buttons: event.buttons!.toInt());
|
||||
_convertEventsToPointerData(data: pointerData, event: event, details: move);
|
||||
_callback(event, pointerData);
|
||||
_callback(pointerData);
|
||||
});
|
||||
|
||||
_addMouseEventListener(flutterViewElement, 'mouseleave', (DomMouseEvent event) {
|
||||
@@ -1359,7 +1132,7 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin {
|
||||
final _SanitizedDetails? details = _sanitizer.sanitizeLeaveEvent(buttons: event.buttons!.toInt());
|
||||
if (details != null) {
|
||||
_convertEventsToPointerData(data: pointerData, event: event, details: details);
|
||||
_callback(event, pointerData);
|
||||
_callback(pointerData);
|
||||
}
|
||||
}, useCapture: false);
|
||||
|
||||
@@ -1369,7 +1142,7 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin {
|
||||
final _SanitizedDetails? sanitizedDetails = _sanitizer.sanitizeUpEvent(buttons: event.buttons?.toInt());
|
||||
if (sanitizedDetails != null) {
|
||||
_convertEventsToPointerData(data: pointerData, event: event, details: sanitizedDetails);
|
||||
_callback(event, pointerData);
|
||||
_callback(pointerData);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1980,9 +1980,8 @@ class EngineSemanticsOwner {
|
||||
}
|
||||
|
||||
/// Receives DOM events from the pointer event system to correlate with the
|
||||
/// semantics events.
|
||||
///
|
||||
/// Returns true if the event should be forwarded to the framework.
|
||||
/// semantics events; returns true if the event should be forwarded to the
|
||||
/// framework.
|
||||
///
|
||||
/// The browser sends us both raw pointer events and gestures from
|
||||
/// [SemanticsObject.element]s. There could be three possibilities:
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import '../dom.dart';
|
||||
import '../platform_dispatcher.dart';
|
||||
import 'semantics.dart';
|
||||
|
||||
/// Sets the "button" ARIA role.
|
||||
class Button extends PrimaryRoleManager {
|
||||
Button(SemanticsObject semanticsObject) : super.withBasics(PrimaryRole.button, semanticsObject) {
|
||||
@@ -30,45 +33,41 @@ class Button extends PrimaryRoleManager {
|
||||
/// the browser may not send us pointer events. In that mode we forward HTML
|
||||
/// click as [ui.SemanticsAction.tap].
|
||||
class Tappable extends RoleManager {
|
||||
Tappable(SemanticsObject semanticsObject) : super(Role.tappable, semanticsObject) {
|
||||
_clickListener = createDomEventListener((DomEvent click) {
|
||||
PointerBinding.instance!.clickDebouncer.onClick(
|
||||
click,
|
||||
semanticsObject.id,
|
||||
_isListening,
|
||||
);
|
||||
});
|
||||
semanticsObject.element.addEventListener('click', _clickListener);
|
||||
}
|
||||
Tappable(SemanticsObject semanticsObject)
|
||||
: super(Role.tappable, semanticsObject);
|
||||
|
||||
DomEventListener? _clickListener;
|
||||
bool _isListening = false;
|
||||
|
||||
@override
|
||||
void update() {
|
||||
final bool wasListening = _isListening;
|
||||
_isListening = semanticsObject.enabledState() != EnabledState.disabled && semanticsObject.isTappable;
|
||||
if (wasListening != _isListening) {
|
||||
_updateAttribute();
|
||||
if (!semanticsObject.isTappable || semanticsObject.enabledState() == EnabledState.disabled) {
|
||||
_stopListening();
|
||||
} else {
|
||||
if (_clickListener == null) {
|
||||
_clickListener = createDomEventListener((DomEvent event) {
|
||||
if (semanticsObject.owner.gestureMode != GestureMode.browserGestures) {
|
||||
return;
|
||||
}
|
||||
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
|
||||
semanticsObject.id, ui.SemanticsAction.tap, null);
|
||||
});
|
||||
semanticsObject.element.addEventListener('click', _clickListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _updateAttribute() {
|
||||
// The `flt-tappable` attribute marks the element for the ClickDebouncer to
|
||||
// to know that it should debounce click events on this element. The
|
||||
// contract is that the element that has this attribute is also the element
|
||||
// that receives pointer and "click" events.
|
||||
if (_isListening) {
|
||||
semanticsObject.element.setAttribute('flt-tappable', '');
|
||||
} else {
|
||||
semanticsObject.element.removeAttribute('flt-tappable');
|
||||
void _stopListening() {
|
||||
if (_clickListener == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
semanticsObject.element.removeEventListener('click', _clickListener);
|
||||
_clickListener = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
semanticsObject.element.removeEventListener('click', _clickListener);
|
||||
_clickListener = null;
|
||||
super.dispose();
|
||||
_stopListening();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,23 +293,17 @@ String canonicalizeHtml(
|
||||
html_package.Element.tag(replacementTag);
|
||||
|
||||
if (mode != HtmlComparisonMode.noAttributes) {
|
||||
// Sort the attributes so tests are not sensitive to their order, which
|
||||
// does not matter in terms of functionality.
|
||||
final List<String> attributeNames = original.attributes.keys.cast<String>().toList();
|
||||
attributeNames.sort();
|
||||
for (final String name in attributeNames) {
|
||||
final String value = original.attributes[name]!;
|
||||
if (name == 'style') {
|
||||
// The style attribute is handled separately because it contains substructure.
|
||||
continue;
|
||||
original.attributes.forEach((dynamic name, String value) {
|
||||
if (name is! String) {
|
||||
throw ArgumentError('"$name" should be String but was ${name.runtimeType}.');
|
||||
}
|
||||
|
||||
// These are the only attributes we're interested in testing. This list
|
||||
// can change over time.
|
||||
if (name.startsWith('aria-') || name.startsWith('flt-') || name == 'role') {
|
||||
if (name == 'style') {
|
||||
return;
|
||||
}
|
||||
if (name.startsWith('aria-')) {
|
||||
replacement.attributes[name] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (original.attributes.containsKey('style')) {
|
||||
final String styleValue = original.attributes['style']!;
|
||||
|
||||
@@ -3087,385 +3087,6 @@ void testMain() {
|
||||
packets.clear();
|
||||
},
|
||||
);
|
||||
|
||||
group('ClickDebouncer', () {
|
||||
_testClickDebouncer();
|
||||
});
|
||||
}
|
||||
|
||||
typedef CapturedSemanticsEvent = ({
|
||||
ui.SemanticsAction type,
|
||||
int nodeId,
|
||||
});
|
||||
|
||||
void _testClickDebouncer() {
|
||||
final DateTime testTime = DateTime(2018, 12, 17);
|
||||
late List<ui.PointerChange> pointerPackets;
|
||||
late List<CapturedSemanticsEvent> semanticsActions;
|
||||
late _PointerEventContext context;
|
||||
late PointerBinding binding;
|
||||
|
||||
void testWithSemantics(
|
||||
String description,
|
||||
Future<void> Function() body,
|
||||
) {
|
||||
test(
|
||||
description,
|
||||
() async {
|
||||
EngineSemanticsOwner.instance
|
||||
..debugOverrideTimestampFunction(() => testTime)
|
||||
..semanticsEnabled = true;
|
||||
await body();
|
||||
EngineSemanticsOwner.instance.semanticsEnabled = false;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
setUp(() {
|
||||
context = _PointerEventContext();
|
||||
pointerPackets = <ui.PointerChange>[];
|
||||
semanticsActions = <CapturedSemanticsEvent>[];
|
||||
ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) {
|
||||
for (final ui.PointerData data in packet.data) {
|
||||
pointerPackets.add(data.change);
|
||||
}
|
||||
};
|
||||
EnginePlatformDispatcher.instance.onSemanticsActionEvent = (ui.SemanticsActionEvent event) {
|
||||
semanticsActions.add((type: event.type, nodeId: event.nodeId));
|
||||
};
|
||||
binding = PointerBinding.instance!;
|
||||
binding.debugOverrideDetector(context);
|
||||
binding.clickDebouncer.reset();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
binding.clickDebouncer.reset();
|
||||
});
|
||||
|
||||
test('Forwards to framework when semantics is off', () {
|
||||
expect(EnginePlatformDispatcher.instance.semanticsEnabled, false);
|
||||
expect(binding.clickDebouncer.isDebouncing, false);
|
||||
flutterViewEmbedder.flutterViewElement.dispatchEvent(context.primaryDown());
|
||||
expect(pointerPackets, <ui.PointerChange>[
|
||||
ui.PointerChange.add,
|
||||
ui.PointerChange.down,
|
||||
]);
|
||||
expect(binding.clickDebouncer.isDebouncing, false);
|
||||
expect(semanticsActions, isEmpty);
|
||||
});
|
||||
|
||||
testWithSemantics('Forwards to framework when not debouncing', () async {
|
||||
expect(EnginePlatformDispatcher.instance.semanticsEnabled, true);
|
||||
expect(binding.clickDebouncer.isDebouncing, false);
|
||||
|
||||
// This test DOM element is missing the `flt-tappable` attribute on purpose
|
||||
// so that the debouncer does not debounce events and simply lets
|
||||
// everything through.
|
||||
final DomElement testElement = createDomElement('flt-semantics');
|
||||
flutterViewEmbedder.semanticsHostElement!.appendChild(testElement);
|
||||
|
||||
testElement.dispatchEvent(context.primaryDown());
|
||||
testElement.dispatchEvent(context.primaryUp());
|
||||
expect(binding.clickDebouncer.isDebouncing, false);
|
||||
|
||||
expect(pointerPackets, <ui.PointerChange>[
|
||||
ui.PointerChange.add,
|
||||
ui.PointerChange.down,
|
||||
ui.PointerChange.up,
|
||||
]);
|
||||
expect(semanticsActions, isEmpty);
|
||||
});
|
||||
|
||||
testWithSemantics('Accumulates pointer events starting from pointerdown', () async {
|
||||
expect(EnginePlatformDispatcher.instance.semanticsEnabled, true);
|
||||
expect(binding.clickDebouncer.isDebouncing, false);
|
||||
|
||||
final DomElement testElement = createDomElement('flt-semantics');
|
||||
testElement.setAttribute('flt-tappable', '');
|
||||
flutterViewEmbedder.semanticsHostElement!.appendChild(testElement);
|
||||
|
||||
testElement.dispatchEvent(context.primaryDown());
|
||||
expect(
|
||||
reason: 'Should start debouncing at first pointerdown',
|
||||
binding.clickDebouncer.isDebouncing,
|
||||
true,
|
||||
);
|
||||
|
||||
testElement.dispatchEvent(context.primaryUp());
|
||||
expect(
|
||||
reason: 'Should still be debouncing after pointerup',
|
||||
binding.clickDebouncer.isDebouncing,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(
|
||||
reason: 'Events are withheld from the framework while debouncing',
|
||||
pointerPackets,
|
||||
<ui.PointerChange>[],
|
||||
);
|
||||
expect(
|
||||
binding.clickDebouncer.debugState!.target,
|
||||
testElement,
|
||||
);
|
||||
expect(
|
||||
binding.clickDebouncer.debugState!.timer.isActive,
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
binding.clickDebouncer.debugState!.queue.map<String>((QueuedEvent e) => e.event.type),
|
||||
<String>['pointerdown', 'pointerup'],
|
||||
);
|
||||
|
||||
await Future<void>.delayed(const Duration(milliseconds: 250));
|
||||
expect(
|
||||
reason: 'Should stop debouncing after timer expires.',
|
||||
binding.clickDebouncer.isDebouncing,
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
reason: 'Queued up events should be flushed to the framework.',
|
||||
pointerPackets,
|
||||
<ui.PointerChange>[
|
||||
ui.PointerChange.add,
|
||||
ui.PointerChange.down,
|
||||
ui.PointerChange.up,
|
||||
],
|
||||
);
|
||||
expect(semanticsActions, isEmpty);
|
||||
});
|
||||
|
||||
testWithSemantics('Flushes events to framework when target changes', () async {
|
||||
expect(EnginePlatformDispatcher.instance.semanticsEnabled, true);
|
||||
expect(binding.clickDebouncer.isDebouncing, false);
|
||||
|
||||
final DomElement testElement = createDomElement('flt-semantics');
|
||||
testElement.setAttribute('flt-tappable', '');
|
||||
flutterViewEmbedder.semanticsHostElement!.appendChild(testElement);
|
||||
|
||||
testElement.dispatchEvent(context.primaryDown());
|
||||
expect(
|
||||
reason: 'Should start debouncing at first pointerdown',
|
||||
binding.clickDebouncer.isDebouncing,
|
||||
true,
|
||||
);
|
||||
|
||||
final DomElement newTarget = createDomElement('flt-semantics');
|
||||
newTarget.setAttribute('flt-tappable', '');
|
||||
flutterViewEmbedder.semanticsHostElement!.appendChild(newTarget);
|
||||
newTarget.dispatchEvent(context.primaryUp());
|
||||
|
||||
expect(
|
||||
reason: 'Should stop debouncing when target changes.',
|
||||
binding.clickDebouncer.isDebouncing,
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
reason: 'The state should be cleaned up after stopping debouncing.',
|
||||
binding.clickDebouncer.debugState,
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
reason: 'Queued up events should be flushed to the framework.',
|
||||
pointerPackets,
|
||||
<ui.PointerChange>[
|
||||
ui.PointerChange.add,
|
||||
ui.PointerChange.down,
|
||||
ui.PointerChange.up,
|
||||
],
|
||||
);
|
||||
expect(semanticsActions, isEmpty);
|
||||
});
|
||||
|
||||
testWithSemantics('Forwards click to framework when not debouncing but listening', () async {
|
||||
expect(binding.clickDebouncer.isDebouncing, false);
|
||||
|
||||
final DomElement testElement = createDomElement('flt-semantics');
|
||||
testElement.setAttribute('flt-tappable', '');
|
||||
flutterViewEmbedder.semanticsHostElement!.appendChild(testElement);
|
||||
|
||||
final DomEvent click = createDomMouseEvent(
|
||||
'click',
|
||||
<Object?, Object?>{
|
||||
'clientX': testElement.getBoundingClientRect().x,
|
||||
'clientY': testElement.getBoundingClientRect().y,
|
||||
}
|
||||
);
|
||||
|
||||
binding.clickDebouncer.onClick(click, 42, true);
|
||||
expect(binding.clickDebouncer.isDebouncing, false);
|
||||
expect(pointerPackets, isEmpty);
|
||||
expect(semanticsActions, <CapturedSemanticsEvent>[
|
||||
(type: ui.SemanticsAction.tap, nodeId: 42)
|
||||
]);
|
||||
});
|
||||
|
||||
testWithSemantics('Forwards click to framework when debouncing and listening', () async {
|
||||
expect(binding.clickDebouncer.isDebouncing, false);
|
||||
|
||||
final DomElement testElement = createDomElement('flt-semantics');
|
||||
testElement.setAttribute('flt-tappable', '');
|
||||
flutterViewEmbedder.semanticsHostElement!.appendChild(testElement);
|
||||
testElement.dispatchEvent(context.primaryDown());
|
||||
expect(binding.clickDebouncer.isDebouncing, true);
|
||||
|
||||
final DomEvent click = createDomMouseEvent(
|
||||
'click',
|
||||
<Object?, Object?>{
|
||||
'clientX': testElement.getBoundingClientRect().x,
|
||||
'clientY': testElement.getBoundingClientRect().y,
|
||||
}
|
||||
);
|
||||
|
||||
binding.clickDebouncer.onClick(click, 42, true);
|
||||
expect(pointerPackets, isEmpty);
|
||||
expect(semanticsActions, <CapturedSemanticsEvent>[
|
||||
(type: ui.SemanticsAction.tap, nodeId: 42)
|
||||
]);
|
||||
});
|
||||
|
||||
testWithSemantics('Dedupes click if debouncing but not listening', () async {
|
||||
expect(binding.clickDebouncer.isDebouncing, false);
|
||||
|
||||
final DomElement testElement = createDomElement('flt-semantics');
|
||||
testElement.setAttribute('flt-tappable', '');
|
||||
flutterViewEmbedder.semanticsHostElement!.appendChild(testElement);
|
||||
testElement.dispatchEvent(context.primaryDown());
|
||||
expect(binding.clickDebouncer.isDebouncing, true);
|
||||
|
||||
final DomEvent click = createDomMouseEvent(
|
||||
'click',
|
||||
<Object?, Object?>{
|
||||
'clientX': testElement.getBoundingClientRect().x,
|
||||
'clientY': testElement.getBoundingClientRect().y,
|
||||
}
|
||||
);
|
||||
|
||||
binding.clickDebouncer.onClick(click, 42, false);
|
||||
expect(
|
||||
reason: 'When tappable declares that it is not listening to click events '
|
||||
'the debouncer flushes the pointer events to the framework and '
|
||||
'lets it sort it out.',
|
||||
pointerPackets,
|
||||
<ui.PointerChange>[
|
||||
ui.PointerChange.add,
|
||||
ui.PointerChange.down,
|
||||
],
|
||||
);
|
||||
expect(semanticsActions, isEmpty);
|
||||
});
|
||||
|
||||
testWithSemantics('Dedupes click if pointer down/up flushed recently', () async {
|
||||
expect(EnginePlatformDispatcher.instance.semanticsEnabled, true);
|
||||
expect(binding.clickDebouncer.isDebouncing, false);
|
||||
|
||||
final DomElement testElement = createDomElement('flt-semantics');
|
||||
testElement.setAttribute('flt-tappable', '');
|
||||
flutterViewEmbedder.semanticsHostElement!.appendChild(testElement);
|
||||
|
||||
testElement.dispatchEvent(context.primaryDown());
|
||||
|
||||
// Simulate the user holding the pointer down for some time before releasing,
|
||||
// such that the pointerup event happens close to timer expiration. This
|
||||
// will create the situation that the click event arrives just after the
|
||||
// pointerup is flushed. Forwarding the click to the framework would look
|
||||
// like a double-click, so the click event is deduped.
|
||||
await Future<void>.delayed(const Duration(milliseconds: 190));
|
||||
|
||||
testElement.dispatchEvent(context.primaryUp());
|
||||
expect(binding.clickDebouncer.isDebouncing, true);
|
||||
expect(
|
||||
reason: 'Timer has not expired yet',
|
||||
pointerPackets, isEmpty,
|
||||
);
|
||||
|
||||
// Wait for the timer to expire to make sure pointer events are flushed.
|
||||
await Future<void>.delayed(const Duration(milliseconds: 20));
|
||||
|
||||
expect(
|
||||
reason: 'Queued up events should be flushed to the framework because the '
|
||||
'time expired before the click event arrived.',
|
||||
pointerPackets,
|
||||
<ui.PointerChange>[
|
||||
ui.PointerChange.add,
|
||||
ui.PointerChange.down,
|
||||
ui.PointerChange.up,
|
||||
],
|
||||
);
|
||||
|
||||
final DomEvent click = createDomMouseEvent(
|
||||
'click',
|
||||
<Object?, Object?>{
|
||||
'clientX': testElement.getBoundingClientRect().x,
|
||||
'clientY': testElement.getBoundingClientRect().y,
|
||||
}
|
||||
);
|
||||
binding.clickDebouncer.onClick(click, 42, true);
|
||||
|
||||
expect(
|
||||
reason: 'Because the DOM click event was deduped.',
|
||||
semanticsActions,
|
||||
isEmpty,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
testWithSemantics('Forwards click if enough time passed after the last flushed pointerup', () async {
|
||||
expect(EnginePlatformDispatcher.instance.semanticsEnabled, true);
|
||||
expect(binding.clickDebouncer.isDebouncing, false);
|
||||
|
||||
final DomElement testElement = createDomElement('flt-semantics');
|
||||
testElement.setAttribute('flt-tappable', '');
|
||||
flutterViewEmbedder.semanticsHostElement!.appendChild(testElement);
|
||||
|
||||
testElement.dispatchEvent(context.primaryDown());
|
||||
|
||||
// Simulate the user holding the pointer down for some time before releasing,
|
||||
// such that the pointerup event happens close to timer expiration. This
|
||||
// makes it possible for the click to arrive early. However, this test in
|
||||
// particular will delay the click to check that the delay is checked
|
||||
// correctly. The inverse situation was already tested in the previous test.
|
||||
await Future<void>.delayed(const Duration(milliseconds: 190));
|
||||
|
||||
testElement.dispatchEvent(context.primaryUp());
|
||||
expect(binding.clickDebouncer.isDebouncing, true);
|
||||
expect(
|
||||
reason: 'Timer has not expired yet',
|
||||
pointerPackets, isEmpty,
|
||||
);
|
||||
|
||||
// Wait for the timer to expire to make sure pointer events are flushed.
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
expect(
|
||||
reason: 'Queued up events should be flushed to the framework because the '
|
||||
'time expired before the click event arrived.',
|
||||
pointerPackets,
|
||||
<ui.PointerChange>[
|
||||
ui.PointerChange.add,
|
||||
ui.PointerChange.down,
|
||||
ui.PointerChange.up,
|
||||
],
|
||||
);
|
||||
|
||||
final DomEvent click = createDomMouseEvent(
|
||||
'click',
|
||||
<Object?, Object?>{
|
||||
'clientX': testElement.getBoundingClientRect().x,
|
||||
'clientY': testElement.getBoundingClientRect().y,
|
||||
}
|
||||
);
|
||||
binding.clickDebouncer.onClick(click, 42, true);
|
||||
|
||||
expect(
|
||||
reason: 'The DOM click should still be sent to the framework because it '
|
||||
'happened far enough from the last pointerup that it is unlikely '
|
||||
'to be a duplicate.',
|
||||
semanticsActions,
|
||||
<CapturedSemanticsEvent>[
|
||||
(type: ui.SemanticsAction.tap, nodeId: 42)
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class MockSafariPointerEventWorkaround implements SafariPointerEventWorkaround {
|
||||
|
||||
@@ -377,7 +377,7 @@ void _testEngineSemanticsOwner() {
|
||||
expectSemanticsTree('''
|
||||
<sem style="$rootSemanticStyle">
|
||||
<sem-c>
|
||||
<sem role="text" aria-label="Hello"></sem>
|
||||
<sem aria-label="Hello"></sem>
|
||||
</sem-c>
|
||||
</sem>''');
|
||||
|
||||
@@ -387,7 +387,7 @@ void _testEngineSemanticsOwner() {
|
||||
expectSemanticsTree('''
|
||||
<sem style="$rootSemanticStyle">
|
||||
<sem-c>
|
||||
<sem role="text" aria-label="World"></sem>
|
||||
<sem aria-label="World"></sem>
|
||||
</sem-c>
|
||||
</sem>''');
|
||||
|
||||
@@ -397,7 +397,7 @@ void _testEngineSemanticsOwner() {
|
||||
expectSemanticsTree('''
|
||||
<sem style="$rootSemanticStyle">
|
||||
<sem-c>
|
||||
<sem role="text"></sem>
|
||||
<sem></sem>
|
||||
</sem-c>
|
||||
</sem>''');
|
||||
|
||||
@@ -430,7 +430,7 @@ void _testEngineSemanticsOwner() {
|
||||
expectSemanticsTree('''
|
||||
<sem style="$rootSemanticStyle">
|
||||
<sem-c>
|
||||
<sem role="text" aria-label="tooltip\nHello"></sem>
|
||||
<sem aria-label="tooltip\nHello"></sem>
|
||||
</sem-c>
|
||||
</sem>''');
|
||||
|
||||
@@ -440,7 +440,7 @@ void _testEngineSemanticsOwner() {
|
||||
expectSemanticsTree('''
|
||||
<sem style="$rootSemanticStyle">
|
||||
<sem-c>
|
||||
<sem role="text"></sem>
|
||||
<sem></sem>
|
||||
</sem-c>
|
||||
</sem>''');
|
||||
|
||||
@@ -1388,7 +1388,7 @@ void _testIncrementables() {
|
||||
semantics().updateSemantics(builder.build());
|
||||
expectSemanticsTree('''
|
||||
<sem style="$rootSemanticStyle">
|
||||
<input role="slider" aria-valuenow="1" aria-valuetext="d" aria-valuemax="1" aria-valuemin="1">
|
||||
<input aria-valuenow="1" aria-valuetext="d" aria-valuemax="1" aria-valuemin="1">
|
||||
</sem>''');
|
||||
|
||||
final SemanticsObject node = semantics().debugSemanticsTree![0]!;
|
||||
@@ -1421,7 +1421,7 @@ void _testIncrementables() {
|
||||
semantics().updateSemantics(builder.build());
|
||||
expectSemanticsTree('''
|
||||
<sem style="$rootSemanticStyle">
|
||||
<input role="slider" aria-valuenow="1" aria-valuetext="d" aria-valuemax="2" aria-valuemin="1">
|
||||
<input aria-valuenow="1" aria-valuetext="d" aria-valuemax="2" aria-valuemin="1">
|
||||
</sem>''');
|
||||
|
||||
final DomHTMLInputElement input =
|
||||
@@ -1454,7 +1454,7 @@ void _testIncrementables() {
|
||||
semantics().updateSemantics(builder.build());
|
||||
expectSemanticsTree('''
|
||||
<sem style="$rootSemanticStyle">
|
||||
<input role="slider" aria-valuenow="1" aria-valuetext="d" aria-valuemax="1" aria-valuemin="0">
|
||||
<input aria-valuenow="1" aria-valuetext="d" aria-valuemax="1" aria-valuemin="0">
|
||||
</sem>''');
|
||||
|
||||
final DomHTMLInputElement input =
|
||||
@@ -1489,7 +1489,7 @@ void _testIncrementables() {
|
||||
semantics().updateSemantics(builder.build());
|
||||
expectSemanticsTree('''
|
||||
<sem style="$rootSemanticStyle">
|
||||
<input role="slider" aria-valuenow="1" aria-valuetext="d" aria-valuemax="2" aria-valuemin="0">
|
||||
<input aria-valuenow="1" aria-valuetext="d" aria-valuemax="2" aria-valuemin="0">
|
||||
</sem>''');
|
||||
|
||||
semantics().semanticsEnabled = false;
|
||||
@@ -1632,7 +1632,7 @@ void _testCheckables() {
|
||||
|
||||
semantics().updateSemantics(builder.build());
|
||||
expectSemanticsTree('''
|
||||
<sem aria-label="test label" flt-tappable role="switch" aria-checked="true" style="$rootSemanticStyle"></sem>
|
||||
<sem aria-label="test label" role="switch" aria-checked="true" style="$rootSemanticStyle"></sem>
|
||||
''');
|
||||
|
||||
final SemanticsObject node = semantics().debugSemanticsTree![0]!;
|
||||
@@ -1690,7 +1690,7 @@ void _testCheckables() {
|
||||
|
||||
semantics().updateSemantics(builder.build());
|
||||
expectSemanticsTree('''
|
||||
<sem role="switch" flt-tappable aria-checked="false" style="$rootSemanticStyle"></sem>
|
||||
<sem role="switch" aria-checked="false" style="$rootSemanticStyle"></sem>
|
||||
''');
|
||||
|
||||
semantics().semanticsEnabled = false;
|
||||
@@ -1716,7 +1716,7 @@ void _testCheckables() {
|
||||
|
||||
semantics().updateSemantics(builder.build());
|
||||
expectSemanticsTree('''
|
||||
<sem role="checkbox" flt-tappable aria-checked="true" style="$rootSemanticStyle"></sem>
|
||||
<sem role="checkbox" aria-checked="true" style="$rootSemanticStyle"></sem>
|
||||
''');
|
||||
|
||||
semantics().semanticsEnabled = false;
|
||||
@@ -1766,7 +1766,7 @@ void _testCheckables() {
|
||||
|
||||
semantics().updateSemantics(builder.build());
|
||||
expectSemanticsTree('''
|
||||
<sem role="checkbox" flt-tappable aria-checked="false" style="$rootSemanticStyle"></sem>
|
||||
<sem role="checkbox" aria-checked="false" style="$rootSemanticStyle"></sem>
|
||||
''');
|
||||
|
||||
semantics().semanticsEnabled = false;
|
||||
@@ -1793,7 +1793,7 @@ void _testCheckables() {
|
||||
|
||||
semantics().updateSemantics(builder.build());
|
||||
expectSemanticsTree('''
|
||||
<sem role="radio" flt-tappable aria-checked="true" style="$rootSemanticStyle"></sem>
|
||||
<sem role="radio" aria-checked="true" style="$rootSemanticStyle"></sem>
|
||||
''');
|
||||
|
||||
semantics().semanticsEnabled = false;
|
||||
@@ -1845,7 +1845,7 @@ void _testCheckables() {
|
||||
|
||||
semantics().updateSemantics(builder.build());
|
||||
expectSemanticsTree('''
|
||||
<sem role="radio" flt-tappable aria-checked="false" style="$rootSemanticStyle"></sem>
|
||||
<sem role="radio" aria-checked="false" style="$rootSemanticStyle"></sem>
|
||||
''');
|
||||
|
||||
semantics().semanticsEnabled = false;
|
||||
@@ -1918,7 +1918,7 @@ void _testTappable() {
|
||||
tester.apply();
|
||||
|
||||
expectSemanticsTree('''
|
||||
<sem role="button" flt-tappable style="$rootSemanticStyle"></sem>
|
||||
<sem role="button" style="$rootSemanticStyle"></sem>
|
||||
''');
|
||||
|
||||
final SemanticsObject node = semantics().debugSemanticsTree![0]!;
|
||||
@@ -1979,14 +1979,14 @@ void _testTappable() {
|
||||
'<sem role="button" aria-disabled="true" style="$rootSemanticStyle"></sem>');
|
||||
|
||||
updateTappable(enabled: true);
|
||||
expectSemanticsTree('<sem role="button" flt-tappable style="$rootSemanticStyle"></sem>');
|
||||
expectSemanticsTree('<sem role="button" style="$rootSemanticStyle"></sem>');
|
||||
|
||||
updateTappable(enabled: false);
|
||||
expectSemanticsTree(
|
||||
'<sem role="button" aria-disabled="true" style="$rootSemanticStyle"></sem>');
|
||||
|
||||
updateTappable(enabled: true);
|
||||
expectSemanticsTree('<sem role="button" flt-tappable style="$rootSemanticStyle"></sem>');
|
||||
expectSemanticsTree('<sem role="button" style="$rootSemanticStyle"></sem>');
|
||||
|
||||
semantics().semanticsEnabled = false;
|
||||
});
|
||||
@@ -2623,11 +2623,11 @@ void _testDialog() {
|
||||
tester.apply();
|
||||
|
||||
expectSemanticsTree('''
|
||||
<sem role="dialog" aria-describedby="flt-semantic-node-2" style="$rootSemanticStyle">
|
||||
<sem aria-describedby="flt-semantic-node-2" style="$rootSemanticStyle">
|
||||
<sem-c>
|
||||
<sem>
|
||||
<sem-c>
|
||||
<sem role="text" aria-label="$label"></sem>
|
||||
<sem aria-label="$label"></sem>
|
||||
</sem-c>
|
||||
</sem>
|
||||
</sem-c>
|
||||
@@ -2716,7 +2716,7 @@ void _testDialog() {
|
||||
<sem-c>
|
||||
<sem>
|
||||
<sem-c>
|
||||
<sem role="text" aria-label="Hello"></sem>
|
||||
<sem aria-label="Hello"></sem>
|
||||
</sem-c>
|
||||
</sem>
|
||||
</sem-c>
|
||||
@@ -2853,9 +2853,9 @@ void _testFocusable() {
|
||||
}
|
||||
|
||||
expectSemanticsTree('''
|
||||
<sem style="$rootSemanticStyle">
|
||||
<sem role="group" style="$rootSemanticStyle">
|
||||
<sem-c>
|
||||
<sem role="text" aria-label="focusable text"></sem>
|
||||
<sem aria-label="focusable text"></sem>
|
||||
</sem-c>
|
||||
</sem>
|
||||
''');
|
||||
|
||||
Reference in New Issue
Block a user