diff --git a/.changeset/soft-files-explain.md b/.changeset/soft-files-explain.md new file mode 100644 index 0000000000..e8d0b64c04 --- /dev/null +++ b/.changeset/soft-files-explain.md @@ -0,0 +1,7 @@ +--- +'@leafygreen-ui/confirmation-modal': major +'@leafygreen-ui/marketing-modal': major +'@leafygreen-ui/modal': major +--- + +Upgrades components to use the native `dialog` HTML element. This means we no longer have to handle focus trapping ourselves, and can rely on the browser to do that for us. The API for all modal components is not changing, but the DOM structure of the Modal components themselves have changed drastically, as well as where they are placed within the DOM itself. diff --git a/packages/confirmation-modal/src/ConfirmationModal/ConfirmationModal.spec.tsx b/packages/confirmation-modal/src/ConfirmationModal/ConfirmationModal.spec.tsx index a5fdb6fda8..df04ae043b 100644 --- a/packages/confirmation-modal/src/ConfirmationModal/ConfirmationModal.spec.tsx +++ b/packages/confirmation-modal/src/ConfirmationModal/ConfirmationModal.spec.tsx @@ -1,10 +1,5 @@ import React, { useState } from 'react'; -import { - act, - fireEvent, - render, - waitForElementToBeRemoved, -} from '@testing-library/react'; +import { act, fireEvent, render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { axe } from 'jest-axe'; @@ -22,10 +17,12 @@ const WrappedModal = ({ return ( setOpen(false), + children: 'Confirm', + }} + cancelButtonProps={{ onClick: () => setOpen(false) }} open={open} - onConfirm={() => setOpen(false)} - onCancel={() => setOpen(false)} {...props} > {props.children ?? 'Content text'} @@ -40,6 +37,26 @@ function renderModal( } describe('packages/confirmation-modal', () => { + beforeAll(() => { + HTMLDialogElement.prototype.show = jest.fn(function mock( + this: HTMLDialogElement, + ) { + this.open = true; + }); + + HTMLDialogElement.prototype.showModal = jest.fn(function mock( + this: HTMLDialogElement, + ) { + this.open = true; + }); + + HTMLDialogElement.prototype.close = jest.fn(function mock( + this: HTMLDialogElement, + ) { + this.open = false; + }); + }); + describe('a11y', () => { test('does not have basic accessibility issues', async () => { const { container, getByText } = renderModal({ open: true }); @@ -55,9 +72,10 @@ describe('packages/confirmation-modal', () => { }); }); - test('does not render if closed', () => { - renderModal(); - expect(document.body.innerHTML).toEqual('
'); + test('is not visible when closed', () => { + const { getByRole } = renderModal(); + const dialog = getByRole('dialog', { hidden: true }); + expect(dialog).not.toBeVisible(); }); test('renders if open', () => { @@ -188,21 +206,19 @@ describe('packages/confirmation-modal', () => { describe('closes when', () => { test('escape key is pressed', async () => { const { getByRole } = renderModal({ open: true }); - const modal = getByRole('dialog'); fireEvent.keyDown(document, { key: 'Escape', keyCode: 27 }); - await waitForElementToBeRemoved(modal); + await waitFor(() => getByRole('dialog', { hidden: true })); }); test('x icon is clicked', async () => { const { getByLabelText, getByRole } = renderModal({ open: true }); - const modal = getByRole('dialog'); const x = getByLabelText('Close modal'); fireEvent.click(x); - await waitForElementToBeRemoved(modal); + await waitFor(() => getByRole('dialog', { hidden: true })); }); }); @@ -298,7 +314,7 @@ describe('packages/confirmation-modal', () => { userEvent.click(buttonToClick); - await waitForElementToBeRemoved(modal); + await waitFor(() => getByRole('dialog', { hidden: true })); rerender( { const confirmationButton = getByText('Confirm').closest('button'); expect(confirmationButton).toHaveAttribute('aria-disabled', 'false'); - const modal = getByRole('dialog'); const button = getByText('Confirm'); expect(button).toBeVisible(); // Modal doesn't close when button is clicked fireEvent.click(button); - await waitForElementToBeRemoved(modal); + await waitFor(() => getByRole('dialog', { hidden: true })); }); test('"confirmButtonProps" has "disabled: false"', async () => { @@ -369,13 +384,12 @@ describe('packages/confirmation-modal', () => { const confirmationButton = getByText('Confirm').closest('button'); expect(confirmationButton).toHaveAttribute('aria-disabled', 'false'); - const modal = getByRole('dialog'); const button = getByText('Confirm'); expect(button).toBeVisible(); // Modal doesn't close when button is clicked fireEvent.click(button); - await waitForElementToBeRemoved(modal); + await waitFor(() => getByRole('dialog', { hidden: true })); }); }); diff --git a/packages/confirmation-modal/src/ConfirmationModal/ConfirmationModal.tsx b/packages/confirmation-modal/src/ConfirmationModal/ConfirmationModal.tsx index e302192b50..b5c1078997 100644 --- a/packages/confirmation-modal/src/ConfirmationModal/ConfirmationModal.tsx +++ b/packages/confirmation-modal/src/ConfirmationModal/ConfirmationModal.tsx @@ -17,7 +17,7 @@ import { buttonStyle, contentDarkModeStyles, contentStyle, - contentVariantStyles, + footerStyle, textEntryInputStyle, titleStyle, warningIconStyle, @@ -40,7 +40,7 @@ export const ConfirmationModal = React.forwardRef( cancelButtonProps = {}, ...modalProps }: ConfirmationModalProps, - forwardRef: React.ForwardedRef, + forwardRef: React.ForwardedRef, ) => { const [confirmEnabled, setConfirmEnabled] = useState(!requiredInputText); const { theme, darkMode } = useDarkMode(darkModeProp); @@ -97,13 +97,13 @@ export const ConfirmationModal = React.forwardRef( return (
@@ -125,7 +125,7 @@ export const ConfirmationModal = React.forwardRef( {children} {textEntryConfirmation}
-