Confirm Dialog
v0.2.0TODO: Add component description.
registry/behivetech/molecules/confirm-dialog · depends on @behivetech/atoms.button, @behivetech/atoms.modal, @behivetech/get-class-name
confirm-dialog.tsx
1import { type ReactNode } from "react";2import { getClassName } from "@behivetech/get-class-name";3import { Modal } from "@behivetech/atoms.modal";4import { Button } from "@behivetech/atoms.button";5import styles from "./confirm-dialog.module.scss";67export interface ConfirmDialogProps {8 /** Whether the dialog is open */9 open: boolean;10 /** Called when the dialog should close (cancel, confirm, backdrop click, Escape) */11 onOpenChange: (open: boolean) => void;12 /** Dialog title */13 title: string;14 /** Confirmation message/body content */15 message: ReactNode;16 /** Confirm button label; defaults to "Confirm" */17 confirmLabel?: string;18 /** Cancel button label; defaults to "Cancel" */19 cancelLabel?: string;20 /** Called when the confirm button is clicked, before the dialog closes */21 onConfirm: () => void;22 /** Renders the confirm button as a destructive action; defaults to "default" */23 variant?: "default" | "danger";24}2526/**27 * A `Modal` pre-wired with a message and confirm/cancel actions, for "are you28 * sure?" moments — e.g. confirming a delete.29 */30export const ConfirmDialog = ({31 open,32 onOpenChange,33 title,34 message,35 confirmLabel = "Confirm",36 cancelLabel = "Cancel",37 onConfirm,38 variant = "default",39}: ConfirmDialogProps) => {40 const [, getChildClass] = getClassName({41 rootClass: "BHT__ConfirmDialog",42 styles,43 });4445 if (!open) return null;4647 return (48 <Modal title={title} onClose={() => onOpenChange(false)}>49 <p className={getChildClass("message")}>{message}</p>50 <div className={getChildClass("actions")}>51 <Button variant="ghost" onClick={() => onOpenChange(false)}>52 {cancelLabel}53 </Button>54 <Button55 variant={variant === "danger" ? "danger" : "primary"}56 onClick={() => {57 onConfirm();58 onOpenChange(false);59 }}60 >61 {confirmLabel}62 </Button>63 </div>64 </Modal>65 );66};
confirm-dialog.module.scss
1// Content of a Material Design 3 basic dialog; the dialog itself is Modal.23.BHT__ConfirmDialog {4 &__message {5 font-family: var(--md-sys-typescale-body-medium-font, var(--font-body));6 font-size: var(--md-sys-typescale-body-medium-size, 0.875rem);7 font-weight: var(--md-sys-typescale-body-medium-weight, 400);8 line-height: var(--md-sys-typescale-body-medium-line-height, 1.25rem);9 letter-spacing: var(--md-sys-typescale-body-medium-tracking, 0.0179em);10 color: var(--md-sys-color-on-surface-variant, var(--color-text-muted));11 margin: 0 0 var(--spacing-5, 1.25rem);12 }1314 &__actions {15 display: flex;16 justify-content: flex-end;17 gap: var(--spacing-3, 0.75rem);18 }19}
index.ts
1export { ConfirmDialog } from "./confirm-dialog.js";2export type { ConfirmDialogProps } from "./confirm-dialog.js";
confirm-dialog.composition.tsx
1import { useState } from "react";2import { ConfirmDialog } from "./confirm-dialog.js";34export const BasicConfirmDialog = () => {5 const [open, setOpen] = useState(false);67 return (8 <>9 <button type="button" onClick={() => setOpen(true)}>10 Delete page11 </button>12 <ConfirmDialog13 open={open}14 onOpenChange={setOpen}15 title="Delete page"16 message='Are you sure you want to delete "About Us"? This cannot be undone.'17 confirmLabel="Delete"18 variant="danger"19 onConfirm={() => console.log("confirmed delete")}20 />21 </>22 );23};
confirm-dialog.spec.tsx
1import { render, screen } from "@testing-library/react";2import userEvent from "@testing-library/user-event";3import { ConfirmDialog } from "./confirm-dialog.js";45describe("ConfirmDialog", () => {6 it("renders nothing when closed", () => {7 render(8 <ConfirmDialog9 open={false}10 onOpenChange={() => {}}11 title="Delete file"12 message="Are you sure?"13 onConfirm={() => {}}14 />,15 );16 expect(screen.queryByText("Are you sure?")).toBeNull();17 });1819 it("renders the title and message when open", () => {20 render(21 <ConfirmDialog22 open23 onOpenChange={() => {}}24 title="Delete file"25 message="Are you sure?"26 onConfirm={() => {}}27 />,28 );29 expect(screen.getByText("Delete file")).toBeTruthy();30 expect(screen.getByText("Are you sure?")).toBeTruthy();31 });3233 it("calls onConfirm and closes when the confirm button is clicked", async () => {34 const handleConfirm = vi.fn();35 const handleOpenChange = vi.fn();36 const user = userEvent.setup();37 render(38 <ConfirmDialog39 open40 onOpenChange={handleOpenChange}41 title="Delete file"42 message="Are you sure?"43 onConfirm={handleConfirm}44 />,45 );46 await user.click(screen.getByRole("button", { name: "Confirm" }));47 expect(handleConfirm).toHaveBeenCalledTimes(1);48 expect(handleOpenChange).toHaveBeenCalledWith(false);49 });5051 it("closes without calling onConfirm when cancel is clicked", async () => {52 const handleConfirm = vi.fn();53 const handleOpenChange = vi.fn();54 const user = userEvent.setup();55 render(56 <ConfirmDialog57 open58 onOpenChange={handleOpenChange}59 title="Delete file"60 message="Are you sure?"61 onConfirm={handleConfirm}62 />,63 );64 await user.click(screen.getByRole("button", { name: "Cancel" }));65 expect(handleConfirm).not.toHaveBeenCalled();66 expect(handleOpenChange).toHaveBeenCalledWith(false);67 });6869 it("uses custom confirm/cancel labels", () => {70 render(71 <ConfirmDialog72 open73 onOpenChange={() => {}}74 title="Delete file"75 message="Are you sure?"76 onConfirm={() => {}}77 confirmLabel="Delete"78 cancelLabel="Keep it"79 />,80 );81 expect(screen.getByRole("button", { name: "Delete" })).toBeTruthy();82 expect(screen.getByRole("button", { name: "Keep it" })).toBeTruthy();83 });84});