Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';

import { ConfirmationDialog } from './confirmation-dialog';

jest.mock('i18next', () => ({
t: (key: string) => key,
}));

describe('ConfirmationDialog', () => {
it('renders headerLeading beside the title and description', () => {
render(
<ConfirmationDialog
isOpen
onOpenChange={jest.fn()}
title="Launch Campaign?"
description="Please verify settings."
onConfirm={jest.fn()}
onCancel={jest.fn()}
headerLeading={<span data-testid="leading-icon">!</span>}
/>,
);

expect(screen.getByTestId('leading-icon')).toBeInTheDocument();
expect(
screen.getByRole('heading', { name: 'Launch Campaign?' }),
).toBeInTheDocument();
expect(screen.getByText('Please verify settings.')).toBeInTheDocument();
});

it('invokes onConfirm when the confirm button is clicked', async () => {
const user = userEvent.setup();
const onConfirm = jest.fn();

render(
<ConfirmationDialog
isOpen
onOpenChange={jest.fn()}
title="Title"
description="Description"
confirmButtonText="Go"
onConfirm={onConfirm}
onCancel={jest.fn()}
/>,
);

await user.click(screen.getByRole('button', { name: 'Go' }));
expect(onConfirm).toHaveBeenCalledTimes(1);
});

it('renders a rich-text description node', () => {
render(
<ConfirmationDialog
isOpen
onOpenChange={jest.fn()}
title="Delete campaign?"
description={
<>
You will lose <strong data-testid="highlight">$8,200</strong> in
savings.
</>
}
onConfirm={jest.fn()}
onCancel={jest.fn()}
/>,
);

expect(screen.getByTestId('highlight')).toHaveTextContent('$8,200');
});

it('renders the confirm button with the destructive variant when requested', () => {
render(
<ConfirmationDialog
isOpen
onOpenChange={jest.fn()}
title="Delete campaign?"
description="Description"
confirmButtonText="Delete"
confirmButtonVariant="destructive"
onConfirm={jest.fn()}
onCancel={jest.fn()}
/>,
);

expect(screen.getByRole('button', { name: 'Delete' })).toHaveClass(
'bg-destructive',
);
});

it('renders the confirm button with the default variant when no variant is given', () => {
render(
<ConfirmationDialog
isOpen
onOpenChange={jest.fn()}
title="Title"
description="Description"
confirmButtonText="Go"
onConfirm={jest.fn()}
onCancel={jest.fn()}
/>,
);

expect(screen.getByRole('button', { name: 'Go' })).not.toHaveClass(
'bg-destructive',
);
});

it('invokes onCancel when the cancel button is clicked', async () => {
const user = userEvent.setup();
const onCancel = jest.fn();

render(
<ConfirmationDialog
isOpen
onOpenChange={jest.fn()}
title="Title"
description="Description"
onConfirm={jest.fn()}
onCancel={onCancel}
/>,
);

await user.click(screen.getByRole('button', { name: 'Cancel' }));
expect(onCancel).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { t } from 'i18next';
import { Button } from '../../ui/button';
import type { ReactNode } from 'react';
import { cn } from '../../lib/cn';
import { Button, ButtonProps } from '../../ui/button';
import {
Dialog,
DialogContent,
Expand All @@ -17,13 +19,16 @@ type ConfirmationDialogProps = {
className?: string;
titleClassName?: string;
descriptionClassName?: string;
/** Renders beside the title and description (e.g. warning icon) for modal layouts that need a leading visual. */
headerLeading?: ReactNode;
children?: React.ReactNode;
};

export type ConfirmationDialogContent = {
title: string;
description: string;
description: ReactNode;
confirmButtonText?: string;
confirmButtonVariant?: ButtonProps['variant'];
cancelButtonText?: string;
};

Expand All @@ -33,21 +38,43 @@ const ConfirmationDialog = ({
title,
description,
confirmButtonText,
confirmButtonVariant,
cancelButtonText,
onConfirm,
onCancel,
titleClassName,
descriptionClassName,
headerLeading,
className,
children,
}: ConfirmationDialogProps & ConfirmationDialogContent) => {
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader className="mb-0">
<DialogTitle className={titleClassName}>{title}</DialogTitle>
<DialogDescription className={descriptionClassName}>
{description}
</DialogDescription>
<DialogContent className={cn(className)}>
<DialogHeader
className={cn(
'mb-0',
headerLeading != null && 'gap-0 space-y-0 sm:text-left',
)}
>
{headerLeading != null ? (
<div className="flex w-full items-start gap-4">
<div className="shrink-0">{headerLeading}</div>
<div className="flex min-w-0 flex-1 flex-col gap-1 text-left">
<DialogTitle className={titleClassName}>{title}</DialogTitle>
<DialogDescription className={descriptionClassName}>
{description}
</DialogDescription>
</div>
</div>
) : (
<>
<DialogTitle className={titleClassName}>{title}</DialogTitle>
<DialogDescription className={descriptionClassName}>
{description}
</DialogDescription>
</>
)}
</DialogHeader>
{children}
<DialogFooter>
Expand All @@ -56,7 +83,7 @@ const ConfirmationDialog = ({
{cancelButtonText ? cancelButtonText : t('Cancel')}
</Button>
)}
<Button size="sm" onClick={onConfirm}>
<Button size="sm" variant={confirmButtonVariant} onClick={onConfirm}>
{confirmButtonText ? confirmButtonText : t('Confirm')}
</Button>
</DialogFooter>
Expand Down
Loading