Completion Dialog
Dialog displayed upon completing a task or workflow.
A dialog for displaying completion status with confetti animation.
Installation
pnpm dlx shadcn@latest add https://ui.vllnt.com/r/completion-dialog.jsonbash
Code
"use client";
import { memo, useCallback, useEffect, useRef } from "react";
import type { ReactNode } from "react";
import { cn } from "../../lib/utils";
import { Button } from "../button";
export type CompletionDialogProps = {
cancelLabel?: string;
cancelShortcut?: string;
className?: string;
closeIcon?: ReactNode;
confirmLabel?: string;
confirmShortcut?: string;
description?: ReactNode;
isOpen: boolean;
onCancel: () => void;
onClose: () => void;
onConfirm: () => void;
title: string;
};
type DialogContentProps = Omit<CompletionDialogProps, "isOpen">;
// eslint-disable-next-line max-lines-per-function -- Dialog content with keyboard handling
function DialogContent({
cancelLabel = "Skip",
cancelShortcut = "S",
className,
closeIcon,
confirmLabel = "Done",
confirmShortcut = "D",
description,
onCancel,
onClose,
onConfirm,
title,
}: DialogContentProps): React.ReactNode {
const confirmButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
confirmButtonRef.current?.focus();
}, []);
return (
<div
className={cn(
"relative z-10 w-full max-w-md mx-4 p-6 bg-background border border-border rounded-lg shadow-lg",
"animate-in fade-in-0 zoom-in-95 duration-200",
className,
)}
>
<button
aria-label="Close"
className="absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100 transition-opacity"
onClick={onClose}
type="button"
>
{closeIcon ?? (
<svg
className="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M6 18L18 6M6 6l12 12"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
/>
</svg>
)}
</button>
<div className="mb-4">
<h2 className="text-lg font-semibold" id="completion-dialog-title">
{title}
</h2>
{description ? (
<div className="text-sm text-muted-foreground mt-1.5">
{description}
</div>
) : null}
</div>
<div className="flex flex-row gap-2">
<Button className="flex-1 gap-2" onClick={onCancel} variant="outline">
<span>{cancelLabel}</span>
{cancelShortcut ? (
<kbd className="hidden md:inline-flex px-1.5 py-0.5 text-[10px] font-mono bg-muted rounded">
{cancelShortcut}
</kbd>
) : null}
</Button>
<Button
className="flex-1 gap-2"
onClick={onConfirm}
ref={confirmButtonRef}
>
<span>{confirmLabel}</span>
{confirmShortcut ? (
<kbd className="hidden md:inline-flex px-1.5 py-0.5 text-[10px] font-mono bg-primary-foreground/20 rounded">
{confirmShortcut}
</kbd>
) : null}
</Button>
</div>
</div>
);
}
// eslint-disable-next-line max-lines-per-function -- Modal with keyboard handling
function CompletionDialogImpl({
cancelLabel,
cancelShortcut = "S",
className,
closeIcon,
confirmLabel,
confirmShortcut = "D",
description,
isOpen,
onCancel,
onClose,
onConfirm,
title,
}: CompletionDialogProps): React.ReactNode {
const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
if (!isOpen) return;
if (event.key === "Escape") {
event.preventDefault();
event.stopPropagation();
onClose();
return;
}
if (
event.key === "Enter" ||
event.key.toLowerCase() === confirmShortcut.toLowerCase()
) {
event.preventDefault();
event.stopPropagation();
onConfirm();
return;
}
if (event.key.toLowerCase() === cancelShortcut.toLowerCase()) {
event.preventDefault();
event.stopPropagation();
onCancel();
}
},
[isOpen, onClose, onConfirm, onCancel, confirmShortcut, cancelShortcut],
);
useEffect(() => {
if (!isOpen) return;
document.addEventListener("keydown", handleKeyDown, true);
return () => {
document.removeEventListener("keydown", handleKeyDown, true);
};
}, [isOpen, handleKeyDown]);
if (!isOpen) return null;
return (
<div
aria-labelledby="completion-dialog-title"
aria-modal="true"
className="absolute inset-0 z-[100] flex items-center justify-center"
role="dialog"
>
<div
aria-hidden="true"
className={cn(
"absolute inset-0 bg-background/80 backdrop-blur-sm",
"animate-in fade-in-0 duration-200",
)}
onClick={onClose}
/>
<DialogContent
cancelLabel={cancelLabel}
cancelShortcut={cancelShortcut}
className={className}
closeIcon={closeIcon}
confirmLabel={confirmLabel}
confirmShortcut={confirmShortcut}
description={description}
onCancel={onCancel}
onClose={onClose}
onConfirm={onConfirm}
title={title}
/>
</div>
);
}
export const CompletionDialog = memo(CompletionDialogImpl);
CompletionDialog.displayName = "CompletionDialog";
typescript