All files / src/components/display ConfirmDialog.tsx

0% Statements 0/6
0% Branches 0/9
0% Functions 0/3
0% Lines 0/5

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86                                                                                                                                                                           
import { useId, type JSX } from "react";
import { AnimatePresence, motion } from "motion/react";
import styles from "./ConfirmDialog.module.scss";
 
/** Props for the ConfirmDialog component. */
interface ConfirmDialogProps {
  /** Whether the dialog is currently visible. */
  isOpen: boolean;
  /** Short, action-oriented title (e.g. "Delete Task?"). */
  title: string;
  /** Consequence description shown below the title. */
  description?: string;
  /** Label for the danger confirm button. Defaults to "Delete". */
  confirmLabel?: string;
  /** Called when the user confirms the destructive action. */
  onConfirm: () => void;
  /** Called when the user cancels or clicks the overlay backdrop. */
  onCancel: () => void;
}
 
/**
 * Modal confirmation dialog with glass card aesthetic and motion animation.
 *
 * Replaces native `window.confirm()` for destructive actions, providing a
 * styled in-app dialog that matches the dark glass UI.
 */
export function ConfirmDialog({
  isOpen,
  title,
  description,
  confirmLabel = "Delete",
  onConfirm,
  onCancel,
}: ConfirmDialogProps): JSX.Element {
  const titleId = useId();
  const descriptionId = useId();
 
  return (
    <AnimatePresence>
      {isOpen && (
        <motion.div
          className={styles.overlay}
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          transition={{ duration: 0.15 }}
          onClick={onCancel}
          onKeyDown={(e) => {
            if (e.key === "Escape") onCancel();
          }}
          role="dialog"
          aria-modal="true"
          aria-labelledby={titleId}
          aria-describedby={description ? descriptionId : undefined}
        >
          <motion.div
            className={styles.dialog}
            initial={{ opacity: 0, scale: 0.93, y: -10 }}
            animate={{ opacity: 1, scale: 1, y: 0 }}
            exit={{ opacity: 0, scale: 0.93, y: -10 }}
            transition={{ duration: 0.15, ease: [0.16, 1, 0.3, 1] }}
            onClick={(e) => e.stopPropagation()}
          >
            <h3 id={titleId} className={styles.title}>
              {title}
            </h3>
            {description && (
              <p id={descriptionId} className={styles.description}>
                {description}
              </p>
            )}
            <div className={styles.actions}>
              <button type="button" className={styles.cancelButton} onClick={onCancel} autoFocus>
                Cancel
              </button>
              <button type="button" className={styles.confirmButton} onClick={onConfirm}>
                {confirmLabel}
              </button>
            </div>
          </motion.div>
        </motion.div>
      )}
    </AnimatePresence>
  );
}