Skip to content

fix(form): keep Toggle, Checkbox and RadioGroup clickable inside Sheet - #3

Merged
MiniCodeMonkey merged 2 commits into
mainfrom
fix/swipe-ignore-role-controls
Sep 29, 2026
Merged

MiniCodeMonkey merged 2 commits into
mainfrom
fix/swipe-ignore-role-controls

Conversation

@MiniCodeMonkey

Copy link
Copy Markdown
Member

Symptom

Toggle and Checkbox placed inside a Sheet (a Base UI Drawer with
swipe-to-dismiss) silently ignore a mouse click. onCheckedChange never
fires and nothing errors. RadioGroup has the same problem.

Root cause

Switch.Root, Checkbox.Root and Radio.Root render as
<span role="switch"|"checkbox"|"radio">, not a native form element.
Base UI's swipe-to-dismiss gesture only treats
button,a,input,select,textarea,label,[role="button"] as interactive by
default (@base-ui/react/utils/useSwipeDismiss.mjs, DEFAULT_IGNORE_SELECTOR
at line 16). Everything else is fair game for the drag gesture.

A mouse press on one of these spans inside an open Sheet therefore starts
the drawer's swipe gesture instead of the control's own click handling.
DrawerViewport.mjs's pointerdown handling calls setPointerCapture on the
popup element (around line 383 of useSwipeDismiss.mjs, gated by
isSwipeIgnoredTarget in DrawerViewport.mjs), which retargets the
eventual click away from the control before onCheckedChange ever runs.

jsdom has no pointer capture implementation, so none of the existing unit
tests reproduce this -- it only shows up in a real browser.

Base UI's own opt-out is the data-base-ui-swipe-ignore attribute
(internals/constants.mjs; a legacy data-swipe-ignore also exists), which
isSwipeIgnoredTarget checks via target.closest(...).

Fix

  • Toggle.tsx: data-base-ui-swipe-ignore on Switch.Root.
  • Checkbox.tsx: data-base-ui-swipe-ignore on Checkbox.Root.
  • RadioGroup.tsx: data-base-ui-swipe-ignore on each Radio.Root option.

Checked the rest of the package for other non-native role-span controls that
could sit in a Sheet: Select.Trigger and Combobox.Input render a real
<button>/<input> (already covered by Base UI's default ignore list), and
Menu's CheckboxItem/RadioItem popups portal to <body>, outside the
Sheet's own DOM subtree, so they're never a descendant the swipe gesture can
capture regardless of role. No Slider or Tabs component exists in this
package yet.

Each attribute is added as one of the component's own controlled props,
spread after ...rest, keeping the existing rule (see Toggle.tsx's doc
comment): rest props are spread before the component's own controlled props,
so a caller can't silently clobber them.

Tests

  • src/form/swipeIgnore.test.tsx (unit, jsdom-safe): asserts the attribute
    renders on Toggle's root, Checkbox's root, and every RadioGroup option.
  • playground/tests/sheet.spec.ts: a new Playwright test opens the right
    Sheet (now carrying a Toggle in its body), clicks it, and asserts
    onCheckedChange fired and aria-checked flipped. Verified this test
    fails without the fix (the toggle stays unchecked, the counter stays at
    0) and passes with it.

No changelog file exists in this repo, and past fix PRs (4e25deb,
c08a19b, 60accca) didn't bump package.json -- version bumps happen in
separate release: x.y.z commits -- so this PR doesn't touch the version.
playground/package-lock.json picks up an unrelated pre-existing drift
(root package.json was already at 0.3.1, the lockfile still said
0.3.0), refreshed in its own commit matching the repo's existing
chore(playground): refresh the lockfile pattern.

Test plan

  • npm run lint
  • npm run typecheck
  • npm run build
  • npm test (vitest, includes the new unit test)
  • cd playground && npx playwright test tests/sheet.spec.ts tests/choice.spec.ts (36 passed, includes the new browser test)
  • Confirmed the new Playwright test fails without the fix and passes with it

https://claude.ai/code/session_01MThGUPSpK3FBPz62WLoVuW

…e, Checkbox and RadioGroup

Switch.Root, Checkbox.Root and Radio.Root render as <span role="...">
rather than a native control. Base UI's swipe-to-dismiss gesture
(Sheet/Dialog) only skips button,a,input,select,textarea,label,
[role="button"] by default (useSwipeDismiss.mjs), so a mouse press on
one of these controls inside an open Sheet started the drawer's swipe
gesture instead, which calls setPointerCapture on the popup
(DrawerViewport.mjs) and retargets the click away -- onCheckedChange
never fired, with no error. jsdom has no pointer capture, so this
never reproduced in the existing unit tests.

Each affected root now sets data-base-ui-swipe-ignore, Base UI's own
opt-out for this. Adds a unit test asserting the attribute is present,
and a Playwright test that clicks a Toggle inside an open Sheet and
asserts onCheckedChange fired -- verified it fails without the fix and
passes with it.

Claude-Session: https://claude.ai/code/session_01MThGUPSpK3FBPz62WLoVuW
@MiniCodeMonkey
MiniCodeMonkey merged commit aadbaf3 into main Sep 29, 2026
1 check passed
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.

1 participant