From 0414eff00347885ef67681f1d54096ede5c36e9e Mon Sep 17 00:00:00 2001 From: vian Date: Mon, 3 Aug 2026 01:30:43 +0530 Subject: [PATCH] fix(VMenu): avoid aria-owns for menu activators Keep the accessible menu relationship through aria-controls without exposing aria-owns, which causes Safari VoiceOver to drop item labels. Preserve nested overlay ownership through an internal data attribute. fixes #22540 --- packages/vuetify/src/components/VMenu/VMenu.tsx | 2 +- .../VMenu/__tests__/VMenu.spec.browser.tsx | 16 ++++++++++++++++ .../vuetify/src/components/VOverlay/VOverlay.tsx | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/vuetify/src/components/VMenu/VMenu.tsx b/packages/vuetify/src/components/VMenu/VMenu.tsx index c6bf8287724..87de7af8981 100644 --- a/packages/vuetify/src/components/VMenu/VMenu.tsx +++ b/packages/vuetify/src/components/VMenu/VMenu.tsx @@ -169,7 +169,7 @@ export const VMenu = genericComponent()({ 'aria-haspopup': 'menu', 'aria-expanded': String(isActive.value), 'aria-controls': id.value, - 'aria-owns': id.value, + 'data-v-overlay-activator': id.value, onKeydown: onActivatorKeydown, }, props.activatorProps) ) diff --git a/packages/vuetify/src/components/VMenu/__tests__/VMenu.spec.browser.tsx b/packages/vuetify/src/components/VMenu/__tests__/VMenu.spec.browser.tsx index 9788d7ee18e..d2fb73f79a1 100644 --- a/packages/vuetify/src/components/VMenu/__tests__/VMenu.spec.browser.tsx +++ b/packages/vuetify/src/components/VMenu/__tests__/VMenu.spec.browser.tsx @@ -12,6 +12,22 @@ import { commands, render, screen, userEvent, wait } from '@test' import { ref } from 'vue' describe('VMenu', () => { + it('should associate the activator with the menu using aria-controls', () => { + render(() => ( + + {{ + activator: ({ props }: any) => Menu, + default: () => , + }} + + )) + + const activator = screen.getByTestId('activator') + + expect(activator.getAttribute('aria-controls')).toMatch(/^v-menu-v-\d+$/) + expect(activator).not.toHaveAttribute('aria-owns') + }) + describe('open-on-focus with template activator', () => { beforeEach(() => commands.setFocusEmulationDisabled()) diff --git a/packages/vuetify/src/components/VOverlay/VOverlay.tsx b/packages/vuetify/src/components/VOverlay/VOverlay.tsx index d7f3c042788..3478a9bed1a 100644 --- a/packages/vuetify/src/components/VOverlay/VOverlay.tsx +++ b/packages/vuetify/src/components/VOverlay/VOverlay.tsx @@ -226,7 +226,7 @@ export const VOverlay = genericComponent()({ if (el === contentEl.value) return true visited.add(el) const ownerId = el.closest('.v-overlay')?.id - current = ownerId ? document.querySelector(`[aria-owns~="${CSS.escape(ownerId)}"]`) : null + current = ownerId ? document.querySelector(`[data-v-overlay-activator="${CSS.escape(ownerId)}"]`) : null } return false }