-
Notifications
You must be signed in to change notification settings - Fork 29.7k
[web] ClickDebouncer workaround for iOS Safari click behavior #172995
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[web] ClickDebouncer workaround for iOS Safari click behavior #172995
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces a workaround for an iOS Safari behavior where click events are delayed if a timer is scheduled from a pointerdown listener. The fix cleverly delays the start of the ClickDebouncer's timer by using Timer.run, ensuring the click event is correctly handled. The changes are well-implemented and include corresponding test updates. My feedback includes suggestions to add documentation to the new methods in accordance with the Flutter style guide and a minor improvement to the test helper for better robustness.
engine/src/flutter/lib/web_ui/test/engine/pointer_binding_test.dart
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces a workaround for an iOS Safari behavior where click events are delayed if a timer is set in a pointerdown listener. The fix defers the timer creation in ClickDebouncer using Timer.run. The approach is sound, but the implementation introduces a race condition that could lead to leaked timers if multiple pointerdown events occur in the same event loop. I've provided a detailed comment with a suggested fix for this issue. The test changes correctly adapt to the new asynchronous behavior.
harryterkelsen
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! Nice job finding a fix for a tricky bug
…r#172995) It turns out iOS Safari in some cases tracks timers that are scheduled from within a `pointerdown` listener, and it delays the `click` event until those timers have expired (with a max waiting time of 350ms or so). The `ClickDebouncer` sets a timer of 200ms to see if a `click` event is received by then. But because of the Safari behavior explained above, the `click` event will always arrive right after the `ClickDebouncer`'s timer, so we always misattribute the `click` event. Fixes flutter#172180
Roll Flutter from c3279caa127d to 871849e4b6bf (56 revisions) flutter/flutter@c3279ca...871849e 2025-08-01 [email protected] Roll Dart SDK from 6832e04cf2f9 to 6e1bb159c860 (8 revisions) (flutter/flutter#173119) 2025-08-01 [email protected] Add `--profile-startup` flag to `flutter run` (flutter/flutter#172990) 2025-08-01 [email protected] Add `side` to `RadioThemeData` (flutter/flutter#171945) 2025-08-01 [email protected] Update GCA instructions (flutter/flutter#173001) 2025-08-01 [email protected] [engine] Null aware elements clean-ups (flutter/flutter#173075) 2025-08-01 [email protected] Roll Packages from db6988d to f0645d8 (3 revisions) (flutter/flutter#173111) 2025-08-01 98614782+auto-submit[bot]@users.noreply.github.com Reverts "Reland licenses cpp switch 3 (#173063)" (flutter/flutter#173113) 2025-08-01 [email protected] Update embedder API CODEOWNERS (flutter/flutter#173081) 2025-08-01 [email protected] Move android_obfuscate_test from devicelab into tools integration.shard (flutter/flutter#169798) 2025-08-01 [email protected] [A11y] RangeSlider should have 2 focus node (flutter/flutter#172729) 2025-07-31 [email protected] Upload the linux arm64 embedder to cloud buckets. (flutter/flutter#173068) 2025-07-31 [email protected] Reland licenses cpp switch 3 (flutter/flutter#173063) 2025-07-31 [email protected] [ Tool ] Mark IOOverrides subclasses as `final` (flutter/flutter#173078) 2025-07-31 [email protected] [macOS] Remove duplicate object initialization (flutter/flutter#171767) 2025-07-31 [email protected] Redistribute Android test owners (flutter/flutter#172886) 2025-07-31 [email protected] Avoid negatives in the styleguide.md (flutter/flutter#172917) 2025-07-31 [email protected] Roll Skia from 42e3bed42110 to 49e39cd3cf18 (7 revisions) (flutter/flutter#173069) 2025-07-31 [email protected] Fix the issue where calling showOnScreen on a sliver after a pinned child in SliverMainAxisGroup does not reveal it. (flutter/flutter#171339) 2025-07-31 [email protected] Improve assertion message in `EdgeInsetsDirectional.resolve` (flutter/flutter#172099) 2025-07-31 [email protected] licenses_cpp: Switched to lexically_relative for 2x speed boost. (flutter/flutter#173048) 2025-07-31 [email protected] Add dartvm to the dart_sdk_entitlement_config list. (flutter/flutter#173044) 2025-07-31 [email protected] [web] ClickDebouncer workaround for iOS Safari click behavior (flutter/flutter#172995) 2025-07-31 98614782+auto-submit[bot]@users.noreply.github.com Reverts "Reland licenses cpp switch 2 (#172996)" (flutter/flutter#173059) 2025-07-31 [email protected] [web] Text editing test accepts both behaviors in Firefox (flutter/flutter#172767) 2025-07-31 [email protected] Reland licenses cpp switch 2 (flutter/flutter#172996) 2025-07-31 [email protected] Roll Packages from d914120 to db6988d (2 revisions) (flutter/flutter#173039) 2025-07-31 [email protected] Add a SliverList code sample (flutter/flutter#172986) 2025-07-31 [email protected] fix: adjust scrollable size assertion with tolerance (flutter/flutter#171426) 2025-07-31 [email protected] impeller: Shrink `Command` 40 bytes (flutter/flutter#173004) 2025-07-31 [email protected] [web] Remove outdated comment about HTML renderer (flutter/flutter#172877) 2025-07-31 [email protected] Roll Skia from ff4da49305c5 to 42e3bed42110 (1 revision) (flutter/flutter#173038) 2025-07-31 [email protected] Roll Skia from 7d468a4868cb to ff4da49305c5 (3 revisions) (flutter/flutter#173028) 2025-07-31 [email protected] Roll Skia from 951277895c86 to 7d468a4868cb (1 revision) (flutter/flutter#173027) 2025-07-31 [email protected] Manual roll of Dart from 14ea8d342149 to 6832e04cf2f9 (flutter/flutter#173015) 2025-07-31 [email protected] Roll Skia from 8bdf4cdcf4ed to 951277895c86 (2 revisions) (flutter/flutter#173022) 2025-07-31 [email protected] Roll Fuchsia Linux SDK from bQVQlLssTxxLjoDU0... to xo_bqOoFuBKFmgKxn... (flutter/flutter#173021) 2025-07-31 [email protected] feat: Add `cursorHeight` to `DropdownMenu` (flutter/flutter#172615) 2025-07-31 [email protected] Roll Skia from a3347cee2d73 to 8bdf4cdcf4ed (3 revisions) (flutter/flutter#173013) 2025-07-31 [email protected] [ Widget Preview ] Add `--web-server` support (flutter/flutter#172978) 2025-07-30 [email protected] Bump customer tests for zulip fix 2 (flutter/flutter#173003) 2025-07-30 [email protected] Migrate to null aware elements - Part 4 (flutter/flutter#172322) 2025-07-30 [email protected] Marks Linux linux_feature_flags_test to be unflaky (flutter/flutter#172629) 2025-07-30 [email protected] [ Widget Previews ] Add support for `MultiPreview`s (flutter/flutter#172852) 2025-07-30 [email protected] Made licenses_cpp simpatico with google licenses (flutter/flutter#172991) 2025-07-30 [email protected] Roll Skia from d579722d65c6 to a3347cee2d73 (6 revisions) (flutter/flutter#172989) 2025-07-30 [email protected] Migrate to null aware elements - Part 5 (flutter/flutter#172418) ...
…lutter into add-non-uniform-table-border * 'add-non-uniform-table-border' of github.com:korca0220/flutter: (185 commits) Refactor `distinctVisibleOuterColors` -> private method Refactor Modify docs and method name Refactor Remove comments Refactor Modify docs and method parameters Feat Add tests Feat Add to non-uniform TableBorder - Add method for check outerUniform - Add internal helper method Add dartvm to the dart_sdk_entitlement_config list. (flutter#173044) [web] ClickDebouncer workaround for iOS Safari click behavior (flutter#172995) Reverts "Reland licenses cpp switch 2 (flutter#172996)" (flutter#173059) [web] Text editing test accepts both behaviors in Firefox (flutter#172767) Reland licenses cpp switch 2 (flutter#172996) Roll Packages from d914120 to db6988d (2 revisions) (flutter#173039) Add a SliverList code sample (flutter#172986) fix: adjust scrollable size assertion with tolerance (flutter#171426) impeller: Shrink `Command` 40 bytes (flutter#173004) [web] Remove outdated comment about HTML renderer (flutter#172877) Roll Skia from ff4da49305c5 to 42e3bed42110 (1 revision) (flutter#173038) Roll Skia from 7d468a4868cb to ff4da49305c5 (3 revisions) (flutter#173028) Roll Skia from 951277895c86 to 7d468a4868cb (1 revision) (flutter#173027) Manual roll of Dart from 14ea8d342149 to 6832e04cf2f9 (flutter#173015) ... # Conflicts: # packages/flutter/lib/src/rendering/table_border.dart # packages/flutter/test/rendering/table_border_test.dart
…r#172995) It turns out iOS Safari in some cases tracks timers that are scheduled from within a `pointerdown` listener, and it delays the `click` event until those timers have expired (with a max waiting time of 350ms or so). The `ClickDebouncer` sets a timer of 200ms to see if a `click` event is received by then. But because of the Safari behavior explained above, the `click` event will always arrive right after the `ClickDebouncer`'s timer, so we always misattribute the `click` event. Fixes flutter#172180
When using VoiceOver, clicking the button through `ctrl+opt+space` causes the browser to send `pointerdown`, `pointerup` and `click` events successively within the same event loop. This case wasn't handled correct by the recent `ClickDebouncer` change here: #172995 More details: We currently wait until the end of the event loop to set the `ClickDebouncer`'s state. When other events arrive before the end of the event loop, they expect the `state` to already be set. The fix is to set the `state` immediately to allow events to be queued right away, but still keep the debouncing delayed until the end of the event loop so that Safari continues to work correctly (issue: #172180) Fixes #173741
When using VoiceOver, clicking the button through `ctrl+opt+space` causes the browser to send `pointerdown`, `pointerup` and `click` events successively within the same event loop. This case wasn't handled correct by the recent `ClickDebouncer` change here: flutter#172995 More details: We currently wait until the end of the event loop to set the `ClickDebouncer`'s state. When other events arrive before the end of the event loop, they expect the `state` to already be set. The fix is to set the `state` immediately to allow events to be queued right away, but still keep the debouncing delayed until the end of the event loop so that Safari continues to work correctly (issue: flutter#172180) Fixes flutter#173741
When using VoiceOver, clicking the button through `ctrl+opt+space` causes the browser to send `pointerdown`, `pointerup` and `click` events successively within the same event loop. This case wasn't handled correct by the recent `ClickDebouncer` change here: flutter#172995 More details: We currently wait until the end of the event loop to set the `ClickDebouncer`'s state. When other events arrive before the end of the event loop, they expect the `state` to already be set. The fix is to set the `state` immediately to allow events to be queued right away, but still keep the debouncing delayed until the end of the event loop so that Safari continues to work correctly (issue: flutter#172180) Fixes flutter#173741
…r#172995) It turns out iOS Safari in some cases tracks timers that are scheduled from within a `pointerdown` listener, and it delays the `click` event until those timers have expired (with a max waiting time of 350ms or so). The `ClickDebouncer` sets a timer of 200ms to see if a `click` event is received by then. But because of the Safari behavior explained above, the `click` event will always arrive right after the `ClickDebouncer`'s timer, so we always misattribute the `click` event. Fixes flutter#172180
When using VoiceOver, clicking the button through `ctrl+opt+space` causes the browser to send `pointerdown`, `pointerup` and `click` events successively within the same event loop. This case wasn't handled correct by the recent `ClickDebouncer` change here: flutter#172995 More details: We currently wait until the end of the event loop to set the `ClickDebouncer`'s state. When other events arrive before the end of the event loop, they expect the `state` to already be set. The fix is to set the `state` immediately to allow events to be queued right away, but still keep the debouncing delayed until the end of the event loop so that Safari continues to work correctly (issue: flutter#172180) Fixes flutter#173741
…r#172995) It turns out iOS Safari in some cases tracks timers that are scheduled from within a `pointerdown` listener, and it delays the `click` event until those timers have expired (with a max waiting time of 350ms or so). The `ClickDebouncer` sets a timer of 200ms to see if a `click` event is received by then. But because of the Safari behavior explained above, the `click` event will always arrive right after the `ClickDebouncer`'s timer, so we always misattribute the `click` event. Fixes flutter#172180
When using VoiceOver, clicking the button through `ctrl+opt+space` causes the browser to send `pointerdown`, `pointerup` and `click` events successively within the same event loop. This case wasn't handled correct by the recent `ClickDebouncer` change here: flutter#172995 More details: We currently wait until the end of the event loop to set the `ClickDebouncer`'s state. When other events arrive before the end of the event loop, they expect the `state` to already be set. The fix is to set the `state` immediately to allow events to be queued right away, but still keep the debouncing delayed until the end of the event loop so that Safari continues to work correctly (issue: flutter#172180) Fixes flutter#173741
When using VoiceOver, clicking the button through `ctrl+opt+space` causes the browser to send `pointerdown`, `pointerup` and `click` events successively within the same event loop. This case wasn't handled correct by the recent `ClickDebouncer` change here: flutter#172995 More details: We currently wait until the end of the event loop to set the `ClickDebouncer`'s state. When other events arrive before the end of the event loop, they expect the `state` to already be set. The fix is to set the `state` immediately to allow events to be queued right away, but still keep the debouncing delayed until the end of the event loop so that Safari continues to work correctly (issue: flutter#172180) Fixes flutter#173741
…r#172995) It turns out iOS Safari in some cases tracks timers that are scheduled from within a `pointerdown` listener, and it delays the `click` event until those timers have expired (with a max waiting time of 350ms or so). The `ClickDebouncer` sets a timer of 200ms to see if a `click` event is received by then. But because of the Safari behavior explained above, the `click` event will always arrive right after the `ClickDebouncer`'s timer, so we always misattribute the `click` event. Fixes flutter#172180
When using VoiceOver, clicking the button through `ctrl+opt+space` causes the browser to send `pointerdown`, `pointerup` and `click` events successively within the same event loop. This case wasn't handled correct by the recent `ClickDebouncer` change here: flutter#172995 More details: We currently wait until the end of the event loop to set the `ClickDebouncer`'s state. When other events arrive before the end of the event loop, they expect the `state` to already be set. The fix is to set the `state` immediately to allow events to be queued right away, but still keep the debouncing delayed until the end of the event loop so that Safari continues to work correctly (issue: flutter#172180) Fixes flutter#173741
It turns out iOS Safari in some cases tracks timers that are scheduled from within a
pointerdownlistener, and it delays theclickevent until those timers have expired (with a max waiting time of 350ms or so).The
ClickDebouncersets a timer of 200ms to see if aclickevent is received by then. But because of the Safari behavior explained above, theclickevent will always arrive right after theClickDebouncer's timer, so we always misattribute theclickevent.Fixes #172180