Skip to content

Fix action tracker hold getting permanently stuck on tablets#110

Open
luanzeba wants to merge 1 commit intomainfrom
fix/stuck-hold-state-on-pointer-cancel
Open

Fix action tracker hold getting permanently stuck on tablets#110
luanzeba wants to merge 1 commit intomainfrom
fix/stuck-hold-state-on-pointer-cancel

Conversation

@luanzeba
Copy link
Collaborator

@luanzeba luanzeba commented Mar 9, 2026

Got a report that sometimes on tablets, pressing and releasing an action button in hold mode leaves the button stuck in the "held" state (glowing ring + pulse). Only way out was refreshing the page.

Video of the bug:
https://github.com/user-attachments/assets/83bef037-a7b5-4614-8395-cf0a75685c08

What was happening

To be honest, I couldn't reproduce this bug on our tablet, but this is what the AI says:

Two things going on:

  1. pointercancel was being ignored for confirmed holds. The idea was that the browser might fire spurious cancels while the user is still holding, but on mobile pointercancel is actually a terminal event (no pointerup follows). So when the OS decides to cancel the touch (palm rejection, finger hitting the bezel, gesture takeover, etc.), the hold just never ends.

  2. handlePointerEnd was reading activePointers from its React state closure, but with rapid tapping the state could be stale (not yet re-rendered), so the handler would miss the pointer ID entirely and silently skip cleanup.

Fix

  • Treat pointercancel as a valid end event for confirmed holds (same as pointerup). Only pointerleave is still ignored.
  • Added a ref mirror of activePointers so event handlers always read the latest state regardless of render timing.
  • Wrapped releasePointerCapture in try/catch since it can throw after a pointercancel.

Two bugs caused the hold indicator to get permanently stuck on mobile tablets:

1. pointercancel was being ignored for confirmed holds, but on mobile
   touch devices it's a terminal event — no pointerup follows after it.
   When the OS/browser cancels the pointer (e.g. palm rejection, gesture
   takeover, finger sliding to bezel), the hold would never end.

2. handlePointerEnd read activePointers from its React state closure,
   but rapid pointer events could fire before React re-rendered with
   the updated Map, causing the handler to miss the pointer ID entirely.

Fixes:
- Treat pointercancel as a valid end event (same as pointerup) for
  confirmed holds. Only pointerleave is still ignored.
- Mirror activePointers in a ref (activePointersRef) so event handlers
  always see the latest state regardless of render timing.
- Wrap releasePointerCapture in try/catch since the capture may already
  be released after a pointercancel.
- Use updateActivePointers() helper everywhere to keep state and ref
  in sync.
- Remove activePointers from blur handler dependency array (reads from
  ref now) to avoid unnecessary effect re-subscriptions.
@github-actions
Copy link

github-actions bot commented Mar 9, 2026

PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://FRC2713.github.io/QRScout/pr-preview/pr-110/

Built to branch gh-pages at 2026-03-09 21:40 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@luanzeba luanzeba requested a review from tytremblay March 9, 2026 21:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants