From 6d07867476459cc39ca388c3a4c67baec8cccf53 Mon Sep 17 00:00:00 2001 From: J-Sek Date: Wed, 10 Sep 2025 22:53:25 +0200 Subject: [PATCH 1/3] fix(VMenu): avoid scrolling to the off-screen menu --- packages/vuetify/src/components/VMenu/VMenu.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/vuetify/src/components/VMenu/VMenu.tsx b/packages/vuetify/src/components/VMenu/VMenu.tsx index 7150113d79d..129b2b06d6f 100644 --- a/packages/vuetify/src/components/VMenu/VMenu.tsx +++ b/packages/vuetify/src/components/VMenu/VMenu.tsx @@ -109,6 +109,16 @@ export const VMenu = genericComponent()({ }) onDeactivated(() => isActive.value = false) + let focusTrapSuppressed = false + let focusTrapSuppressionTimeout = -1 + + async function onPointerdown () { + focusTrapSuppressed = true + focusTrapSuppressionTimeout = window.setTimeout(() => { + focusTrapSuppressed = false + }, 100) + } + async function onFocusIn (e: FocusEvent) { const before = e.relatedTarget as HTMLElement | null const after = e.target as HTMLElement | null @@ -116,6 +126,7 @@ export const VMenu = genericComponent()({ await nextTick() if ( + !focusTrapSuppressed && isActive.value && before !== after && overlay.value?.contentEl && @@ -128,6 +139,8 @@ export const VMenu = genericComponent()({ ) { const focusable = focusableChildren(overlay.value.contentEl) focusable[0]?.focus() + + document.removeEventListener('pointerdown', onPointerdown) } } @@ -135,11 +148,14 @@ export const VMenu = genericComponent()({ if (val) { parent?.register() if (IN_BROWSER && !props.disableInitialFocus) { + document.addEventListener('pointerdown', onPointerdown) document.addEventListener('focusin', onFocusIn, { once: true }) } } else { parent?.unregister() if (IN_BROWSER) { + clearTimeout(focusTrapSuppressionTimeout) + document.removeEventListener('pointerdown', onPointerdown) document.removeEventListener('focusin', onFocusIn) } } From 0030e5d5083771ba6eace0d92d62287a4aa42122 Mon Sep 17 00:00:00 2001 From: J-Sek Date: Thu, 11 Sep 2025 02:46:05 +0200 Subject: [PATCH 2/3] another fix: ensure close on external focus fixes #20569 --- packages/vuetify/src/components/VMenu/VMenu.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/vuetify/src/components/VMenu/VMenu.tsx b/packages/vuetify/src/components/VMenu/VMenu.tsx index 129b2b06d6f..c1eebae688a 100644 --- a/packages/vuetify/src/components/VMenu/VMenu.tsx +++ b/packages/vuetify/src/components/VMenu/VMenu.tsx @@ -126,7 +126,6 @@ export const VMenu = genericComponent()({ await nextTick() if ( - !focusTrapSuppressed && isActive.value && before !== after && overlay.value?.contentEl && @@ -137,10 +136,14 @@ export const VMenu = genericComponent()({ // It isn't inside the menu body !overlay.value.contentEl.contains(after) ) { - const focusable = focusableChildren(overlay.value.contentEl) - focusable[0]?.focus() + if (focusTrapSuppressed) { + isActive.value = false + } else { + const focusable = focusableChildren(overlay.value.contentEl) + focusable[0]?.focus() - document.removeEventListener('pointerdown', onPointerdown) + document.removeEventListener('pointerdown', onPointerdown) + } } } From ed6cdc88b72e2bb5a60a59cca83a9ead32cb506c Mon Sep 17 00:00:00 2001 From: J-Sek Date: Mon, 6 Oct 2025 18:49:15 +0200 Subject: [PATCH 3/3] another fix: keep hover menus, apply to globalTop fixes #21015 --- packages/vuetify/src/components/VMenu/VMenu.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/vuetify/src/components/VMenu/VMenu.tsx b/packages/vuetify/src/components/VMenu/VMenu.tsx index c1eebae688a..1399e141a85 100644 --- a/packages/vuetify/src/components/VMenu/VMenu.tsx +++ b/packages/vuetify/src/components/VMenu/VMenu.tsx @@ -129,15 +129,15 @@ export const VMenu = genericComponent()({ isActive.value && before !== after && overlay.value?.contentEl && - // We're the topmost menu - overlay.value?.globalTop && // It isn't the document or the menu body ![document, overlay.value.contentEl].includes(after!) && // It isn't inside the menu body !overlay.value.contentEl.contains(after) ) { if (focusTrapSuppressed) { - isActive.value = false + if (!props.openOnHover) { + isActive.value = false + } } else { const focusable = focusableChildren(overlay.value.contentEl) focusable[0]?.focus()