import clsx from "clsx"; import { X } from "lucide-react"; import * as React from "react"; import { createPortal, flushSync } from "react-dom"; import { useNavigate } from "react-router"; import { SendouButton, type SendouButtonProps, } from "~/components/elements/Button"; import { useHydrated } from "~/hooks/useHydrated"; import styles from "./Dialog.module.css"; interface DialogElementProps { id?: string; className?: string; isDismissable?: boolean; onClose?: () => void; "aria-label"?: string; "aria-labelledby"?: string; children: React.ReactNode; ref?: React.Ref; } /** * Unstyled native `` shell: shows itself modally on mount, closes on * Escape (and outside clicks when `isDismissable`) and reports every close * through `onClose`. The caller owns visibility by mounting/unmounting it. * Focus lands on the dialog itself rather than its first control. * * Portaled to `` so a dialog holding a form can be rendered from inside * another form without nesting the `
` elements. Renders nothing on the * server. */ export function SendouModal({ ref, ...rest }: DialogElementProps) { const isHydrated = useHydrated(); if (!isHydrated) return null; return createPortal( { if (typeof ref === "function") { ref(dialog); } else if (ref) { ref.current = dialog; } if (dialog && !dialog.open) { dialog.showModal(); dialog.focus(); } }} {...rest} />, document.body, ); } function DialogElement({ id, className, isDismissable, onClose, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, children, ref, }: DialogElementProps) { return ( {children} ); } // Safari 26 is missing `closedby`, close on backdrop clicks manually function closeOnBackdropClick(event: React.MouseEvent) { if (event.target !== event.currentTarget) return; const rect = event.currentTarget.getBoundingClientRect(); const outside = event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom; if (outside) { event.currentTarget.close(); } } /** Invoker commands open and close the dialog natively; this guards the JS fallback for browsers without them. */ function supportsInvokerCommands() { return "commandForElement" in HTMLButtonElement.prototype; } interface SendouDialogProps { /** * Button-like element that opens the dialog through `commandfor`. With a * trigger the dialog is rendered in place, closed, so it opens even before * hydration. Its content is remounted on every close. */ trigger?: React.ReactElement< Pick >; children?: React.ReactNode; heading?: string; showHeading?: boolean; onClose?: () => void; /** URL to navigate to on close */ onCloseTo?: string; onOpenChange?: (isOpen: boolean) => void; isOpen?: boolean; /** Closing by clicking outside the dialog. */ isDismissable?: boolean; className?: string; "aria-label"?: string; /** takes over the full screen, hiding the content below */ isFullScreen?: boolean; /** show the close button even without onClose */ showCloseButton?: boolean; /** * Trigger mode: mount the content only while open, for content that is * expensive or does work on mount. Costs the pre-hydration open. */ lazy?: boolean; } /** * Dialog that is open by default without a `trigger` (or controlled via `isOpen`), or opened by * the given `trigger` element. */ export function SendouDialog({ trigger, lazy, children, ...rest }: SendouDialogProps) { if (trigger) { return ( {children} ); } const props = typeof rest.isOpen === "boolean" ? rest : { ...rest, isOpen: true }; return {children}; } type DialogChromeProps = Pick< SendouDialogProps, | "heading" | "showHeading" | "className" | "showCloseButton" | "isDismissable" | "isFullScreen" | "onClose" | "onCloseTo" | "aria-label" >; function TriggeredDialog({ trigger, lazy, children, ...chrome }: DialogChromeProps & { trigger: NonNullable; lazy?: boolean; children?: React.ReactNode; }) { const navigate = useNavigate(); const dialogId = React.useId(); const dialogRef = React.useRef(null); const [open, setOpen] = React.useState(false); const [contentKey, remountContent] = React.useReducer( (key: number) => key + 1, 0, ); const handleClosed = () => { remountContent(); if (chrome.onCloseTo) { navigate(chrome.onCloseTo); } else { chrome.onClose?.(); } }; // React wires `onToggle` on a hydrated only when it is also a // popover, so opens are observed natively (also seeding from a dialog // opened before hydration). Lazy content is committed on `beforetoggle`, // which fires synchronously before the dialog shows, so it is in the // dialog's first painted frame rather than a frame behind it. Wired once // on mount: a ref callback would rerun on every render and, on an open // dialog, take the focus back from whatever inside it the user is typing in. React.useEffect(() => { const dialog = dialogRef.current; if (!dialog) return; const handleOpened = () => { setOpen(true); dialog.focus(); }; const onBeforeToggle = (event: Event) => { if ((event as ToggleEvent).newState === "open") { flushSync(() => setOpen(true)); } }; const onToggle = (event: Event) => { if ((event as ToggleEvent).newState === "open") { handleOpened(); } else { setOpen(false); } }; dialog.addEventListener("beforetoggle", onBeforeToggle); dialog.addEventListener("toggle", onToggle); if (dialog.open) handleOpened(); return () => { dialog.removeEventListener("beforetoggle", onBeforeToggle); dialog.removeEventListener("toggle", onToggle); }; }, []); return ( <> {React.cloneElement(trigger, { commandfor: dialogId, command: "show-modal", onClick: (event) => { trigger.props.onClick?.(event); if (!supportsInvokerCommands()) { dialogRef.current?.showModal(); } }, })} {lazy && !open ? null : children} ); } function PortaledDialog({ children, isOpen, onOpenChange, ...chrome }: DialogChromeProps & Pick) { const navigate = useNavigate(); const dialogId = React.useId(); const handleClosed = () => { if (onOpenChange) { onOpenChange(false); } else if (chrome.onCloseTo) { navigate(chrome.onCloseTo); } else { chrome.onClose?.(); } }; if (!isOpen) return null; return ( {children} ); } function dialogElementProps( { className, isFullScreen, isDismissable, heading, "aria-label": ariaLabel, }: DialogChromeProps, dialogId: string, onClose: () => void, ) { return { className: clsx(className, styles.modal, "scrollbar", { [styles.fullScreenModal]: isFullScreen, }), isDismissable, onClose, "aria-label": ariaLabel, "aria-labelledby": !ariaLabel && heading ? headingIdFor(dialogId) : undefined, }; } function headingIdFor(dialogId: string) { return `${dialogId}-heading`; } function DialogChrome({ dialogId, heading, showHeading = true, showCloseButton, onClose, onCloseTo, children, }: DialogChromeProps & { dialogId: string; children: React.ReactNode }) { if (!showHeading) return children; return ( <>
{heading ? (

{heading}

) : null} {showCloseButton || onClose || onCloseTo ? ( } shape="circle" variant="minimal-destructive" className="ml-auto" aria-label="Close" commandfor={dialogId} command="close" onClick={(event) => { if (!supportsInvokerCommands()) { event.currentTarget.closest("dialog")?.close(); } }} /> ) : null}
{children} ); }