Unhandled cancel events leave widgets stuck in a pressed state
Aliases: stuck pressed state · leftover highlight · unhandled cancel
What it is
A control that clears its pressed state only on end will, on cancel, leave highlight, scale, a drag ghost, or an “sending…” caption on screen. The user is no longer touching; the interface still looks held. That is not a skin bug. The state machine is missing a termination edge: begin pushed the control into a candidate, and nothing symmetric tears the candidate down.
Why it happens
A press typically mutates three places: drawing (a darker fill), an internal flag (isPressed), and maybe a timer or transaction (long-press clock, scroll capture, a request being prepared). End retracts all three. When cancel arrives and the handler is empty or falls through as “unknown type,” none of the three move. The next real begin then finds the old flag still true, producing a double highlight, a tap on empty space that fires the old button, or a drag that accumulates a ghost displacement. More quietly, a scroll container that captured the touch still believes it is tracking a finger and swallows later unrelated contacts.
Studying it
Run a paired implementation: the same button handles only end versus routing end and cancel through one cleanup function. Insert a system interruption while held, photograph whether the pressed highlight remains, then immediately tap elsewhere and see whether the old action still fires. Dependent measures are residue duration, false-trigger count, and whether a scroll container can hand later contacts to other controls. Independent variables include control type (momentary button, toggle, slider) and whether the interruption landed before or after the long-press threshold. Do not rely on the pixels alone; an internal flag can linger for a frame that looks fine.
Where it stops holding
Some declarative frameworks force a redraw on window deactivation, so “we never handled cancel and it still recovered” is the framework tearing down pressed state for you. That does not transfer to a custom canvas control or a game HUD. Handling cancel also does not license a commit: the cancel path must forbid side effects. If the app already fired an irrevocable request on begin, perfect cleanup cannot undo it—that is a misplaced commit point, not a missed event. Accessibility simulated clicks usually never see a touch cancel, so finger-only tests miss analogous stuck states on switches and keyboards.
Applying it
- Extract one function that clears pressed drawing, internal flags, and in-flight timers; call it from both end and cancel, and forbid submit on the cancel branch.
- Add a QA step: hold Pay or Send, pull the notification shade, and check that highlight, copy, and network activity all revert.
- Unit-test custom drawing and the Web
touchcancel/pointercancelpath, asserting the pressed flag is false and no click callback ran.
Related
- Same group: C2.16.1 Touch sequences are modeled as begin, move, end, and cancel · C2.16.2 System interruptions inject cancel into an in-flight touch sequence · C2.16.4 Each multitouch contact has an independent begin-move-end lifecycle
- Nearby: C2.07 Touch-down and touch-up activation · C3.03 Long press
- Search terms:
pressed state·pointercancel·touchcancel