import { useEffect, useId, type ReactNode } from "react"; import { createPortal } from "react-dom"; import { FocusTrap } from "@mantine/core"; import "@shared/components/Drawer.css"; export type DrawerSide = "right" | "left"; export type DrawerWidth = "sm" | "md" | "lg"; export interface DrawerProps { open: boolean; onClose: () => void; side?: DrawerSide; /** Width preset. sm=22rem, md=27.5rem, lg=36rem. */ width?: DrawerWidth; title?: ReactNode; subtitle?: ReactNode; /** Sticky footer slot below the body. */ footer?: ReactNode; disableBackdropClose?: boolean; disableEscapeClose?: boolean; className?: string; ariaLabel?: string; children?: ReactNode; } /** * Side drawer — sibling to {@link Modal}, with our own brand shell. Tab focus * trapping, initial focus, and focus restoration on close are delegated to * Mantine's ; we keep the slide-in chrome, scroll lock, and ESC / * backdrop dismissal. */ export function Drawer({ open, onClose, side = "right", width = "md", title, subtitle, footer, disableBackdropClose = false, disableEscapeClose = false, className, ariaLabel, children, }: DrawerProps) { const titleId = useId(); useEffect(() => { if (!open || disableEscapeClose) return; function onKey(e: KeyboardEvent) { if (e.key === "Escape") onClose(); } document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [open, disableEscapeClose, onClose]); useEffect(() => { if (!open) return; const previous = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = previous; }; }, [open]); if (!open) return null; function onBackdropClick() { if (!disableBackdropClose) onClose(); } const hasTitle = title !== undefined && title !== null; return createPortal( <>
, document.body, ); }