Skip to content

fix(Combobox): open the trigger on a touch tap instead of on touch down - #2156

Open
xyrolle wants to merge 1 commit into
huntabyte:mainfrom
xyrolle:fix/combobox-trigger-touch-tap
Open

xyrolle wants to merge 1 commit into
huntabyte:mainfrom
xyrolle:fix/combobox-trigger-touch-tap

Conversation

@xyrolle

@xyrolle xyrolle commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

The problem

Combobox.Trigger toggles the list from pointerdown for every pointer type:

onpointerdown(e: BitsPointerEvent) {
	if (this.root.opts.disabled.current || !this.root.domContext) return;
	e.preventDefault();
	if (this.root.domContext.getActiveElement() !== this.root.inputNode) {
		this.root.inputNode?.focus();
	}
	this.root.toggleMenu();
}

On a touch device pointerdown fires the moment a finger lands, before the browser knows whether this is a tap or the start of a scroll. A finger that lands on the trigger while scrolling a form opens the list under it (and focuses the input). Scroll a page with a few comboboxes on a phone and they pop open as you go.

Select.Trigger already handles this — it bails on touch in onpointerdown ("prevent opening on touch down which can be triggered when scrolling on touch devices") and opens from onpointerup, which native panning does not deliver: once the browser takes the gesture as a scroll it fires pointercancel instead.

The fix

SelectComboTriggerState takes the same touch timing as the select trigger: pointerdown still prevents default for every pointer (that is what keeps focus on the input instead of the button) and still toggles for mouse and pen; for touch it returns, and a new onpointerup toggles on the tap. The focus-then-toggle body is shared by the keyboard, pointerdown and pointerup paths.

This aligns the touch timing only. The select trigger's other handler details (mouse-button / ctrl filtering, explicit capture release) belong to its own focus model and are not copied; the combobox's mouse, pen and input-focus behaviour is unchanged.

Tests

Two tests in combobox.browser.test.ts, under a new Touch block, driving the trigger with PointerEvents of pointerType: "touch":

  • should not open the trigger on touch down, which also fires when a scroll starts on it — after pointerdown the list is closed and open is false; after the pointercancel a scroll produces, still closed.
  • should open the trigger on a tap — closed after pointerdown, open with the input focused after pointerup.

Both assert the state after down and before cancel/up — closed, and the input not focused — so an implementation that opens or focuses on down and undoes it on cancel cannot pass. Both fail on unpatched main (the list is open after the pointerdown). pnpm -F tests test:browser --run src/tests/combobox passes on chromium and webkit (134 tests). Synthetic events: they pin the handler timing, not native panning or the software keyboard.

The combobox trigger opened the list from pointerdown for every pointer type, so a finger that landed on it while scrolling a form opened the list under it. It now mirrors the select trigger: touch down does nothing, and pointerup — which a scroll never delivers, the browser cancels the pointer instead — opens on the tap. Mouse still opens on press.
@xyrolle

xyrolle commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Run evidence (macOS, chromium + webkit via @vitest/browser + playwright, retry: 3 as configured; each block lists the per-file result line, the totals, and every failing test name after retries):

New tests against unpatched main (chromium, -t Touch)c2-red.log

  • ❯ |browser (chromium)| src/tests/combobox/combobox.browser.test.ts (67 tests | 2 failed | 65 skipped) 8476ms
  • Tests 2 failed | 65 skipped (67)
  • failing tests: Touch > should not open the trigger on touch down, which also fires when a scroll starts on it; Touch > should open the trigger on a tap

Branch: combobox, chromium + webkitc3.log

  • ✓ |browser (chromium)| src/tests/combobox/combobox.browser.test.ts (67 tests) 2426ms
  • ✓ |browser (webkit)| src/tests/combobox/combobox.browser.test.ts (67 tests) 3067ms
  • Tests 134 passed (134)

svelte-checkc2-check.log

  • svelte-check found 0 errors and 0 warnings

@changeset-bot

changeset-bot Bot commented Sep 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e1c6f61

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
bits-ui Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor
built with Refined Cloudflare Pages Action

⚡ Cloudflare Pages Deployment

Name Status Preview Last Commit
bits-ui ✅ Ready (View Log) Visit Preview e1c6f61

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