Skip to content

Combobox: open on focus even when the options have not arrived yet - #28

Merged
aicodebar merged 2 commits into
mainfrom
fix/combobox-opens-before-remote-options-arrive
Aug 10, 2026
Merged

Combobox: open on focus even when the options have not arrived yet#28
aicodebar merged 2 commits into
mainfrom
fix/combobox-opens-before-remote-options-arrive

Conversation

@aicodebar

Copy link
Copy Markdown
Collaborator

The bug

Combobox's focus handler was open = filtered.length > 0 — a click into the field opens the list only if options are already present, and nothing reopens it when they arrive later.

For a static options list that is invisible. For the remote-search shape, where a consumer replaces options with each debounced response, it is a race between the click and the first response. The user who loses it clicks into the field, sees nothing, and only typing or ArrowDown recovers.

How it was found

The consuming app's impersonation picker (flows.portal.docuhub.app, ImpersonateModal) passed its browser test locally on every run — the response wins the race by milliseconds there — and failed all three CI retries, where the runner is slow enough that the click reliably wins. Screenshots showed a focused combobox with no dropdown, options long since loaded.

The fix

@focus="open = true" — unconditional. The listbox itself is still gated on having options or an empty-message to show (v-if="open && (filtered.length > 0 || emptyMessage !== null)"), so an open flag over a truly empty, message-less list renders nothing; but when options land while the field is focused, they now appear.

The story that pins it

A race a fast machine never loses is a race a test on a fast machine never catches — so RemoteOptions manufactures it: options arrive 600 ms after mount, the play function clicks first, and the listbox must appear with no typing, no ArrowDown, no second click. Verified red on the previous handler by inverting the fix. It also asserts the open flag alone paints nothing while the list is still empty, and that the late-arriving list is live (ArrowDown+Enter picks from it).

One visible change besides the fix

A filter that matches nothing now shows the empty-message on refocus, where before the closed list hid it. No call site in the consuming app depends on the old behavior.

Verified

  • npx playwright test — 247 passed (smoke suite executes every play function)
  • npm run build (incl. verify:externals, verify:dev-warnings, verify:props), npm run typecheck, npm run lint (0 errors; the 7 warnings pre-exist in untouched files)

🤖 Generated with Claude Code

aicodebar and others added 2 commits August 10, 2026 04:53
The focus handler was `open = filtered.length > 0`, so a click into the field
opened the list only if options were already present — and nothing reopened it
when they arrived later. For a static list that is invisible. For the
remote-search shape, where a consumer replaces `options` with each debounced
response, it is a race between the click and the first response: the user who
loses it clicks into the field, sees nothing, and only typing or ArrowDown
recovers.

Found the expensive way, which is why the story pins the exact sequence. The
consuming app's impersonation picker passed its browser test locally on every
run — the response wins by milliseconds there — and failed all three CI
retries, where the runner is slow enough that the click reliably wins. A race
a fast machine can never lose is a race a test on a fast machine can never
catch, so the RemoteOptions story manufactures it: options arrive 600ms after
mount, the play function clicks first, and the listbox must appear with no
typing, no ArrowDown and no second click. It fails on the previous handler
(verified by inverting the fix) and also asserts the flag alone paints
nothing while the list is still empty and message-less.

Focus now sets the open flag unconditionally; the listbox itself is still
gated on having options or an empty-message to show. One visible change
besides the fix: a filter that matches nothing now shows the empty-message on
refocus, where before the closed list hid it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
verify:version refuses a package.json equal to an existing tag, and it is
right to: v1.19.0 is spent whether or not its release succeeded. The
CHANGELOG's Unreleased heading becomes the version for the same reason —
this repo's entries are named at PR time, not at release time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aicodebar
aicodebar merged commit 615c74d into main Aug 10, 2026
2 checks 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