import React, { createElement, useEffect } from 'react' import { render } from '@testing-library/react' import { Popover } from './popover' import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs' import { PopoverState, assertPopoverPanel, assertPopoverButton, getPopoverButton, getPopoverPanel, getByText, assertActiveElement, assertContainsActiveElement, getPopoverOverlay, } from '../../test-utils/accessibility-assertions' import { click, press, Keys, MouseButton, shift } from '../../test-utils/interactions' import { Portal } from '../portal/portal' import { Transition } from '../transitions/transition' jest.mock('../../hooks/use-id') afterAll(() => jest.restoreAllMocks()) function nextFrame() { return new Promise(resolve => { requestAnimationFrame(() => { requestAnimationFrame(() => { resolve() }) }) }) } describe('Safe guards', () => { it.each([ ['Popover.Button', Popover.Button], ['Popover.Panel', Popover.Panel], ['Popover.Overlay', Popover.Overlay], ])( 'should error when we are using a <%s /> without a parent ', suppressConsoleLogs((name, Component) => { expect(() => render(createElement(Component))).toThrowError( `<${name} /> is missing a parent component.` ) }) ) it( 'should be possible to render a Popover without crashing', suppressConsoleLogs(async () => { render( Trigger Contents ) assertPopoverButton({ state: PopoverState.InvisibleUnmounted, attributes: { id: 'headlessui-popover-button-1' }, }) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) }) ) }) describe('Rendering', () => { describe('Popover.Group', () => { it( 'should be possible to render a Popover.Group with multiple Popover components', suppressConsoleLogs(async () => { render( Trigger 1 Panel 1 Trigger 2 Panel 2 ) assertPopoverButton({ state: PopoverState.InvisibleUnmounted }, getByText('Trigger 1')) assertPopoverButton({ state: PopoverState.InvisibleUnmounted }, getByText('Trigger 2')) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }, getByText('Panel 1')) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }, getByText('Panel 2')) await click(getByText('Trigger 1')) assertPopoverButton({ state: PopoverState.Visible }, getByText('Trigger 1')) assertPopoverButton({ state: PopoverState.InvisibleUnmounted }, getByText('Trigger 2')) assertPopoverPanel({ state: PopoverState.Visible }, getByText('Panel 1')) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }, getByText('Panel 2')) await click(getByText('Trigger 2')) assertPopoverButton({ state: PopoverState.InvisibleUnmounted }, getByText('Trigger 1')) assertPopoverButton({ state: PopoverState.Visible }, getByText('Trigger 2')) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }, getByText('Panel 1')) assertPopoverPanel({ state: PopoverState.Visible }, getByText('Panel 2')) }) ) }) describe('Popover', () => { it( 'should be possible to render a Popover using a render prop', suppressConsoleLogs(async () => { render( {({ open }) => ( <> Trigger Panel is: {open ? 'open' : 'closed'} )} ) assertPopoverButton({ state: PopoverState.InvisibleUnmounted, attributes: { id: 'headlessui-popover-button-1' }, }) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) await click(getPopoverButton()) assertPopoverButton({ state: PopoverState.Visible, attributes: { id: 'headlessui-popover-button-1' }, }) assertPopoverPanel({ state: PopoverState.Visible, textContent: 'Panel is: open' }) }) ) }) describe('Popover.Button', () => { it( 'should be possible to render a Popover.Button using a render prop', suppressConsoleLogs(async () => { render( {JSON.stringify} ) assertPopoverButton({ state: PopoverState.InvisibleUnmounted, attributes: { id: 'headlessui-popover-button-1' }, textContent: JSON.stringify({ open: false }), }) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) await click(getPopoverButton()) assertPopoverButton({ state: PopoverState.Visible, attributes: { id: 'headlessui-popover-button-1' }, textContent: JSON.stringify({ open: true }), }) assertPopoverPanel({ state: PopoverState.Visible }) }) ) it( 'should be possible to render a Popover.Button using a render prop and an `as` prop', suppressConsoleLogs(async () => { render( {JSON.stringify} ) assertPopoverButton({ state: PopoverState.InvisibleUnmounted, attributes: { id: 'headlessui-popover-button-1' }, textContent: JSON.stringify({ open: false }), }) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) await click(getPopoverButton()) assertPopoverButton({ state: PopoverState.Visible, attributes: { id: 'headlessui-popover-button-1' }, textContent: JSON.stringify({ open: true }), }) assertPopoverPanel({ state: PopoverState.Visible }) }) ) }) describe('Popover.Panel', () => { it( 'should be possible to render Popover.Panel using a render prop', suppressConsoleLogs(async () => { render( Trigger {JSON.stringify} ) assertPopoverButton({ state: PopoverState.InvisibleUnmounted, attributes: { id: 'headlessui-popover-button-1' }, }) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) await click(getPopoverButton()) assertPopoverButton({ state: PopoverState.Visible, attributes: { id: 'headlessui-popover-button-1' }, }) assertPopoverPanel({ state: PopoverState.Visible, textContent: JSON.stringify({ open: true }), }) }) ) it('should be possible to always render the Popover.Panel if we provide it a `static` prop', () => { render( Trigger Contents ) // Let's verify that the Popover is already there expect(getPopoverPanel()).not.toBe(null) }) it('should be possible to use a different render strategy for the Popover.Panel', async () => { render( Trigger Contents ) getPopoverButton()?.focus() assertPopoverButton({ state: PopoverState.InvisibleHidden }) assertPopoverPanel({ state: PopoverState.InvisibleHidden }) // Let's open the Popover, to see if it is not hidden anymore await click(getPopoverButton()) assertPopoverButton({ state: PopoverState.Visible }) assertPopoverPanel({ state: PopoverState.Visible }) // Let's re-click the Popover, to see if it is hidden again await click(getPopoverButton()) assertPopoverButton({ state: PopoverState.InvisibleHidden }) assertPopoverPanel({ state: PopoverState.InvisibleHidden }) }) it( 'should be possible to move the focus inside the panel to the first focusable element (very first link)', suppressConsoleLogs(async () => { render( Trigger Link 1 ) // Focus the button getPopoverButton()?.focus() // Ensure the button is focused assertActiveElement(getPopoverButton()) // Open the popover await click(getPopoverButton()) // Ensure the active element is within the Panel assertContainsActiveElement(getPopoverPanel()) assertActiveElement(getByText('Link 1')) }) ) it( 'should close the Popover, when Popover.Panel has the focus prop and you focus the open button', suppressConsoleLogs(async () => { render( Trigger Link 1 ) // Focus the button getPopoverButton()?.focus() // Ensure the button is focused assertActiveElement(getPopoverButton()) // Open the popover await click(getPopoverButton()) // Ensure the active element is within the Panel assertContainsActiveElement(getPopoverPanel()) assertActiveElement(getByText('Link 1')) // Focus the button again getPopoverButton()?.focus() // Ensure the Popover is closed again assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) }) ) it( 'should be possible to move the focus inside the panel to the first focusable element (skip hidden link)', suppressConsoleLogs(async () => { render( Trigger Link 1 Link 2 ) // Focus the button getPopoverButton()?.focus() // Ensure the button is focused assertActiveElement(getPopoverButton()) // Open the popover await click(getPopoverButton()) // Ensure the active element is within the Panel assertContainsActiveElement(getPopoverPanel()) assertActiveElement(getByText('Link 2')) }) ) it( 'should be possible to move the focus inside the panel to the first focusable element (very first link) when the hidden render strategy is used', suppressConsoleLogs(async () => { render( Trigger Link 1 ) // Focus the button getPopoverButton()?.focus() // Ensure the button is focused assertActiveElement(getPopoverButton()) // Open the popover await click(getPopoverButton()) // Ensure the active element is within the Panel assertContainsActiveElement(getPopoverPanel()) assertActiveElement(getByText('Link 1')) }) ) }) }) describe('Composition', () => { function Debug({ fn, name }: { fn: (text: string) => void; name: string }) { useEffect(() => { fn(`Mounting - ${name}`) return () => { fn(`Unmounting - ${name}`) } }, [fn, name]) return null } it( 'should be possible to wrap the Popover.Panel with a Transition component', suppressConsoleLogs(async () => { let orderFn = jest.fn() render( Trigger ) // Open the popover await click(getPopoverButton()) // Close the popover await click(getPopoverButton()) // Wait for all transitions to finish await nextFrame() // Verify that we tracked the `mounts` and `unmounts` in the correct order expect(orderFn.mock.calls).toEqual([ ['Mounting - Popover'], ['Mounting - Transition'], ['Mounting - Transition.Child'], ['Unmounting - Transition.Child'], ['Unmounting - Transition'], ]) }) ) }) describe('Keyboard interactions', () => { describe('`Enter` key', () => { it( 'should be possible to open the Popover with Enter', suppressConsoleLogs(async () => { render( Trigger Contents ) assertPopoverButton({ state: PopoverState.InvisibleUnmounted, attributes: { id: 'headlessui-popover-button-1' }, }) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) // Focus the button getPopoverButton()?.focus() // Open popover await press(Keys.Enter) // Verify it is open assertPopoverButton({ state: PopoverState.Visible }) assertPopoverPanel({ state: PopoverState.Visible, attributes: { id: 'headlessui-popover-panel-2' }, }) // Close popover await press(Keys.Enter) assertPopoverButton({ state: PopoverState.InvisibleUnmounted }) }) ) it( 'should not be possible to open the popover with Enter when the button is disabled', suppressConsoleLogs(async () => { render( Trigger Content ) assertPopoverButton({ state: PopoverState.InvisibleUnmounted, attributes: { id: 'headlessui-popover-button-1' }, }) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) // Focus the button getPopoverButton()?.focus() // Try to open the popover await press(Keys.Enter) // Verify it is still closed assertPopoverButton({ state: PopoverState.InvisibleUnmounted, attributes: { id: 'headlessui-popover-button-1' }, }) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) }) ) it( 'should be possible to close the popover with Enter when the popover is open', suppressConsoleLogs(async () => { render( Trigger Contents ) assertPopoverButton({ state: PopoverState.InvisibleUnmounted, attributes: { id: 'headlessui-popover-button-1' }, }) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) // Focus the button getPopoverButton()?.focus() // Open popover await press(Keys.Enter) // Verify it is open assertPopoverButton({ state: PopoverState.Visible }) assertPopoverPanel({ state: PopoverState.Visible, attributes: { id: 'headlessui-popover-panel-2' }, }) // Close popover await press(Keys.Enter) // Verify it is closed again assertPopoverButton({ state: PopoverState.InvisibleUnmounted }) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) }) ) it( 'should close other popover menus when we open a new one', suppressConsoleLogs(async () => { render( Trigger 1 Panel 1 Trigger 2 Panel 2 ) // Open the first Popover await click(getByText('Trigger 1')) // Verify the correct popovers are open assertPopoverButton({ state: PopoverState.Visible }, getByText('Trigger 1')) assertPopoverButton({ state: PopoverState.InvisibleUnmounted }, getByText('Trigger 2')) // Focus trigger 2 getByText('Trigger 2')?.focus() // Verify the correct popovers are open assertPopoverButton({ state: PopoverState.Visible }, getByText('Trigger 1')) assertPopoverButton({ state: PopoverState.InvisibleUnmounted }, getByText('Trigger 2')) // Open the second popover await press(Keys.Enter) // Verify the correct popovers are open assertPopoverButton({ state: PopoverState.InvisibleUnmounted }, getByText('Trigger 1')) assertPopoverButton({ state: PopoverState.Visible }, getByText('Trigger 2')) }) ) it( 'should close the Popover by pressing `Enter` on a Popover.Button inside a Popover.Panel', suppressConsoleLogs(async () => { render( Open Close ) // Open the popover await click(getPopoverButton()) let closeBtn = getByText('Close') expect(closeBtn).not.toHaveAttribute('id') expect(closeBtn).not.toHaveAttribute('aria-controls') expect(closeBtn).not.toHaveAttribute('aria-expanded') // The close button should close the popover await press(Keys.Enter, closeBtn) // Verify it is closed assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) // Verify we restored the Open button assertActiveElement(getPopoverButton()) }) ) }) describe('`Escape` key', () => { it( 'should close the Popover menu, when pressing escape on the Popover.Button', suppressConsoleLogs(async () => { render( Trigger Contents ) // Focus the button getPopoverButton()?.focus() // Verify popover is closed assertPopoverButton({ state: PopoverState.InvisibleUnmounted }) // Open popover await click(getPopoverButton()) // Verify popover is open assertPopoverButton({ state: PopoverState.Visible }) // Close popover await press(Keys.Escape) // Verify popover is closed assertPopoverButton({ state: PopoverState.InvisibleUnmounted }) // Verify button is (still) focused assertActiveElement(getPopoverButton()) }) ) it( 'should close the Popover menu, when pressing escape on the Popover.Panel', suppressConsoleLogs(async () => { render( Trigger Link ) // Focus the button getPopoverButton()?.focus() // Verify popover is closed assertPopoverButton({ state: PopoverState.InvisibleUnmounted }) // Open popover await click(getPopoverButton()) // Verify popover is open assertPopoverButton({ state: PopoverState.Visible }) // Tab to next focusable item await press(Keys.Tab) // Verify the active element is inside the panel assertContainsActiveElement(getPopoverPanel()) // Close popover await press(Keys.Escape) // Verify popover is closed assertPopoverButton({ state: PopoverState.InvisibleUnmounted }) // Verify button is focused again assertActiveElement(getPopoverButton()) }) ) it( 'should be possible to close a sibling Popover when pressing escape on a sibling Popover.Button', suppressConsoleLogs(async () => { render( Trigger 1 Panel 1 Trigger 2 Panel 2 ) // Focus the button of the first Popover getByText('Trigger 1')?.focus() // Verify popover is closed assertPopoverButton({ state: PopoverState.InvisibleUnmounted }, getByText('Trigger 1')) assertPopoverButton({ state: PopoverState.InvisibleUnmounted }, getByText('Trigger 2')) // Open popover await click(getByText('Trigger 1')) // Verify popover is open assertPopoverButton({ state: PopoverState.Visible }, getByText('Trigger 1')) assertPopoverButton({ state: PopoverState.InvisibleUnmounted }, getByText('Trigger 2')) assertPopoverPanel({ state: PopoverState.Visible }, getByText('Panel 1')) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }, getByText('Panel 2')) // Focus the button of the second popover menu getByText('Trigger 2')?.focus() // Close popover await press(Keys.Escape) // Verify both popovers are closed assertPopoverButton({ state: PopoverState.InvisibleUnmounted }, getByText('Trigger 1')) assertPopoverButton({ state: PopoverState.InvisibleUnmounted }, getByText('Trigger 2')) // Verify the button of the second popover is still focused assertActiveElement(getByText('Trigger 2')) }) ) }) describe('`Tab` key', () => { it( 'should be possible to Tab through the panel contents onto the next Popover.Button', suppressConsoleLogs(async () => { render( Trigger 1 Link 1 Link 2 Trigger 2 Panel 2 ) // Focus the button of the first Popover getByText('Trigger 1')?.focus() // Open popover await click(getByText('Trigger 1')) // 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 second button is focused assertActiveElement(getByText('Trigger 2')) }) ) it( 'should be possible to place a focusable item in the Popover.Group, and keep the Popover open when we focus the focusable element', suppressConsoleLogs(async () => { render( Trigger 1 Link 1 Link 2 Link in between Trigger 2 Panel 2 ) // Focus the button of the first Popover getByText('Trigger 1')?.focus() // Open popover await click(getByText('Trigger 1')) // 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 to the in between link await press(Keys.Tab) // Verify that the first Popover is still open assertPopoverButton({ state: PopoverState.Visible }) assertPopoverPanel({ state: PopoverState.Visible }) // Verify that the in between link is focused assertActiveElement(getByText('Link in between')) }) ) it( 'should close the Popover menu once we Tab out of the Popover.Group', suppressConsoleLogs(async () => { render( <> Trigger 1 Link 1 Link 2 Trigger 2 Link 3 Link 4 Next ) // Focus the button of the first Popover getByText('Trigger 1')?.focus() // Open popover await click(getByText('Trigger 1')) // 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 second button is focused assertActiveElement(getByText('Trigger 2')) // Let's Tab out of the Popover.Group await press(Keys.Tab) // Verify the next link is now focused assertActiveElement(getByText('Next')) // Verify the popover is closed assertPopoverButton({ state: PopoverState.InvisibleUnmounted }) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) }) ) it( 'should close the Popover menu once we Tab out of the Popover', suppressConsoleLogs(async () => { render( <> Trigger 1 Link 1 Link 2 Next ) // Focus the button of the first Popover getByText('Trigger 1')?.focus() // Open popover await click(getByText('Trigger 1')) // 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 out of the Popover await press(Keys.Tab) // Verify the next link is now focused assertActiveElement(getByText('Next')) // Verify the popover is closed assertPopoverButton({ state: PopoverState.InvisibleUnmounted }) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) }) ) it( 'should close the Popover when the Popover.Panel has a focus prop', suppressConsoleLogs(async () => { render( <> Previous Trigger Link 1 Link 2 Next ) // Open the popover await click(getPopoverButton()) // Focus should be within the panel assertContainsActiveElement(getPopoverPanel()) // Tab out of the component await press(Keys.Tab) // Tab to link 1 await press(Keys.Tab) // Tab out // The popover should be closed assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) // The active element should be the Next link outside of the popover assertActiveElement(getByText('Next')) }) ) it( 'should close the Popover when the Popover.Panel has a focus prop (Popover.Panel uses a Portal)', suppressConsoleLogs(async () => { render( <> Previous Trigger Link 1 Link 2 Next ) // Open the popover await click(getPopoverButton()) // Focus should be within the panel assertContainsActiveElement(getPopoverPanel()) // The focus should be on the first link assertActiveElement(getByText('Link 1')) // Tab to the next link await press(Keys.Tab) // The focus should be on the second link assertActiveElement(getByText('Link 2')) // Tab out of the component await press(Keys.Tab) // The popover should be closed assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) // The active element should be the Next link outside of the popover assertActiveElement(getByText('Next')) }) ) it( 'should close the Popover when the Popover.Panel has a focus prop (Popover.Panel uses a Portal), and focus the next focusable item in line', suppressConsoleLogs(async () => { render( <> Previous Trigger Link 1 Link 2 ) // Open the popover await click(getPopoverButton()) // Focus should be within the panel assertContainsActiveElement(getPopoverPanel()) // The focus should be on the first link assertActiveElement(getByText('Link 1')) // Tab to the next link await press(Keys.Tab) // The focus should be on the second link assertActiveElement(getByText('Link 2')) // Tab out of the component await press(Keys.Tab) // The popover should be closed assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) // The active element should be the Previous link outside of the popover, this is the next one in line assertActiveElement(getByText('Previous')) }) ) }) describe('`Shift+Tab` key', () => { it( 'should close the Popover menu once we Tab out of the Popover.Group', suppressConsoleLogs(async () => { render( <> Previous Trigger 1 Link 1 Link 2 Trigger 2 Link 3 Link 4 ) // Focus the button of the second Popover getByText('Trigger 2')?.focus() // Open popover await click(getByText('Trigger 2')) // Verify we can tab to Trigger 1 await press(shift(Keys.Tab)) assertActiveElement(getByText('Trigger 1')) // Let's Tab out of the Popover.Group await press(shift(Keys.Tab)) // Verify the previous link is now focused assertActiveElement(getByText('Previous')) // Verify the popover is closed assertPopoverButton({ state: PopoverState.InvisibleUnmounted }) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) }) ) it( 'should close the Popover menu once we Tab out of the Popover', suppressConsoleLogs(async () => { render( <> Previous Trigger 1 Link 1 Link 2 ) // Focus the button of the Popover getPopoverButton()?.focus() // Open popover await click(getPopoverButton()) // Let's Tab out of the Popover await press(shift(Keys.Tab)) // Verify the previous link is now focused assertActiveElement(getByText('Previous')) // Verify the popover is closed assertPopoverButton({ state: PopoverState.InvisibleUnmounted }) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) }) ) it( 'should focus the previous Popover.Button when Shift+Tab on the second Popover.Button', suppressConsoleLogs(async () => { render( Trigger 1 Link 1 Link 2 Trigger 2 Link 3 Link 4 ) // Open the second popover await click(getByText('Trigger 2')) // Ensure the second popover is open assertPopoverButton({ state: PopoverState.Visible }, getByText('Trigger 2')) // Close the popover await press(Keys.Escape) // Ensure the popover is now closed assertPopoverButton({ state: PopoverState.InvisibleUnmounted }, getByText('Trigger 2')) // Ensure the second Popover.Button is focused assertActiveElement(getByText('Trigger 2')) // Tab backwards await press(shift(Keys.Tab)) // Ensure the first Popover.Button is open assertActiveElement(getByText('Trigger 1')) }) ) it( 'should focus the Popover.Button when pressing Shift+Tab when we focus inside the Popover.Panel', suppressConsoleLogs(async () => { render( Trigger 1 Link 1 Link 2 ) // 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 focus the Popover.Button when pressing Shift+Tab when we focus inside the Popover.Panel (inside a Portal)', suppressConsoleLogs(async () => { render( Trigger 1 Link 1 Link 2 ) // 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 Popover.Panel when pressing Shift+Tab on the next Popover.Button', suppressConsoleLogs(async () => { render( Trigger 1 Link 1 Link 2 Trigger 2 Link 3 Link 4 ) // Open the popover await click(getByText('Trigger 1')) // Ensure the popover is open assertPopoverButton({ state: PopoverState.Visible }) // Focus the second button getByText('Trigger 2')?.focus() // Verify the second button is focused assertActiveElement(getByText('Trigger 2')) // Ensure the first Popover is still open assertPopoverButton({ state: PopoverState.Visible }) assertPopoverPanel({ state: PopoverState.Visible }) // Press shift+tab, to move focus to the last item in the Popover.Panel await press(shift(Keys.Tab), getByText('Trigger 2')) // Verify we are focusing the last link of the first Popover assertActiveElement(getByText('Link 2')) }) ) it( "should be possible to focus the last item in the Popover.Panel when pressing Shift+Tab on the next Popover.Button (using Portal's)", suppressConsoleLogs(async () => { render( Trigger 1 Link 1 Link 2 Trigger 2 Link 3 Link 4 ) // Open the popover await click(getByText('Trigger 1')) // Ensure the popover is open assertPopoverButton({ state: PopoverState.Visible }) // Focus the second button getByText('Trigger 2')?.focus() // Verify the second button is focused assertActiveElement(getByText('Trigger 2')) // Ensure the first Popover is still open assertPopoverButton({ state: PopoverState.Visible }) assertPopoverPanel({ state: PopoverState.Visible }) // Press shift+tab, to move focus to the last item in the Popover.Panel await press(shift(Keys.Tab), getByText('Trigger 2')) // Verify we are focusing the last link of the first Popover assertActiveElement(getByText('Link 2')) }) ) }) describe('`Space` key', () => { it( 'should be possible to open the popover with Space', suppressConsoleLogs(async () => { render( Trigger Contents ) assertPopoverButton({ state: PopoverState.InvisibleUnmounted, attributes: { id: 'headlessui-popover-button-1' }, }) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) // Focus the button getPopoverButton()?.focus() // Open popover await press(Keys.Space) // Verify it is open assertPopoverButton({ state: PopoverState.Visible }) assertPopoverPanel({ state: PopoverState.Visible, attributes: { id: 'headlessui-popover-panel-2' }, }) }) ) it( 'should not be possible to open the popover with Space when the button is disabled', suppressConsoleLogs(async () => { render( Trigger Contents ) assertPopoverButton({ state: PopoverState.InvisibleUnmounted, attributes: { id: 'headlessui-popover-button-1' }, }) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) // Focus the button getPopoverButton()?.focus() // Try to open the popover await press(Keys.Space) // Verify it is still closed assertPopoverButton({ state: PopoverState.InvisibleUnmounted, attributes: { id: 'headlessui-popover-button-1' }, }) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) }) ) it( 'should be possible to close the popover with Space when the popover is open', suppressConsoleLogs(async () => { render( Trigger Contents ) assertPopoverButton({ state: PopoverState.InvisibleUnmounted, attributes: { id: 'headlessui-popover-button-1' }, }) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) // Focus the button getPopoverButton()?.focus() // Open popover await press(Keys.Space) // Verify it is open assertPopoverButton({ state: PopoverState.Visible }) assertPopoverPanel({ state: PopoverState.Visible, attributes: { id: 'headlessui-popover-panel-2' }, }) // Close popover await press(Keys.Space) // Verify it is closed again assertPopoverButton({ state: PopoverState.InvisibleUnmounted }) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) }) ) it( 'should close other popover menus when we open a new one', suppressConsoleLogs(async () => { render( Trigger 1 Panel 1 Trigger 2 Panel 2 ) // Open the first Popover await click(getByText('Trigger 1')) // Verify the correct popovers are open assertPopoverButton({ state: PopoverState.Visible }, getByText('Trigger 1')) assertPopoverButton({ state: PopoverState.InvisibleUnmounted }, getByText('Trigger 2')) // Focus trigger 2 getByText('Trigger 2')?.focus() // Verify the correct popovers are open assertPopoverButton({ state: PopoverState.Visible }, getByText('Trigger 1')) assertPopoverButton({ state: PopoverState.InvisibleUnmounted }, getByText('Trigger 2')) // Open the second popover await press(Keys.Space) // Verify the correct popovers are open assertPopoverButton({ state: PopoverState.InvisibleUnmounted }, getByText('Trigger 1')) assertPopoverButton({ state: PopoverState.Visible }, getByText('Trigger 2')) }) ) it( 'should close the Popover by pressing `Space` on a Popover.Button inside a Popover.Panel', suppressConsoleLogs(async () => { render( Open Close ) // Open the popover await click(getPopoverButton()) let closeBtn = getByText('Close') expect(closeBtn).not.toHaveAttribute('id') expect(closeBtn).not.toHaveAttribute('aria-controls') expect(closeBtn).not.toHaveAttribute('aria-expanded') // The close button should close the popover await press(Keys.Space, closeBtn) // Verify it is closed assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) // Verify we restored the Open button assertActiveElement(getPopoverButton()) }) ) }) }) describe('Mouse interactions', () => { it( 'should be possible to open a popover on click', suppressConsoleLogs(async () => { render( Trigger Contents ) assertPopoverButton({ state: PopoverState.InvisibleUnmounted, attributes: { id: 'headlessui-popover-button-1' }, }) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) // Open popover await click(getPopoverButton()) // Verify it is open assertPopoverButton({ state: PopoverState.Visible }) assertPopoverPanel({ state: PopoverState.Visible, attributes: { id: 'headlessui-popover-panel-2' }, }) }) ) it( 'should not be possible to open a popover on right click', suppressConsoleLogs(async () => { render( Trigger Contents ) assertPopoverButton({ state: PopoverState.InvisibleUnmounted, attributes: { id: 'headlessui-popover-button-1' }, }) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) // Open popover await click(getPopoverButton(), MouseButton.Right) // Verify it is still closed assertPopoverButton({ state: PopoverState.InvisibleUnmounted, attributes: { id: 'headlessui-popover-button-1' }, }) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) }) ) it( 'should not be possible to open a popover on click when the button is disabled', suppressConsoleLogs(async () => { render( Trigger Contents ) assertPopoverButton({ state: PopoverState.InvisibleUnmounted, attributes: { id: 'headlessui-popover-button-1' }, }) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) // Try to open the popover await click(getPopoverButton()) // Verify it is still closed assertPopoverButton({ state: PopoverState.InvisibleUnmounted, attributes: { id: 'headlessui-popover-button-1' }, }) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) }) ) it( 'should be possible to close a popover on click', suppressConsoleLogs(async () => { render( Trigger Contents ) getPopoverButton()?.focus() // Open popover await click(getPopoverButton()) // Verify it is open assertPopoverButton({ state: PopoverState.Visible }) // Click to close await click(getPopoverButton()) // Verify it is closed assertPopoverButton({ state: PopoverState.InvisibleUnmounted }) assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) }) ) it( 'should be possible to close a Popover using a click on the Popover.Overlay', suppressConsoleLogs(async () => { render( Trigger Contents ) // Open popover await click(getPopoverButton()) // Verify it is open assertPopoverButton({ state: PopoverState.Visible }) // Click the overlay to close await click(getPopoverOverlay()) // Verify it is open assertPopoverButton({ state: PopoverState.InvisibleUnmounted }) }) ) it( 'should be possible to close the popover, and re-focus the button when we click outside on the body element', suppressConsoleLogs(async () => { render( Trigger Contents ) // Open popover await click(getPopoverButton()) // Verify it is open assertPopoverButton({ state: PopoverState.Visible }) // Click the body to close await click(document.body) // Verify it is closed assertPopoverButton({ state: PopoverState.InvisibleUnmounted }) // Verify the button is focused assertActiveElement(getPopoverButton()) }) ) it( 'should be possible to close the popover, and re-focus the button when we click outside on a non-focusable element', suppressConsoleLogs(async () => { render( <> Trigger Contents I am just text ) // Open popover await click(getPopoverButton()) // Verify it is open assertPopoverButton({ state: PopoverState.Visible }) // Click the span to close await click(getByText('I am just text')) // Verify it is closed assertPopoverButton({ state: PopoverState.InvisibleUnmounted }) // Verify the button is focused assertActiveElement(getPopoverButton()) }) ) it( 'should be possible to close the popover, by clicking outside the popover on another focusable element', suppressConsoleLogs(async () => { render( <> Trigger Contents ) // Open popover await click(getPopoverButton()) // Verify it is open assertPopoverButton({ state: PopoverState.Visible }) // Click the extra button to close await click(getByText('Different button')) // Verify it is closed assertPopoverButton({ state: PopoverState.InvisibleUnmounted }) // Verify the other button is focused assertActiveElement(getByText('Different button')) }) ) it( 'should be possible to close the popover, by clicking outside the popover on another element inside a focusable element', suppressConsoleLogs(async () => { let focusFn = jest.fn() render( <> Trigger Contents ) // Open popover await click(getPopoverButton()) // Verify it is open assertPopoverButton({ state: PopoverState.Visible }) // Click the span inside the extra button to close await click(getByText('Different button')) // Verify it is closed assertPopoverButton({ state: PopoverState.InvisibleUnmounted }) // Verify the other button is focused assertActiveElement(document.getElementById('btn')) // Ensure that the focus button only got focus once (first click) expect(focusFn).toHaveBeenCalledTimes(1) }) ) it( 'should be possible to close the Popover by clicking on a Popover.Button inside a Popover.Panel', suppressConsoleLogs(async () => { render( Open Close ) // Open the popover await click(getPopoverButton()) let closeBtn = getByText('Close') expect(closeBtn).not.toHaveAttribute('id') expect(closeBtn).not.toHaveAttribute('aria-controls') expect(closeBtn).not.toHaveAttribute('aria-expanded') // The close button should close the popover await click(closeBtn) // Verify it is closed assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) // Verify we restored the Open button assertActiveElement(getPopoverButton()) }) ) })