@@ -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
176178typedef 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.
209212class 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
361427class PointerSupportDetector {
0 commit comments