Skip to content

fix(Combobox): defer handleBlur close to let FocusScope restore focus#2663

Merged
zernonia merged 1 commit into
unovue:v2from
benjamincanac:fix/combobox-blur-race-condition
May 29, 2026
Merged

fix(Combobox): defer handleBlur close to let FocusScope restore focus#2663
zernonia merged 1 commit into
unovue:v2from
benjamincanac:fix/combobox-blur-race-condition

Conversation

@benjamincanac

@benjamincanac benjamincanac commented May 27, 2026

Copy link
Copy Markdown
Contributor

🔗 Linked issue

Resolves nuxt/ui#6437, resolves nuxt/ui#6511

❓ Type of change

  • 🐞 Bug fix (a non-breaking change that fixes an issue)

📚 Description

When two ComboboxRoot instances are on the same page and the first is open, clicking the second's trigger causes it to open briefly then immediately close. This only happens with a quick click (short pointerdown→pointerup). Holding the mouse for ~200ms before releasing works fine.

Regression introduced in v2.9.3 (commit 9102386FocusScope.handleMutations null-guard removed an incidental focus-protection that previously masked this race condition).

Root cause: ComboboxInput.handleBlur fires before FocusScope's handleFocusOut (document focusout listener) has a chance to restore focus inside the scope. Since blur fires before focusout in DOM event order, handleBlur sees focus outside and synchronousl combobox, unmounting the FocusScope before it can act.

Fix: wrap handleBlur's close logic in a requestAnimationFrame that re-checks document.activeElement after giving FocusScope's focus-restoration mechanism a chance to run. If focus ends up back inside the combobox root or content, the close is skipped.

📝 Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed premature closing of the combobox popup during rapid focus transitions. The popup now correctly waits to confirm focus has definitively left the component before closing, improving behavior when switching quickly between comboboxes or when focus is managed by external tools.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

ComboboxInput's handleBlur now delays closing via requestAnimationFrame and re-checks document.activeElement after the frame; tests were added/updated to assert popup stays open when focus is restored inside before the deferred callback and closes when focus remains outside.

Changes

Combobox Deferred-Close Implementation and Tests

Layer / File(s) Summary
Deferred-close implementation in handleBlur
packages/core/src/Combobox/ComboboxInput.vue
handleBlur now delays close via requestAnimationFrame, re-checking document.activeElement after the frame to confirm focus is still outside both root and content before calling onOpenChange(false).
Test coverage for openOnFocus deferred-close behavior
packages/core/src/Combobox/Combobox.test.ts
Updated existing "close when focus moves outside" test to use deferred-close cycle (focus external button, wait for RAF + nextTick). New test verifies popup does not close when a synthetic blur reports an outside relatedTarget but focus is restored inside before the deferred close runs.
Test coverage for handleBlur deferred-close behavior
packages/core/src/Combobox/Combobox.test.ts
New "handleBlur with deferred close" suite with two tests: one verifies popup stays open when focus is restored inside before deferred close fires; the other verifies popup closes when focus remains outside after the deferred-close timing.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

  • unovue/reka-ui#2429: Related deferred-close and openOnFocus test adjustments for Combobox behavior.
  • unovue/reka-ui#2501: Overlapping changes to ComboboxInput.handleBlur and related tests.
  • unovue/reka-ui#2393: Related focus-restoration/FocusScope changes that interact with deferred blur behavior.

Poem

🐰 A blur that paused to watch a frame hop by,
If focus bounces home, the popup stays nigh.
If focus wanders out and the frame ticks through,
The menu folds soft — a quiet adieu.
Hooray for frames that give focus a try!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title directly describes the main fix: deferring handleBlur close to allow FocusScope to restore focus, which is the core change across both modified files (ComboboxInput.vue and test file).
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new

pkg-pr-new Bot commented May 27, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/reka-ui@2663

commit: 5601e74

When two ComboboxRoot instances are on the same page, clicking the second's
trigger while the first is open causes the second to open briefly then
immediately close. This happens because handleBlur fires before FocusScope's
focus-restoration mechanism can pull focus back inside.

Wrap handleBlur's close call in a requestAnimationFrame that re-checks
document.activeElement, giving FocusScope's handleFocusOut a chance to
restore focus first. If focus ends up back inside, the close is skipped.
@benjamincanac benjamincanac force-pushed the fix/combobox-blur-race-condition branch from 5125133 to 5601e74 Compare May 27, 2026 12:30
@zernonia zernonia merged commit 7e13d0a into unovue:v2 May 29, 2026
7 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

2 participants