Vectrum
Sign In
80 lines · 2.8 KB · TypeScript
"use client";

import { useRef, useState, type ReactNode } from "react";
import { useFormStatus } from "react-dom";
import { AlertDialog, Spinner } from "@unlocalhosted/metalui";
import { Button, type ButtonVariant } from "./ui";

function Trigger({ children, variant, onOpen }: { children: ReactNode; variant: ButtonVariant; onOpen: () => void }) {
  const { pending } = useFormStatus();
  return (
    <Button
      type="button"
      variant={variant}
      disabled={pending}
      aria-busy={pending || undefined}
      icon={pending ? <Spinner size="small" label="Working" /> : undefined}
      onClick={onOpen}
    >
      {children}
    </Button>
  );
}

/** "Delete the label “bug”? It will be removed from 3 issues." → the question, then what follows. */
function splitMessage(message: string) {
  const end = message.indexOf("?");
  if (end < 0) return { question: message, rest: "" };
  return { question: message.slice(0, end + 1), rest: message.slice(end + 1).trim() };
}

/**
 * A small form with one button that asks before running a server action: MetalUI's AlertDialog, with
 * the question as its title, what is lost below it, Cancel, and the confirm cap naming the action.
 * Confirming submits the form, so the server action runs as a normal form submission.
 */
export function ConfirmButton({
  action,
  message,
  children,
  variant = "danger",
  title,
  description,
  confirmLabel,
}: {
  action: () => Promise<void>;
  /** The question and what it costs, in one sentence pair: "Delete X? Anything using it stops working." */
  message: string;
  children: ReactNode;
  variant?: ButtonVariant;
  /** Overrides the question taken from `message`. */
  title?: string;
  /** Overrides what is lost, taken from `message` after its question. */
  description?: ReactNode;
  /** The confirm cap's words; defaults to the button's own. */
  confirmLabel?: ReactNode;
}) {
  const form = useRef<HTMLFormElement>(null);
  const [open, setOpen] = useState(false);
  const { question, rest } = splitMessage(message);

  return (
    <form ref={form} action={action}>
      <Trigger variant={variant} onOpen={() => setOpen(true)}>
        {children}
      </Trigger>
      <AlertDialog open={open} onOpenChange={setOpen}>
        <AlertDialog.Popup>
          <AlertDialog.Title>{title ?? question}</AlertDialog.Title>
          <AlertDialog.Description>{description ?? (rest || "This can’t be undone.")}</AlertDialog.Description>
          <AlertDialog.Actions>
            <AlertDialog.Cancel />
            <AlertDialog.Confirm tone={variant === "primary" || variant === "success" ? "primary" : "destructive"} onClick={() => form.current?.requestSubmit()}>
              {confirmLabel ?? children}
            </AlertDialog.Confirm>
          </AlertDialog.Actions>
        </AlertDialog.Popup>
      </AlertDialog>
    </form>
  );
}