import { render } from '@testing-library/react' import React, { createElement, Fragment, useCallback, useEffect, useRef, useState } from 'react' import { createPortal } from 'react-dom' import { OpenClosedProvider, State } from '../../internal/open-closed' import { assertActiveElement, assertDialog, assertDialogDescription, assertDialogOverlay, assertDialogTitle, assertPopoverPanel, DialogState, getByText, getDialog, getDialogBackdrop, getDialogOverlay, getDialogOverlays, getDialogs, getPopoverButton, PopoverState, } from '../../test-utils/accessibility-assertions' import { click, focus, Keys, mouseDrag, press, shift } from '../../test-utils/interactions' import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs' import type { PropsOf } from '../../types' import { Popover } from '../popover/popover' import { Transition } from '../transitions/transition' import { Dialog } from './dialog' jest.mock('../../hooks/use-id') // @ts-expect-error global.ResizeObserver = class FakeResizeObserver { observe() {} disconnect() {} } afterAll(() => jest.restoreAllMocks()) function nextFrame() { return frames(1) } async function frames(count: number) { for (let n = 0; n <= count; n++) { await new Promise((resolve) => requestAnimationFrame(() => resolve())) } } function TabSentinel(props: PropsOf<'button'>) { return

Contents

) assertDialog({ state: DialogState.InvisibleUnmounted, attributes: { id: 'headlessui-dialog-1' }, }) }) ) }) describe('Rendering', () => { describe('Dialog', () => { it( 'should complain when the `open` and `onClose` prop are missing', suppressConsoleLogs(async () => { // @ts-expect-error expect(() => render()).toThrowErrorMatchingInlineSnapshot( `"You have to provide an \`open\` and an \`onClose\` prop to the \`Dialog\` component."` ) expect.hasAssertions() }) ) it( 'should be able to explicitly choose role=dialog', suppressConsoleLogs(async () => { function Example() { let [isOpen, setIsOpen] = useState(false) return ( <> ) } render() assertDialog({ state: DialogState.InvisibleUnmounted }) await click(document.getElementById('trigger')) await nextFrame() assertDialog({ state: DialogState.Visible, attributes: { role: 'dialog' } }) }) ) it( 'should be able to explicitly choose role=alertdialog', suppressConsoleLogs(async () => { function Example() { let [isOpen, setIsOpen] = useState(false) return ( <> ) } render() assertDialog({ state: DialogState.InvisibleUnmounted }) await click(document.getElementById('trigger')) await nextFrame() assertDialog({ state: DialogState.Visible, attributes: { role: 'alertdialog' } }) }) ) it( 'should fall back to role=dialog for an invalid role', suppressConsoleLogs(async () => { function Example() { let [isOpen, setIsOpen] = useState(false) return ( <> ) } render() assertDialog({ state: DialogState.InvisibleUnmounted }) await click(document.getElementById('trigger')) await nextFrame() assertDialog({ state: DialogState.Visible, attributes: { role: 'dialog' } }) }, 'warn') ) it( 'should complain when an `open` prop is provided without an `onClose` prop', suppressConsoleLogs(async () => { // @ts-expect-error expect(() => render()).toThrowErrorMatchingInlineSnapshot( `"You provided an \`open\` prop to the \`Dialog\`, but forgot an \`onClose\` prop."` ) expect.hasAssertions() }) ) it( 'should complain when an `onClose` prop is provided without an `open` prop', suppressConsoleLogs(async () => { expect(() => render( {}} />) ).toThrowErrorMatchingInlineSnapshot( `"You provided an \`onClose\` prop to the \`Dialog\`, but forgot an \`open\` prop."` ) expect.hasAssertions() }) ) it( 'should complain when an `open` prop is not a boolean', suppressConsoleLogs(async () => { expect(() => // @ts-expect-error render() ).toThrowErrorMatchingInlineSnapshot( `"You provided an \`open\` prop to the \`Dialog\`, but the value is not a boolean. Received: null"` ) expect.hasAssertions() }) ) it( 'should complain when an `onClose` prop is not a function', suppressConsoleLogs(async () => { expect(() => // @ts-expect-error render() ).toThrowErrorMatchingInlineSnapshot( `"You provided an \`onClose\` prop to the \`Dialog\`, but the value is not a function. Received: null"` ) expect.hasAssertions() }) ) it( 'should be possible to render a Dialog using a render prop', suppressConsoleLogs(async () => { function Example() { let [isOpen, setIsOpen] = useState(false) return ( <> {(data) => ( <>
{JSON.stringify(data)}
)}
) } render() assertDialog({ state: DialogState.InvisibleUnmounted }) await click(document.getElementById('trigger')) assertDialog({ state: DialogState.Visible, textContent: JSON.stringify({ open: true }) }) }) ) it('should be possible to always render the Dialog if we provide it a `static` prop (and enable focus trapping based on `open`)', async () => { let focusCounter = jest.fn() render( <>

Contents

) await nextFrame() // Let's verify that the Dialog is already there expect(getDialog()).not.toBe(null) expect(focusCounter).toHaveBeenCalledTimes(1) }) it('should be possible to always render the Dialog if we provide it a `static` prop (and disable focus trapping based on `open`)', () => { let focusCounter = jest.fn() render( <>

Contents

) // Let's verify that the Dialog is already there expect(getDialog()).not.toBe(null) expect(focusCounter).toHaveBeenCalledTimes(0) }) it('should be possible to use a different render strategy for the Dialog', async () => { function Example() { let [isOpen, setIsOpen] = useState(false) return ( <> ) } render() await nextFrame() assertDialog({ state: DialogState.InvisibleHidden }) // Let's open the Dialog, to see if it is not hidden anymore await click(document.getElementById('trigger')) assertDialog({ state: DialogState.Visible }) // Let's close the Dialog await press(Keys.Escape) assertDialog({ state: DialogState.InvisibleHidden }) }) it( 'should add a scroll lock to the html tag', suppressConsoleLogs(async () => { function Example() { let [isOpen, setIsOpen] = useState(false) return ( <> ) } render() // No overflow yet expect(document.documentElement.style.overflow).toBe('') let btn = document.getElementById('trigger') // Open the dialog await click(btn) // Expect overflow expect(document.documentElement.style.overflow).toBe('hidden') }) ) it( 'should wait to add a scroll lock to the html tag when unmount is false in a Transition', suppressConsoleLogs(async () => { function Example() { let [isOpen, setIsOpen] = useState(false) return ( <> setIsOpen(false)} unmount={false}> ) } render() // No overflow yet expect(document.documentElement.style.overflow).toBe('') let btn = document.getElementById('trigger') // Open the dialog await click(btn) // Expect overflow expect(document.documentElement.style.overflow).toBe('hidden') }) ) it( 'scroll locking should work when transitioning between dialogs', suppressConsoleLogs(async () => { // While we don't support multiple dialogs // We at least want to work towards supporting it at some point // The first step is just making sure that scroll locking works // when there are multiple dialogs open at the same time function Example() { let [dialogs, setDialogs] = useState([]) let toggle = useCallback( (id: string, state: 'open' | 'close') => { if (state === 'open' && !dialogs.includes(id)) { setDialogs([id]) } else if (state === 'close' && dialogs.includes(id)) { setDialogs(dialogs.filter((x) => x !== id)) } }, [dialogs] ) return ( <> ) } function DialogWrapper({ id, dialogs, toggle, }: { id: string dialogs: string[] toggle: (id: string, state: 'open' | 'close') => void }) { return ( <> toggle(id, 'close')}> ) } render() // No overflow yet expect(document.documentElement.style.overflow).toBe('') let open1 = () => document.getElementById('open_d1') let open2 = () => document.getElementById('open_d2') let open3 = () => document.getElementById('open_d3') let close3 = () => document.getElementById('close_d3') // Open the dialog & expect overflow await click(open1()) await frames(2) expect(document.documentElement.style.overflow).toBe('hidden') // Open the dialog & expect overflow await click(open2()) await frames(2) expect(document.documentElement.style.overflow).toBe('hidden') // Open the dialog & expect overflow await click(open3()) await frames(2) expect(document.documentElement.style.overflow).toBe('hidden') // At this point only the last dialog should be open // Close the dialog & dont expect overflow await click(close3()) await frames(2) expect(document.documentElement.style.overflow).toBe('') }) ) it( 'should remove the scroll lock when the open closed state is `Closing`', suppressConsoleLogs(async () => { function Example({ value = State.Open }) { return ( {}}> ) } let { rerender } = render() // The overflow should be there expect(document.documentElement.style.overflow).toBe('hidden') // Re-render but with the `Closing` state rerender() // The moment the dialog is closing, the overflow should be gone expect(document.documentElement.style.overflow).toBe('') }) ) it( 'should not have a scroll lock when the transition marked as not shown', suppressConsoleLogs(async () => { function Example() { return ( {}}> ) } render() await nextFrame() // The overflow should NOT be there expect(document.documentElement.style.overflow).toBe('') }) ) }) describe('Dialog.Overlay', () => { it( 'should be possible to render Dialog.Overlay using a render prop', suppressConsoleLogs(async () => { let overlay = jest.fn().mockReturnValue(null) function Example() { let [isOpen, setIsOpen] = useState(false) return ( <> {overlay} ) } render() assertDialogOverlay({ state: DialogState.InvisibleUnmounted, attributes: { id: 'headlessui-dialog-overlay-2' }, }) await click(document.getElementById('trigger')) assertDialogOverlay({ state: DialogState.Visible, attributes: { id: 'headlessui-dialog-overlay-2' }, }) expect(overlay).toHaveBeenCalledWith({ open: true }) }) ) }) describe('Dialog.Backdrop', () => { it( 'should throw an error if a Dialog.Backdrop is used without a Dialog.Panel', suppressConsoleLogs(async () => { function Example() { let [isOpen, setIsOpen] = useState(false) return ( <> ) } render() try { await click(document.getElementById('trigger')) expect(true).toBe(false) } catch (e: unknown) { expect((e as Error).message).toBe( 'A component is being used, but a component is missing.' ) } }) ) it( 'should not throw an error if a Dialog.Backdrop is used with a Dialog.Panel', suppressConsoleLogs(async () => { function Example() { let [isOpen, setIsOpen] = useState(false) return ( <> ) } render() await click(document.getElementById('trigger')) }) ) it( 'should portal the Dialog.Backdrop', suppressConsoleLogs(async () => { function Example() { let [isOpen, setIsOpen] = useState(false) return ( <> ) } render() await click(document.getElementById('trigger')) let dialog = getDialog() let backdrop = getDialogBackdrop() expect(dialog).not.toBe(null) dialog = dialog as HTMLElement expect(backdrop).not.toBe(null) backdrop = backdrop as HTMLElement // It should not be nested let position = dialog.compareDocumentPosition(backdrop) expect(position & Node.DOCUMENT_POSITION_CONTAINED_BY).not.toBe( Node.DOCUMENT_POSITION_CONTAINED_BY ) // It should be a sibling expect(position & Node.DOCUMENT_POSITION_FOLLOWING).toBe(Node.DOCUMENT_POSITION_FOLLOWING) }) ) }) describe('Dialog.Title', () => { it( 'should be possible to render Dialog.Title using a render prop', suppressConsoleLogs(async () => { render( {JSON.stringify} ) await nextFrame() assertDialog({ state: DialogState.Visible, attributes: { id: 'headlessui-dialog-1' }, }) assertDialogTitle({ state: DialogState.Visible, textContent: JSON.stringify({ open: true }), }) }) ) }) describe('Dialog.Description', () => { it( 'should be possible to render Dialog.Description using a render prop', suppressConsoleLogs(async () => { render( {JSON.stringify} ) await nextFrame() assertDialog({ state: DialogState.Visible, attributes: { id: 'headlessui-dialog-1' }, }) assertDialogDescription({ state: DialogState.Visible, textContent: JSON.stringify({ open: true }), }) }) ) }) }) describe('Composition', () => { it( 'should be possible to open a dialog from inside a Popover (and then close it)', suppressConsoleLogs(async () => { function Example() { let [isDialogOpen, setIsDialogOpen] = useState(false) return (
Open Popover
) } render() await nextFrame() // Nothing is open initially assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) assertDialog({ state: DialogState.InvisibleUnmounted }) assertActiveElement(document.body) // Open the popover await click(getPopoverButton()) // The popover should be open but the dialog should not assertPopoverPanel({ state: PopoverState.Visible }) assertDialog({ state: DialogState.InvisibleUnmounted }) assertActiveElement(getPopoverButton()) // Open the dialog from inside the popover await click(document.getElementById('openDialog')) // The dialog should be open but the popover should not assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) assertDialog({ state: DialogState.Visible }) assertActiveElement(document.getElementById('closeDialog')) // Close the dialog from inside itself await click(document.getElementById('closeDialog')) // Nothing should be open assertPopoverPanel({ state: PopoverState.InvisibleUnmounted }) assertDialog({ state: DialogState.InvisibleUnmounted }) assertActiveElement(getPopoverButton()) }) ) it( 'should be possible to open the Dialog via a Transition component', suppressConsoleLogs(async () => { render( {JSON.stringify} ) await nextFrame() assertDialog({ state: DialogState.Visible }) assertDialogDescription({ state: DialogState.Visible, textContent: JSON.stringify({ open: true }), }) }) ) it( 'should be possible to close the Dialog via a Transition component', suppressConsoleLogs(async () => { render( {JSON.stringify} ) await nextFrame() assertDialog({ state: DialogState.InvisibleUnmounted }) }) ) }) describe('Keyboard interactions', () => { describe('`Escape` key', () => { it( 'should be possible to close the dialog with Escape', suppressConsoleLogs(async () => { function Example() { let [isOpen, setIsOpen] = useState(false) return ( <> Contents ) } render() assertDialog({ state: DialogState.InvisibleUnmounted }) // Open dialog await click(document.getElementById('trigger')) // Verify it is open assertDialog({ state: DialogState.Visible, attributes: { id: 'headlessui-dialog-1' }, }) // Close dialog await press(Keys.Escape) // Verify it is close assertDialog({ state: DialogState.InvisibleUnmounted }) }) ) it( 'should be possible to close the dialog with Escape, when a field is focused', suppressConsoleLogs(async () => { function Example() { let [isOpen, setIsOpen] = useState(false) return ( <> Contents ) } render() assertDialog({ state: DialogState.InvisibleUnmounted }) // Open dialog await click(document.getElementById('trigger')) // Verify it is open assertDialog({ state: DialogState.Visible, attributes: { id: 'headlessui-dialog-1' }, }) // Close dialog await press(Keys.Escape) // Verify it is close assertDialog({ state: DialogState.InvisibleUnmounted }) }) ) it( 'should not be possible to close the dialog with Escape, when a field is focused but cancels the event', suppressConsoleLogs(async () => { function Example() { let [isOpen, setIsOpen] = useState(false) return ( <> Contents { event.preventDefault() event.stopPropagation() }} /> ) } render() assertDialog({ state: DialogState.InvisibleUnmounted }) // Open dialog await click(document.getElementById('trigger')) // Verify it is open assertDialog({ state: DialogState.Visible, attributes: { id: 'headlessui-dialog-1' }, }) // Try to close the dialog await press(Keys.Escape) // Verify it is still open assertDialog({ state: DialogState.Visible }) }) ) }) describe('`Tab` key', () => { it( 'should be possible to tab around when using the initialFocus ref', suppressConsoleLogs(async () => { function Example() { let [isOpen, setIsOpen] = useState(false) let initialFocusRef = useRef(null) return ( <> Contents ) } render() assertDialog({ state: DialogState.InvisibleUnmounted }) // Open dialog await click(document.getElementById('trigger')) // Verify it is open assertDialog({ state: DialogState.Visible, attributes: { id: 'headlessui-dialog-1' }, }) // Verify that the input field is focused assertActiveElement(document.getElementById('b')) // Verify that we can tab around await press(Keys.Tab) assertActiveElement(document.getElementById('a')) // Verify that we can tab around await press(Keys.Tab) assertActiveElement(document.getElementById('b')) // Verify that we can tab around await press(Keys.Tab) assertActiveElement(document.getElementById('a')) }) ) it( 'should not escape the FocusTrap when there is only 1 focusable element (going forwards)', suppressConsoleLogs(async () => { function Example() { let [isOpen, setIsOpen] = useState(false) return ( <> ) } render() assertDialog({ state: DialogState.InvisibleUnmounted }) // Open dialog await click(document.getElementById('trigger')) // Verify it is open assertDialog({ state: DialogState.Visible, attributes: { id: 'headlessui-dialog-1' }, }) // Verify that the input field is focused assertActiveElement(document.getElementById('a')) // Verify that we stay within the Dialog await press(Keys.Tab) assertActiveElement(document.getElementById('a')) // Verify that we stay within the Dialog await press(Keys.Tab) assertActiveElement(document.getElementById('a')) // Verify that we stay within the Dialog await press(Keys.Tab) assertActiveElement(document.getElementById('a')) }) ) it( 'should not escape the FocusTrap when there is only 1 focusable element (going backwards)', suppressConsoleLogs(async () => { function Example() { let [isOpen, setIsOpen] = useState(false) return ( <> ) } render() assertDialog({ state: DialogState.InvisibleUnmounted }) // Open dialog await click(document.getElementById('trigger')) // Verify it is open assertDialog({ state: DialogState.Visible, attributes: { id: 'headlessui-dialog-1' }, }) // Verify that the input field is focused assertActiveElement(document.getElementById('a')) // Verify that we stay within the Dialog await press(shift(Keys.Tab)) assertActiveElement(document.getElementById('a')) // Verify that we stay within the Dialog await press(shift(Keys.Tab)) assertActiveElement(document.getElementById('a')) // Verify that we stay within the Dialog await press(shift(Keys.Tab)) assertActiveElement(document.getElementById('a')) }) ) }) }) describe('Mouse interactions', () => { it( 'should be possible to close a Dialog using a click on the Dialog.Overlay', suppressConsoleLogs(async () => { function Example() { let [isOpen, setIsOpen] = useState(false) return ( <> Contents ) } render() // Open dialog await click(document.getElementById('trigger')) // Verify it is open assertDialog({ state: DialogState.Visible }) // Click to close await click(getDialogOverlay()) // Verify it is closed assertDialog({ state: DialogState.InvisibleUnmounted }) }) ) it( 'should not close the Dialog when clicking on contents of the Dialog.Overlay', suppressConsoleLogs(async () => { function Example() { let [isOpen, setIsOpen] = useState(false) return ( <> Contents ) } render() // Open dialog await click(document.getElementById('trigger')) // Verify it is open assertDialog({ state: DialogState.Visible }) // Click on an element inside the overlay await click(getByText('hi')) // Verify it is still open assertDialog({ state: DialogState.Visible }) }) ) it( 'should be possible to close the dialog, and re-focus the button when we click outside on the body element', suppressConsoleLogs(async () => { function Example() { let [isOpen, setIsOpen] = useState(false) return ( <> Contents ) } render() // Open dialog await click(getByText('Trigger')) // Verify it is open assertDialog({ state: DialogState.Visible }) // Click the body to close await click(document.body) // Verify it is closed assertDialog({ state: DialogState.InvisibleUnmounted }) // Verify the button is focused assertActiveElement(getByText('Trigger')) }) ) it( 'should be possible to close the dialog, and keep focus on the focusable element', suppressConsoleLogs(async () => { function Example() { let [isOpen, setIsOpen] = useState(false) return ( <> Contents ) } render() // Open dialog await click(getByText('Trigger')) // Verify it is open assertDialog({ state: DialogState.Visible }) // Click the button to close (outside click) await click(getByText('Hello')) // Verify it is closed assertDialog({ state: DialogState.InvisibleUnmounted }) // Verify the button is focused assertActiveElement(getByText('Hello')) }) ) it( 'should stop propagating click events when clicking on the Dialog.Overlay', suppressConsoleLogs(async () => { let wrapperFn = jest.fn() function Example() { let [isOpen, setIsOpen] = useState(true) return (
Contents
) } render() await nextFrame() // Verify it is open assertDialog({ state: DialogState.Visible }) // Verify that the wrapper function has not been called yet expect(wrapperFn).toHaveBeenCalledTimes(0) // Click the Dialog.Overlay to close the Dialog await click(getDialogOverlay()) // Verify it is closed assertDialog({ state: DialogState.InvisibleUnmounted }) // Verify that the wrapper function has not been called yet expect(wrapperFn).toHaveBeenCalledTimes(0) }) ) it( 'should be possible to submit a form inside a Dialog', suppressConsoleLogs(async () => { let submitFn = jest.fn() function Example() { let [isOpen, setIsOpen] = useState(true) return (
) } render() await nextFrame() // Verify it is open assertDialog({ state: DialogState.Visible }) // Submit the form await click(getByText('Submit')) // Verify that the submitFn function has been called expect(submitFn).toHaveBeenCalledTimes(1) }) ) it( 'should stop propagating click events when clicking on an element inside the Dialog', suppressConsoleLogs(async () => { let wrapperFn = jest.fn() function Example() { let [isOpen, setIsOpen] = useState(true) return (
Contents
) } render() await nextFrame() // Verify it is open assertDialog({ state: DialogState.Visible }) // Verify that the wrapper function has not been called yet expect(wrapperFn).toHaveBeenCalledTimes(0) // Click the button inside the the Dialog await click(getByText('Inside')) // Verify it is closed assertDialog({ state: DialogState.InvisibleUnmounted }) // Verify that the wrapper function has not been called yet expect(wrapperFn).toHaveBeenCalledTimes(0) }) ) it( 'should should be possible to click on removed elements without closing the Dialog', suppressConsoleLogs(async () => { function Example() { let [isOpen, setIsOpen] = useState(true) let wrapper = useRef(null) return (
Contents
) } render() await nextFrame() // Verify it is open assertDialog({ state: DialogState.Visible }) // Click the button inside the the Dialog await click(getByText('Inside')) // Verify it is still open assertDialog({ state: DialogState.Visible }) }) ) it( 'should be possible to click on elements created by third party libraries', suppressConsoleLogs(async () => { let fn = jest.fn() function ThirdPartyLibrary() { return createPortal( <> , document.body ) } function Example() { let [isOpen, setIsOpen] = useState(true) return (
Main app
Contents
) } render() await nextFrame() // Verify it is open assertDialog({ state: DialogState.Visible }) // Click the button inside the 3rd party library await click(document.querySelector('[data-lib]')) // Verify we clicked on the 3rd party button expect(fn).toHaveBeenCalledTimes(1) // Verify the dialog is still open assertDialog({ state: DialogState.Visible }) }) ) it( 'should be possible to focus elements created by third party libraries', suppressConsoleLogs(async () => { let fn = jest.fn() let handleFocus = jest.fn() function ThirdPartyLibrary() { return createPortal( <> , document.body ) } function Example() { let [isOpen, setIsOpen] = useState(true) return (
Main app
Contents
) } render() await nextFrame() // Verify it is open assertDialog({ state: DialogState.Visible }) // Click the button inside the 3rd party library await focus(document.querySelector('[data-lib]')) // Verify that the focus is on the 3rd party button, and that we are not redirecting focus to // the dialog again. assertActiveElement(document.querySelector('[data-lib]')) // This should only have been called once (when opening the Dialog) expect(handleFocus).toHaveBeenCalledTimes(1) // Verify the dialog is still open assertDialog({ state: DialogState.Visible }) }) ) it( 'should be possible to click elements inside the dialog when they reside inside a shadow boundary', suppressConsoleLogs(async () => { let fn = jest.fn() function ShadowChildren({ id, buttonId }: { id: string; buttonId: string }) { let container = useRef(null) useEffect(() => { if (!container.current || container.current.shadowRoot) { return } let shadowRoot = container.current.attachShadow({ mode: 'open' }) let button = document.createElement('button') button.id = buttonId button.textContent = 'Inside shadow root' button.addEventListener('click', fn) shadowRoot.appendChild(button) }, []) return
} function Example() { let [isOpen, setIsOpen] = useState(true) return (
setIsOpen(false)}>
) } render() await nextFrame() // Verify it is open assertDialog({ state: DialogState.Visible }) // Click the button inside the dialog (light DOM) await click(document.querySelector('#btn_inside_light')) // Verify the button was clicked expect(fn).toHaveBeenCalledTimes(1) // Verify the dialog is still open assertDialog({ state: DialogState.Visible }) // Click the button inside the dialog (shadow DOM) await click( document.querySelector('#inside_shadow')?.shadowRoot?.querySelector('#btn_inside_shadow') ?? null ) // Verify the button was clicked expect(fn).toHaveBeenCalledTimes(2) // Verify the dialog is still open assertDialog({ state: DialogState.Visible }) // Click the button outside the dialog (shadow DOM) await click( document .querySelector('#outside_shadow') ?.shadowRoot?.querySelector('#btn_outside_shadow') ?? null ) // Verify the button was clicked expect(fn).toHaveBeenCalledTimes(3) // Verify the dialog is closed assertDialog({ state: DialogState.InvisibleUnmounted }) }) ) // NOTE: This test doesn't actually fail in JSDOM when it's supposed to // We're keeping it around for documentation purposes it( 'should not close the Dialog if it starts open and we click inside the Dialog when it has only a panel', suppressConsoleLogs(async () => { function Example() { let [isOpen, setIsOpen] = useState(true) return ( <> setIsOpen(false)}>

My content

) } render() // Open the dialog await click(document.getElementById('trigger')) assertDialog({ state: DialogState.Visible }) // Click the p tag inside the dialog await click(document.getElementById('inside')) // It should not have closed assertDialog({ state: DialogState.Visible }) }) ) it( 'should close the Dialog if we click outside the Dialog.Panel', suppressConsoleLogs(async () => { function Example() { let [isOpen, setIsOpen] = useState(false) return ( <> ) } render() await click(document.getElementById('trigger')) assertDialog({ state: DialogState.Visible }) await click(document.getElementById('outside')) assertDialog({ state: DialogState.InvisibleUnmounted }) }) ) it( 'should not close the Dialog if we click inside the Dialog.Panel', suppressConsoleLogs(async () => { function Example() { let [isOpen, setIsOpen] = useState(false) return ( <> ) } render() await click(document.getElementById('trigger')) assertDialog({ state: DialogState.Visible }) await click(document.getElementById('inside')) assertDialog({ state: DialogState.Visible }) }) ) it( 'should not close the dialog if opened during mouse up', suppressConsoleLogs(async () => { function Example() { let [isOpen, setIsOpen] = useState(false) return ( <> ) } render() await click(document.getElementById('trigger')) assertDialog({ state: DialogState.Visible }) await click(document.getElementById('inside')) assertDialog({ state: DialogState.Visible }) }) ) it( 'should not close the dialog if click starts inside the dialog but ends outside', suppressConsoleLogs(async () => { function Example() { let [isOpen, setIsOpen] = useState(false) return ( <>
this thing
) } render() // Open the dialog await click(document.getElementById('trigger')) assertDialog({ state: DialogState.Visible }) // Start a click inside the dialog and end it outside await mouseDrag(document.getElementById('inside'), document.getElementById('imoutside')) // It should not have hidden assertDialog({ state: DialogState.Visible }) await click(document.getElementById('imoutside')) // It's gone assertDialog({ state: DialogState.InvisibleUnmounted }) }) ) }) describe('Nesting', () => { type RenderStrategy = 'mounted' | 'always' function Nested({ onClose, open = true, level = 1, renderWhen = 'mounted', }: { onClose: (value: boolean) => void open?: boolean level?: number renderWhen?: RenderStrategy }) { let [showChild, setShowChild] = useState(false) return (

Level: {level}

{renderWhen === 'always' ? ( ) : ( showChild && )}
) } function Example({ renderWhen = 'mounted' }: { renderWhen: RenderStrategy }) { let [open, setOpen] = useState(false) return ( <> {open && } ) } it.each` strategy | when | action ${'with `Escape`'} | ${'mounted'} | ${() => press(Keys.Escape)} ${'with `Outside Click`'} | ${'mounted'} | ${() => click(document.body)} ${'with `Click on Dialog.Overlay`'} | ${'mounted'} | ${() => click(getDialogOverlays().pop()!)} ${'with `Escape`'} | ${'always'} | ${() => press(Keys.Escape)} ${'with `Outside Click`'} | ${'always'} | ${() => click(document.body)} `( 'should be possible to open nested Dialog components (visible when $when) and close them $strategy', async ({ when, action }) => { render() // Verify we have no open dialogs expect(getDialogs()).toHaveLength(0) // Open Dialog 1 await click(getByText('Open 1')) // Verify that we have 1 open dialog expect(getDialogs()).toHaveLength(1) // Verify that the `Open 2 a` has focus assertActiveElement(getByText('Open 2 a')) // Verify that we can tab around await press(Keys.Tab) assertActiveElement(getByText('Open 2 b')) // Verify that we can tab around await press(Keys.Tab) assertActiveElement(getByText('Open 2 c')) // Verify that we can tab around await press(Keys.Tab) assertActiveElement(getByText('Open 2 a')) // Open Dialog 2 via the second button await click(getByText('Open 2 b')) // Verify that we have 2 open dialogs expect(getDialogs()).toHaveLength(2) // Verify that the `Open 3 a` has focus assertActiveElement(getByText('Open 3 a')) // Verify that we can tab around await press(Keys.Tab) assertActiveElement(getByText('Open 3 b')) // Verify that we can tab around await press(Keys.Tab) assertActiveElement(getByText('Open 3 c')) // Verify that we can tab around await press(Keys.Tab) assertActiveElement(getByText('Open 3 a')) // Close the top most Dialog await action() // Verify that we have 1 open dialog expect(getDialogs()).toHaveLength(1) // Verify that the `Open 2 b` button got focused again assertActiveElement(getByText('Open 2 b')) // Verify that we can tab around await press(Keys.Tab) assertActiveElement(getByText('Open 2 c')) // Verify that we can tab around await press(Keys.Tab) assertActiveElement(getByText('Open 2 a')) // Verify that we can tab around await press(Keys.Tab) assertActiveElement(getByText('Open 2 b')) // Open Dialog 2 via button b await click(getByText('Open 2 b')) // Verify that the `Open 3 a` has focus assertActiveElement(getByText('Open 3 a')) // Verify that we can tab around await press(Keys.Tab) assertActiveElement(getByText('Open 3 b')) // Verify that we can tab around await press(Keys.Tab) assertActiveElement(getByText('Open 3 c')) // Verify that we can tab around await press(Keys.Tab) assertActiveElement(getByText('Open 3 a')) // Verify that we have 2 open dialogs expect(getDialogs()).toHaveLength(2) // Open Dialog 3 via button c await click(getByText('Open 3 c')) // Verify that the `Open 4 a` has focus assertActiveElement(getByText('Open 4 a')) // Verify that we can tab around await press(Keys.Tab) assertActiveElement(getByText('Open 4 b')) // Verify that we can tab around await press(Keys.Tab) assertActiveElement(getByText('Open 4 c')) // Verify that we can tab around await press(Keys.Tab) assertActiveElement(getByText('Open 4 a')) // Verify that we have 3 open dialogs expect(getDialogs()).toHaveLength(3) // Close the top most Dialog await action() // Verify that the `Open 3 c` button got focused again assertActiveElement(getByText('Open 3 c')) // Verify that we can tab around await press(Keys.Tab) assertActiveElement(getByText('Open 3 a')) // Verify that we can tab around await press(Keys.Tab) assertActiveElement(getByText('Open 3 b')) // Verify that we can tab around await press(Keys.Tab) assertActiveElement(getByText('Open 3 c')) // Verify that we have 2 open dialogs expect(getDialogs()).toHaveLength(2) // Close the top most Dialog await action() // Verify that we have 1 open dialog expect(getDialogs()).toHaveLength(1) // Verify that the `Open 2 b` button got focused again assertActiveElement(getByText('Open 2 b')) // Verify that we can tab around await press(Keys.Tab) assertActiveElement(getByText('Open 2 c')) // Verify that we can tab around await press(Keys.Tab) assertActiveElement(getByText('Open 2 a')) // Verify that we can tab around await press(Keys.Tab) assertActiveElement(getByText('Open 2 b')) // Close the top most Dialog await action() // Verify that we have 0 open dialogs expect(getDialogs()).toHaveLength(0) // Verify that the `Open 1` button got focused again assertActiveElement(getByText('Open 1')) } ) })