Skip to content

[USelectMenu] Input inside trigger slot loses caret visibility when clicking from search input #5745

@Cjumelin

Description

@Cjumelin

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

StackBlitz Reproduction

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

  1. Click the trigger to open the dropdown
  2. Click in the search input and type something
  3. Click on the text input inside the trigger
  4. 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:

  1. Input receives focus
  2. Caret is visible
  3. 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

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingtriageAwaiting initial review and prioritizationv4#4488

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions