diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md
index 7314e7ecdce49f..82cd74a6b758d0 100644
--- a/packages/components/CHANGELOG.md
+++ b/packages/components/CHANGELOG.md
@@ -38,6 +38,7 @@
- `Popover`: Remove `scroll` and `resize` listeners for iframe overflow parents and rely on recently added native Floating UI support ([#54286](https://github.com/WordPress/gutenberg/pull/54286)).
- `Button`: Update documentation to remove the button `focus` prop ([#54397](https://github.com/WordPress/gutenberg/pull/54397)).
- `Toolbar/ToolbarGroup`: Convert component to TypeScript ([#54317](https://github.com/WordPress/gutenberg/pull/54317)).
+- `Modal`: add more unit tests ([#54569](https://github.com/WordPress/gutenberg/pull/54569)).
### Experimental
diff --git a/packages/components/src/confirm-dialog/test/index.js b/packages/components/src/confirm-dialog/test/index.js
index 4aecd43f570861..adf19b292898f8 100644
--- a/packages/components/src/confirm-dialog/test/index.js
+++ b/packages/components/src/confirm-dialog/test/index.js
@@ -1,13 +1,7 @@
/**
* External dependencies
*/
-import {
- render,
- screen,
- fireEvent,
- waitForElementToBeRemoved,
- waitFor,
-} from '@testing-library/react';
+import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
/**
@@ -137,6 +131,7 @@ describe( 'Confirm', () => {
} );
it( 'should not render if dialog is closed by clicking the overlay, and the `onCancel` callback should be called', async () => {
+ const user = userEvent.setup();
const onCancel = jest.fn().mockName( 'onCancel()' );
render(
@@ -147,11 +142,9 @@ describe( 'Confirm', () => {
const confirmDialog = screen.getByRole( 'dialog' );
- //The overlay click is handled by detecting an onBlur from the modal frame.
- // TODO: replace with `@testing-library/user-event`
- fireEvent.blur( confirmDialog );
-
- await waitForElementToBeRemoved( confirmDialog );
+ // Disable reason: Semantic queries can’t reach the overlay.
+ // eslint-disable-next-line testing-library/no-node-access
+ await user.click( confirmDialog.parentElement );
expect( confirmDialog ).not.toBeInTheDocument();
expect( onCancel ).toHaveBeenCalled();
@@ -315,6 +308,7 @@ describe( 'Confirm', () => {
} );
it( 'should call the `onCancel` callback if the overlay is clicked', async () => {
+ const user = userEvent.setup();
const onCancel = jest.fn().mockName( 'onCancel()' );
render(
@@ -329,13 +323,11 @@ describe( 'Confirm', () => {
const confirmDialog = screen.getByRole( 'dialog' );
- //The overlay click is handled by detecting an onBlur from the modal frame.
- // TODO: replace with `@testing-library/user-event`
- fireEvent.blur( confirmDialog );
+ // Disable reason: Semantic queries can’t reach the overlay.
+ // eslint-disable-next-line testing-library/no-node-access
+ await user.click( confirmDialog.parentElement );
- // Wait for a DOM side effect here, so that the `queueBlurCheck` in the
- // `useFocusOutside` hook properly executes its timeout task.
- await waitFor( () => expect( onCancel ).toHaveBeenCalled() );
+ expect( onCancel ).toHaveBeenCalled();
} );
it( 'should call the `onCancel` callback if the `Escape` key is pressed', async () => {
diff --git a/packages/components/src/modal/test/index.tsx b/packages/components/src/modal/test/index.tsx
index c2ab277f721570..d3bd6aea888132 100644
--- a/packages/components/src/modal/test/index.tsx
+++ b/packages/components/src/modal/test/index.tsx
@@ -116,6 +116,113 @@ describe( 'Modal', () => {
expect( opener ).toHaveFocus();
} );
+ it( 'should request closing of any non nested modal when opened', async () => {
+ const user = userEvent.setup();
+ const onRequestClose = jest.fn();
+
+ const DismissAdjacent = () => {
+ const [ isShown, setIsShown ] = useState( false );
+ return (
+ <>
+
+
+
+ { isShown && (
+ setIsShown( false ) }>
+ Adjacent modal content
+
+ ) }
+ >
+ );
+ };
+ render( );
+
+ await user.click( screen.getByRole( 'button', { name: '💥' } ) );
+ expect( onRequestClose ).toHaveBeenCalled();
+ } );
+
+ it( 'should support nested modals', async () => {
+ const user = userEvent.setup();
+ const onRequestClose = jest.fn();
+
+ const NestSupport = () => {
+ const [ isShown, setIsShown ] = useState( false );
+ return (
+ <>
+
+
+ { isShown && (
+ setIsShown( false ) }>
+ Nested modal content
+
+ ) }
+
+ >
+ );
+ };
+ render( );
+
+ await user.click( screen.getByRole( 'button', { name: '🪆' } ) );
+ expect( onRequestClose ).not.toHaveBeenCalled();
+ } );
+
+ // TODO enable once nested modals hide outer modals.
+ it.skip( 'should accessibly hide and show siblings including outer modals', async () => {
+ const user = userEvent.setup();
+
+ const AriaDemo = () => {
+ const [ isOuterShown, setIsOuterShown ] = useState( false );
+ const [ isInnerShown, setIsInnerShown ] = useState( false );
+ return (
+ <>
+
+ { isOuterShown && (
+ setIsOuterShown( false ) }
+ >
+
+ { isInnerShown && (
+
+ setIsInnerShown( false )
+ }
+ >
+ Nested modal content
+
+ ) }
+
+ ) }
+ >
+ );
+ };
+ const { container } = render( );
+
+ // Opens outer modal > hides container.
+ await user.click( screen.getByRole( 'button', { name: 'Start' } ) );
+ expect( container ).toHaveAttribute( 'aria-hidden', 'true' );
+
+ // Disable reason: No semantic query can reach the overlay.
+ // eslint-disable-next-line testing-library/no-node-access
+ const outer = screen.getByRole( 'dialog' ).parentElement!;
+
+ // Opens inner modal > hides outer modal.
+ await user.click( screen.getByRole( 'button', { name: 'Nest' } ) );
+ expect( outer ).toHaveAttribute( 'aria-hidden', 'true' );
+
+ // Closes inner modal > Unhides outer modal and container stays hidden.
+ await user.keyboard( '[Escape]' );
+ expect( outer ).not.toHaveAttribute( 'aria-hidden' );
+ expect( container ).toHaveAttribute( 'aria-hidden', 'true' );
+
+ // Closes outer modal > Unhides container.
+ await user.keyboard( '[Escape]' );
+ expect( container ).not.toHaveAttribute( 'aria-hidden' );
+ } );
+
it( 'should render `headerActions` React nodes', async () => {
render(