The problem
A <v-select> inside a v-dialog that was opened from a v-menu. Click the select, and the whole dialog closes — unsaved changes and all.
Vuetify's v-menu uses provide/inject so nested menus can find their nearest ancestor and cascade-close on an outside click:
const parent = inject(VMenuSymbol, null)
provide(VMenuSymbol, {
register() { /* add self to parent's children */ },
unregister() { /* remove self */ },
closeParents(e) {
if (clickedOutside && !openChildren.value.size && !props.persistent) {
isActive.value = false // close yourself
parent?.closeParents(e) // cascade upward
}
}
})
v-dialog doesn't provide VMenuSymbol, so the select injects the outer menu as its parent. An outside click on the select closes that outer menu — unmounting the dialog with it.
The solution
Provide a no-op stand-in at the dialog boundary:
import { provide } from 'vue'
const VMenuSymbol = Symbol.for('vuetify:v-menu')
export function useMenuBoundary() {
provide(VMenuSymbol, {
register() {},
unregister() {},
closeParents() {},
})
}
Why it works
- The select's menu now injects this stub and hits a no-op, so the cascade stops at the dialog.
- It doesn't forward to a real parent, so the outer menu's own click-outside still works.
Symbol.for(notSymbol()) matches Vuetify's globally-registered key, so the inject actually resolves to the stub.- No forks, no hacks — it plugs into Vuetify's own DI contract, and it's a one-line opt-in.
provide/inject is a graph of invisible contracts — sometimes the fix is just a wall where the framework assumed there was a menu.