fix: Cancel click on touch long-press menu#23596
Conversation
On Skia targets, long-pressing a control (e.g. a Button) with a ContextFlyout both opened the menu AND raised Click, instead of behaving like a right-tap. WinUI suppresses the click because opening the menu's popup steals the OS pointer capture (PointerCaptureLost); Uno's in-canvas popups don't, so the pressed state was never cleared. When a context menu is shown on a touch press-and-hold, release the pressing element's pointer capture and raise an unhandled PointerCaptureLost so the control clears its pressed state and does not click on release. fixes #22229 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WQEkhvePgnJMns2KhVNUxr
On touch devices the browser starts a text-selection gesture on long-press, firing a haptic buzz and selection/callout UI before the cancelable 'contextmenu' event - so preventing the context menu does not stop the buzz, and it conflicts with the app's own press-and-hold gestures (e.g. opening a ContextFlyout). Uno Skia draws all text and selection with Skia, so the browser selection is never useful. Disable it (and the long-press callout) on the app root via user-select/-webkit-touch-callout, and re-enable selection on the hidden IME input so text editing keeps working. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WQEkhvePgnJMns2KhVNUxr
| /// Releases the explicit pointer capture and raises an <em>unhandled</em> PointerCaptureLost so the | ||
| /// pressing control clears its pressed state. Used when a context menu is shown on a touch press-and-hold: | ||
| /// WinUI clears the pressed state via the OS pointer-capture-changed raised when the menu's popup steals | ||
| /// capture, but Uno's popups don't, so we do it explicitly (issue #22229). |
There was a problem hiding this comment.
Minor style (AGENTS.md): "Don't reference the current task, fix, or callers ('handles the case from issue #123') since those belong in the PR description and rot as the codebase evolves." Remove (issue #22229) from the prose; the [GitHubWorkItem] annotations on the tests are the right place for that cross-reference.
| /// capture, but Uno's popups don't, so we do it explicitly (issue #22229). | |
| /// Releases the explicit pointer capture and raises an <em>unhandled</em> PointerCaptureLost so the | |
| /// pressing control clears its pressed state. Used when a context menu is shown on a touch press-and-hold: | |
| /// WinUI clears the pressed state via the OS pointer-capture-changed raised when the menu's popup steals | |
| /// capture, but Uno's popups don't, so we do it explicitly. |
| /// gesture, raising PointerCaptureLost so the pressing element (e.g. a Button) clears its pressed | ||
| /// state and does not raise Click/Tapped on release (issue #22229). | ||
| /// </summary> | ||
| private void ReleaseContextMenuHoldingPointerCapture() |
There was a problem hiding this comment.
Same minor style issue — (issue #22229) in the doc comment body. Drop it; issue context lives in the commit message and PR description, not in code prose.
| private void ReleaseContextMenuHoldingPointerCapture() | |
| /// Releases the explicit pointer capture held for the touch pointer that triggered the holding | |
| /// gesture, raising PointerCaptureLost so the pressing element (e.g. a Button) clears its pressed | |
| /// state and does not raise Click/Tapped on release. |
| { | ||
| foreach (var capture in captures) | ||
| { | ||
| if (capture.Pointer.PointerId == _contextMenuOnHoldingPointerId |
There was a problem hiding this comment.
When SetContextMenuOnHoldingPointer is never called (e.g. a code path that calls RaiseContextRequestedEvent with isTouchInput: true but skips SetContextMenuOnHoldingPointer), _contextMenuOnHoldingPointerId is 0. Iterating all captures looking for pointer ID 0 is harmless in practice (no real pointer uses ID 0), but an explicit early-return makes the invariant clear:
| if (capture.Pointer.PointerId == _contextMenuOnHoldingPointerId | |
| private void ReleaseContextMenuHoldingPointerCapture() | |
| { | |
| if (_contextMenuOnHoldingPointerId == 0) | |
| { | |
| return; | |
| } | |
| if (PointerCapture.Any(out var captures)) |

On Skia targets, long-pressing a control (e.g. a Button) with a
ContextFlyout both opened the menu AND raised Click, instead of
behaving like a right-tap. WinUI suppresses the click because opening
the menu's popup steals the OS pointer capture (PointerCaptureLost);
Uno's in-canvas popups don't, so the pressed state was never cleared.
When a context menu is shown on a touch press-and-hold, release the
pressing element's pointer capture and raise an unhandled
PointerCaptureLost so the control clears its pressed state and does not
click on release.
fixes #22229
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01WQEkhvePgnJMns2KhVNUxr**GitHub Issue:** closes #
PR Type:
What changed? 🚀
PR Checklist ✅
Screenshots Compare Test Runresults.