feat: provide reusable UI and state galleries
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user