Spaces:
Sleeping
Sleeping
File size: 4,117 Bytes
0fff343 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | "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);
}
|