shadimage

Confirm Action

A confirmation dialog that wraps an async action with loading state

src/registry/base/confirm-action/components/confirm-action.tsx

import {
  AlertDialog,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogMedia,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { type ComponentProps, type ReactNode } from "react";
import { Button } from "@/components/ui/button";
import {
  AsyncButton,
  type AsyncButtonProps,
} from "@/components/ui/async-button";
import { useControlledState } from "@/hooks/use-controlled-state";

interface ConfirmActionProps<TData = unknown, TError = unknown> extends Omit<
  AsyncButtonProps<TData, TError, []>,
  "title"
> {
  title?: ReactNode;
  description?: ReactNode;
  cancelText?: ReactNode;
  confirmText?: ReactNode;
  confirmVariant?: ComponentProps<typeof Button>["variant"];
  size?: ComponentProps<typeof AlertDialogContent>["size"];
  media?: ReactNode;
  /** Controlled open state. Provide alongside `onOpenChange` to manage the dialog externally. */
  open?: boolean;
  onOpenChange?: (open: boolean) => void;
  /** Trigger element. Omit when controlling `open` externally. */
  children?: ReactNode;
}

/**
 * Guards inreversible actions behind a confirm dialog
 *
 * **Required:** `action`
 */
export function ConfirmAction<TData = unknown, TError = unknown>({
  action,
  title = "Are you sure?",
  description = "This action cannot be undone.",
  cancelText = "Cancel",
  confirmText = "Continue",
  confirmVariant = "default",
  size,
  media,
  loadingText,
  onSuccess,
  onError,
  onSettled,
  open: openProp,
  onOpenChange,
  children,
  ...props
}: ConfirmActionProps<TData, TError>) {
  const [open, setOpen] = useControlledState({
    value: openProp,
    defaultValue: false,
    onChange: onOpenChange,
  });

  return (
    <AlertDialog open={open} onOpenChange={setOpen}>
      {children && <AlertDialogTrigger asChild>{children}</AlertDialogTrigger>}

      <AlertDialogContent size={size}>
        <AlertDialogHeader>
          {media && <AlertDialogMedia>{media}</AlertDialogMedia>}
          <AlertDialogTitle>{title}</AlertDialogTitle>
          <AlertDialogDescription>{description}</AlertDialogDescription>
        </AlertDialogHeader>

        <AlertDialogFooter>
          <AlertDialogCancel>{cancelText}</AlertDialogCancel>
          <AsyncButton
            variant={confirmVariant}
            action={action}
            loadingText={loadingText ?? confirmText}
            onSuccess={(data, args) => {
              onSuccess?.(data, args);
              setOpen(false);
            }}
            onError={onError}
            onSettled={onSettled}
            {...props}
          >
            {confirmText}
          </AsyncButton>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  );
}
Where it goes
stats
Interaction
animated-countermodal