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

Commit 3ca695f

Browse files
committed
add tests and docs
1 parent f18e7dc commit 3ca695f

File tree

3 files changed

+468
-26
lines changed

3 files changed

+468
-26
lines changed

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

Lines changed: 84 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ class PointerBinding {
111111
void dispose() {
112112
_adapter.clearListeners();
113113
_pointerDataConverter.clearPointerState();
114+
clickDebouncer.reset();
114115
}
115116

116117
final DomElement flutterViewElement;
@@ -173,9 +174,11 @@ class PointerBinding {
173174
}
174175
}
175176

177+
@visibleForTesting
176178
typedef QueuedEvent = ({ DomEvent event, Duration timeStamp, List<ui.PointerData> data });
177179

178-
typedef _DebounceState = ({
180+
@visibleForTesting
181+
typedef DebounceState = ({
179182
DomElement target,
180183
Timer timer,
181184
List<QueuedEvent> queue,
@@ -207,10 +210,39 @@ typedef _DebounceState = ({
207210
///
208211
/// This mechanism is in place to deal with https://github.com/flutter/flutter/issues/130162.
209212
class ClickDebouncer {
210-
_DebounceState? _state;
213+
DebounceState? _state;
214+
215+
@visibleForTesting
216+
DebounceState? get debugState => _state;
211217

218+
// The timestamp of the last "pointerup" DOM event that was flushed.
219+
//
220+
// Not to be confused with the time when it was flushed. The two may be far
221+
// apart because the flushing can happen after a delay due to timer, or events
222+
// that happen after the said "pointerup".
223+
Duration? _lastFlushedPointerUpTimeStamp;
224+
225+
/// Returns true if the debouncer has a non-empty queue of pointer events that
226+
/// were withheld from the framework.
227+
///
228+
/// This value is normally false, and it flips to true when the first
229+
/// pointerdown is observed that lands on a tappable semantics node, denoted
230+
/// by the presence of the `flt-tappable` attribute.
212231
bool get isDebouncing => _state != null;
213232

233+
/// Processes a pointer event.
234+
///
235+
/// If semantics are off, simply forwards the event to the framework.
236+
///
237+
/// If currently debouncing events (see [isDebouncing]), adds the event to
238+
/// the debounce queue, unless the target of the event is different from the
239+
/// target that initiated the debouncing process, in which case stops
240+
/// debouncing and flushes pointer events to the framework.
241+
///
242+
/// If the event is a `pointerdown` and the target is `flt-tappable`, begins
243+
/// debouncing events.
244+
///
245+
/// In all other situations forwards the event to the framework.
214246
void onPointerData(DomEvent event, List<ui.PointerData> data) {
215247
if (!EnginePlatformDispatcher.instance.semanticsEnabled) {
216248
_sendToFramework(event, data);
@@ -226,21 +258,41 @@ class ClickDebouncer {
226258
}
227259
}
228260

229-
/// Notifies the debouncer about a browser-recognized click event.
261+
/// Notifies the debouncer of the browser-detected "click" DOM event.
230262
///
231-
/// Returns true if the click event should be deduplicated. Returns false if
232-
/// the click event should not be deduplicated.
233-
bool onClick(DomEvent click) {
263+
/// Forwards the event to the framework, unless it is deduplicated because
264+
/// the corresponding pointer down/up events were recently flushed to the
265+
/// framework already.
266+
void onClick(DomEvent click, int semanticsNodeId, bool isListening) {
234267
assert(click.type == 'click');
235268

236269
if (!isDebouncing) {
237-
return _shouldDeduplicateClickEvent(click);
270+
// There's no pending queue of pointer events that are being debounced. It
271+
// is a standalone click event. Unless pointer down/up were flushed
272+
// recently and if the node is currently listening to event, forward to
273+
// the framework.
274+
if (isListening && _shouldSendClickEventToFramework(click)) {
275+
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
276+
semanticsNodeId, ui.SemanticsAction.tap, null);
277+
}
278+
return;
238279
}
239280

240-
final _DebounceState state = _state!;
241-
_state = null;
242-
state.timer.cancel();
243-
return false;
281+
if (isListening) {
282+
// There's a pending queue of pointer events. Prefer sendind the tap action
283+
// instead of pointer events, because the pointer events may not land on the
284+
// combined semantic node and miss the click/tap.
285+
final DebounceState state = _state!;
286+
_state = null;
287+
state.timer.cancel();
288+
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
289+
semanticsNodeId, ui.SemanticsAction.tap, null);
290+
} else {
291+
// The semantic node is not listening to taps. Flush the pointer events
292+
// for the framework to figure out what to do with them. It's possible
293+
// the framework is interested in gestures other than taps.
294+
_flush();
295+
}
244296
}
245297

246298
void _startDebouncing(DomEvent event, List<ui.PointerData> data) {
@@ -287,7 +339,7 @@ class ClickDebouncer {
287339
'Cannot debounce event. Debouncing state not established by _startDebouncing.'
288340
);
289341

290-
final _DebounceState state = _state!;
342+
final DebounceState state = _state!;
291343
state.queue.add((
292344
event: event,
293345
timeStamp: _BaseAdapter._eventTimeStampToDuration(event.timeStamp!),
@@ -311,28 +363,26 @@ class ClickDebouncer {
311363
_flush();
312364
}
313365

314-
Duration? _lastFlushedPointerUpTimeStamp;
315-
316366
// If the click event happens soon after the last `pointerup` event that was
317367
// already flushed to the framework, the click event is dropped to avoid
318368
// double click.
319-
bool _shouldDeduplicateClickEvent(DomEvent click) {
369+
bool _shouldSendClickEventToFramework(DomEvent click) {
320370
final Duration? lastFlushedPointerUpTimeStamp = _lastFlushedPointerUpTimeStamp;
321371

322372
if (lastFlushedPointerUpTimeStamp == null) {
323373
// We haven't seen a pointerup. It's standalone click event. Let it through.
324-
return false;
374+
return true;
325375
}
326376

327377
final Duration clickTimeStamp = _BaseAdapter._eventTimeStampToDuration(click.timeStamp!);
328378
final Duration delta = clickTimeStamp - lastFlushedPointerUpTimeStamp;
329-
return delta < const Duration(milliseconds: 50);
379+
return delta >= const Duration(milliseconds: 50);
330380
}
331381

332382
void _flush() {
333383
assert(_state != null);
334384

335-
final _DebounceState state = _state!;
385+
final DebounceState state = _state!;
336386
state.timer.cancel();
337387

338388
final List<ui.PointerData> aggregateData = <ui.PointerData>[];
@@ -356,6 +406,22 @@ class ClickDebouncer {
356406
}
357407
EnginePlatformDispatcher.instance.invokeOnPointerDataPacket(packet);
358408
}
409+
410+
/// Cancels any pending debounce process and forgets anything that happened so
411+
/// far.
412+
///
413+
/// This object can be used as if it was just initialized.
414+
void reset() {
415+
final DebounceState? state = _state;
416+
_state = null;
417+
_lastFlushedPointerUpTimeStamp = null;
418+
419+
if (state == null) {
420+
return;
421+
}
422+
423+
state.timer.cancel();
424+
}
359425
}
360426

361427
class PointerSupportDetector {

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

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,11 @@ class Button extends PrimaryRoleManager {
3232
class Tappable extends RoleManager {
3333
Tappable(SemanticsObject semanticsObject) : super(Role.tappable, semanticsObject) {
3434
_clickListener = createDomEventListener((DomEvent click) {
35-
final bool shouldDeduplicateClickEvent = PointerBinding.instance!.clickDebouncer.onClick(click);
36-
37-
if (!_isListening || shouldDeduplicateClickEvent) {
38-
return;
39-
}
40-
41-
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
42-
semanticsObject.id, ui.SemanticsAction.tap, null);
35+
PointerBinding.instance!.clickDebouncer.onClick(
36+
click,
37+
semanticsObject.id,
38+
_isListening,
39+
);
4340
});
4441
semanticsObject.element.addEventListener('click', _clickListener);
4542
}

0 commit comments

Comments
 (0)