"use client";
import { useEffect, useRef, type ReactNode } from "react";
import { createPortal } from "react-dom";
const FOCUSABLE = 'a[href],button:not([disabled]),input,select,textarea,[tabindex]:not([tabindex="-1"])';
/** Accessible modal: portal to
, ESC + backdrop close, focus trap, scroll lock,
* focus restore on close. Renders nothing when closed. */
export function Modal({
open,
onClose,
title,
children,
wide,
}: {
open: boolean;
onClose: () => void;
title?: ReactNode;
children: ReactNode;
wide?: boolean;
}) {
const panelRef = useRef(null);
const lastFocus = useRef(null);
// Keep the latest onClose in a ref so the setup effect can depend on [open] alone. If it
// depended on onClose, a caller passing a fresh callback each render (the common case)
// would re-run the effect on every parent re-render — and panelRef.focus() below would
// yank focus off a just-typed back onto the panel (the add-artist modal bug).
const onCloseRef = useRef(onClose);
onCloseRef.current = onClose;
useEffect(() => {
if (!open) return;
lastFocus.current = document.activeElement as HTMLElement | null;
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
panelRef.current?.focus();
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") {
onCloseRef.current();
return;
}
if (e.key !== "Tab") return;
const nodes = panelRef.current?.querySelectorAll(FOCUSABLE);
if (!nodes || nodes.length === 0) return;
const first = nodes[0];
const last = nodes[nodes.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
document.addEventListener("keydown", onKey);
return () => {
document.removeEventListener("keydown", onKey);
document.body.style.overflow = prevOverflow;
lastFocus.current?.focus();
};
}, [open]);
if (!open || typeof document === "undefined") return null;
return createPortal(
e.target === e.currentTarget && onClose()}>
,
document.body,
);
}