69 lines
1.7 KiB
TypeScript
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>
|
|
);
|
|
}
|