chore: tweaked confirmation modal (#6471)

Co-authored-by: Johannes <johannes@formbricks.com>
This commit is contained in:
Dhruwang Jariwala
2025-08-27 18:41:23 +05:30
committed by GitHub
parent c39c9998f0
commit e5e8941016
12 changed files with 389 additions and 33 deletions
@@ -170,7 +170,7 @@ vi.mock("@/modules/ui/components/confirmation-modal", () => ({
open,
setOpen,
title,
text,
body,
buttonText,
onConfirm,
buttonVariant,
@@ -182,7 +182,7 @@ vi.mock("@/modules/ui/components/confirmation-modal", () => ({
data-loading={buttonLoading}
data-variant={buttonVariant}>
<div data-testid="modal-title">{title}</div>
<div data-testid="modal-text">{text}</div>
<div data-testid="modal-text">{body}</div>
<button type="button" onClick={onConfirm} data-testid="confirm-button">
{buttonText}
</button>
@@ -235,7 +235,7 @@ export const SurveyAnalysisCTA = ({
open={isResetModalOpen}
setOpen={setIsResetModalOpen}
title={t("environments.surveys.summary.delete_all_existing_responses_and_displays")}
text={t("environments.surveys.summary.reset_survey_warning")}
body={t("environments.surveys.summary.reset_survey_warning")}
buttonText={t("environments.surveys.summary.reset_survey")}
onConfirm={handleResetSurvey}
buttonVariant="destructive"
@@ -214,7 +214,7 @@ export const PricingCard = ({
}}
open={upgradeModalOpen}
setOpen={setUpgradeModalOpen}
text={t("environments.settings.billing.switch_plan_confirmation_text", {
body={t("environments.settings.billing.switch_plan_confirmation_text", {
plan: plan.name,
price: planPeriod === "monthly" ? plan.price.monthly : plan.price.yearly,
period:
@@ -45,7 +45,7 @@ export function DefaultLanguageSelect({
t("environments.surveys.edit.confirm_default_language") +
": " +
getLanguageLabel(languageCode, locale),
text: t(
body: t(
"environments.surveys.edit.once_set_the_default_language_for_this_survey_can_only_be_changed_by_disabling_the_multi_language_option_and_deleting_all_translations"
),
buttonText: t("common.confirm"),
@@ -263,7 +263,7 @@ export function EditLanguage({
setOpen={() => {
setConfirmationModal((prev) => ({ ...prev, isOpen: !prev.isOpen }));
}}
text={confirmationModal.text}
body={confirmationModal.text}
title={t("environments.project.languages.remove_language")}
/>
</div>
@@ -34,7 +34,7 @@ interface MultiLanguageCardProps {
}
export interface ConfirmationModalProps {
text: string;
body: string;
open: boolean;
title: string;
buttonText: string;
@@ -60,7 +60,7 @@ export const MultiLanguageCard: FC<MultiLanguageCardProps> = ({
const [confirmationModalInfo, setConfirmationModalInfo] = useState<ConfirmationModalProps>({
title: "",
open: false,
text: "",
body: "",
buttonText: "",
onConfirm: () => {},
});
@@ -154,7 +154,7 @@ export const MultiLanguageCard: FC<MultiLanguageCardProps> = ({
setConfirmationModalInfo({
open: true,
title: t("environments.surveys.edit.remove_translations"),
text: t("environments.surveys.edit.this_action_will_remove_all_the_translations_from_this_survey"),
body: t("environments.surveys.edit.this_action_will_remove_all_the_translations_from_this_survey"),
buttonText: t("environments.surveys.edit.remove_translations"),
buttonVariant: "destructive",
onConfirm: () => {
@@ -319,7 +319,7 @@ export const MultiLanguageCard: FC<MultiLanguageCardProps> = ({
setOpen={() => {
setConfirmationModalInfo((prev) => ({ ...prev, open: !prev.open }));
}}
text={confirmationModalInfo.text}
body={confirmationModalInfo.body}
title={confirmationModalInfo.title}
/>
</div>
@@ -297,7 +297,7 @@ export const EditEndingCard = ({
}}
open={openDeleteConfirmationModal}
setOpen={setOpenDeleteConfirmationModal}
text={t("environments.surveys.edit.follow_ups_ending_card_delete_modal_text")}
body={t("environments.surveys.edit.follow_ups_ending_card_delete_modal_text")}
title={t("environments.surveys.edit.follow_ups_ending_card_delete_modal_title")}
/>
</div>
@@ -310,7 +310,7 @@ export const EditorCardMenu = ({
open={logicWarningModal}
setOpen={setLogicWarningModal}
title={t("environments.surveys.edit.logic_error_warning")}
text={t("environments.surveys.edit.logic_error_warning_text")}
body={t("environments.surveys.edit.logic_error_warning_text")}
buttonText={t("environments.surveys.edit.change_anyway")}
onConfirm={onConfirm}
/>
@@ -218,7 +218,7 @@ export const FollowUpItem = ({
};
});
}}
text={t("environments.surveys.edit.follow_ups_delete_modal_text")}
body={t("environments.surveys.edit.follow_ups_delete_modal_text")}
title={t("environments.surveys.edit.follow_ups_delete_modal_title")}
buttonVariant="destructive"
/>
@@ -33,6 +33,7 @@ vi.mock("@/modules/ui/components/dialog", () => ({
DialogTitle: vi.fn(({ children }) => <h2 data-testid="dialog-title">{children}</h2>),
DialogDescription: vi.fn(({ children }) => <div data-testid="dialog-description">{children}</div>),
DialogFooter: vi.fn(({ children }) => <div data-testid="dialog-footer">{children}</div>),
DialogBody: vi.fn(({ children }) => <div data-testid="dialog-body">{children}</div>),
}));
vi.mock("@/modules/ui/components/button", () => ({
@@ -63,7 +64,7 @@ describe("ConfirmationModal", () => {
open={true}
setOpen={mockSetOpen}
onConfirm={mockOnConfirm}
text="Test confirmation text"
body="Test confirmation text"
buttonText="Confirm Action"
/>
);
@@ -71,7 +72,7 @@ describe("ConfirmationModal", () => {
expect(screen.getByTestId("dialog-component")).toBeInTheDocument();
expect(screen.getByTestId("dialog-component")).toHaveAttribute("data-open", "true");
expect(screen.getByTestId("dialog-title")).toHaveTextContent("Test Title");
expect(screen.getByTestId("dialog-description")).toContainHTML("Test confirmation text");
expect(screen.getByTestId("dialog-body")).toContainHTML("Test confirmation text");
// Check that buttons exist
const buttons = screen.getAllByTestId("mock-button");
@@ -96,7 +97,7 @@ describe("ConfirmationModal", () => {
open={true}
setOpen={mockSetOpen}
onConfirm={mockOnConfirm}
text="Test confirmation text"
body="Test confirmation text"
buttonText="Confirm Action"
/>
);
@@ -119,7 +120,7 @@ describe("ConfirmationModal", () => {
open={true}
setOpen={mockSetOpen}
onConfirm={mockOnConfirm}
text="Test confirmation text"
body="Test confirmation text"
buttonText="Confirm Action"
/>
);
@@ -141,7 +142,7 @@ describe("ConfirmationModal", () => {
open={true}
setOpen={mockSetOpen}
onConfirm={mockOnConfirm}
text="Test confirmation text"
body="Test confirmation text"
buttonText="Confirm Action"
/>
);
@@ -163,7 +164,7 @@ describe("ConfirmationModal", () => {
open={true}
setOpen={mockSetOpen}
onConfirm={mockOnConfirm}
text="Test confirmation text"
body="Test confirmation text"
buttonText="Confirm Action"
isButtonDisabled={true}
/>
@@ -184,7 +185,7 @@ describe("ConfirmationModal", () => {
open={true}
setOpen={mockSetOpen}
onConfirm={mockOnConfirm}
text="Test confirmation text"
body="Test confirmation text"
buttonText="Confirm Action"
isButtonDisabled={true}
/>
@@ -206,7 +207,7 @@ describe("ConfirmationModal", () => {
open={true}
setOpen={mockSetOpen}
onConfirm={mockOnConfirm}
text="Test confirmation text"
body="Test confirmation text"
buttonText="Confirm Action"
buttonLoading={true}
/>
@@ -226,7 +227,7 @@ describe("ConfirmationModal", () => {
open={true}
setOpen={mockSetOpen}
onConfirm={mockOnConfirm}
text="Test confirmation text"
body="Test confirmation text"
buttonText="Confirm Action"
hideCloseButton={true}
closeOnOutsideClick={false}
@@ -247,7 +248,7 @@ describe("ConfirmationModal", () => {
open={true}
setOpen={mockSetOpen}
onConfirm={mockOnConfirm}
text="Test confirmation text"
body="Test confirmation text"
buttonText="Confirm Action"
buttonVariant="default"
/>
@@ -267,7 +268,7 @@ describe("ConfirmationModal", () => {
open={false}
setOpen={mockSetOpen}
onConfirm={mockOnConfirm}
text="Test confirmation text"
body="Test confirmation text"
buttonText="Confirm Action"
/>
);
@@ -3,6 +3,7 @@
import { Button } from "@/modules/ui/components/button";
import {
Dialog,
DialogBody,
DialogContent,
DialogDescription,
DialogFooter,
@@ -10,6 +11,7 @@ import {
DialogTitle,
} from "@/modules/ui/components/dialog";
import { useTranslate } from "@tolgee/react";
import { CircleAlert } from "lucide-react";
import React from "react";
type ConfirmationModalProps = {
@@ -17,13 +19,16 @@ type ConfirmationModalProps = {
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
onConfirm: () => void;
text: string;
description?: string;
body: string;
buttonText: string;
isButtonDisabled?: boolean;
buttonVariant?: "destructive" | "default";
buttonLoading?: boolean;
closeOnOutsideClick?: boolean;
hideCloseButton?: boolean;
cancelButtonText?: string;
Icon?: React.ElementType;
};
export const ConfirmationModal = ({
@@ -31,13 +36,16 @@ export const ConfirmationModal = ({
onConfirm,
open,
setOpen,
text,
description,
body,
buttonText,
isButtonDisabled = false,
buttonVariant = "destructive",
buttonLoading = false,
closeOnOutsideClick = true,
hideCloseButton,
cancelButtonText,
Icon,
}: ConfirmationModalProps) => {
const { t } = useTranslate();
const handleButtonAction = async () => {
@@ -50,17 +58,30 @@ export const ConfirmationModal = ({
<DialogContent
hideCloseButton={hideCloseButton}
disableCloseOnOutsideClick={!closeOnOutsideClick}
className="max-w-[540px]">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription className="text-slate-900">
<span className="mt-2 whitespace-pre-wrap">{text}</span>
</DialogDescription>
className="max-w-[540px] space-y-4">
<DialogHeader className="flex justify-center gap-2">
{Icon ? (
<Icon className="h-4 w-4 text-slate-500" />
) : (
<CircleAlert className="h-4 w-4 text-slate-500" />
)}
<div className="flex flex-col">
<DialogTitle className="w-full text-left">{title}</DialogTitle>
<DialogDescription className="w-full text-left">
<span className="mt-2 whitespace-pre-wrap">
{description || t("environments.project.general.this_action_cannot_be_undone")}
</span>
</DialogDescription>
</div>
</DialogHeader>
<DialogBody>
<p>{body}</p>
</DialogBody>
<DialogFooter>
<Button variant="ghost" onClick={() => setOpen(false)}>
{t("common.cancel")}
{cancelButtonText || t("common.cancel")}
</Button>
<Button
loading={buttonLoading}
@@ -0,0 +1,334 @@
import { Meta, StoryObj } from "@storybook/react-vite";
import { AlertTriangle, Download, Pencil, RefreshCw } from "lucide-react";
import { useState } from "react";
import { fn } from "storybook/test";
import { Button } from "../button";
import { ConfirmationModal } from "./index";
type StoryProps = React.ComponentProps<typeof ConfirmationModal>;
const meta: Meta<StoryProps> = {
title: "UI/ConfirmationModal",
component: ConfirmationModal,
tags: ["autodocs"],
parameters: {
layout: "centered",
controls: { sort: "alpha", exclude: ["open", "setOpen", "onConfirm", "Icon"] },
docs: {
description: {
component:
"The **ConfirmationModal** component provides a modal dialog for confirming user actions. It supports customizable content, button variants, loading states, and flexible interaction patterns for both destructive and non-destructive confirmations.",
},
},
},
argTypes: {
isButtonDisabled: {
control: "boolean",
description: "Disables the confirmation button",
table: {
category: "Behavior",
type: { summary: "boolean" },
defaultValue: { summary: "false" },
},
order: 1,
},
buttonLoading: {
control: "boolean",
description: "Shows loading state on confirmation button",
table: {
category: "Behavior",
type: { summary: "boolean" },
defaultValue: { summary: "false" },
},
order: 2,
},
closeOnOutsideClick: {
control: "boolean",
description: "Allows closing modal by clicking outside",
table: {
category: "Behavior",
type: { summary: "boolean" },
defaultValue: { summary: "true" },
},
order: 3,
},
hideCloseButton: {
control: "boolean",
description: "Hides the close button (X) in the modal",
table: {
category: "Behavior",
type: { summary: "boolean" },
defaultValue: { summary: "false" },
},
order: 4,
},
onConfirm: {
action: "onConfirm",
description: "Function called when confirmation button is clicked",
table: {
category: "Behavior",
type: { summary: "function" },
},
order: 5,
},
setOpen: {
action: "setOpen",
description: "Function to control modal open state",
table: {
category: "Behavior",
type: { summary: "function" },
},
order: 6,
},
// Component Props - Appearance Category
buttonVariant: {
control: "select",
options: ["destructive", "default"],
description: "Visual variant of the confirmation button",
table: {
category: "Appearance",
type: { summary: "string" },
defaultValue: { summary: "destructive" },
},
order: 1,
},
// Component Props - Content Category
title: {
control: "text",
description: "Title text displayed in the modal header",
table: {
category: "Content",
type: { summary: "string" },
},
order: 1,
},
description: {
control: "text",
description: "Optional description text below the title",
table: {
category: "Content",
type: { summary: "string" },
},
order: 2,
},
body: {
control: "text",
description: "Main body text content of the modal",
table: {
category: "Content",
type: { summary: "string" },
},
order: 3,
},
buttonText: {
control: "text",
description: "Text displayed on the confirmation button",
table: {
category: "Content",
type: { summary: "string" },
},
order: 4,
},
cancelButtonText: {
control: "text",
description: "Text displayed on the close button",
table: {
category: "Content",
type: { summary: "string" },
},
order: 5,
},
},
args: {
onConfirm: fn(),
setOpen: fn(),
},
};
export default meta;
type Story = StoryObj<typeof ConfirmationModal>;
// Create a render function for interactive modals
const RenderConfirmationModal = (args: StoryProps) => {
const [isOpen, setIsOpen] = useState(false);
// Extract component props
const {
title,
body,
buttonText,
description,
isButtonDisabled = false,
buttonVariant = "destructive",
buttonLoading = false,
closeOnOutsideClick = true,
hideCloseButton = false,
onConfirm,
setOpen,
cancelButtonText,
Icon,
} = args;
return (
<div>
<Button variant="default" onClick={() => setIsOpen(true)}>
Open Modal
</Button>
<ConfirmationModal
open={isOpen}
setOpen={(open) => {
setIsOpen(open);
setOpen?.(open);
}}
title={title}
description={description}
body={body}
buttonText={buttonText}
isButtonDisabled={isButtonDisabled}
buttonVariant={buttonVariant}
buttonLoading={buttonLoading}
closeOnOutsideClick={closeOnOutsideClick}
hideCloseButton={hideCloseButton}
cancelButtonText={cancelButtonText}
Icon={Icon}
onConfirm={() => {
onConfirm?.();
setIsOpen(false);
}}
/>
</div>
);
};
export const Default: Story = {
render: RenderConfirmationModal,
args: {
title: "Edit Survey",
description: "Are you sure you want to edit this survey?",
body: "This action cannot be undone. All collected responses and analytics data will be permanently removed.",
buttonText: "Edit Survey",
Icon: Pencil,
isButtonDisabled: false,
buttonVariant: "default",
buttonLoading: false,
closeOnOutsideClick: true,
hideCloseButton: false,
},
};
export const Loading: Story = {
render: RenderConfirmationModal,
args: {
title: "Export Survey Data",
description: "Generating your export file...",
body: "Please wait while we prepare your survey data for export. This may take a few moments depending on the amount of data.",
buttonText: "Exporting...",
Icon: Download,
isButtonDisabled: false,
buttonVariant: "default",
buttonLoading: true,
closeOnOutsideClick: true,
hideCloseButton: false,
},
parameters: {
docs: {
description: {
story: "Shows loading state during async operations like data export or processing.",
},
},
},
};
export const Disabled: Story = {
render: RenderConfirmationModal,
args: {
title: "Publish Survey",
description: "This survey cannot be published yet.",
body: "Please complete all required questions and configure your survey settings before publishing. Check the survey builder for any validation errors.",
buttonText: "Publish Survey",
Icon: AlertTriangle,
isButtonDisabled: true,
buttonVariant: "default",
buttonLoading: false,
closeOnOutsideClick: true,
hideCloseButton: false,
},
parameters: {
docs: {
description: {
story:
"Use when the confirmation action is temporarily unavailable due to validation errors or missing requirements.",
},
},
},
};
export const NoDescription: Story = {
render: RenderConfirmationModal,
args: {
title: "Reset Form",
description: "",
body: "All form data will be cleared and returned to default values. This action cannot be undone.",
buttonText: "Reset Form",
Icon: RefreshCw,
isButtonDisabled: false,
buttonVariant: "destructive",
buttonLoading: false,
closeOnOutsideClick: true,
hideCloseButton: false,
},
parameters: {
docs: {
description: {
story: "Modal without description text, showing only title and body content.",
},
},
},
};
export const LongContent: Story = {
render: RenderConfirmationModal,
args: {
title: "Delete API Integration",
description: "This will permanently remove the integration and all its associated data.",
body: "Deleting this API integration will permanently remove all configuration settings, authentication tokens, webhook endpoints, and data mapping rules. Any automated workflows that depend on this integration will stop working immediately.",
buttonText: "Delete Integration",
isButtonDisabled: false,
buttonVariant: "destructive",
buttonLoading: false,
closeOnOutsideClick: true,
hideCloseButton: false,
},
parameters: {
docs: {
description: {
story: "Example with extensive content to test modal layout with longer text descriptions.",
},
},
},
};
export const CustomStyling: Story = {
render: RenderConfirmationModal,
args: {
title: "Custom Styled Modal",
description: "This modal demonstrates custom content styling.",
body: "You can customize the appearance and behavior of confirmation modals to match your specific use case and design requirements.",
buttonText: "Proceed",
isButtonDisabled: false,
buttonVariant: "default",
buttonLoading: false,
closeOnOutsideClick: true,
hideCloseButton: false,
},
parameters: {
docs: {
description: {
story: "Example showing how the modal can be customized for different use cases and styling needs.",
},
},
},
};