Files
kandev/apps/packages/ui/src/dialog.tsx
T
2026-09-01 11:56:52 +00:00

188 lines
5.6 KiB
TypeScript

"use client";
import * as React from "react";
import { Dialog as DialogPrimitive } from "radix-ui";
import { cn } from "./lib/utils";
import { Button } from "./button";
import { IconX } from "@tabler/icons-react";
import { handleDialogDefaultActionKeyDown } from "./lib/dialog-default-action";
import {
DIALOG_CLOSE_RECOVERY_MS,
finishClosingDialogAnimations,
subscribeDialogCloseRecovery,
} from "./lib/dialog-body-lock";
function Dialog({ onOpenChange, ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
const finishClosingAnimations = React.useCallback(
() => finishClosingDialogAnimations(document),
[],
);
// Unmount is one way to strand the lock — Radix's animation-based cleanup
// never runs when the dialog disappears mid-close.
React.useEffect(
() => () => {
finishClosingAnimations();
},
[finishClosingAnimations],
);
// Becoming visible again is the other. Dialog roots share one document-level
// listener because the body lock and closing portals are document-global.
React.useEffect(() => subscribeDialogCloseRecovery(document), []);
// And the general case: once a close has been requested, the page must be
// usable shortly afterwards whether or not animationend ever arrives.
const handleOpenChange = React.useCallback(
(open: boolean) => {
onOpenChange?.(open);
if (!open) window.setTimeout(finishClosingAnimations, DIALOG_CLOSE_RECOVERY_MS);
},
[finishClosingAnimations, onOpenChange],
);
return <DialogPrimitive.Root data-slot="dialog" onOpenChange={handleOpenChange} {...props} />;
}
function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}
function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"bg-background/70 data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 duration-100 fixed inset-0 isolate z-50",
className,
)}
{...props}
/>
);
}
function DialogContent({
className,
children,
showCloseButton = true,
overlayClassName,
enterConfirms = true,
onKeyDown,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean;
overlayClassName?: string;
/** Pressing Enter activates the semantic action button. Default: true. */
enterConfirms?: boolean;
}) {
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
onKeyDown?.(event);
if (enterConfirms) handleDialogDefaultActionKeyDown(event);
};
return (
<DialogPortal>
<DialogOverlay className={overlayClassName} />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"border-border bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 grid max-w-[calc(100%-2rem)] gap-4 rounded-lg border p-4 text-xs/relaxed shadow-md duration-100 sm:max-w-lg fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2",
className,
)}
onKeyDown={handleKeyDown}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close data-slot="dialog-close" asChild>
<Button variant="ghost" className="absolute top-2 right-2" size="icon-sm">
<IconX />
<span className="sr-only">Close</span>
</Button>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
);
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div data-slot="dialog-header" className={cn("gap-1 flex flex-col", className)} {...props} />
);
}
function DialogFooter({
className,
showCloseButton = false,
children,
...props
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean;
}) {
return (
<div
data-slot="dialog-footer"
className={cn("gap-2 flex flex-col-reverse sm:flex-row sm:justify-end", className)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close asChild>
<Button variant="outline">Close</Button>
</DialogPrimitive.Close>
)}
</div>
);
}
function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("min-w-0 text-sm font-medium text-balance wrap-anywhere", className)}
{...props}
/>
);
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn(
"text-muted-foreground min-w-0 *:[a]:hover:text-foreground text-xs/relaxed text-pretty wrap-anywhere *:[a]:underline *:[a]:underline-offset-3",
className,
)}
{...props}
/>
);
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};