Skip to content

fix(Switch): a mouse or pen press released away from the switch never toggles it - #9686

Merged
cixzhang merged 1 commit into
meta:mainfrom
vjeux:switch/aborted-press-fix
Nov 3, 2026
Merged

cixzhang merged 1 commit into
meta:mainfrom
vjeux:switch/aborted-press-fix

Conversation

@vjeux

@vjeux vjeux commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

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

  • Why it occurs: Switch FR15 committed a flipped drag wherever the pointer was released.
  • Why it harms the task: sliding away no longer cancels a press, so a misplaced press changes a setting.
  • Why that matters: WCAG 2.5.2 is Level A, and the required contract switch.state.survives-an-aborted-press blocks the release.
  • Fix: a mouse's or a pen's release outside the switch's tracking bounds (its box grown by UIControl's 70 px tracking expansion) commits nothing, flipped or not, as the native checkbox does. A flipped drag released inside those bounds, such as onto its label, still commits. A finger keeps FR15's slide-off toggle (spec:AST-020 FR9, DEC-5); the contract does not exercise a finger.

Expected behavior and authority

  • spec:AST-021 switch pattern, switch.state.survives-an-aborted-press (WCAG 2.2 2.5.2, required), unchanged.
  • component:Switch FR15, 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.md is 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 pen touchUpOutside abandons the press, flipped or not. FR15 and the unreleased every-pointer changeset say so. No new API, constant, listener or click guard.

Evidence

  • Before: Switch.a11y.chromium.spec.ts on 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".
  • After: 12/12 pass, with the contract untouched.
  • Unchanged paths: a mouse or pen drag that flips and is released just off the switch, inside its tracking bounds, commits; a press slid off without a flip leaves it alone; a click toggles once; a finger's slide-off and flipped drag still toggle. SwitchPointer: 14/14 in Chromium, 11 pass in WebKit (3 Chromium-only skips).
  • Regression tests: two new SwitchPointer cases, a mouse and a pen drag that flip, slide off and are released 200 px away, fail on main (the switch turns on) and pass.
pnpm exec playwright test packages/core/src/Switch/__tests__/Switch.a11y.chromium.spec.ts, local runs

With main's Switch code (Storybook built at e6c1eda, #9636's head; Switch/, the press tracker and the click guard are unchanged on main 322c0d6):

  1) [chromium] › Switch.a11y.chromium.spec.ts:177:3 › Switch [off] — the default: a labelled switch that is off and can be turned on
  2) [chromium] › Switch.a11y.chromium.spec.ts:177:3 › Switch [described] — a switch with supporting text under its label
  3) [chromium] › Switch.a11y.chromium.spec.ts:177:3 › Switch [label-hidden] — a switch whose label is read by assistive technology but not rendered visibly
  4) [chromium] › Switch.a11y.chromium.spec.ts:177:3 › Switch [required] — a switch that has to be on before the form can be submitted
  5) [chromium] › Switch.a11y.chromium.spec.ts:177:3 › Switch [invalid] — a required switch reporting an error, with the message attached
     pressing the switch and releasing away from it still turned it on: the change happens on the way down, so a press cannot be taken back
  5 failed
  7 passed (3.4m)

With this fix, at a08d86ed1c (the head, dbd6e21, adds only record text), twice:

  12 passed (1.8m)
  12 passed (1.7m)

Scope

  • One defect is restored.
  • Separable contradictory or unsettled tagalongs were removed or split.
  • No new public API, visual representation, default, or interaction model is hidden under the bug-fix label.
  • Public text and artifacts contain no internal Meta context.

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

Checks Result
Switch.a11y.chromium.spec.ts (the switch accessibility contract) 12/12
SwitchPointer, Chromium and WebKit 14/14; 11 pass, 3 Chromium-only skips
Strict ESLint on touched files, tsc, Prettier, check:knowledge Pass
Build, typecheck, strict lint, i18n runtime, knowledge, changesets; private-attribute baseline Pass; strict lint 0 errors
Provenance and barrel baselines Differ only in entries from main; the touched files have no findings
Unit tests, ui and node projects ui: 18,668 pass. node: 16,045 pass
Chromium on 2 pinned cores, 1 worker, frame proofs ×5 40/40
Chromium on 2 pinned cores, 4 workers, both Switch browser specs ×10 554/580; every red is a frame proof, report-only at this load, as on main
WebKit --cpus=4, CI, 1 worker, both Switch browser specs ×10 470 pass, 0 red; the cases that skip are Chromium-only (screencast frame proofs and the engine pen)
The whole accessibility contract suite (pnpm test:a11y-contract), locally 1,884 pass, Switch's included. The 36 that fail are local-only: 32 capture specs need the source stamp the CI Storybook build carries, and 4 AspectRatio cases wait on picsum.photos, which this machine cannot reach. On main after the merge, the release-check runs Switch's contract 12/12

… 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.
@vercel

vercel Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Nov 3, 2026 3:24am UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Nov 3, 2026
@astracat-bot

astracat-bot Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 3, 2026, 3:33 AM UTC

@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · dbd6e21

Storybook · Sandbox · built from this PR's head

Modified components

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

@cixzhang

cixzhang commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

/approve-spec dbd6e21

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 3, 2026
@cixzhang cixzhang added ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) ci:run-a11y Run the accessibility suite on this PR; write access, next push (AST-030 FR12) labels Nov 3, 2026
@cixzhang
cixzhang marked this pull request as draft November 3, 2026 03:20
@cixzhang
cixzhang marked this pull request as ready for review November 3, 2026 03:20

@astracat-bot astracat-bot Bot left a comment

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.

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 |

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.

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".

Comment on lines +622 to +625
// 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.

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.

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."

@vjeux

vjeux commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Local evidence for the release-only accessibility contract is in the description: Switch.a11y.chromium.spec.ts fails 5 states (off, described, label-hidden, required, invalid) and passes 7 with main's Switch code, and passes 12/12 with this fix. The whole contract suite is still running; I'll add its result there.

@cixzhang
cixzhang merged commit 6690e33 into meta:main Nov 3, 2026
104 of 109 checks passed
@vjeux
vjeux deleted the switch/aborted-press-fix branch November 3, 2026 03:52

This branch was successfully deployed

1 active deployment
Preview — dbd6e219 Deployed Nov 3, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) ci:run-a11y Run the accessibility suite on this PR; write access, next push (AST-030 FR12) CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants