Files
vantage-app/adminsite/components/Modal.tsx
T

64 lines
2.4 KiB
TypeScript

"use client";
import { useEffect, useRef } from "react";
/*
* A native <dialog>, not a div with a fixed overlay.
*
* showModal() gives focus trapping, inert background, Escape and the top layer
* for free — all four are things a hand-rolled overlay gets wrong, and the third
* is the one staff will actually reach for. The only wiring needed is keeping
* React state and the element's open state in step, and routing every close —
* Escape, backdrop, button — through one onClose.
*/
export function Modal({
open,
onClose,
title,
meta,
footer,
children,
}: {
open: boolean;
onClose: () => void;
title: string;
meta?: React.ReactNode;
footer?: React.ReactNode;
children: React.ReactNode;
}) {
const ref = useRef<HTMLDialogElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
if (open && !el.open) el.showModal();
if (!open && el.open) el.close();
}, [open]);
return (
<dialog
ref={ref}
onCancel={(e) => {
e.preventDefault();
onClose();
}}
/* Clicking the backdrop hits the dialog element itself, never a
* child — so this closes on backdrop and not on content. */
onClick={(e) => {
if (e.target === ref.current) onClose();
}}
className="w-[min(44rem,94vw)] rounded border border-rule bg-panel p-0 text-ink shadow-lg backdrop:bg-[rgba(4,12,24,0.55)]"
>
<header className="flex flex-wrap items-center gap-3 border-b border-rule-soft bg-panel-2 px-4 py-3">
<h2 className="text-[1.02rem] font-bold tracking-[-0.01em]">{title}</h2>
{meta && <span className="font-mono text-[0.68rem] uppercase tracking-[0.12em] text-ink-3">{meta}</span>}
<button type="button" onClick={onClose} className="ml-auto rounded border border-rule px-2 py-1 font-mono text-[0.68rem] uppercase tracking-[0.1em] text-ink-2 hover:border-ink-3" aria-label="Close">
Esc
</button>
</header>
<div className="grid max-h-[68vh] gap-4 overflow-y-auto p-4">{children}</div>
{footer && <footer className="flex flex-wrap items-center gap-3 border-t border-rule-soft bg-panel-2 px-4 py-3">{footer}</footer>}
</dialog>
);
}