import { render } from '@testing-library/react' import React, { createElement, Suspense, useEffect, useRef } from 'react' import { assertActiveElement, assertDisclosureButton, assertDisclosurePanel, DisclosureState, getByText, getDisclosureButton, getDisclosurePanel, } from '../../test-utils/accessibility-assertions' import { click, focus, Keys, MouseButton, press } from '../../test-utils/interactions' import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs' import { Transition } from '../transitions/transition' import { Disclosure } from './disclosure' jest.mock('../../hooks/use-id') afterAll(() => jest.restoreAllMocks()) function nextFrame() { return new Promise((resolve) => { requestAnimationFrame(() => { requestAnimationFrame(() => { resolve() }) }) }) } describe('Safe guards', () => { it.each([ ['Disclosure.Button', Disclosure.Button], ['Disclosure.Panel', Disclosure.Panel], ])( '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 Disclosure without crashing', suppressConsoleLogs(async () => { render( Trigger Contents ) assertDisclosureButton({ state: DisclosureState.InvisibleUnmounted, attributes: { id: 'headlessui-disclosure-button-1' }, }) assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) }) ) }) describe('Rendering', () => { describe('Disclosure', () => { it( 'should be possible to render a Disclosure using a render prop', suppressConsoleLogs(async () => { render( {({ open }) => ( <> Trigger Panel is: {open ? 'open' : 'closed'} )} ) assertDisclosureButton({ state: DisclosureState.InvisibleUnmounted, attributes: { id: 'headlessui-disclosure-button-1' }, }) assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) await click(getDisclosureButton()) assertDisclosureButton({ state: DisclosureState.Visible, attributes: { id: 'headlessui-disclosure-button-1' }, }) assertDisclosurePanel({ state: DisclosureState.Visible, textContent: 'Panel is: open' }) }) ) it('should be possible to render a Disclosure in an open state by default', async () => { render( {({ open }) => ( <> Trigger Panel is: {open ? 'open' : 'closed'} )} ) assertDisclosureButton({ state: DisclosureState.Visible, attributes: { id: 'headlessui-disclosure-button-1' }, }) assertDisclosurePanel({ state: DisclosureState.Visible, textContent: 'Panel is: open' }) await click(getDisclosureButton()) assertDisclosureButton({ state: DisclosureState.InvisibleUnmounted }) }) it( 'should expose a close function that closes the disclosure', suppressConsoleLogs(async () => { render( {({ close }) => ( <> Trigger )} ) // Focus the button await focus(getDisclosureButton()) // Ensure the button is focused assertActiveElement(getDisclosureButton()) // Open the disclosure await click(getDisclosureButton()) // Ensure we can click the close button await click(getByText('Close me')) // Ensure the disclosure is closed assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) // Ensure the Disclosure.Button got the restored focus assertActiveElement(getByText('Trigger')) }) ) it( 'should expose a close function that closes the disclosure and restores to a specific element', suppressConsoleLogs(async () => { render( <> {({ close }) => ( <> Trigger )} ) // Focus the button await focus(getDisclosureButton()) // Ensure the button is focused assertActiveElement(getDisclosureButton()) // Open the disclosure await click(getDisclosureButton()) // Ensure we can click the close button await click(getByText('Close me')) // Ensure the disclosure is closed assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) // Ensure the restoreable button got the restored focus assertActiveElement(getByText('restoreable')) }) ) it( 'should expose a close function that closes the disclosure and restores to a ref', suppressConsoleLogs(async () => { function Example() { let elementRef = useRef(null) return ( <> {({ close }) => ( <> Trigger )} ) } render() // Focus the button await focus(getDisclosureButton()) // Ensure the button is focused assertActiveElement(getDisclosureButton()) // Open the disclosure await click(getDisclosureButton()) // Ensure we can click the close button await click(getByText('Close me')) // Ensure the disclosure is closed assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) // Ensure the restoreable button got the restored focus assertActiveElement(getByText('restoreable')) }) ) it('should not crash when using Suspense boundaries', async () => { render( Click me!

Hi there

) }) }) describe('Disclosure.Button', () => { it( 'should be possible to render a Disclosure.Button using a render prop', suppressConsoleLogs(async () => { render( {JSON.stringify} ) assertDisclosureButton({ state: DisclosureState.InvisibleUnmounted, attributes: { id: 'headlessui-disclosure-button-1' }, textContent: JSON.stringify({ open: false }), }) assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) await click(getDisclosureButton()) assertDisclosureButton({ state: DisclosureState.Visible, attributes: { id: 'headlessui-disclosure-button-1' }, textContent: JSON.stringify({ open: true }), }) assertDisclosurePanel({ state: DisclosureState.Visible }) }) ) it( 'should be possible to render a Disclosure.Button using a render prop and an `as` prop', suppressConsoleLogs(async () => { render( {JSON.stringify} ) assertDisclosureButton({ state: DisclosureState.InvisibleUnmounted, attributes: { id: 'headlessui-disclosure-button-1' }, textContent: JSON.stringify({ open: false }), }) assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) await click(getDisclosureButton()) assertDisclosureButton({ state: DisclosureState.Visible, attributes: { id: 'headlessui-disclosure-button-1' }, textContent: JSON.stringify({ open: true }), }) assertDisclosurePanel({ state: DisclosureState.Visible }) }) ) describe('`type` attribute', () => { it('should set the `type` to "button" by default', async () => { render( Trigger ) expect(getDisclosureButton()).toHaveAttribute('type', 'button') }) it('should not set the `type` to "button" if it already contains a `type`', async () => { render( Trigger ) expect(getDisclosureButton()).toHaveAttribute('type', 'submit') }) it('should set the `type` to "button" when using the `as` prop which resolves to a "button"', async () => { let CustomButton = React.forwardRef((props, ref) => ( }
) // Focus the button await focus(getDisclosureButton()) // Ensure the button is focused assertActiveElement(getDisclosureButton()) // Open the disclosure await click(getDisclosureButton()) // Ensure we can click the close button await click(getByText('Close me')) // Ensure the disclosure is closed assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) // Ensure the Disclosure.Button got the restored focus assertActiveElement(getByText('Trigger')) }) ) it( 'should expose a close function that closes the disclosure and restores to a specific element', suppressConsoleLogs(async () => { render( <> Trigger {({ close }) => ( )} ) // Focus the button await focus(getDisclosureButton()) // Ensure the button is focused assertActiveElement(getDisclosureButton()) // Open the disclosure await click(getDisclosureButton()) // Ensure we can click the close button await click(getByText('Close me')) // Ensure the disclosure is closed assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) // Ensure the restoreable button got the restored focus assertActiveElement(getByText('restoreable')) }) ) it( 'should expose a close function that closes the disclosure and restores to a ref', suppressConsoleLogs(async () => { function Example() { let elementRef = useRef(null) return ( <> Trigger {({ close }) => } ) } render() // Focus the button await focus(getDisclosureButton()) // Ensure the button is focused assertActiveElement(getDisclosureButton()) // Open the disclosure await click(getDisclosureButton()) // Ensure we can click the close button await click(getByText('Close me')) // Ensure the disclosure is closed assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) // Ensure the restoreable button got the restored focus assertActiveElement(getByText('restoreable')) }) ) }) }) 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 control the Disclosure.Panel by wrapping it in a Transition component', suppressConsoleLogs(async () => { let orderFn = jest.fn() render( Trigger ) // Verify the Disclosure is hidden assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) // Open the Disclosure component await click(getDisclosureButton()) // Verify the Disclosure is visible assertDisclosurePanel({ state: DisclosureState.Visible }) // Unmount the full tree await click(getDisclosureButton()) // Wait for all transitions to finish await nextFrame() await nextFrame() // Verify that we tracked the `mounts` and `unmounts` in the correct order expect(orderFn.mock.calls).toEqual([ ['Mounting - Disclosure'], ['Mounting - Transition'], ['Mounting - Transition.Child'], ['Unmounting - Transition'], ['Unmounting - Transition.Child'], ]) }) ) }) describe('Keyboard interactions', () => { describe('`Enter` key', () => { it( 'should be possible to open the Disclosure with Enter', suppressConsoleLogs(async () => { render( Trigger Contents ) assertDisclosureButton({ state: DisclosureState.InvisibleUnmounted, attributes: { id: 'headlessui-disclosure-button-1' }, }) assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) // Focus the button await focus(getDisclosureButton()) // Open disclosure await press(Keys.Enter) // Verify it is open assertDisclosureButton({ state: DisclosureState.Visible }) assertDisclosurePanel({ state: DisclosureState.Visible, attributes: { id: 'headlessui-disclosure-panel-2' }, }) // Close disclosure await press(Keys.Enter) assertDisclosureButton({ state: DisclosureState.InvisibleUnmounted }) }) ) it( 'should not be possible to open the disclosure with Enter when the button is disabled', suppressConsoleLogs(async () => { render( Trigger Content ) assertDisclosureButton({ state: DisclosureState.InvisibleUnmounted, attributes: { id: 'headlessui-disclosure-button-1' }, }) assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) // Focus the button await focus(getDisclosureButton()) // Try to open the disclosure await press(Keys.Enter) // Verify it is still closed assertDisclosureButton({ state: DisclosureState.InvisibleUnmounted, attributes: { id: 'headlessui-disclosure-button-1' }, }) assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) }) ) it( 'should be possible to close the disclosure with Enter when the disclosure is open', suppressConsoleLogs(async () => { render( Trigger Contents ) assertDisclosureButton({ state: DisclosureState.InvisibleUnmounted, attributes: { id: 'headlessui-disclosure-button-1' }, }) assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) // Focus the button await focus(getDisclosureButton()) // Open disclosure await press(Keys.Enter) // Verify it is open assertDisclosureButton({ state: DisclosureState.Visible }) assertDisclosurePanel({ state: DisclosureState.Visible, attributes: { id: 'headlessui-disclosure-panel-2' }, }) // Close disclosure await press(Keys.Enter) // Verify it is closed again assertDisclosureButton({ state: DisclosureState.InvisibleUnmounted }) assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) }) ) }) describe('`Space` key', () => { it( 'should be possible to open the disclosure with Space', suppressConsoleLogs(async () => { render( Trigger Contents ) assertDisclosureButton({ state: DisclosureState.InvisibleUnmounted, attributes: { id: 'headlessui-disclosure-button-1' }, }) assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) // Focus the button await focus(getDisclosureButton()) // Open disclosure await press(Keys.Space) // Verify it is open assertDisclosureButton({ state: DisclosureState.Visible }) assertDisclosurePanel({ state: DisclosureState.Visible, attributes: { id: 'headlessui-disclosure-panel-2' }, }) }) ) it( 'should not be possible to open the disclosure with Space when the button is disabled', suppressConsoleLogs(async () => { render( Trigger Contents ) assertDisclosureButton({ state: DisclosureState.InvisibleUnmounted, attributes: { id: 'headlessui-disclosure-button-1' }, }) assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) // Focus the button await focus(getDisclosureButton()) // Try to open the disclosure await press(Keys.Space) // Verify it is still closed assertDisclosureButton({ state: DisclosureState.InvisibleUnmounted, attributes: { id: 'headlessui-disclosure-button-1' }, }) assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) }) ) it( 'should be possible to close the disclosure with Space when the disclosure is open', suppressConsoleLogs(async () => { render( Trigger Contents ) assertDisclosureButton({ state: DisclosureState.InvisibleUnmounted, attributes: { id: 'headlessui-disclosure-button-1' }, }) assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) // Focus the button await focus(getDisclosureButton()) // Open disclosure await press(Keys.Space) // Verify it is open assertDisclosureButton({ state: DisclosureState.Visible }) assertDisclosurePanel({ state: DisclosureState.Visible, attributes: { id: 'headlessui-disclosure-panel-2' }, }) // Close disclosure await press(Keys.Space) // Verify it is closed again assertDisclosureButton({ state: DisclosureState.InvisibleUnmounted }) assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) }) ) }) }) describe('Mouse interactions', () => { it( 'should be possible to open a disclosure on click', suppressConsoleLogs(async () => { render( Trigger Contents ) assertDisclosureButton({ state: DisclosureState.InvisibleUnmounted, attributes: { id: 'headlessui-disclosure-button-1' }, }) assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) // Open disclosure await click(getDisclosureButton()) // Verify it is open assertDisclosureButton({ state: DisclosureState.Visible }) assertDisclosurePanel({ state: DisclosureState.Visible, attributes: { id: 'headlessui-disclosure-panel-2' }, }) }) ) it( 'should not be possible to open a disclosure on right click', suppressConsoleLogs(async () => { render( Trigger Contents ) assertDisclosureButton({ state: DisclosureState.InvisibleUnmounted, attributes: { id: 'headlessui-disclosure-button-1' }, }) assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) // Open disclosure await click(getDisclosureButton(), MouseButton.Right) // Verify it is still closed assertDisclosureButton({ state: DisclosureState.InvisibleUnmounted, attributes: { id: 'headlessui-disclosure-button-1' }, }) assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) }) ) it( 'should not be possible to open a disclosure on click when the button is disabled', suppressConsoleLogs(async () => { render( Trigger Contents ) assertDisclosureButton({ state: DisclosureState.InvisibleUnmounted, attributes: { id: 'headlessui-disclosure-button-1' }, }) assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) // Try to open the disclosure await click(getDisclosureButton()) // Verify it is still closed assertDisclosureButton({ state: DisclosureState.InvisibleUnmounted, attributes: { id: 'headlessui-disclosure-button-1' }, }) assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) }) ) it( 'should be possible to close a disclosure on click', suppressConsoleLogs(async () => { render( Trigger Contents ) // Open disclosure await click(getDisclosureButton()) // Verify it is open assertDisclosureButton({ state: DisclosureState.Visible }) // Click to close await click(getDisclosureButton()) // Verify it is closed assertDisclosureButton({ state: DisclosureState.InvisibleUnmounted }) assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) }) ) it( 'should be possible to close the Disclosure by clicking on a Disclosure.Button inside a Disclosure.Panel', suppressConsoleLogs(async () => { render( Open Close ) // Open the disclosure await click(getDisclosureButton()) 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 disclosure await click(closeBtn) // Verify it is closed assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted }) // Verify we restored the Open button assertActiveElement(getDisclosureButton()) }) ) })