From d8c469c919e16262f535cfe8207d44c028f7f4ef Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 10 Sep 2022 00:50:08 +0200 Subject: [PATCH] make it work in Vue --- .../src/components/popover/popover.test.ts | 50 ++++++++++++++++++- .../src/components/popover/popover.ts | 21 ++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/packages/@headlessui-vue/src/components/popover/popover.test.ts b/packages/@headlessui-vue/src/components/popover/popover.test.ts index 0f3a7a7af5..041134c198 100644 --- a/packages/@headlessui-vue/src/components/popover/popover.test.ts +++ b/packages/@headlessui-vue/src/components/popover/popover.test.ts @@ -1,4 +1,4 @@ -import { defineComponent, nextTick, ref, watch, h } from 'vue' +import { defineComponent, nextTick, ref, watch, h, onMounted } from 'vue' import { createRenderTemplate, render } from '../../test-utils/vue-testing-library' import { Popover, PopoverGroup, PopoverButton, PopoverPanel, PopoverOverlay } from './popover' @@ -1696,6 +1696,54 @@ describe('Keyboard interactions', () => { }) ) + it( + 'should focus the Popover.Button when pressing Shift+Tab when we focus inside the Popover.Panel (heuristc based portal)', + suppressConsoleLogs(async () => { + renderTemplate({ + template: html` + + Trigger 1 + + + Link 1 + Link 2 + + + +
+ + + `, + setup() { + let ready = ref(false) + onMounted(() => { + ready.value = true + }) + return { ready } + }, + }) + + // Open the popover + await click(getPopoverButton()) + + // Ensure the popover is open + assertPopoverButton({ state: PopoverState.Visible }) + + // Ensure the Link 1 is focused + assertActiveElement(getByText('Link 1')) + + // Tab out of the Panel + await press(shift(Keys.Tab)) + + // Ensure the Popover.Button is focused again + assertActiveElement(getPopoverButton()) + + // Ensure the Popover is closed + assertPopoverButton({ state: PopoverState.InvisibleUnmounted }) + assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) + }) + ) + it( 'should be possible to focus the last item in the PopoverPanel when pressing Shift+Tab on the next PopoverButton', suppressConsoleLogs(async () => { diff --git a/packages/@headlessui-vue/src/components/popover/popover.ts b/packages/@headlessui-vue/src/components/popover/popover.ts index 61c425c1eb..61ea6c34c6 100644 --- a/packages/@headlessui-vue/src/components/popover/popover.ts +++ b/packages/@headlessui-vue/src/components/popover/popover.ts @@ -118,12 +118,33 @@ export let Popover = defineComponent({ if (!dom(button)) return false if (!dom(panel)) return false + // We are part of a different "root" tree, so therefore we can consider it portalled. This is a + // heuristic because 3rd party tools could use some form of portal, typically rendered at the + // end of the body but we don't have an actual reference to that. for (let root of document.querySelectorAll('body > *')) { if (Number(root?.contains(dom(button))) ^ Number(root?.contains(dom(panel)))) { return true } } + // Use another heuristic to try and calculate wether or not the focusable elements are near + // eachother (aka, following the default focus/tab order from the browser). If they are then it + // doesn't really matter if they are portalled or not because we can follow the default tab + // order. But if they are not, then we can consider it being portalled so that we can ensure + // that tab and shift+tab (hopefully) go to the correct spot. + let elements = getFocusableElements() + let buttonIdx = elements.indexOf(dom(button)!) + + let beforeIdx = (buttonIdx + elements.length - 1) % elements.length + let afterIdx = (buttonIdx + 1) % elements.length + + let beforeElement = elements[beforeIdx] + let afterElement = elements[afterIdx] + + if (!dom(panel)?.contains(beforeElement) && !dom(panel)?.contains(afterElement)) { + return true + } + return false })