nuxt / nuxt/ui

UPopover dismisses immediately when opened programmatically after a UDropdownMenu item selection

Open
#6,463 0 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

triage
Dominant language
TypeScript
Stars
6.9k
Forks
1.1k
Avg merge
1d 7h
Merged PRs (30d)
57

Description

Environment
  • macos v26.3
  • node version: v22.22.0
  • pnpm version: 10.33.2
Is this bug related to Nuxt or Vue?

Nuxt

Package

v4.x

Version

4.7.1

Reproduction

https://play.ui.nuxt.com/#eNp9UsuKnEAU/ZWDWWhDPyBkJd0Dk4TsAoEhq7YXRq+JpLqqqIfJIP773NKqaacXs1HKc+49j3LMHrXeD56yMjvaxvTawZLz+qGS/VUr4zDCULflR924fiBM6Iy6IuehvJJAJRslrUPXC0fG4hT4xfmySYDSJKn9obQaEr6sKsaJSYnWO7oG9ByWjhD1LxIl8idXO2/zLZR8IkGNK1FscHpA3bbfZskitwtng2n7dviL8tKZ5/enm0ji8UpegqHOSzao5Ir1l543GMP2mHM/1MLTXnv7pxjB8BbzlxLSC4GQDHfRz8y6cEJnPFWSxY6HpXIumw+cX4vaEZ+AY9sPaERt7anKOkH/8bvWu49VNqOM/4xblyMw7DplAne2h14mo1WWKCXrvzL2fLhBw+6qWq4sGGbKne/bxCWNRBvByGfvHHdVzqXf7cfhRkz58IEv3JEMsRfoLq7efeKcUR2RDI6HcYypwnZM0/HAczeFw5sKw4fU0tzwYverUbpV/+R3kh7l/Nex5vx+bXcVK6V6bNuovUrFAut1y81FTys32fQC120kEQ==

Description

UPopover dismisses immediately when opened programmatically after a UDropdownMenu item selection

Description

When a UPopover is opened programmatically (via v-model:open) inside the onSelect callback of a UDropdownMenu item, the popover briefly mounts and then dismisses itself.

The dismissal happens after the dropdown's exit animation completes (a few hundred ms later), which makes the popover appear to flash open and immediately close.

Reproduction

A list of programmatically-controlled popovers, plus a dropdown menu whose items add new entries to that list and open the corresponding popover.

<script setup>
const filters = ref([])
const openedPopovers = reactive({})

const items = [
  { label: 'Status', onSelect: () => addFilter('status') },
  { label: 'Country', onSelect: () => addFilter('country') }
]

function addFilter(key) {
  filters.value.push({ key, value: null })
  openedPopovers[key] = true
}
</script>

<template>
  <div class="flex gap-2">
    <UPopover
      v-for="filter in filters"
      :key="filter.key"
      v-model:open="openedPopovers[filter.key]"
    >
      <UButton :label="filter.key" />
      <template #content>
        <div class="p-4">Popover content for {{ filter.key }}</div>
      </template>
    </UPopover>

    <UDropdownMenu :items="items">
      <UButton label="Add filter" />
    </UDropdownMenu>
  </div>
</template>

Expected behavior

The newly added popover opens and stays open until the user explicitly dismisses it (click outside, escape, Tab out).

Actual behavior

The popover mounts and renders its content, then dismisses itself ~200ms later (matching the dropdown's exit animation duration). To the user, it looks like the popover "flashes" open.

Root cause

The dismissal chain, captured via console.trace on update:open (false):

handleAnimationEnd (reka-ui/Presence/usePresence)
  -> state machine dispatch
  -> Vue scheduler patch
  -> unmount FocusScope, MenuContent, etc.
  -> FocusScope cleanupFn fires unmountAutoFocus
  -> DropdownMenu.Content.handleCloseAutoFocus
     -> setTimeout(0) -> rootContext.triggerElement.focus()
        -> focusin on dropdown trigger (outside popover layer)
        -> Popover DismissableLayer.handleFocus (async, awaits nextTick x2)
           -> dispatch focusOutside event
              -> emit dismiss
                 -> PopoverContentImpl.onDismiss
                    -> PopoverRoot.onOpenChange(false)
                       -> emit update:open(false)
                          -> popover closes

In sequence:

  1. addFilter sets openedPopovers[key] = true. The popover mounts and its DismissableLayer registers a focusin listener on the document.
  2. The dropdown closes with an exit animation (usePresence). After the animation ends, the dropdown content unmounts.
  3. During unmount, the dropdown's FocusScope fires unmountAutoFocus. Reka's DropdownMenuContent.handleCloseAutoFocus schedules a setTimeout(0) that focuses the dropdown's trigger element (intended a11y behavior — returns focus to the trigger after the menu closes).
  4. The focus() call fires focusin on the document. The newly mounted popover's useFocusOutside listener sees the target (the dropdown trigger) is outside its layer and emits dismiss.
  5. dismiss propagates to PopoverRoot.onOpenChange(false) and the popover closes.

This is a timing collision between two correct, independent a11y behaviors:

  • DropdownMenu returning focus to its trigger on close.
  • Popover dismissing itself when focus moves outside its layer.

When the second popover opens during the first menu's close cycle, the first menu's focus restoration is interpreted as a focus-out-of-popover and dismisses it.

This is not specific to the dynamic component inside the popover. The dismiss happens whatever the popover renders. It just becomes more visible with content that does not synchronously grab focus (e.g. USelect/USelectMenu, whose autofocus is setTimeout-based, vs UInput which can mask the issue when the input is focused fast enough to set isFocusInsideDOMTree=true before the dropdown's setTimeout fires).

Workarounds

Two workarounds, both with tradeoffs:

1. Prevent the dropdown from restoring focus to its trigger
<UDropdownMenu
  :content="{ onCloseAutoFocus: event => event.preventDefault() }"
  :items="items"
>

Skips the chain at step 3 — the trigger never gets focused, so no focusin outside the popover.

Tradeoff: keyboard users lose the focus-return-to-trigger when closing the dropdown via Escape or by clicking outside. The reliability of this workaround also depends on the order in which Vue runs the multiple closeAutoFocus listeners (forwarded emit vs. internal handler); the internal handler may run first and schedule the setTimeout before our preventDefault is reached.

2. Prevent the popover from dismissing on focus-outside
<UPopover
  v-model:open="openedPopovers[key]"
  :content="{ onFocusOutside: event => event.preventDefault() }"
>

Skips the chain at step 4 — focusOutside is prevented, no dismiss.

Tradeoff: the popover no longer closes when focus moves outside via Tab. pointerDownOutside and Escape still work, so this is acceptable for most uses but does break the standard keyboard-dismiss expectation.

Suggestion

It would be ideal if either:

  • UDropdownMenu/Reka DropdownMenu deferred its closeAutoFocus setTimeout only when no other dismissable layer has been mounted in the meantime, or
  • UPopover/Reka Popover ignored focusOutside events whose target is a DropdownMenu trigger that was open at the moment the popover was created.

Even just documenting the interaction and exposing a clean way to suppress the focus restoration (without losing the a11y of trigger refocus on user-driven close) would help.

Environment

  • Nuxt UI: 4.7.1
  • Reka UI: 2.9.6
  • Vue: 3.5.34
  • Nuxt: 4.x
Additional context

No response

Logs

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the UPopover and UDropdownMenu focus and dismissal entry points described in the issue, then reproduce the behavior using the linked playground. Trace the dropdown's close-auto-focus sequence alongside the popover's focus-outside handling. Done means a programmatically opened popover remains open after the menu closes while normal keyboard focus restoration and dismissal behavior are preserved.

Written by the indexing model from the issue text.

Assessment

Tech stack
nuxt, typescript
Domain
accessibility, frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.