Files
nxdns/web/src/ui/Dialog.tsx
T

69 lines
1.7 KiB
TypeScript

/**
* 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`.
*/
import type { ReactNode } from "react";
import * as stylex from "@stylexjs/stylex";
import { Dialog as AriaDialog, Modal, ModalOverlay } from "react-aria-components";
import { colors } from "./tokens.stylex";
interface Props {
/** The dialog's accessible name. */
label: string;
isOpen: boolean;
onClose: () => void;
children: ReactNode;
}
const styles = stylex.create({
overlay: {
position: "fixed",
inset: 0,
zIndex: 50,
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "1rem",
backgroundColor: "rgba(0, 0, 0, 0.4)",
},
panel: {
width: "100%",
maxWidth: "28rem",
borderRadius: "0.5rem",
borderWidth: 1,
borderStyle: "solid",
borderColor: colors.border,
backgroundColor: colors.surfaceRaised,
color: colors.text,
padding: "1.5rem",
boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
},
/** The panel already draws the boundary; the dialog's own ring would double it. */
body: {
outlineStyle: "none",
},
});
export default function Dialog({ label, isOpen, onClose, children }: Props) {
return (
<ModalOverlay
isOpen={isOpen}
onOpenChange={(open) => {
if (!open) onClose();
}}
isDismissable
className={() => stylex.props(styles.overlay).className ?? ""}
>
<Modal className={() => stylex.props(styles.panel).className ?? ""}>
<AriaDialog aria-label={label} {...stylex.props(styles.body)}>
{children}
</AriaDialog>
</Modal>
</ModalOverlay>
);
}