From 098cadf854440e40eb84f177f91b8091b5f232ec Mon Sep 17 00:00:00 2001 From: Gab Date: Tue, 11 Aug 2026 15:02:20 -0300 Subject: [PATCH] fix(webkit): restore focus to the popover trigger's focusable child focusTrigger() called .focus() on the passthrough wrapper, which is not focusable, so focus fell to on every close. It now focuses the wrapper when the wrapper itself is focusable and its first focusable descendant otherwise. --- .../components/overlay/popover/popover.vue | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/webkit/src/components/overlay/popover/popover.vue b/packages/webkit/src/components/overlay/popover/popover.vue index bbad4c29e..1fe5de47d 100644 --- a/packages/webkit/src/components/overlay/popover/popover.vue +++ b/packages/webkit/src/components/overlay/popover/popover.vue @@ -11,6 +11,10 @@ inheritAttrs: false }) + /** Focusable, enabled elements — used both to trap Tab and to restore focus on close. */ + const FOCUSABLE_SELECTOR = + 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])' + defineSlots<{ default(): unknown }>() @@ -99,16 +103,25 @@ } function focusTrigger() { - triggerRef.value?.focus({ preventScroll: true }) + const trigger = triggerRef.value + if (!trigger) return + // `PopoverTrigger` is a PASSTHROUGH with no tabindex, so focusing it directly + // is a no-op: the focusable thing is the consumer's child (a Button, a link). Calling + // `.focus()` on the wrapper therefore left focus wherever it was — and since the + // panel is unmounting at that moment, the browser dropped it on , silently + // losing the user's place every time an overlay closed with focus inside it. + // (Dialog/Drawer triggers do not have this problem: their wrappers carry tabindex="0".) + const target = trigger.matches(FOCUSABLE_SELECTOR) + ? trigger + : (trigger.querySelector(FOCUSABLE_SELECTOR) ?? trigger) + target.focus({ preventScroll: true }) } /** Returns the focusable, enabled elements inside the panel, in DOM order. */ function getFocusable(): globalThis.HTMLElement[] { const panel = panelRef.value if (!panel) return [] - const selector = - 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])' - return Array.from(panel.querySelectorAll(selector)) + return Array.from(panel.querySelectorAll(FOCUSABLE_SELECTOR)) } function onDocumentMousedown(event: globalThis.MouseEvent) {