Skip to content

Commit

Permalink
prevent close
Browse files Browse the repository at this point in the history
  • Loading branch information
sirineJ committed Dec 2, 2024
1 parent e691c5c commit b39027b
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 56 deletions.
12 changes: 7 additions & 5 deletions packages/circuit-ui/components/Dialog/Dialog.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,15 @@ describe('Dialog', () => {
expect(children).toBeVisible();
});

it('should remove animation classes when pressing the Escape key', async () => {
const { container } = render(<Dialog {...props} open />);
it('should not close modal on backdrop click if preventClose is true', async () => {
const { container } = render(<Dialog {...props} open preventClose />);
// eslint-disable-next-line testing-library/no-container
const dialog = container.querySelector('dialog') as HTMLDialogElement;
expect(dialog.className).toContain('show');
await userEvent.keyboard('{Escape}');
expect(dialog.className).not.toContain('show');
await userEvent.click(dialog);
act(() => {
vi.advanceTimersByTime(animationDuration);
});
expect(props.onClose).not.toHaveBeenCalled();
});

it('should open in immersive mode', async () => {
Expand Down
34 changes: 0 additions & 34 deletions packages/circuit-ui/components/Dialog/Dialog.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,10 @@
* limitations under the License.
*/

import type { Decorator } from '@storybook/react';
import { Fragment, type ReactNode, useRef, useState } from 'react';
import { screen, userEvent, within } from '@storybook/test';

import { modes } from '../../../../.storybook/modes.js';
import { FullViewport } from '../../../../.storybook/components/index.js';
import { Headline } from '../Headline/index.js';
import { Body } from '../Body/index.js';
import { Button } from '../Button/index.js';
Expand All @@ -39,13 +37,6 @@ export default {
},
},
},
decorators: [
(Story) => (
<FullViewport>
<Story />
</FullViewport>
),
] as Decorator[],
};

const defaultModalChildren = (): ReactNode => (
Expand Down Expand Up @@ -96,31 +87,6 @@ export const Base = () => {
);
};

export const Modal = () => {
const [modalOpen, setModalOpen] = useState(false);
return (
<>
<Button
type="button"
onClick={() => {
setModalOpen(true);
}}
>
Open modal
</Button>
<Dialog
open={modalOpen}
onClose={() => setModalOpen(false)}
closeButtonLabel="Close"
aria-label="Hello World!"
aria-describedby="log1"
>
{defaultModalChildren}
</Dialog>
</>
);
};

export const Simultaneous = () => {
const [modalOpen, setModalOpen] = useState(false);
const [modalOpen2, setModalOpen2] = useState(false);
Expand Down
76 changes: 59 additions & 17 deletions packages/circuit-ui/components/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,42 @@ import {
isSufficientlyLabelled,
} from '../../util/errors.js';
import type { ClickEvent } from '../../types/events.js';
import { useEscapeKey } from '../../hooks/useEscapeKey/index.js';
import { isEscape } from '../../util/key-codes.js';

import classes from './Dialog.module.css';
import { createUseModalDialog } from './createUseModalDialog.js';
import { getFirstFocusableElement } from './DialogService.js';

export interface DialogProps
extends Omit<HTMLAttributes<HTMLDialogElement>, 'children'> {
/**
* Whether the dialog is open or not.
*/
open: boolean;
/**
* Callback when the dialog is closed.
*/
onClose?: () => void;
/**
* a function that returns the content of the dialog.
*/
children: () => ReactNode;
/**
* Text label for the close button for screen readers.
* Important for accessibility.
*/
closeButtonLabel: string;
/**
* Use the `contextual` variant when the modal content requires the context
* of the page underneath to be understood, otherwise, use the `immersive`
* variant to focus the user's attention.
*/
variant?: 'contextual' | 'immersive';
/**
* Prevent users from closing the modal by clicking/tapping the overlay or
* pressing the escape key. Default `false`.
*/
preventClose?: boolean;
}

export const animationDuration = 300;
Expand All @@ -60,6 +83,7 @@ export const Dialog = forwardRef<HTMLDialogElement, DialogProps>(
variant = 'contextual',
children,
className,
preventClose,
...props
},
ref,
Expand Down Expand Up @@ -99,7 +123,12 @@ export const Dialog = forwardRef<HTMLDialogElement, DialogProps>(
// restore scroll tp page
document.documentElement.style.overflowY = 'unset';
// trigger close animation
clearAnimationClasses();
dialogElement.classList.remove(classes.show);
if (!hasNativeDialog) {
(dialogElement.nextSibling as HTMLDivElement).classList.remove(
classes['backdrop-visible'],
);
}
// trigger dialog close after animation
setTimeout(() => {
if (dialogElement.open) {
Expand All @@ -108,7 +137,18 @@ export const Dialog = forwardRef<HTMLDialogElement, DialogProps>(
}, animationDuration);
}, []);

useEscapeKey(clearAnimationClasses, open);
function onPolyfillDialogKeydown(event: KeyboardEvent) {
if (isEscape(event) && preventClose) {
event.preventDefault();
event.stopPropagation();
}
}

function onPolyfillBackdropClick(event: MouseEvent) {
if (preventClose) {
event.preventDefault();
}
}

useEffect(() => {
const dialogElement = dialogRef.current;
Expand All @@ -118,6 +158,9 @@ export const Dialog = forwardRef<HTMLDialogElement, DialogProps>(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore The package is bundled incorrectly
dialogPolyfill.registerDialog(dialogElement);
if (preventClose) {
dialogElement.addEventListener('keydown', onPolyfillDialogKeydown);
}
if (onClose) {
dialogElement.addEventListener('close', onClose);
}
Expand All @@ -126,6 +169,13 @@ export const Dialog = forwardRef<HTMLDialogElement, DialogProps>(
if (onClose) {
dialogElement.removeEventListener('close', onClose);
}
if (!hasNativeDialog && dialogElement.nextSibling) {
(dialogElement.nextSibling as HTMLDivElement).removeEventListener(
'click',
onPolyfillBackdropClick,
);
dialogElement.removeEventListener('keydown', onPolyfillDialogKeydown);
}
};
}, [onClose]);

Expand All @@ -147,6 +197,11 @@ export const Dialog = forwardRef<HTMLDialogElement, DialogProps>(
classes['backdrop-visible'],
classes.backdrop,
);
// intercept and prevent modal closing if preventClose is true
(dialogElement.nextSibling as HTMLDivElement).addEventListener(
'click',
onPolyfillBackdropClick,
);
}

// trigger show animation
Expand All @@ -168,24 +223,11 @@ export const Dialog = forwardRef<HTMLDialogElement, DialogProps>(
const onDialogClick = (
event: ClickEvent<HTMLDialogElement> | ClickEvent<HTMLDivElement>,
) => {
if (event.target === event.currentTarget) {
if (event.target === event.currentTarget && !preventClose) {
handleDialogClose();
}
};

function clearAnimationClasses() {
const dialogElement = dialogRef.current;
if (!dialogElement) {
return;
}
dialogElement.classList.remove(classes.show);
if (!hasNativeDialog) {
(dialogElement.nextSibling as HTMLDivElement).classList.remove(
classes['backdrop-visible'],
);
}
}

return (
<>
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}
Expand Down

0 comments on commit b39027b

Please sign in to comment.