72 lines
1.6 KiB
TypeScript
72 lines
1.6 KiB
TypeScript
import { useEffect, useRef, type ReactNode } from "react";
|
|
|
|
export function ModalDialog({
|
|
id,
|
|
open,
|
|
labelledBy,
|
|
onClose,
|
|
children,
|
|
className = "",
|
|
}: {
|
|
readonly id: string;
|
|
readonly open: boolean;
|
|
readonly labelledBy: string;
|
|
readonly onClose: () => void;
|
|
readonly children: ReactNode;
|
|
readonly className?: string;
|
|
}) {
|
|
const dialog = useRef<HTMLDialogElement>(null);
|
|
|
|
useEffect(() => {
|
|
const element = dialog.current;
|
|
if (!element) return;
|
|
|
|
if (open && !element.open) {
|
|
if (typeof element.showModal === "function") {
|
|
element.showModal();
|
|
} else {
|
|
// jsdom and older browsers can still expose the content for a usable
|
|
// non-modal fallback.
|
|
element.setAttribute("open", "");
|
|
}
|
|
(
|
|
element.querySelector<HTMLElement>("[data-dialog-initial-focus]") ??
|
|
element
|
|
).focus();
|
|
return;
|
|
}
|
|
|
|
if (!open && element.open) {
|
|
if (typeof element.close === "function") {
|
|
element.close();
|
|
} else {
|
|
element.removeAttribute("open");
|
|
}
|
|
}
|
|
}, [open]);
|
|
|
|
return (
|
|
<dialog
|
|
ref={dialog}
|
|
id={id}
|
|
className={`app-dialog auxiliary-dialog ${className}`.trim()}
|
|
aria-labelledby={labelledBy}
|
|
aria-modal="true"
|
|
tabIndex={-1}
|
|
onClose={onClose}
|
|
onCancel={(event) => {
|
|
event.preventDefault();
|
|
onClose();
|
|
}}
|
|
onKeyDownCapture={(event) => {
|
|
if (event.key !== "Escape") return;
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
onClose();
|
|
}}
|
|
>
|
|
{children}
|
|
</dialog>
|
|
);
|
|
}
|