Skip to content

[web] Cancel touch pointers WebKit abandons mid-gesture - #189608

Merged
auto-submit[bot] merged 11 commits into
flutter:masterfrom
flutter-zl:issue_188781_fix
Jul 24, 2026
Merged

auto-submit[bot] merged 11 commits into
flutter:masterfrom
flutter-zl:issue_188781_fix

Conversation

@flutter-zl

Copy link
Copy Markdown
Contributor

Fixes #188781

Problem
On iOS 27, WebKit stops dispatching pointer events for a touch it promotes to a native gesture, such as dragging the caret in a text field, and delivers neither pointerup nor pointercancel. The framework is left holding a pointer that never lifts, which wedges BaseTapAndDragGestureRecognizer so every later tap is silently dropped: the field never regains focus and the keyboard never returns. Affects every iOS 27 browser, since they all run WebKit; iOS 26 is unaffected.

Fix
_PointerAdapter tracks which touch devices are down and, on touchend and touchcancel, cancels any the browser no longer lists in touches. Gated to iOSWebKit.

Demo
Before: https://flutter-demo-47-before.web.app
After: https://flutter-demo-47-after.web.app

Repro steps:
type in the field, long-press the caret and drag it, lift, then tap the field.
Before, the field is permanently dead after a few tries.
After, it recovers.

@github-actions github-actions Bot added engine flutter/engine related. See also e: labels. platform-web Web applications specifically team-web Owned by Web platform team labels Jul 16, 2026
@flutter-zl flutter-zl added the CICD Run CI/CD label Jul 16, 2026
@flutter-zl
flutter-zl marked this pull request as ready for review July 16, 2026 19:15

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

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 implements a safety mechanism for iOS WebKit to handle abandoned touch pointers that fail to dispatch 'pointerup' or 'pointercancel' events. It introduces a tracking set for active touch devices and a reconciliation method that identifies and cancels stale pointers by comparing them against the browser's 'touches' list. The changes include updates to 'dom.dart' for touch list access, 'pointer_binding.dart' for tracking and cancellation logic, and comprehensive regression tests in 'pointer_binding_test.dart'. The reviewer suggested improving the robustness of the tests by explicitly asserting both 'cancel' and 'remove' events.

Comment thread engine/src/flutter/lib/web_ui/test/engine/pointer_binding_test.dart
void _removePointerIfUnhoverable(DomPointerEvent event) {
if (event.pointerType == 'touch') {
_sanitizers.remove(event.pointerId);
_downTouchDevices.remove(_getPointerId(event));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should getPointerId(event) just be event.pointerId like the line above?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about it more maybe the line above should become _sanitizers.remove(_getPointerId(event)).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. updated.


// The abandoned touch is reconciled while debouncing is active. Its cancel
// must bypass the queue and reach the framework immediately.
rootElement.dispatchEvent(_createTouchEvent('touchend', <int>[3], remaining: <int>[300]));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we define a touch for device 300 in this test.

Maybe the above event

tappable.dispatchEvent(context.primaryDown(clientX: 300, clientY: 300));

should be changed, defining a device id and making it a touch event.

tappable.dispatchEvent(
  context.multiTouchDown(const <_TouchDetails>[
    _TouchDetails(pointer: 5, clientX: 300, clientY: 300),
  ]).single,
);

and this line can become

rootElement.dispatchEvent(_createTouchEvent('touchend', <int>[3], remaining: <int>[5]));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. updated.

/// For synthetic events that must not be queued or dropped by an in-progress
/// debounce, such as cancels that repair a pointer the browser abandoned.
void sendImmediately(List<ui.PointerData> data) {
_sendToFramework(null, data);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we flush the events currently in the queue during debouncing here before forwarding to framework?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the debouncing case, if there is a down event queued and we immediately dispatch the cancel/touchend event to the framework, then the queued down event may come after that and put the framework in a stuck state again.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. updated.

@Renzo-Olivares Renzo-Olivares left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall the approach looks good to me, just had a few small comments.

@Renzo-Olivares Renzo-Olivares left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks for the fix!

@flutter-zl flutter-zl added the autosubmit Merge PR when tree becomes green via auto submit App label Jul 23, 2026
@auto-submit
auto-submit Bot added this pull request to the merge queue Jul 23, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to no response for status checks Jul 23, 2026
@flutter-dashboard flutter-dashboard Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Jul 23, 2026
@flutter-zl flutter-zl added the autosubmit Merge PR when tree becomes green via auto submit App label Jul 23, 2026
@auto-submit auto-submit Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Jul 23, 2026
@auto-submit

auto-submit Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

autosubmit label was removed for flutter/flutter/189608, because - The status or check suite Dashboard Checks has failed. Please fix the issues identified (or deflake) before re-applying this label.

@flutter-zl flutter-zl added the autosubmit Merge PR when tree becomes green via auto submit App label Jul 24, 2026
@auto-submit
auto-submit Bot added this pull request to the merge queue Jul 24, 2026
Merged via the queue into flutter:master with commit b4f7989 Jul 24, 2026
20 checks passed
@flutter-dashboard flutter-dashboard Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Jul 24, 2026
auto-submit Bot pushed a commit to flutter/packages that referenced this pull request Jul 25, 2026
flutter/flutter@b65f4d9...e292197

2026-07-25 [email protected] Roll Skia from 2c014dc494f1 to 62442d6cf0ec (1 revision) (flutter/flutter#190022)
2026-07-25 [email protected] Roll Skia from d8d20eef4dbd to 2c014dc494f1 (1 revision) (flutter/flutter#190020)
2026-07-25 [email protected] [Impeller] Add 2D texture array support (kTexture2DArray) (flutter/flutter#188778)
2026-07-24 [email protected] Bumped Engine AGP Dependencies to 9.1.0 for 3.50 (flutter/flutter#189417)
2026-07-24 [email protected] Roll Dart SDK from 153abd9a8296 to 28e63ac22d8d (2 revisions) (flutter/flutter#190013)
2026-07-24 [email protected] ci: add mac verify binaries workflow (flutter/flutter#190010)
2026-07-24 [email protected] Roll Skia from f2f9b52fe1ee to d8d20eef4dbd (21 revisions) (flutter/flutter#190011)
2026-07-24 [email protected] fix(ci): quote labels to fix yaml mapping error (flutter/flutter#190002)
2026-07-24 [email protected] Update contributing guidelines for commit message format (flutter/flutter#189922)
2026-07-24 [email protected] Migrate a11y_assessments to uiscene (flutter/flutter#189960)
2026-07-24 [email protected] docs(contributing): add git worktree and rebase workflow guide (flutter/flutter#189992)
2026-07-24 [email protected] Roll Dart SDK from e3fc57eae9eb to 153abd9a8296 (3 revisions) (flutter/flutter#189987)
2026-07-24 [email protected] Rename RegularWindow* to Window* in the windowing API + rename sizedToContent to shrinkWrap in the windowing API (flutter/flutter#189363)
2026-07-24 [email protected] Remove ancient `tools:tools` dep from `android_sdk` bundle (flutter/flutter#189962)
2026-07-24 [email protected] Reduce the number of gtest-parallel workers when running Impeller tests on Mac Minis used by CI (flutter/flutter#189813)
2026-07-24 [email protected] Roll Packages from 8260a1e to 6969329 (21 revisions) (flutter/flutter#189983)
2026-07-24 [email protected] Roll Fuchsia Linux SDK from 9org0yL3yZkp80x5S... to Cdb1RGKCqpcz9z4cl... (flutter/flutter#189975)
2026-07-24 [email protected] Roll Skia from 6e9c4687c001 to f2f9b52fe1ee (3 revisions) (flutter/flutter#189959)
2026-07-24 [email protected] [web] Cancel touch pointers WebKit abandons mid-gesture (flutter/flutter#189608)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages
Please CC [email protected],[email protected] on the revert to ensure that a human
is aware of the problem.

To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
auto-submit Bot pushed a commit that referenced this pull request Jul 29, 2026
This pull request cherry-picks #189608 into the beta release branch (flutter-3.47-candidate.0) so the fix ships in the next stable release.

### Issue Link:
#188781

### Impact Description:
P0 regression. On iOS 27, which affects every browser since they all run WebKit, a text field in a Flutter web app can become permanently unresponsive after a common gesture. When the user long-press-drags the selection caret and
lifts, WebKit abandons the touch without delivering `pointerup` or `pointercancel`, leaving the framework holding a pointer that never lifts. This wedges the tap-and-drag gesture recognizer, so every later tap is silently dropped: the field never regains focus and the keyboard never returns, and the user can no longer type. It breaks text input in shipping production web apps. iOS 26 is unaffected.

### Changelog Description:
[flutter/188781] When dragging the text selection caret on iOS 27 web browsers, text fields could become permanently unresponsive to taps and never regain focus; they now recover.

### Workaround:
No in-app workaround. Reloading the page temporarily restores the field, but it breaks again the next time the gesture occurs.

### Risk:
The change is small and strictly gated to iOS WebKit. It only cancels touch pointers the browser has already dropped from its `touches` list, so it cannot affect other platforms or normal pointer handling.

### Test Coverage:
The fix adds 341 lines of engine unit tests in `pointer_binding_test.dart` covering the touch-cancel logic.

### Validation Steps:
1. Open a Flutter web app containing a TextField on an iOS 27 device, any browser.
2. Tap the field to focus it and type some text.
3. Long-press the selection caret, drag it, then lift your finger.
4. Tap the field again, and repeat a few times.
5. Before the fix the field becomes permanently dead: taps do nothing, no focus, no keyboard. After the fix the field keeps responding and recovers.

Demo before: https://flutter-demo-47-before.web.app
Demo after:  https://flutter-demo-47-after.web.app
@mdebbar mdebbar added the cp: stable cherry pick this pull request to stable release candidate branch label Sep 3, 2026
@flutteractionsbot

Copy link
Copy Markdown
Contributor

Failed to create CP due to merge conflicts.
You will need to create the PR manually. See the cherrypick wiki for more info.

@mdebbar mdebbar removed the cp: stable cherry pick this pull request to stable release candidate branch label Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD engine flutter/engine related. See also e: labels. platform-web Web applications specifically team-web Owned by Web platform team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Input box issue in Flutter Web under iOS 27 system

4 participants