Ensure tabbing to a portalled <PopoverPanel> component moves focus inside (without using <PortalGroup>) (#3239)

* 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.

* add test

* update changelog
This commit is contained in:
Robin Malfait
2024-05-24 23:48:09 +02:00
committed by GitHub
parent b822c8a400
commit c2754bcb16
3 changed files with 55 additions and 2 deletions
+1
View File
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [internal] Dont set a focus fallback for Dialogs 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 `<PopoverPanel>` component moves focus inside (without using `<PortalGroup>`) ([#3239](https://github.com/tailwindlabs/headlessui/pull/3239))
### Deprecated
@@ -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(
<Popover>
<PopoverButton>Trigger</PopoverButton>
<PopoverPanel portal>
<a href="/">Link 1</a>
<a href="/">Link 2</a>
</PopoverPanel>
</Popover>
)
// 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 () => {
@@ -92,6 +92,7 @@ interface StateDefinition {
beforePanelSentinel: MutableRefObject<HTMLButtonElement | null>
afterPanelSentinel: MutableRefObject<HTMLButtonElement | null>
afterButtonSentinel: MutableRefObject<HTMLButtonElement | null>
__demoMode: boolean
}
@@ -256,9 +257,19 @@ function PopoverFn<TTag extends ElementType = typeof DEFAULT_POPOVER_TAG>(
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<TTag extends ElementType = typeof DEFAULT_POPOVER_TAG>(
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<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
{visible && !isWithinPanel && isPortalled && (
<Hidden
id={sentinelId}
ref={state.afterButtonSentinel}
features={HiddenFeatures.Focusable}
data-headlessui-focus-guard
as="button"