An extremely detailed overview of how to use the native <dialog> element in React to build a state-driven modal component. As ever, the answer is significantly more complicated than it would be outside of React land, but just about every step is thought through here.
The one issue I had when following along was the onKeyDown event listener. Assigning this to the <dialog> directly only works if the user uses the Escape key whilst focused on the modal. If they click the background, or anywhere else on the browser, then use Escape the modal will still close, but the state will not be updated correctly. I had to modify the code to use a more traditional event listener instead. Here's my final snippet:
interface ModalProps {
title: string
isOpen?: boolean
onClose?: () => void
}
export const Modal = ({
title,
isOpen = false,
onClose,
children,
...props
}: PropsWithChildren<ModalProps>) => {
const modalRef = React.useRef<HTMLDialogElement | null>(null)
const [open, setOpen] = React.useState(isOpen)
// Function: Closes the modal and syncs state with parent
const closeModal = () => {
if (onClose) {
onClose()
}
setOpen(false)
}
// Function: Control modal via props/parent
React.useEffect(() => {
setOpen(isOpen)
}, [isOpen])
// Function: Control the modal with native browser APIs
React.useEffect(() => {
const modal = modalRef.current
if (modal) {
if (open) {
modal.showModal()
} else {
modal.close()
}
}
}, [open])
// Function: Listen for escape key and close modal / sync state
React.useEffect(() => {
const escapeModal = (event: KeyboardEvent) => {
if (event.key === "Escape") {
if (onClose) {
onClose()
}
setOpen(false)
}
}
document.addEventListener("keydown", (e) => escapeModal(e))
return () =>
document.removeEventListener("keydown", (e) => escapeModal(e))
}, [onClose])
return (
<dialog
ref={modalRef}
className="modal"
aria-labelledby="modalTitle"
{...props}
>
<header>
<h2 id="modalTitle">{title}</h2>
<button
aria-label="Close"e"
onClick={() => closeModal()}
>
<CloseIcon />
</button>
</header>
{children}
</dialog>
)
}
On how to handle native escape key functionality:
However, since we are managing the states of ourModalcomponent using theuseStateHook, we need to update it accordingly when the escape key is pressed to ensure the proper functioning of theModaldialog.