This repository has been archived by the owner on Dec 20, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added separate components for different behaviours of Modal vs Non-Mo…
…dal.
- Loading branch information
Showing
14 changed files
with
596 additions
and
137 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,4 @@ | ||
package.json | ||
.eslintrc | ||
.gitignore | ||
.eslintrc | ||
.next |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import React, { useEffect, useRef, useCallback, type ReactNode } from 'react'; | ||
import closeStyle from './Close.module.css'; | ||
import styles from './Dialog.module.css'; | ||
import { modalStack } from './DialogManager'; | ||
|
||
type ModalProps = { | ||
children: ReactNode; | ||
isOpen: boolean; | ||
onClose: () => void; | ||
}; | ||
|
||
export const Dialog: React.FC<ModalProps> = (props: ModalProps) => { | ||
const dialogRef = useRef<HTMLDialogElement>(null); | ||
const dialogRootRef = useRef<HTMLDivElement>(null); | ||
const { isOpen, onClose, children } = props; | ||
|
||
// Handle tab key press | ||
const handleTabKey = useCallback((event: KeyboardEvent) => { | ||
if (event.key === 'Tab') { | ||
const focusableElements = | ||
dialogRef.current?.querySelectorAll<HTMLElement>( | ||
'a[href], button, textarea, input[type="text"], input[type="radio"], input[type="checkbox"], select' | ||
); | ||
|
||
if (!focusableElements || focusableElements.length === 0) return; | ||
|
||
const firstFocusableElement = focusableElements[0]; | ||
const lastFocusableElement = | ||
focusableElements[focusableElements.length - 1]; | ||
|
||
if (event.shiftKey && document.activeElement === firstFocusableElement) { | ||
event.preventDefault(); | ||
lastFocusableElement?.focus(); | ||
} else if ( | ||
!event.shiftKey && | ||
document.activeElement === lastFocusableElement | ||
) { | ||
event.preventDefault(); | ||
firstFocusableElement?.focus(); | ||
} | ||
} | ||
}, []); | ||
|
||
useEffect(() => { | ||
if (isOpen) { | ||
modalStack.add(dialogRef.current as HTMLDialogElement); | ||
document.addEventListener('keydown', handleTabKey); | ||
document.body.classList.add(styles['blockScrolling'] as string); | ||
} else { | ||
modalStack.delete(dialogRef.current as HTMLDialogElement); | ||
document.removeEventListener('keydown', handleTabKey); | ||
document.body.classList.remove(styles['blockScrolling'] as string); | ||
} | ||
|
||
return () => { | ||
document.removeEventListener('keydown', handleTabKey); | ||
document.body.classList.remove(styles['blockScrolling'] as string); | ||
}; | ||
}, [isOpen, handleTabKey]); | ||
|
||
// Close modal, either by clicking on the backdrop or the "close" icon. | ||
const handleClose = useCallback(() => { | ||
// @ts-expect-error will be refactored | ||
dialogRootRef.current?.classList.add(styles.dialogRootFadeOut); | ||
setTimeout(() => { | ||
// @ts-expect-error will be refactored | ||
dialogRootRef.current?.classList.toggle(styles.dialogRootFadeOut); | ||
// @ts-expect-error will be refactored | ||
dialogRootRef.current?.classList.add(styles.hide); | ||
}, 250); | ||
|
||
// Calls the user of this component passed event handler. | ||
setTimeout(onClose, 500); | ||
modalStack.delete(dialogRef.current as HTMLDialogElement); | ||
}, [onClose]); | ||
|
||
// Close modal when clicking outside of it | ||
const handleOutsideClick = useCallback( | ||
(event: React.MouseEvent) => { | ||
event.stopPropagation(); | ||
|
||
// eslint-disable-next-line eqeqeq | ||
const modalStackArray = [...modalStack]; | ||
if ( | ||
event.target === dialogRootRef.current && | ||
modalStackArray.at(-1) === dialogRef.current | ||
) { | ||
handleClose(); | ||
} | ||
}, | ||
[handleClose] | ||
); | ||
|
||
if (!isOpen) return null; | ||
|
||
return ( | ||
<div | ||
className={styles['dialogRoot']} | ||
data-testid="dialog-root" | ||
onClick={handleOutsideClick} | ||
ref={dialogRootRef} | ||
> | ||
<dialog className={styles['dialog']} open={isOpen} ref={dialogRef}> | ||
<a className={closeStyle['close']} onClick={handleClose}></a> | ||
{children} | ||
</dialog> | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { render, screen, fireEvent } from '@testing-library/react'; | ||
import React from 'react'; | ||
import { DialogModal as Dialog } from './DialogModal'; | ||
import '@testing-library/jest-dom'; | ||
import {describe} from "@jest/globals"; | ||
|
||
describe('DialogModal', () => { | ||
test('renders and opens the dialog when isOpen is true', () => { | ||
const onClose = jest.fn(); | ||
const children = <div>Test Content</div>; | ||
|
||
render( | ||
<Dialog isOpen={true} onClose={onClose}> | ||
{children} | ||
</Dialog> | ||
); | ||
|
||
// Dialog should be in the document | ||
const dialogElement = screen.getByTestId('dialog'); | ||
expect(dialogElement).toBeInTheDocument(); | ||
|
||
// Dialog should have the "open" attribute set to true | ||
setTimeout(() => { | ||
expect(dialogElement).toHaveAttribute('open', ''); | ||
}, 500); | ||
|
||
// Children should be rendered inside the dialog | ||
const childElement = screen.getByText('Test Content'); | ||
expect(childElement).toBeInTheDocument(); | ||
}); | ||
|
||
test('does not render and closes the dialog when isOpen is false', () => { | ||
const onClose = jest.fn(); | ||
const children = <div>Test Content</div>; | ||
|
||
render( | ||
<Dialog isOpen={false} onClose={onClose}> | ||
{children} | ||
</Dialog> | ||
); | ||
|
||
// Dialog should not be in the document | ||
const dialogElement = screen.queryByRole('dialog'); | ||
expect(dialogElement).not.toBeInTheDocument(); | ||
}); | ||
|
||
test('calls onClose when clicking outside the dialog', () => { | ||
const onClose = jest.fn(); | ||
const children = <div>Test Content</div>; | ||
|
||
render( | ||
<Dialog isOpen={true} onClose={onClose}> | ||
{children} | ||
</Dialog> | ||
); | ||
|
||
// Click outside the dialog | ||
fireEvent( | ||
document, | ||
new MouseEvent('click', { | ||
bubbles: true, | ||
cancelable: true, | ||
clientX: 25, | ||
clientY: 25, | ||
}) | ||
); | ||
|
||
// onClose should be called | ||
setTimeout(() => { | ||
expect(onClose).toHaveBeenCalledTimes(1); | ||
}, 500); | ||
}); | ||
}) |
Oops, something went wrong.