Skip to content

fix: Cancel click on touch long-press menu#23596

Draft
MartinZikmund wants to merge 2 commits into
masterfrom
dev/mazi/longpress
Draft

fix: Cancel click on touch long-press menu#23596
MartinZikmund wants to merge 2 commits into
masterfrom
dev/mazi/longpress

Conversation

@MartinZikmund

Copy link
Copy Markdown
Member

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 ✅

MartinZikmund and others added 2 commits June 29, 2026 10:00
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
@github-actions github-actions Bot added the platform/wasm 🌐 Categorizes an issue or PR as relevant to the WebAssembly platform label Jun 29, 2026
@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MartinZikmund's task in 6m 51s —— View job


PR Review

  • Gather context and understand the request
  • Read changed files
  • Review ContextMenuProcessor.cs changes
  • Review UIElement.Pointers.cs changes
  • Review runtime tests (Given_ContextRequested.Injection.cs)
  • Review WASM CSS/TS changes
  • Post inline review comments
  • Post summary

Overall: ✅ Correct fix, 2 minor style nits and 1 defensive robustness suggestion.

The approach correctly mirrors WinUI's behavior: when a context popup steals OS pointer capture, WinUI gets a PointerCaptureLost that clears the pressed state. Uno's in-canvas popups don't steal capture, so this PR explicitly releases it. The Handled = false reset before calling ReleasePointerCapture is the critical detail — without it, PointerCaptureLost is filtered by the class handler and the pressed state never clears.

Key correctness verifications:

  • PointerCapture.Any() returns _actives.Values.ToList() — a clone — so calling ReleasePointerCapture inside the foreach loop in ReleaseContextMenuHoldingPointerCapture does not mutate the collection being iterated. ✅
  • captureTarget is correctly the ExplicitTarget (the element that explicitly captured the pointer, e.g. the Button), so ReleasePointerCapture fires PointerCaptureLost on the right element. ✅
  • For draggable elements (500 ms timer path), _contextMenuOnHoldingPointerId is stored before the timer fires, and the capture is only released if the context menu was actually shown (args.Handled). If the user lifts the finger during the delay, the capture is already gone from _actives and the loop silently does nothing. ✅
  • The WASM user-select: none is correctly scoped to html, body (the app root) and re-enabled only on the hidden IME <input>, so text editing continues to work. ✅

Findings (3 inline comments posted):

  • (issue #22229) references in method XML doc bodies — AGENTS.md style rule: issue cross-references belong in commit messages / [GitHubWorkItem] annotations, not in source prose.
  • ReleaseContextMenuHoldingPointerCapture should return early when _contextMenuOnHoldingPointerId == 0 (the default) to make the precondition explicit.

PR template note: "## PR Type:" and "## What changed? 🚀" sections are left empty. The issue is correctly referenced via fixes #22229 in the commit message, but the PR body should complete the template sections.

/// 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).

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.

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.

Suggested change
/// 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()

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.

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.

Suggested change
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

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.

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:

Suggested change
if (capture.Pointer.PointerId == _contextMenuOnHoldingPointerId
private void ReleaseContextMenuHoldingPointerCapture()
{
if (_contextMenuOnHoldingPointerId == 0)
{
return;
}
if (PointerCapture.Any(out var captures))

@unodevops

Copy link
Copy Markdown
Contributor

🤖 Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-23596/wasm-skia-net9/index.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

platform/wasm 🌐 Categorizes an issue or PR as relevant to the WebAssembly platform

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[WASM Skia] Long press clicks before opening context menu

2 participants