Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates Modal component to use dialog element #2579

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Prev Previous commit
Next Next commit
Modal migration
bruugey committed Dec 5, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
commit 84b68dbda27c029377b6ed57adb86dcd61636e28
Original file line number Diff line number Diff line change
@@ -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';

@@ -40,6 +35,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 +70,10 @@ describe('packages/confirmation-modal', () => {
});
});

test('does not render if closed', () => {
renderModal();
expect(document.body.innerHTML).toEqual('<div></div>');
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 +204,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 +312,7 @@ describe('packages/confirmation-modal', () => {

userEvent.click(buttonToClick);

await waitForElementToBeRemoved(modal);
await waitFor(() => getByRole('dialog', { hidden: true }));

rerender(
<ConfirmationModal
@@ -355,7 +369,7 @@ describe('packages/confirmation-modal', () => {

// 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 () => {
@@ -375,7 +389,7 @@ describe('packages/confirmation-modal', () => {

// Modal doesn't close when button is clicked
fireEvent.click(button);
await waitForElementToBeRemoved(modal);
await waitFor(() => getByRole('dialog', { hidden: true }));
});
});

Original file line number Diff line number Diff line change
@@ -41,7 +41,7 @@ export const ConfirmationModal = React.forwardRef(
cancelButtonProps = {},
...modalProps
}: ConfirmationModalProps,
forwardRef: React.ForwardedRef<HTMLDivElement | null>,
forwardRef: React.ForwardedRef<HTMLDialogElement | null>,
) => {
const [confirmEnabled, setConfirmEnabled] = useState(!requiredInputText);
const { theme, darkMode } = useDarkMode(darkModeProp);
Original file line number Diff line number Diff line change
@@ -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 { axe } from 'jest-axe';

import MarketingModal from '..';
@@ -39,6 +34,26 @@ function renderModal(
}

describe('packages/marketing-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 });
@@ -53,13 +68,19 @@ describe('packages/marketing-modal', () => {
expect(newResults).toHaveNoViolations();
});
});
test('does not render if closed', () => {
renderModal();
expect(document.body.innerHTML).toEqual('<div></div>');

test('is not visible when closed', () => {
const { getByRole } = renderModal();
const dialog = getByRole('dialog', { hidden: true });
expect(dialog).not.toBeVisible();
});

test('renders if open', () => {
const { getByText, getByLabelText } = renderModal({ open: true });
test('is visible if open', () => {
const { getByText, getByLabelText, getByRole } = renderModal({
open: true,
});
const dialog = getByRole('dialog');
expect(dialog).toBeVisible();
expect(getByLabelText('Image graphic')).toBeVisible();
expect(getByText('Title text')).toBeVisible();
expect(getByText('Content text')).toBeVisible();
@@ -112,21 +133,19 @@ describe('packages/marketing-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 }));
});
});

4 changes: 2 additions & 2 deletions packages/modal/package.json
Original file line number Diff line number Diff line change
@@ -40,8 +40,8 @@
"@faker-js/faker": "8.0.2",
"@leafygreen-ui/button": "^21.2.0",
"@leafygreen-ui/code": "^14.3.3",
"@leafygreen-ui/copyable": "^8.0.25",
"@leafygreen-ui/select": "^12.1.0",
"@leafygreen-ui/copyable": "^9.0.0",
"@leafygreen-ui/select": "^13.0.0",
"@leafygreen-ui/typography": "^19.1.0",
"@lg-tools/storybook-utils": "^0.1.1"
},
31 changes: 31 additions & 0 deletions packages/modal/src/CloseButton/CloseButton.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { css } from '@leafygreen-ui/emotion';
import { Theme } from '@leafygreen-ui/lib';
import { palette } from '@leafygreen-ui/palette';

import { CloseIconColor } from '../Modal/Modal.types';

const getColor = (theme: Theme, customColor: CloseIconColor) => {
switch (customColor) {
case 'dark':
return palette.black;

case 'light':
return palette.gray.light2;

default:
return theme === Theme.Light ? palette.gray.dark1 : palette.gray.light2;
}
};

export const closeButtonStyles = (
theme: Theme,
customColor: CloseIconColor,
) => css`
position: absolute;
cursor: pointer;
// x-icon should be 24px from edge. IconButton is 28x28 and Icon is 16x16
// so there's already (28 - 16) / 2 = 6px of spacing. 24 - 6 = 18.
right: 18px;
top: 18px;
color: ${getColor(theme, customColor)};
`;
31 changes: 31 additions & 0 deletions packages/modal/src/CloseButton/CloseButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';

import { useIdAllocator } from '@leafygreen-ui/hooks';
import XIcon from '@leafygreen-ui/icon/dist/X';
import IconButton from '@leafygreen-ui/icon-button';
import { useDarkMode } from '@leafygreen-ui/leafygreen-provider';

import { LGIDS_MODAL } from '../constants';
import { CloseIconColor } from '../Modal/Modal.types';
import { closeButtonStyles } from './CloseButton.styles';
import { CloseButtonProps } from './CloseButton.types';

export default function CloseButton({
closeIconColor = CloseIconColor.Default,
handleClose,
}: CloseButtonProps) {
const { theme } = useDarkMode();
const closeId = useIdAllocator({ prefix: 'modal' });

return (
<IconButton
id={closeId}
data-testid={LGIDS_MODAL.close}
onClick={handleClose}
aria-label="Close modal"
className={closeButtonStyles(theme, closeIconColor)}
>
<XIcon />
</IconButton>
);
}
6 changes: 6 additions & 0 deletions packages/modal/src/CloseButton/CloseButton.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { CloseIconColor } from '../Modal/Modal.types';

export interface CloseButtonProps {
closeIconColor?: CloseIconColor;
handleClose?: () => void;
}
1 change: 1 addition & 0 deletions packages/modal/src/CloseButton/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './CloseButton';
1 change: 1 addition & 0 deletions packages/modal/src/Modal.stories.tsx
Original file line number Diff line number Diff line change
@@ -156,6 +156,7 @@ export const DefaultSelect = (args: ModalProps) => {
name="pets"
value={value}
onChange={setValue}
renderMode="top-layer"
>
<OptionGroup label="Common">
<Option value="dog">Dog</Option>
Loading