Skip to content

Commit

Permalink
Update Modal tests and refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
NuckChorris committed Jun 22, 2022
1 parent 349d34b commit 51e2799
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 79 deletions.
76 changes: 48 additions & 28 deletions src/components/Modal/index.test.tsx
@@ -1,26 +1,29 @@
import { describe, test, expect } from 'vitest';
import React from 'react';
import { Router, MemoryRouter } from 'react-router-dom';
import { unstable_HistoryRouter as HistoryRouter } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { render, screen } from 'app/test-utils/testing-library';
import userEvent from '@testing-library/user-event';

import Modal from './index';

describe('with displayMode="modal"', () => {
// TODO: @testing-library/user-event gets confused by the nesting of pointer-events from Body (none)
// to Scrim (auto) to Modal (auto), we should file an issue on their repo to fix this. Until then,
// we just skip the tests which use pointer-events. These have been manually tested in the browser.
describe.skip('with displayMode="modal"', () => {
test('handles clicking out of the modal', async () => {
const history = createMemoryHistory({
initialEntries: ['/modal?returnTo=/back'],
});

render(
<Router history={history}>
<HistoryRouter history={history}>
<Modal displayMode="modal" />
</Router>
</HistoryRouter>
);

expect(history.location.pathname).toBe('/modal');
userEvent.click(screen.getByTestId('scrim'));
await userEvent.click(screen.getByTestId('scrim'));
expect(history.location.pathname).toBe('/back');
});

Expand All @@ -31,38 +34,36 @@ describe('with displayMode="modal"', () => {
});

render(
<Router history={history}>
<Modal displayMode="modal" />
</Router>
<HistoryRouter history={history}>
<Modal displayMode="modal">
<button>Test</button>
</Modal>
</HistoryRouter>
);

screen.debug();

expect(history.location.pathname).toBe('/modal');
userEvent.click(screen.getByTestId('modal'));
await userEvent.click(screen.getByText('Test'));
expect(history.location.pathname).toBe('/modal');
});

test('adds a scroll-lock class to the document body', async () => {
const { unmount } = render(
<MemoryRouter>
<Modal displayMode="modal" />
</MemoryRouter>
);

expect(document.body.classList.contains('scroll-lock')).toBe(true);
unmount();
expect(document.body.classList.contains('scroll-lock')).toBe(false);
});
});

describe('with displayMode="page"', () => {
test('does not add a scroll-lock class to the document body', async () => {
test('clicking on the close button navigates to the returnTo parameter', async () => {
const history = createMemoryHistory({
initialEntries: ['/modal?returnTo=/previous'],
});

render(
<MemoryRouter>
<HistoryRouter history={history}>
<Modal displayMode="page" />
</MemoryRouter>
</HistoryRouter>
);

expect(document.body.classList.contains('scroll-lock')).toBe(false);
expect(history.location.pathname).toBe('/modal');
await userEvent.click(screen.getByText('Close'));
expect(history.location.pathname).toBe('/previous');
});

test('clicking on the scrim navigates to the returnTo parameter', async () => {
Expand All @@ -71,13 +72,32 @@ describe('with displayMode="page"', () => {
});

render(
<Router history={history}>
<HistoryRouter history={history}>
<Modal displayMode="page" />
</Router>
</HistoryRouter>
);

expect(history.location.pathname).toBe('/modal');
userEvent.click(screen.getByTestId('scrim'));
await userEvent.click(screen.getByTestId('scrim'));
expect(history.location.pathname).toBe('/previous');
});

test('clicking in the modal does not navigate away', async () => {
const history = createMemoryHistory({
initialEntries: ['/back', '/modal'],
initialIndex: 1,
});

render(
<HistoryRouter history={history}>
<Modal displayMode="page">
<button>Test</button>
</Modal>
</HistoryRouter>
);

expect(history.location.pathname).toBe('/modal');
await userEvent.click(screen.getByText('Test'));
expect(history.location.pathname).toBe('/modal');
});
});
132 changes: 81 additions & 51 deletions src/components/Modal/index.tsx
Expand Up @@ -10,26 +10,84 @@ import useReturnToFn from 'app/hooks/useReturnToFn';

import styles from './styles.module.css';

export const Scrim = React.forwardRef<
HTMLDivElement,
React.PropsWithChildren<{
displayMode: 'page' | 'modal';
}>
>(function Scrim({ displayMode, children }, ref) {
const PageModal: React.FC<DialogHTMLAttributes<HTMLDialogElement>> = function ({
children,
className,
...args
}) {
const goBack = useReturnToFn();
const { formatMessage } = useIntl();

return (
<div
ref={ref}
data-testid="scrim"
className={
displayMode === 'page' ? styles.pageContainer : styles.modalContainer
}>
{displayMode === 'page' ? (
<HeaderSettings background="opaque" scrollBackground="opaque" />
) : null}
{children}
</div>
<IsModalContextProvider>
<HeaderSettings background="opaque" scrollBackground="opaque" />
<div
className={styles.pageContainer}
onPointerDown={goBack}
data-testid="scrim">
<dialog
data-testid="modal"
open
onPointerDown={(e) => e.stopPropagation()}
className={[className, styles.modal].join(' ')}
{...args}>
<button className={styles.closeButton} onPointerDown={goBack}>
<AccessibleIcon
label={formatMessage({
defaultMessage: 'Close',
description: 'Accessibility label for modal close button',
})}>
<BsX />
</AccessibleIcon>
</button>
{children}
</dialog>
</div>
</IsModalContextProvider>
);
});
};

const OverlayModal: React.FC<DialogHTMLAttributes<HTMLDialogElement>> =
function ({ children, className }) {
const goBack = useReturnToFn();
const { formatMessage } = useIntl();

return (
<IsModalContextProvider>
<Dialog.Root defaultOpen onOpenChange={(isOpen) => isOpen || goBack()}>
<Dialog.Overlay asChild>
<div
onPointerDown={goBack}
data-testid="scrim"
className={styles.modalContainer}>
<Dialog.Content
asChild
onEscapeKeyDown={() => goBack()}
onPointerDownOutside={(e) => e.preventDefault()}>
<dialog
data-testid="modal"
open
onPointerDown={(e) => e.stopPropagation()}
className={[className, styles.modal].join(' ')}>
<Dialog.Close className={styles.closeButton}>
<AccessibleIcon
label={formatMessage({
defaultMessage: 'Close',
description:
'Accessibility label for modal close button',
})}>
<BsX />
</AccessibleIcon>
</Dialog.Close>
{children}
</dialog>
</Dialog.Content>
</div>
</Dialog.Overlay>
</Dialog.Root>
</IsModalContextProvider>
);
};

/**
* A Modal or dialog box component
Expand All @@ -43,39 +101,11 @@ export const Scrim = React.forwardRef<
*/
const Modal: React.FC<
{ displayMode: 'modal' | 'page' } & DialogHTMLAttributes<HTMLDialogElement>
> = function ({ children, className, displayMode = 'modal', ...args }) {
const goBack = useReturnToFn();
const { formatMessage } = useIntl();

return (
<IsModalContextProvider>
<Dialog.Root defaultOpen onOpenChange={(isOpen) => isOpen || goBack()}>
<Dialog.Overlay asChild>
<Scrim displayMode={displayMode} onClick={(e) => goBack()}>
<Dialog.Content
asChild
onInteractOutside={(e) => e.preventDefault()}>
<dialog
data-testid="modal"
open
className={[className, styles.modal].join(' ')}
{...args}>
<Dialog.Close className={styles.closeButton}>
<AccessibleIcon
label={formatMessage({
defaultMessage: 'Close',
description: 'Accessibility label for modal close button',
})}>
<BsX />
</AccessibleIcon>
</Dialog.Close>
{children}
</dialog>
</Dialog.Content>
</Scrim>
</Dialog.Overlay>
</Dialog.Root>
</IsModalContextProvider>
> = function ({ displayMode, ...args }) {
return displayMode === 'modal' ? (
<OverlayModal {...args} />
) : (
<PageModal {...args} />
);
};

Expand Down
1 change: 1 addition & 0 deletions src/components/Modal/styles.module.css
Expand Up @@ -5,6 +5,7 @@
align-items: center;
justify-content: center;
padding: 10px;
pointer-events: auto;
}

.modalContainer {
Expand Down

0 comments on commit 51e2799

Please sign in to comment.