-
Notifications
You must be signed in to change notification settings - Fork 982
Description
Environment
- Nuxt UI version: 3.x (v4)
- Nuxt version: 4.x
- Browser: All browsers (Chrome, Firefox, Safari)
- OS: All platforms
Is this bug related to Nuxt or Vue?
Nuxt
Package
v4.x
Version
v4.0.0
Reproduction
StackBlitz
Minimal Reproduction Code
<script setup lang="ts">
import { ref } from 'vue';
const items = ref(['Backlog', 'Todo', 'In Progress', 'Done']);
const value = ref('Backlog');
const inputRef = ref<HTMLInputElement | null>(null);
const isOpen = ref(false);
// Focus attempt - gets overridden by FocusScope's macrotask
const handleInputMousedown = () => {
if (isOpen.value) {
isOpen.value = false;
}
const input = inputRef.value;
if (input) {
input.focus();
input.setSelectionRange(input.value.length, input.value.length);
}
};
</script>
<template>
<USelectMenu
v-model="value"
v-model:open="isOpen"
:items="items"
:search-input="{ placeholder: 'Search...' }"
>
<template #default>
<div class="flex items-center gap-2 px-3 py-2 border rounded">
<span>{{ value }}</span>
<input
ref="inputRef"
type="text"
placeholder="Type here..."
class="flex-1 bg-transparent outline-none"
@mousedown.prevent.stop="handleInputMousedown"
@click.stop
/>
</div>
</template>
</USelectMenu>
</template>Steps to Reproduce
- Click the trigger to open the dropdown
- Click in the search input and type something
- Click on the text input inside the trigger
- Observe: Caret is not visible, must click again to type
Description
When using an <input> element inside the #default slot of USelectMenu, clicking on the input while the dropdown's search input is focused results in focus transferring to the input but the caret (cursor) not appearing. This prevents users from typing immediately. They must click the input a second time.
Technical Analysis
Root Cause
The issue originates from Reka UI's FocusScope component used internally by USelectMenu:
<!-- From SelectMenu.vue -->
<FocusScope trapped data-slot="focusScope" :class="ui.focusScope(...)">When the dropdown closes, FocusScope fires an unmount-auto-focus event that returns focus to the trigger element. This happens via a macrotask (internal setTimeout), which overrides any synchronous or microtask-based focus attempts.
Event Sequence
1. User clicks input inside trigger
2. mousedown fires → we call focus() synchronously
3. Dropdown closes (open → false)
4. FocusScope unmounts
5. FocusScope's unmount-auto-focus runs (macrotask, ~100ms later)
6. Focus returns to trigger container → our focus is overridden
7. Caret disappears
Why Synchronous focus() Doesn't Work
| Timing Mechanism | Type | Execution Order |
|---|---|---|
Synchronous focus() |
Immediate | Runs first |
nextTick() |
Microtask | Runs second |
FocusScope's unmount-auto-focus |
Macrotask (~100ms) | Runs last, wins |
Expected Behavior
When clicking on an input inside the #default slot while the dropdown is open:
- Input receives focus
- Caret is visible
- User can type immediately
Actual Behavior
- Focus transfers (input is
document.activeElement) - Caret is not visible
- User must click the input a second time to see the caret and type
Additional context
Workaround
The only reliable workaround requires a 150ms setTimeout with retry mechanism to run after FocusScope's macrotask:
const focusInputWithCaret = (attempt = 1) => {
const delay = attempt === 1 ? 150 : 100;
const maxAttempts = 3;
setTimeout(() => {
const input = inputRef.value;
if (input) {
input.focus();
input.setSelectionRange(input.value.length, input.value.length);
// Retry if focus didn't stick
if (document.activeElement !== input && attempt < maxAttempts) {
focusInputWithCaret(attempt + 1);
}
}
}, delay);
};
const handleInputMousedown = () => {
if (isOpen.value) {
isOpen.value = false;
focusInputWithCaret(); // setTimeout instead of synchronous
}
};This workaround is brittle.
Why autofocus / autofocusDelay Don't Help
These props control mount behavior (when component renders), not unmount behavior (when dropdown closes):
| Prop | When | What it does |
|---|---|---|
autofocus |
Mount | Focuses trigger on initial render |
autofocusDelay |
Mount | Delays initial autofocus |
The focus-return-on-close behavior is internal to FocusScope and not currently exposed by USelectMenu.
Proposed Solutions
Option 1: Expose onUnmountAutoFocus event
<USelectMenu :content="{ onUnmountAutoFocus: (e) => e.preventDefault() }">Option 2: Add focusOnClose prop
<USelectMenu :focus-on-close="false">Option 3: Add focusTrap prop
<USelectMenu :focus-trap="false">Related Issues
USelect&USelectMenusetting :autofocus to false or :contentbodyLockto false have no effect #4956 - Similar root cause (Reka UI focus management not exposed), but different timing:USelect&USelectMenusetting :autofocus to false or :contentbodyLockto false have no effect #4956 is about mount behavior (autofocus), this issue is about unmount behavior (onUnmountAutoFocus)- Reka UI FocusScope - Underlying component documentation
- Reka UI FocusScope source - Source code showing
mount-auto-focusandunmount-auto-focusevents
Use Cases
This pattern (input inside select trigger) is common for:
- Phone number inputs: Country code selector + phone number input
- Currency inputs: Currency selector + amount input
- Unit inputs: Unit selector + value input
- Prefixed inputs: Any compound input where a dropdown modifies/prefixes a text value
The current behavior makes these patterns unusable without hacky setTimeout workarounds.