Skip to content

Commit

Permalink
feat(DFD-424): allow close btn but without interactive backdrop (#5110)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmainguytalend authored Jan 12, 2024
1 parent a9824c7 commit e22a7e3
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 22 deletions.
5 changes: 5 additions & 0 deletions .changeset/pink-roses-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@talend/design-system': minor
---

DS (modal) : allow to use close button without using interactive backdrop
3 changes: 3 additions & 0 deletions packages/design-system/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,10 @@
"@talend/scripts-config-typescript": "^11.2.0",
"@talend/storybook-docs": "^2.3.0",
"@testing-library/cypress": "^10.0.1",
"@testing-library/jest-dom": "^6.1.5",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/classnames": "^2.3.1",
"@types/jest": "^29.5.11",
"@types/jest-axe": "^3.5.9",
"@types/react-is": "^18.2.4",
"@types/react": "^18.2.43",
Expand Down Expand Up @@ -105,6 +107,7 @@
"@talend/icons": "^7.0.0",
"@talend/locales-design-system": "^7.15.1",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.5.1",
"i18next": "^23.7.7",
"react": "^16.14.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0",
Expand Down
67 changes: 64 additions & 3 deletions packages/design-system/src/components/Modal/Modal.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,37 @@
/* eslint-disable import/no-extraneous-dependencies */
import { describe, it, expect } from '@jest/globals';
import { useState } from 'react';

import { describe, expect, it } from '@jest/globals';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { axe } from 'jest-axe';
import { render } from '@testing-library/react';
import { Modal } from './';

import { ButtonPrimary } from '../Button';
import { Modal, ModalPropsType } from './';

function ModalComponentTester(props: Partial<ModalPropsType>) {
const [modalOpen, setModalOpen] = useState(false);

return (
<>
<ButtonPrimary onClick={() => setModalOpen(true)} data-testid="open-modal">
See
</ButtonPrimary>

{modalOpen && (
<Modal
header={{ title: '(Default story title)' }}
// eslint-disable-next-line react/no-children-prop
children="(Default story child)"
onClose={() => {
setModalOpen(false);
}}
{...props}
/>
)}
</>
);
}

describe('Message', () => {
it('should render a11y html', async () => {
Expand All @@ -18,4 +47,36 @@ describe('Message', () => {
const results = await axe(document.body);
expect(results).toHaveNoViolations();
});

it('should close the modal on backdrop click', async () => {
const user = userEvent.setup();

render(<ModalComponentTester />);

const openModalBtn = screen.getByTestId('open-modal');
expect(openModalBtn).toBeInTheDocument();
user.click(openModalBtn);

const modalBackdrop = await screen.findByTestId('open-modal');
expect(modalBackdrop).toBeInTheDocument();
user.click(modalBackdrop);

expect(screen.queryByTestId('modal')).not.toBeInTheDocument();
});

it('should not close the modal on backdrop click', async () => {
const user = userEvent.setup();

render(<ModalComponentTester preventInteractiveBackdrop />);

const openModalBtn = screen.getByTestId('open-modal');
expect(openModalBtn).toBeInTheDocument();
user.click(openModalBtn);

const modalBackdrop = await screen.findByTestId('open-modal');
expect(modalBackdrop).toBeInTheDocument();
user.click(modalBackdrop);

expect(await screen.findByTestId('modal')).toBeInTheDocument();
});
});
28 changes: 21 additions & 7 deletions packages/design-system/src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useRef, cloneElement } from 'react';
import type { ReactNode, ReactElement, MouseEvent as ReactMouseEvent } from 'react';
import { cloneElement, useCallback, useEffect, useMemo, useRef } from 'react';
import type { ReactElement, MouseEvent as ReactMouseEvent, ReactNode } from 'react';
import { useTranslation } from 'react-i18next';

import { DeprecatedIconNames } from '../../types';
Expand Down Expand Up @@ -42,6 +42,7 @@ export type ModalPropsType = {
secondaryAction?: ButtonSecondaryPropsType<'M'>;
preventEscaping?: boolean;
children: ReactNode | ReactNode[];
preventInteractiveBackdrop?: boolean;
} & DialogPropsType;

function PrimaryAction(props: PrimaryActionPropsType) {
Expand All @@ -63,6 +64,7 @@ export function Modal(props: ModalPropsType): ReactElement {
secondaryAction,
preventEscaping,
children,
preventInteractiveBackdrop,
...rest
} = props;
const hasDisclosure = 'disclosure' in props;
Expand All @@ -77,13 +79,25 @@ export function Modal(props: ModalPropsType): ReactElement {
dialogRef.current?.focus();
}, [dialogRef]);

const onCloseHandler = hasDisclosure ? () => dialog.hide() : () => onClose && onClose();
const onCloseHandler = useMemo(
() => (hasDisclosure ? () => dialog.hide() : () => onClose && onClose()),
[dialog, hasDisclosure, onClose],
);

const onClickBackdropHandler = useCallback(
(event: ReactMouseEvent<HTMLDivElement, MouseEvent>) => {
if (!preventEscaping && !preventInteractiveBackdrop && event.target === backdropRef.current) {
onCloseHandler();
}
},
[onCloseHandler, preventInteractiveBackdrop, preventEscaping],
);

const onClickBackdropHandler = (event: ReactMouseEvent<HTMLDivElement, MouseEvent>) => {
if (event.target === backdropRef.current) {
const onHideDialog = useCallback(() => {
if (!preventEscaping && !preventInteractiveBackdrop) {
onCloseHandler();
}
};
}, [onCloseHandler, preventInteractiveBackdrop, preventEscaping]);

return (
<>
Expand All @@ -108,7 +122,7 @@ export function Modal(props: ModalPropsType): ReactElement {
data-test="modal"
data-testid="modal"
className={styles.modal}
hide={preventEscaping ? () => undefined : () => onCloseHandler()}
hide={onHideDialog}
aria-labelledby={titleId}
ref={dialogRef}
>
Expand Down
8 changes: 6 additions & 2 deletions packages/design-system/src/stories/layout/Modal.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ Transparent layer behind every modal. Clicking it closes the modal by default, b

<Canvas height="5rem" of={Stories.WithDescription} />

**With no clickable backdrop**

<Canvas height="5rem" of={Stories.WithNoClickableBackdrop} />

**With actions**

<Canvas height="5rem" of={Stories.WithActions} />
Expand All @@ -84,9 +88,9 @@ Transparent layer behind every modal. Clicking it closes the modal by default, b

<Canvas height="5rem" of={Stories.WithDestructivePrimaryAction} />

**With non closing backdrop**
**With no Escape**

<Canvas height="5rem" of={Stories.WithNonClosingBackdrop} />
<Canvas height="5rem" of={Stories.WithNoEscape} />

**With overflowing header**

Expand Down
55 changes: 45 additions & 10 deletions packages/design-system/src/stories/layout/Modal.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useState } from 'react';
import { StoryFn } from '@storybook/react';

import { action } from '@storybook/addon-actions';
import { StoryFn } from '@storybook/react';

import { ButtonPrimary, Modal } from '../../';
import { ButtonPrimary, LinkAsButton, Modal, StackVertical } from '../../';
import { type ModalPropsType } from '../../components/Modal';

export default {
Expand Down Expand Up @@ -183,6 +184,16 @@ export const WithDescription: StoryFn<typeof Modal> = props => (
</ModalStory>
);

export const WithNoClickableBackdrop: StoryFn<typeof Modal> = props => (
<ModalStory
{...props}
header={{ title: 'With no clickable backdrop', icon: 'talend-file-hdfs-o' }}
preventInteractiveBackdrop
>
<p>A basic modal with title, a text content and an icon.</p>
</ModalStory>
);

export const WithActions: StoryFn<typeof Modal> = props => (
<ModalStory
{...props}
Expand Down Expand Up @@ -217,14 +228,38 @@ export const WithDestructivePrimaryAction: StoryFn<typeof Modal> = props => (
</ModalStory>
);

export const WithNonClosingBackdrop: StoryFn<typeof Modal> = props => (
<ModalStory {...props} header={{ title: 'A blocking modal' }} preventEscaping>
<p>
A modal that doesn't trigger <code>onClose</code> when the backdrop is clicked and without the
close button
</p>
</ModalStory>
);
export const WithNoEscape: StoryFn<typeof Modal> = () => {
const [modalOpen, setModalOpen] = useState(false);
return (
<>
<ButtonPrimary onClick={() => setModalOpen(true)} data-test="open-modal">
See
</ButtonPrimary>

{modalOpen && (
<Modal
header={{ title: 'A blocking modal' }}
onClose={() => {
action('onClose');
setModalOpen(false);
}}
preventEscaping
>
<StackVertical gap="M" align="center">
<p>
A modal that doesn't trigger <code>onClose</code> when the backdrop is clicked and
without the close button
</p>

<LinkAsButton onClick={() => setModalOpen(false)} data-test="close-modal">
Close me !
</LinkAsButton>
</StackVertical>
</Modal>
)}
</>
);
};

export const WithOverflowingHeader: StoryFn<typeof Modal> = props => (
<ModalStory
Expand Down

0 comments on commit e22a7e3

Please sign in to comment.