Skip to content

Commit

Permalink
code review Pt 1
Browse files Browse the repository at this point in the history
  • Loading branch information
sirineJ committed Dec 18, 2024
1 parent 972eb20 commit db3f353
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 64 deletions.
33 changes: 11 additions & 22 deletions packages/circuit-ui/components/Modal/Modal.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
background-color: var(--cui-bg-elevated);
border: none;
outline: none;
box-shadow: 0 0 0 100vmax var(--cui-bg-overlay);
animation: fade-out 0.3s forwards;
}

Expand All @@ -20,12 +21,14 @@
position: relative;
}

.base::after {
.scrollable {
position: sticky;
right: 0;
bottom: 0;
left: 0;
display: block;
height: var(--cui-spacings-giga);
pointer-events: none;
content: "";
background: linear-gradient(
color-mix(in sRGB, var(--cui-bg-elevated) 0%, transparent),
Expand All @@ -43,15 +46,9 @@
border-radius: var(--cui-border-radius-mega);
}

.base::after {
height: var(--cui-spacings-giga);
}

.base .content {
padding: var(--cui-spacings-giga);
padding-bottom: calc(
env(safe-area-inset-bottom) + var(--cui-spacings-giga)
);
padding: var(--cui-spacings-giga) var(--cui-spacings-giga) 0;
padding-bottom: env(safe-area-inset-bottom);
}
}

Expand All @@ -63,8 +60,8 @@
left: 0;
width: 100%;
max-width: 100%;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
border-radius: var(--cui-border-radius-mega) var(--cui-border-radius-mega) 0
0;
animation: slide-out 0.3s forwards;
}

Expand All @@ -80,23 +77,15 @@
}

.base .content {
padding: var(--cui-spacings-mega);
padding-bottom: calc(
env(safe-area-inset-bottom) + var(--cui-spacings-mega)
);
padding: var(--cui-spacings-mega) var(--cui-spacings-mega) 0;
padding-bottom: env(safe-area-inset-bottom);
-webkit-overflow-scrolling: touch;
}
}

/* Native Backdrop */
.base::backdrop {
background-color: var(--cui-bg-overlay);
animation: fade-out 0.3s forwards;
}

.base.show::backdrop {
pointer-events: auto;
animation: fade-in 0.3s forwards;
background: transparent;
}

/* Polyfill Backdrop */
Expand Down
53 changes: 30 additions & 23 deletions packages/circuit-ui/components/Modal/Modal.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,55 +67,64 @@ describe('Modal', () => {

it('should forward a ref', () => {
const ref = createRef<HTMLDialogElement>();
const { container } = render(<Modal {...props} ref={ref} />);
render(<Modal {...props} ref={ref} />);
// eslint-disable-next-line testing-library/no-container
const dialog = container.querySelector('dialog');
const dialog = screen.getByRole('dialog', { hidden: true });
expect(ref.current).toBe(dialog);
});

it('should merge a custom class name with the default ones', () => {
const className = 'foo';
const { container } = render(<Modal {...props} className={className} />);
render(<Modal {...props} className={className} />);
// eslint-disable-next-line testing-library/no-container
const dialog = container.querySelector('dialog');
const dialog = screen.getByRole('dialog', { hidden: true });
expect(dialog?.className).toContain(className);
});

it('should render in closed state by default', () => {
const { container } = render(<Modal {...props} />);
render(<Modal {...props} />);
// eslint-disable-next-line testing-library/no-container
const dialog = container.querySelector('dialog') as HTMLDialogElement;
const dialog = screen.getByRole('dialog', { hidden: true });
expect(dialog).not.toBeVisible();
});

it('should open the dialog when the open prop becomes truthy', () => {
const { container, rerender } = render(<Modal {...props} />);
const { rerender } = render(<Modal {...props} />);
// eslint-disable-next-line testing-library/no-container
const dialog = container.querySelector('dialog') as HTMLDialogElement;
const dialog = screen.getByRole('dialog', {
hidden: true,
});
vi.spyOn(dialog, 'showModal');
rerender(<Modal {...props} open />);
expect(dialog.showModal).toHaveBeenCalledOnce();
expect(dialog).toBeVisible();
});

it('should close the dialog when the open prop becomes falsy', () => {
const { container, rerender } = render(<Modal {...props} open />);
const { rerender } = render(<Modal {...props} open />);
// eslint-disable-next-line testing-library/no-container
const dialog = container.querySelector('dialog') as HTMLDialogElement;
const dialog = screen.getByRole('dialog', {
hidden: true,
});
vi.spyOn(dialog, 'close');
rerender(<Modal {...props} />);
act(() => {
vi.advanceTimersByTime(ANIMATION_DURATION);
});
expect(dialog.close).toHaveBeenCalledOnce();
expect(dialog).not.toBeVisible();
});

it('should close the dialog when the component is unmounted', async () => {
const { container, unmount } = render(<Modal {...props} open />);
const { unmount } = render(<Modal {...props} open />);
// eslint-disable-next-line testing-library/no-container
const dialog = container.querySelector('dialog') as HTMLDialogElement;
const dialog = screen.getByRole('dialog', {
hidden: true,
});
vi.spyOn(dialog, 'close');
unmount();
expect(dialog.close).toHaveBeenCalledOnce();
expect(dialog).not.toBeVisible();
});

describe('when the dialog is closed', () => {
Expand All @@ -142,46 +151,43 @@ describe('Modal', () => {
});

it('should not close modal on backdrop click if preventClose is true', async () => {
const { container } = render(<Modal {...props} open preventClose />);
render(<Modal {...props} open preventClose />);
// eslint-disable-next-line testing-library/no-container
const dialog = container.querySelector('dialog') as HTMLDialogElement;
const dialog = screen.getByRole('dialog', { hidden: true });
await userEvent.click(dialog);
act(() => {
vi.advanceTimersByTime(ANIMATION_DURATION);
});
expect(props.onClose).not.toHaveBeenCalled();
});

it('should open in immersive mode', async () => {
const { container } = render(
<Modal {...props} open variant="immersive" />,
);
// eslint-disable-next-line testing-library/no-container
const dialog = container.querySelector('dialog') as HTMLDialogElement;
expect(dialog.className).toContain('immersive');
expect(dialog).toBeVisible();
});

it('should close the dialog when pressing the backdrop', async () => {
render(<Modal {...props} open />);
const dialog = screen.getByRole('dialog', { hidden: true });
await userEvent.click(screen.getByRole('dialog', { hidden: true }));
act(() => {
vi.advanceTimersByTime(ANIMATION_DURATION);
});
expect(props.onClose).toHaveBeenCalledOnce();
expect(dialog).not.toBeVisible();
});

it('should close the dialog when the close button is clicked', async () => {
render(<Modal {...props} open />);
const dialog = screen.getByRole('dialog', { hidden: true });
await userEvent.click(screen.getByRole('button', { name: 'Close' }));
act(() => {
vi.advanceTimersByTime(ANIMATION_DURATION);
});
expect(props.onClose).toHaveBeenCalledOnce();
expect(dialog).not.toBeVisible();
});

it('should remove animation classes when closed when polyfill is used', async () => {
(hasNativeDialogSupport as Mock).mockReturnValue(false);
render(<Modal {...props} open />);
const dialog = screen.getByRole('dialog', { hidden: true });

const backdrop = document.getElementsByClassName('backdrop')[0];
expect(backdrop.classList.toString()).toContain('backdrop-visible');
Expand All @@ -192,6 +198,7 @@ describe('Modal', () => {
});

expect(props.onClose).toHaveBeenCalledOnce();
expect(dialog).not.toBeVisible();
});
});

Expand Down
41 changes: 25 additions & 16 deletions packages/circuit-ui/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use client';
/**
* Copyright 2024, SumUp Ltd.
* Copyright 2019, SumUp Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
Expand Down Expand Up @@ -34,6 +33,7 @@ import type { ClickEvent } from '../../types/events.js';
import { isEscape } from '../../util/key-codes.js';
import { useI18n } from '../../hooks/useI18n/useI18n.js';
import { deprecate } from '../../util/logger.js';
import type { Locale } from '../../util/i18n.js';

import classes from './Modal.module.css';
import { createUseModal } from './createUseModal.js';
Expand Down Expand Up @@ -79,10 +79,16 @@ export interface ModalProps
* Enables focusing a particular element in the dialog content and override default behavior
*/
initialFocusRef?: RefObject<HTMLElement>;

/**
* @deprecated this props was passed to react-modal and is no longer relevant.
* Use preventClose instead. Also see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role#required_javascript_features
* One or more [IETF BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag)
* locale identifiers such as `'de-DE'` or `['GB', 'en-US']`.
* When passing an array, the first supported locale is used.
* Defaults to `navigator.language` in supported environments.
*/
locale?: Locale;
/**
* @deprecated This prop was passed to `react-modal` and is no longer relevant.
* Use the `preventClose` prop instead. Also see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role#required_javascript_features
*/
hideCloseButton?: boolean;
[key: DataAttribute]: string | undefined;
Expand All @@ -94,6 +100,7 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>((props, ref) => {
const {
open,
onClose,
locale,
closeButtonLabel,
variant = 'contextual',
children,
Expand All @@ -110,15 +117,14 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>((props, ref) => {
if (hideCloseButton) {
deprecate(
'Modal',
'The "hideCloseButton" prop has been deprecated. Use the `preventClose` prop instead.',
'The `hideCloseButton` prop has been deprecated. Use the `preventClose` prop instead.',
);
}
}

const hasNativeDialog = hasNativeDialogSupport();

// set initial focus on the modal dialog content
// biome-ignore lint/correctness/useExhaustiveDependencies: we only want to run this effect once
useEffect(() => {
const dialogElement = dialogRef.current;
let timeoutId: NodeJS.Timeout;
Expand All @@ -134,21 +140,20 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>((props, ref) => {
return () => {
clearTimeout(timeoutId);
};
}, [open]);

const setScrollProperty = useCallback(() => {
document.documentElement.style.setProperty(
'--scroll-y',
`${window.scrollY}px`,
);
}, []);
}, [open, initialFocusRef?.current]);

useEffect(() => {
function setScrollProperty() {
document.documentElement.style.setProperty(
'--scroll-y',
`${window.scrollY}px`,
);
}
window.addEventListener('scroll', setScrollProperty);
return () => {
window.removeEventListener('scroll', setScrollProperty);
};
}, [setScrollProperty]);
}, []);

const handleDialogClose = useCallback(() => {
const dialogElement = dialogRef.current;
Expand Down Expand Up @@ -288,6 +293,9 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>((props, ref) => {
const onDialogClick = (
event: ClickEvent<HTMLDialogElement> | ClickEvent<HTMLDivElement>,
) => {
// the dialog content covers the whole dialog element
// leaving the backdrop element as the only clickable area
// that can trigger an onClick event
if (event.target === event.currentTarget && !preventClose) {
handleDialogClose();
}
Expand All @@ -314,6 +322,7 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>((props, ref) => {
{typeof children === 'function'
? children?.({ onClose })
: children}
<div className={classes.scrollable} />
</div>
)}
</dialog>
Expand Down
3 changes: 0 additions & 3 deletions packages/circuit-ui/components/Modal/ModalContext.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,6 @@ describe('ModalContext', () => {
vi.advanceTimersByTime(ANIMATION_DURATION);
});

expect(
(screen.getByTestId('dummy-dialog')).open,
).toBe(true);
expect(screen.getByTestId('dummy-dialog')).toBeVisible();
});

Expand Down

0 comments on commit db3f353

Please sign in to comment.