diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts
index 0715ee888..6581adfb3 100644
--- a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts
+++ b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts
@@ -1796,6 +1796,104 @@ describe('Rendering', () => {
await click(getComboboxButton())
})
)
+
+ it(
+ 'should not auto-select when input was focused and blurred without typing',
+ // Prevents console warnings during test execution
+ suppressConsoleLogs(async () => {
+ const handleChange = jest.fn()
+ const data = [
+ { id: 1, name: 'alice', label: 'Alice' },
+ { id: 2, name: 'bob', label: 'Bob' },
+ ]
+
+ renderTemplate({
+ template: html`
+
+
+
+
+
+ {{ person.label }}
+
+
+
+ `,
+ setup: () => ({ data, value: ref(null), handleChange }),
+ })
+
+ const input = getComboboxInput()
+ expect(input).not.toBeNull()
+
+ // Simulate user focusing the input but not typing or selecting anything
+ await focus(input!)
+
+ // Then immediately blurring (e.g., clicking away or tabbing out)
+ await blur(input!)
+
+ // Assert that no value was selected on blur
+ // Regression check: ensures we don't auto-select the first option
+ expect(handleChange).not.toHaveBeenCalled()
+ })
+ )
+
+ it(
+ 'should not auto-select the active option when the combobox input is blurred',
+ suppressConsoleLogs(async () => {
+ let handleChange = jest.fn()
+ let data = [
+ { id: 1, name: 'alice', label: 'Alice' },
+ { id: 2, name: 'bob', label: 'Bob' },
+ { id: 3, name: 'charlie', label: 'Charlie' },
+ ]
+
+ renderTemplate({
+ template: html`
+
+
+
+
+
+ {{ person.label }}
+
+
+
+ `,
+ setup: () => ({
+ data,
+ value: ref(null),
+ handleChange,
+ }),
+ })
+
+ // Verify initial state - no selection
+ expect(handleChange).not.toHaveBeenCalled()
+
+ // Open the combobox
+ await click(getComboboxButton())
+
+ // Verify it is open and first option is active
+ assertComboboxList({ state: ComboboxState.Visible })
+ let options = getComboboxOptions()
+ assertActiveComboboxOption(options[0])
+
+ // Verify no option is selected yet
+ assertNoSelectedComboboxOption()
+
+ // Blur the combobox input (this is where the bug occurs)
+ await blur(getComboboxInput())
+
+ // Verify it is closed
+ assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
+
+ // The value should still be null and handleChange should not have been called
+ expect(handleChange).not.toHaveBeenCalled()
+
+ // If we open the combobox again, there should still be no selected option
+ await click(getComboboxButton())
+ assertNoSelectedComboboxOption()
+ })
+ )
})
describe('Rendering composition', () => {
diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.ts b/packages/@headlessui-vue/src/components/combobox/combobox.ts
index b5149f271..90b67de9b 100644
--- a/packages/@headlessui-vue/src/components/combobox/combobox.ts
+++ b/packages/@headlessui-vue/src/components/combobox/combobox.ts
@@ -1215,38 +1215,35 @@ export let ComboboxInput = defineComponent({
function handleBlur(event: FocusEvent) {
let relatedTarget =
(event.relatedTarget as HTMLElement) ?? history.find((x) => x !== event.currentTarget)
- isTyping.value = false
- // Focus is moved into the list, we don't want to close yet.
- if (dom(api.optionsRef)?.contains(relatedTarget)) {
- return
- }
+ isTyping.value = false
- if (dom(api.buttonRef)?.contains(relatedTarget)) {
- return
- }
+ // Preserve dropdown if focus moved within combobox (e.g., to options or button).
+ if (dom(api.optionsRef)?.contains(relatedTarget)) return
+ if (dom(api.buttonRef)?.contains(relatedTarget)) return
if (api.comboboxState.value !== ComboboxStates.Open) return
event.preventDefault()
if (api.mode.value === ValueMode.Single) {
- // We want to clear the value when the user presses escape if and only if the current
- // value is not set (aka, they didn't select anything yet, or they cleared the input which
- // caused the value to be set to `null`). If the current value is set, then we want to
- // fallback to that value when we press escape (this part is handled in the watcher that
- // syncs the value with the input field again).
+ // If nullable and user cleared the input, ensure value is cleared explicitly.
if (api.nullable.value && api.value.value === null) {
clear()
}
- // We do have a value, so let's select the active option, unless we were just going through
- // the form and we opened it due to the focus event.
+ // Avoid unintended auto-selection when combobox opened on focus (e.g., on page load or tab focus).
+ // Only select active option if the user typed something and triggered it intentionally.
else if (api.activationTrigger.value !== ActivationTrigger.Focus) {
- api.selectActiveOption()
+ const inputValue = dom(api.inputRef)?.value || ''
+ const hasUserInput = inputValue.trim().length > 0
+
+ if (hasUserInput) {
+ api.selectActiveOption()
+ }
}
}
- return api.closeCombobox()
+ api.closeCombobox()
}
function handleFocus(event: FocusEvent) {