/** * The modal dialog (milestone 23, ruling 4). * * React Aria owns the focus trap, the Escape handler and the `aria-modal` * wiring that the hand-rolled overlay only approximated. State is controlled by * the caller because the trigger is a table row button, not a `DialogTrigger`. * * The title is both the visible heading and the accessible name: `Heading * slot="title"` is what React Aria points `aria-labelledby` at, so a caller * cannot name the dialog one thing and show another. */ import type { ReactNode } from "react"; import * as stylex from "@stylexjs/stylex"; import { Dialog as AriaDialog, Heading, Modal, ModalOverlay } from "react-aria-components"; import { colors, layers, metrics } from "./tokens.stylex"; import { styles as shared } from "./styles"; interface Props { /** The dialog's heading, and with it the dialog's accessible name. */ title: string; isOpen: boolean; onClose: () => void; /** `detail` is the wider panel a record needs; `form` fits a column of fields. */ size?: "form" | "detail"; children: ReactNode; } const styles = stylex.create({ overlay: { position: "fixed", inset: 0, zIndex: layers.overlay, display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem", backgroundColor: "rgba(0, 0, 0, 0.4)", }, /** * A column so the header stays put and the body scrolls under it. The height * cap is what keeps a long record inside the viewport instead of running off * the bottom of a short one. */ panel: { width: "100%", maxHeight: "85vh", display: "flex", flexDirection: "column", borderRadius: "0.5rem", borderWidth: 1, borderStyle: "solid", borderColor: colors.border, backgroundColor: colors.surfaceRaised, color: colors.text, boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)", }, panelForm: { maxWidth: "28rem", }, panelDetail: { maxWidth: "52rem", }, /** * The panel already draws the boundary; the dialog's own ring would double * it. `minHeight: 0` is what lets the body shrink far enough to scroll. */ body: { outlineStyle: "none", display: "flex", flexDirection: "column", minHeight: 0, }, header: { flexShrink: 0, display: "flex", alignItems: "center", justifyContent: "space-between", gap: "1rem", borderBottomWidth: 1, borderBottomStyle: "solid", borderBottomColor: colors.border, paddingInline: "1.5rem", paddingBlock: "0.75rem", }, title: { fontSize: "1.125rem", lineHeight: "1.75rem", fontWeight: 600, }, /** The pointer-target floor on both axes, which the word alone misses. */ close: { minWidth: metrics.hitTarget, minHeight: metrics.hitTarget, display: "inline-flex", alignItems: "center", justifyContent: "center", }, content: { minHeight: 0, overflowY: "auto", paddingInline: "1.5rem", paddingBlock: "1.5rem", }, }); export default function Dialog({ title, isOpen, onClose, size = "form", children }: Props) { return ( { if (!open) onClose(); }} isDismissable className={() => stylex.props(styles.overlay).className ?? ""} > stylex.props(styles.panel, size === "detail" ? styles.panelDetail : styles.panelForm).className ?? "" } >
{title}
{children}
); }