oncodsl / web /app /ParamHelpModal.tsx
govindbalki's picture
Upload folder using huggingface_hub
0fff343 verified
Raw
History Blame Contribute Delete
4.12 kB
"use client";
import { useEffect, useId, useRef } from "react";
import { createPortal } from "react-dom";
import { ParamHelpEntry } from "./paramHelpContent";
interface Props {
entry: ParamHelpEntry;
onClose: () => void;
}
const FOCUSABLE =
'a[href],button:not([disabled]),textarea,input,select,[tabindex]:not([tabindex="-1"])';
/**
* Centered dialog with backdrop. Closes on X, Esc, or backdrop click.
* Focus is trapped inside the modal and returns to the trigger on
* close (the provider handles the return).
*/
export default function ParamHelpModal({ entry, onClose }: Props) {
const titleId = useId();
const containerRef = useRef<HTMLDivElement>(null);
// Lock body scroll while open.
useEffect(() => {
const prev = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = prev;
};
}, []);
// Esc + focus trap.
useEffect(() => {
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") {
e.stopPropagation();
onClose();
return;
}
if (e.key === "Tab" && containerRef.current) {
const focusables = containerRef.current.querySelectorAll<HTMLElement>(
FOCUSABLE,
);
if (focusables.length === 0) return;
const first = focusables[0];
const last = focusables[focusables.length - 1];
const active = document.activeElement as HTMLElement | null;
if (e.shiftKey) {
if (active === first || !containerRef.current.contains(active)) {
e.preventDefault();
last.focus();
}
} else {
if (active === last) {
e.preventDefault();
first.focus();
}
}
}
}
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [onClose]);
// Focus the close button once mounted.
useEffect(() => {
const f = containerRef.current?.querySelector<HTMLElement>(FOCUSABLE);
f?.focus();
}, []);
if (typeof document === "undefined") return null;
const node = (
<div
role="presentation"
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
style={{
position: "fixed",
inset: 0,
zIndex: 100,
background: "rgba(35,48,58,0.32)",
display: "flex",
alignItems: "flex-start",
justifyContent: "center",
padding: "min(8vh,72px) 16px",
overflowY: "auto",
}}
>
<div
ref={containerRef}
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
style={{
background: "#FCFBF8",
border: "1px solid #ECEAE4",
borderRadius: 14,
width: "min(640px, 100%)",
maxHeight: "calc(100vh - 24px)",
overflowY: "auto",
boxShadow: "0 24px 50px -16px rgba(35,48,58,0.30)",
padding: "20px 22px 22px",
fontFamily:
'"Helvetica Neue", Helvetica, Arial, sans-serif',
}}
>
<div className="mb-3 flex items-start justify-between gap-4">
<h2
id={titleId}
style={{
color: "#23303A",
fontSize: 17,
fontWeight: 700,
margin: 0,
lineHeight: 1.3,
}}
>
{entry.title}
</h2>
<button
type="button"
onClick={onClose}
aria-label="Close"
className="-mr-2 -mt-1 inline-flex h-8 w-8 items-center justify-center rounded-md text-muted hover:bg-bg hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"
>
<span aria-hidden style={{ fontSize: 18, lineHeight: 1 }}>
×
</span>
</button>
</div>
<div style={{ color: "#23303A", fontSize: 14, lineHeight: 1.55 }}>
{entry.detailed}
</div>
</div>
</div>
);
return createPortal(node, document.body);
}