From 8d29ee6082c7d4eb8643a5a01150baaf83feebfb Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 24 May 2024 18:51:46 +0200 Subject: [PATCH 1/3] ensure we allow focus in the focus sentinel button We already checked this button when inside of a `PopoverGroup`, but we didn't when you weren't using a `PopoverGroup` component. --- .../src/components/popover/popover.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/@headlessui-react/src/components/popover/popover.tsx b/packages/@headlessui-react/src/components/popover/popover.tsx index 3940555f3d..823b044104 100644 --- a/packages/@headlessui-react/src/components/popover/popover.tsx +++ b/packages/@headlessui-react/src/components/popover/popover.tsx @@ -92,6 +92,7 @@ interface StateDefinition { beforePanelSentinel: MutableRefObject afterPanelSentinel: MutableRefObject + afterButtonSentinel: MutableRefObject __demoMode: boolean } @@ -256,9 +257,19 @@ function PopoverFn( panelId: null, beforePanelSentinel: createRef(), afterPanelSentinel: createRef(), + afterButtonSentinel: createRef(), } as StateDefinition) let [ - { popoverState, button, buttonId, panel, panelId, beforePanelSentinel, afterPanelSentinel }, + { + popoverState, + button, + buttonId, + panel, + panelId, + beforePanelSentinel, + afterPanelSentinel, + afterButtonSentinel, + }, dispatch, ] = reducerBag @@ -346,6 +357,7 @@ function PopoverFn( if (root.contains(event.target)) return if (beforePanelSentinel.current?.contains?.(event.target)) return if (afterPanelSentinel.current?.contains?.(event.target)) return + if (afterButtonSentinel.current?.contains?.(event.target)) return dispatch({ type: ActionTypes.ClosePopover }) }, @@ -700,6 +712,7 @@ function ButtonFn( {visible && !isWithinPanel && isPortalled && ( Date: Fri, 24 May 2024 18:55:41 +0200 Subject: [PATCH 2/3] add test --- .../src/components/popover/popover.test.tsx | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/packages/@headlessui-react/src/components/popover/popover.test.tsx b/packages/@headlessui-react/src/components/popover/popover.test.tsx index 3777ac90f8..02a21debab 100644 --- a/packages/@headlessui-react/src/components/popover/popover.test.tsx +++ b/packages/@headlessui-react/src/components/popover/popover.test.tsx @@ -16,7 +16,7 @@ import { Keys, MouseButton, click, focus, press, shift } from '../../test-utils/ import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs' import { Portal } from '../portal/portal' import { Transition } from '../transition/transition' -import { Popover } from './popover' +import { Popover, PopoverButton, PopoverPanel } from './popover' jest.mock('../../hooks/use-id') @@ -1303,6 +1303,45 @@ describe('Keyboard interactions', () => { }) describe('`Tab` key', () => { + it( + 'should be possible to Tab through the panel contents and end up in the Button again (without PopoverGroup)', + suppressConsoleLogs(async () => { + render( + + Trigger + + Link 1 + Link 2 + + + ) + + // Focus the button of the first Popover + getByText('Trigger')?.focus() + + // Open popover + await click(getByText('Trigger')) + + // Verify we are focused on the first link + await press(Keys.Tab) + assertActiveElement(getByText('Link 1')) + + // Verify we are focused on the second link + await press(Keys.Tab) + assertActiveElement(getByText('Link 2')) + + // Let's Tab again + await press(Keys.Tab) + + // Verify that the first Popover is still open + assertPopoverButton({ state: PopoverState.Visible }) + assertPopoverPanel({ state: PopoverState.Visible }) + + // Verify that the button is focused again + assertActiveElement(getByText('Trigger')) + }) + ) + it( 'should be possible to Tab through the panel contents onto the next Popover.Button', suppressConsoleLogs(async () => { From 778cc93ff3352c447576ef1baa7ec219e884dc42 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 24 May 2024 18:58:49 +0200 Subject: [PATCH 3/3] update changelog --- packages/@headlessui-react/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md index 5f823bc2d3..1f6c34774c 100644 --- a/packages/@headlessui-react/CHANGELOG.md +++ b/packages/@headlessui-react/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [internal] Don’t set a focus fallback for Dialog’s in demo mode ([#3194](https://github.com/tailwindlabs/headlessui/pull/3194)) - Ensure page doesn't scroll down when pressing `Escape` to close the `Dialog` component ([#3218](https://github.com/tailwindlabs/headlessui/pull/3218)) - Fix crash when toggling between `virtual` and non-virtual mode in `Combobox` component ([#3236](https://github.com/tailwindlabs/headlessui/pull/3236)) +- Ensure tabbing to a portalled `` component moves focus inside (without using ``) ([#3239](https://github.com/tailwindlabs/headlessui/pull/3239)) ### Deprecated