Repository navigation
fix(Switch): a mouse or pen press released away from the switch never toggles it - #9686
Conversation
… toggles it Main's accessibility contract failed `switch.state.survives-an-aborted-press` (WCAG 2.5.2 Pointer Cancellation) in five states. A mouse press held, slid 200 px away and released turned the switch on: the slide passed the flip distance, and FR15 committed a flipped drag wherever it was released. Now a mouse's or a pen's release outside the switch's tracking bounds, its box grown by UIControl's 70 px tracking expansion, leaves the switch alone, flipped or not, as the native checkbox does. A flipped drag released inside those bounds still commits. A finger keeps FR15's slide-off toggle (spec:AST-020 FR9, DEC-5). FR15 and the unreleased every-pointer changeset say so, and FR15 now states how a finger's slide-off toggle meets WCAG 2.5.2 (Abort or Undo). Red first: the Switch accessibility contract fails in 5 of 12 states on main and passes in all 12. Two new SwitchPointer cases, a mouse and a pen drag that flip, slide off and are released 200 px away, fail on main and pass.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report🔎 Preview ·
|
| Component | Size | LoC | Visual · A11y · RTL |
|---|---|---|---|
| Switch · Storybook | 86.6 kB (+9 B) · 10.5 kB CSS (0) | 1,780 (+3) | ✓ · ✓ · ✓ |
Package size: @astryxdesign/core 6.00 MB packed (+133 B); no new dependencies.
Touched components checked; add ci:run-visual or ci:run-rtl to run the full suites.
Generated by PR Enrichment workflow | View full report
|
/approve-spec dbd6e21 |
There was a problem hiding this comment.
This looks ready to approve: a mouse or pen press released outside the switch's tracking bounds now leaves Switch alone, flipped or not, which restores WCAG 2.5.2 pointer cancellation, and the FR15 change carries an engineering owner's /approve-spec for this head. The Switch accessibility contract fails in five states without the fix and passes in all twelve with it, and the two new mouse and pen cases fail without the fix and pass with it. Two optional wording suggestions are inline: FR15's "sliding back" phrasing, which only aborts a drag that flipped, and the passive voice in the new comment.
[Automated review]
| | FR12 | `ref`, `onFocus`, and `onBlur` reach the input, and a caller `aria-labelledby` and `aria-describedby` name and describe it. Other base props (`id`, `data-*`, other ARIA, `onClick`, and other handlers), `className`, `style`, `xstyle`, and `width` reach the field root. Caller ARIA attributes name and describe the switch, so they MUST reach the input (`architecture:public-component-api` INV5). Implementation gap: ARIA other than `aria-labelledby` and `aria-describedby`, such as `aria-label`, lands on the field root. | Current tests; `architecture:public-component-api` INV5 | | ||
| | FR13 | The thumb is one circle under any pointer, at each state's size: a touch presentation moves and lifts it and never reshapes it, and its lift and rubber band derive from the box it renders. | Owner direction, 2026-10-26 | | ||
| | FR14 | Under a finger the thumb, lifted or banded past either end, paints inside the track, in either writing direction. | Owner direction, 2026-10-25 | | ||
| | FR15 | For every pointer (`spec:AST-094` FR43) a tap, a hold of any length or a short drag toggles the switch at the release (a tap through the browser's click; where the browser sends no click for a finger's release, as for a hold, the switch clicks its input itself), and a drag commits the side it flipped the switch to, which it reaches once the pointer is the track width less the thumb width from where it pressed, toward the other end, and leaves again once the pointer comes back past where it pressed (measured in `motion/uikit/switch.ts`: `travel`, `flipDistance`). A finger's drag commits wherever the finger lifts; a mouse's or a pen's commits when it is released inside the switch's tracking bounds, its box grown by UIControl's 70 px tracking expansion. Under a finger a press slid off and lifted outside without a flip toggles it too, as `spec:AST-094` FR11 allows for a toggle under `spec:AST-020` FR9's slide-off exception (DEC-5); a mouse or a pen released outside those bounds leaves it alone, flipped or not, as the native checkbox does. Under a finger the thumb follows the drag as UISwitch's does (iOS 26.2); under a fine primary pointer the switch keeps its own look, and the value moves its thumb (`spec:AST-094` FR13). The switch's own click, for a hold and for a flipping drag, is the exception `spec:AST-094` FR11 states for Switch. Under reduced motion nothing lifts or springs: a tap, a release or a change cross-fades the thumb to its rest and the track colour with it, while a dragging finger still moves the thumb. A finger's slide-off toggle (`spec:AST-020` FR9, DEC-5; `spec:AST-094` FR11) meets WCAG 2.5.2 Pointer Cancellation through its Abort or Undo option: sliding back, or toggling again, undoes it. A mouse or pen released outside the tracking bounds cancels the press. | `spec:AST-094` FR11, FR13, FR43; `spec:AST-020` FR9/DEC-5; `design:reduced-motion`; WCAG 2.5.2 | |
There was a problem hiding this comment.
Optional wording: for a finger press slid off without a flip, sliding back does not cancel it (commitOnRelease returns the other side whenever nothing flipped, and a lift back on the switch goes through the browser's click), so only toggling again undoes it. Sliding back aborts only a drag that flipped. A possible rewrite: "toggling again undoes it, and sliding back also aborts a drag that flipped".
| // A mouse or a pen released outside the tracking bounds, flipped or | ||
| // not, leaves the switch alone, as the native checkbox does. So the | ||
| // press can be taken back (WCAG 2.5.2). Only a finger's press slid | ||
| // off toggles. |
There was a problem hiding this comment.
Optional (comment style): "So the press can be taken back" is passive. An active version: "A mouse or pen release outside the tracking bounds cancels the press, flipped or not, as the native checkbox does. So the person can take back a misplaced press. Only a finger's slide-off toggles."
|
Local evidence for the release-only accessibility contract is in the description: |
User impact
With a mouse or a pen, a press on a Switch that the user slides away and releases elsewhere must leave it alone (WCAG 2.5.2 Pointer Cancellation). Since #9636, a slide that passed the flip distance turned the switch on even when released far away, so a misplaced press could not be taken back. Main's release-check accessibility contract fails on it in five states.
Problem and solution fit
switch.state.survives-an-aborted-pressblocks the release.spec:AST-020FR9, DEC-5); the contract does not exercise a finger.Expected behavior and authority
spec:AST-021switch pattern,switch.state.survives-an-aborted-press(WCAG 2.2 2.5.2, required), unchanged.component:SwitchFR15, changed here: "wherever the pointer is released" now applies to a finger, and a mouse or a pen released outside the tracking bounds leaves the switch alone. FR15 also states how a finger's slide-off toggle meets WCAG 2.5.2: through its Abort or Undo option.Switch.spec.mdis owner-gated, so this needs an engineering owner's/approve-spec <head sha>.Smallest restoration
One condition in
useSwitchTouch.ts's lift branch: a mouse or pentouchUpOutsideabandons the press, flipped or not. FR15 and the unreleased every-pointer changeset say so. No new API, constant, listener or click guard.Evidence
Switch.a11y.chromium.spec.tson main: 5 failed, 7 passed. The off, described, label-hidden, required and invalid states report "pressing the switch and releasing away from it still turned it on".pnpm exec playwright test packages/core/src/Switch/__tests__/Switch.a11y.chromium.spec.ts, local runsWith main's Switch code (Storybook built at e6c1eda, #9636's head;
Switch/, the press tracker and the click guard are unchanged on main 322c0d6):With this fix, at a08d86ed1c (the head, dbd6e21, adds only record text), twice:
Scope
Writing rubric results
Switch.spec.md(FR15): R1–R6: none. FR15's last sentence, on the finger and WCAG 2.5.2, is the owner's wording.Testing
Switch.a11y.chromium.spec.ts(the switch accessibility contract)check:knowledge--cpus=4, CI, 1 worker, both Switch browser specs ×10pnpm test:a11y-contract), locally