89 lines
2.4 KiB
React
89 lines
2.4 KiB
React
import { useEffect, useId, useRef } from "react";
|
||
|
||
/**
|
||
* @param {{
|
||
* open: boolean,
|
||
* onClose: () => void,
|
||
* title: string,
|
||
* description?: string,
|
||
* children?: React.ReactNode,
|
||
* actions?: React.ReactNode
|
||
* }} props
|
||
*/
|
||
export function Dialog({
|
||
open,
|
||
onClose,
|
||
title,
|
||
description,
|
||
children,
|
||
actions,
|
||
}) {
|
||
const dialogRef = useRef(/** @type {HTMLDialogElement | null} */ (null));
|
||
const previousFocusRef = useRef(/** @type {HTMLElement | null} */ (null));
|
||
const titleId = useId();
|
||
const descriptionId = useId();
|
||
|
||
useEffect(() => {
|
||
const dialog = dialogRef.current;
|
||
if (!dialog) return;
|
||
|
||
if (open) {
|
||
previousFocusRef.current =
|
||
document.activeElement instanceof HTMLElement
|
||
? document.activeElement
|
||
: null;
|
||
if (!dialog.open) {
|
||
if (typeof dialog.showModal === "function") dialog.showModal();
|
||
else dialog.setAttribute("open", "");
|
||
}
|
||
const firstFocusable = dialog.querySelector(
|
||
"[autofocus], button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])",
|
||
);
|
||
if (firstFocusable instanceof HTMLElement) firstFocusable.focus();
|
||
return;
|
||
}
|
||
|
||
if (dialog.open) {
|
||
if (typeof dialog.close === "function") dialog.close();
|
||
else dialog.removeAttribute("open");
|
||
}
|
||
previousFocusRef.current?.focus();
|
||
previousFocusRef.current = null;
|
||
}, [open]);
|
||
|
||
return (
|
||
<dialog
|
||
className="ui-dialog"
|
||
ref={dialogRef}
|
||
aria-labelledby={titleId}
|
||
aria-describedby={description ? descriptionId : undefined}
|
||
onCancel={(event) => {
|
||
event.preventDefault();
|
||
onClose();
|
||
}}
|
||
onClick={(event) => {
|
||
if (event.target === event.currentTarget) onClose();
|
||
}}
|
||
>
|
||
<div className="ui-dialog__surface">
|
||
<header className="ui-dialog__header">
|
||
<div>
|
||
<h2 id={titleId}>{title}</h2>
|
||
{description ? <p id={descriptionId}>{description}</p> : null}
|
||
</div>
|
||
<button
|
||
className="ui-dialog__close"
|
||
type="button"
|
||
aria-label={`${title} 닫기`}
|
||
onClick={onClose}
|
||
>
|
||
×
|
||
</button>
|
||
</header>
|
||
{children ? <div className="ui-dialog__content">{children}</div> : null}
|
||
{actions ? <footer className="ui-dialog__actions">{actions}</footer> : null}
|
||
</div>
|
||
</dialog>
|
||
);
|
||
}
|