@rovula/ui 0.1.14 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/bundle.js +2 -2
- package/dist/cjs/bundle.js.map +1 -1
- package/dist/cjs/types/components/Form/Form.d.ts +1 -1
- package/dist/cjs/types/patterns/confirm-dialog/ConfirmDialog.d.ts +24 -0
- package/dist/cjs/types/patterns/confirm-dialog/ConfirmDialog.stories.d.ts +53 -0
- package/dist/cjs/types/patterns/form-dialog/FormDialog.d.ts +39 -0
- package/dist/cjs/types/patterns/form-dialog/FormDialog.stories.d.ts +62 -0
- package/dist/components/AlertDialog/AlertDialog.js +1 -1
- package/dist/components/Dialog/Dialog.js +1 -1
- package/dist/components/Form/Form.js +15 -4
- package/dist/esm/bundle.js +1 -1
- package/dist/esm/bundle.js.map +1 -1
- package/dist/esm/types/components/Form/Form.d.ts +1 -1
- package/dist/esm/types/patterns/confirm-dialog/ConfirmDialog.d.ts +24 -0
- package/dist/esm/types/patterns/confirm-dialog/ConfirmDialog.stories.d.ts +53 -0
- package/dist/esm/types/patterns/form-dialog/FormDialog.d.ts +39 -0
- package/dist/esm/types/patterns/form-dialog/FormDialog.stories.d.ts +62 -0
- package/dist/index.d.ts +1 -1
- package/dist/patterns/confirm-dialog/ConfirmDialog.js +44 -0
- package/dist/patterns/confirm-dialog/ConfirmDialog.stories.js +103 -0
- package/dist/patterns/form-dialog/FormDialog.js +10 -0
- package/dist/patterns/form-dialog/FormDialog.stories.js +223 -0
- package/package.json +1 -1
- package/src/components/AlertDialog/AlertDialog.tsx +11 -13
- package/src/components/Dialog/Dialog.tsx +3 -9
- package/src/components/Form/Form.tsx +19 -4
- package/src/patterns/confirm-dialog/ConfirmDialog.stories.tsx +193 -0
- package/src/patterns/confirm-dialog/ConfirmDialog.tsx +153 -0
- package/src/patterns/form-dialog/FormDialog.stories.tsx +437 -0
- package/src/patterns/form-dialog/FormDialog.tsx +137 -0
|
@@ -40,7 +40,7 @@ export declare const createControlledForm: <TFieldValues extends FieldValues>({
|
|
|
40
40
|
FormRoot: React.ForwardRefExoticComponent<Omit<FormProps<TFieldValues>, "methods" | "defaultValues" | "controllerRef"> & React.RefAttributes<FormController<TFieldValues>>>;
|
|
41
41
|
};
|
|
42
42
|
export declare const useControlledForm: <TFieldValues extends FieldValues>({ defaultValues, controllerRef, resolver, validationSchema, mode, reValidateMode, }: UseControlledFormOptions<TFieldValues>) => {
|
|
43
|
-
methods: UseFormReturn<TFieldValues>;
|
|
43
|
+
methods: UseFormReturn<TFieldValues, any, TFieldValues>;
|
|
44
44
|
FormRoot: React.ForwardRefExoticComponent<Omit<FormProps<TFieldValues>, "methods" | "defaultValues" | "controllerRef"> & React.RefAttributes<FormController<TFieldValues>>>;
|
|
45
45
|
};
|
|
46
46
|
type FormComponent = <TFieldValues extends FieldValues>(props: FormProps<TFieldValues> & {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type ConfirmDialogProps = {
|
|
3
|
+
open?: boolean;
|
|
4
|
+
onOpenChange?: (open: boolean) => void;
|
|
5
|
+
title: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
confirmLabel?: string;
|
|
8
|
+
cancelLabel?: string;
|
|
9
|
+
onConfirm?: () => void;
|
|
10
|
+
onCancel?: () => void;
|
|
11
|
+
/** Fires whenever the dialog closes, regardless of how (cancel button, overlay, Escape). */
|
|
12
|
+
onClose?: () => void;
|
|
13
|
+
trigger?: React.ReactNode;
|
|
14
|
+
/**
|
|
15
|
+
* When provided, the user must type this exact text before the confirm button is enabled.
|
|
16
|
+
* e.g. typeToConfirm="confirm" or typeToConfirm="delete"
|
|
17
|
+
*/
|
|
18
|
+
typeToConfirm?: string;
|
|
19
|
+
/**
|
|
20
|
+
* When true, hides the cancel button — useful for info/error alerts that only need one action.
|
|
21
|
+
*/
|
|
22
|
+
hideCancelButton?: boolean;
|
|
23
|
+
};
|
|
24
|
+
export declare const ConfirmDialog: React.FC<ConfirmDialogProps>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { StoryObj } from "@storybook/react";
|
|
3
|
+
import { ConfirmDialog } from "./ConfirmDialog";
|
|
4
|
+
declare const meta: {
|
|
5
|
+
title: string;
|
|
6
|
+
component: React.FC<import("./ConfirmDialog").ConfirmDialogProps>;
|
|
7
|
+
tags: string[];
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: string;
|
|
10
|
+
};
|
|
11
|
+
decorators: ((Story: import("@storybook/csf").PartialStoryFn<import("@storybook/react").ReactRenderer, {
|
|
12
|
+
open?: boolean | undefined;
|
|
13
|
+
onOpenChange?: ((open: boolean) => void) | undefined;
|
|
14
|
+
title: string;
|
|
15
|
+
description?: string | undefined;
|
|
16
|
+
confirmLabel?: string | undefined;
|
|
17
|
+
cancelLabel?: string | undefined;
|
|
18
|
+
onConfirm?: (() => void) | undefined;
|
|
19
|
+
onCancel?: (() => void) | undefined;
|
|
20
|
+
onClose?: (() => void) | undefined;
|
|
21
|
+
trigger?: React.ReactNode;
|
|
22
|
+
typeToConfirm?: string | undefined;
|
|
23
|
+
hideCancelButton?: boolean | undefined;
|
|
24
|
+
}>) => import("react/jsx-runtime").JSX.Element)[];
|
|
25
|
+
argTypes: {
|
|
26
|
+
open: {
|
|
27
|
+
control: "boolean";
|
|
28
|
+
};
|
|
29
|
+
typeToConfirm: {
|
|
30
|
+
control: "text";
|
|
31
|
+
};
|
|
32
|
+
title: {
|
|
33
|
+
control: "text";
|
|
34
|
+
};
|
|
35
|
+
description: {
|
|
36
|
+
control: "text";
|
|
37
|
+
};
|
|
38
|
+
confirmLabel: {
|
|
39
|
+
control: "text";
|
|
40
|
+
};
|
|
41
|
+
cancelLabel: {
|
|
42
|
+
control: "text";
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
export default meta;
|
|
47
|
+
type Story = StoryObj<typeof ConfirmDialog>;
|
|
48
|
+
export declare const Default: Story;
|
|
49
|
+
export declare const WithoutDescription: Story;
|
|
50
|
+
export declare const WithoutCancelButton: Story;
|
|
51
|
+
export declare const RequireConfirmText: Story;
|
|
52
|
+
export declare const FigmaDefaultOpen: Story;
|
|
53
|
+
export declare const FigmaRequirePasswordDefaultOpen: Story;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import Button from "@/components/Button/Button";
|
|
3
|
+
export type FormDialogAction = {
|
|
4
|
+
label: string;
|
|
5
|
+
onClick?: () => void;
|
|
6
|
+
variant?: React.ComponentProps<typeof Button>["variant"];
|
|
7
|
+
color?: React.ComponentProps<typeof Button>["color"];
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
isLoading?: boolean;
|
|
10
|
+
type?: "button" | "submit" | "reset";
|
|
11
|
+
};
|
|
12
|
+
export type FormDialogProps = {
|
|
13
|
+
open?: boolean;
|
|
14
|
+
onOpenChange?: (open: boolean) => void;
|
|
15
|
+
title: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
children?: React.ReactNode;
|
|
18
|
+
trigger?: React.ReactNode;
|
|
19
|
+
/**
|
|
20
|
+
* Primary action (right side of footer).
|
|
21
|
+
*/
|
|
22
|
+
confirmAction?: FormDialogAction;
|
|
23
|
+
/**
|
|
24
|
+
* Secondary cancel action (right side of footer, outline style).
|
|
25
|
+
*/
|
|
26
|
+
cancelAction?: FormDialogAction;
|
|
27
|
+
/**
|
|
28
|
+
* Optional extra action placed on the left side of the footer.
|
|
29
|
+
*/
|
|
30
|
+
extraAction?: FormDialogAction;
|
|
31
|
+
scrollable?: boolean;
|
|
32
|
+
className?: string;
|
|
33
|
+
/**
|
|
34
|
+
* When provided, the confirm button becomes type="submit" and is linked to this form id.
|
|
35
|
+
* Use together with a <Form id={formId} .../> inside children.
|
|
36
|
+
*/
|
|
37
|
+
formId?: string;
|
|
38
|
+
};
|
|
39
|
+
export declare const FormDialog: React.FC<FormDialogProps>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { StoryObj } from "@storybook/react";
|
|
3
|
+
import { FormDialog } from "./FormDialog";
|
|
4
|
+
declare const meta: {
|
|
5
|
+
title: string;
|
|
6
|
+
component: React.FC<import("./FormDialog").FormDialogProps>;
|
|
7
|
+
tags: string[];
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: string;
|
|
10
|
+
};
|
|
11
|
+
decorators: ((Story: import("@storybook/csf").PartialStoryFn<import("@storybook/react").ReactRenderer, {
|
|
12
|
+
open?: boolean | undefined;
|
|
13
|
+
onOpenChange?: ((open: boolean) => void) | undefined;
|
|
14
|
+
title: string;
|
|
15
|
+
description?: string | undefined;
|
|
16
|
+
children?: React.ReactNode;
|
|
17
|
+
trigger?: React.ReactNode;
|
|
18
|
+
confirmAction?: import("./FormDialog").FormDialogAction | undefined;
|
|
19
|
+
cancelAction?: import("./FormDialog").FormDialogAction | undefined;
|
|
20
|
+
extraAction?: import("./FormDialog").FormDialogAction | undefined;
|
|
21
|
+
scrollable?: boolean | undefined;
|
|
22
|
+
className?: string | undefined;
|
|
23
|
+
formId?: string | undefined;
|
|
24
|
+
}>) => import("react/jsx-runtime").JSX.Element)[];
|
|
25
|
+
argTypes: {
|
|
26
|
+
open: {
|
|
27
|
+
control: "boolean";
|
|
28
|
+
};
|
|
29
|
+
title: {
|
|
30
|
+
control: "text";
|
|
31
|
+
};
|
|
32
|
+
description: {
|
|
33
|
+
control: "text";
|
|
34
|
+
};
|
|
35
|
+
scrollable: {
|
|
36
|
+
control: "boolean";
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
export default meta;
|
|
41
|
+
type Story = StoryObj<typeof FormDialog>;
|
|
42
|
+
export declare const Default: Story;
|
|
43
|
+
export declare const WithExtraAction: Story;
|
|
44
|
+
export declare const WithForm: Story;
|
|
45
|
+
export declare const FigmaFormDefaultOpen: Story;
|
|
46
|
+
export declare const FigmaFormWithActionDefaultOpen: Story;
|
|
47
|
+
/**
|
|
48
|
+
* Pattern A: useControlledForm
|
|
49
|
+
*
|
|
50
|
+
* Use when you need to:
|
|
51
|
+
* - access formState (isValid, isDirty) to control the confirm button
|
|
52
|
+
* - reset the form on close
|
|
53
|
+
* - trigger validation outside the form element
|
|
54
|
+
*/
|
|
55
|
+
export declare const WithRovulaForm: Story;
|
|
56
|
+
/**
|
|
57
|
+
* Pattern B: useControlledForm + async API call + loading state
|
|
58
|
+
*
|
|
59
|
+
* Use when the confirm action calls an API.
|
|
60
|
+
* isLoading disables + shows spinner on the confirm button while the request is in flight.
|
|
61
|
+
*/
|
|
62
|
+
export declare const WithRovulaFormAndApiLoading: Story;
|
package/dist/index.d.ts
CHANGED
|
@@ -1207,7 +1207,7 @@ declare const createControlledForm: <TFieldValues extends FieldValues>({ methods
|
|
|
1207
1207
|
FormRoot: React__default.ForwardRefExoticComponent<Omit<FormProps<TFieldValues>, "methods" | "defaultValues" | "controllerRef"> & React__default.RefAttributes<FormController<TFieldValues>>>;
|
|
1208
1208
|
};
|
|
1209
1209
|
declare const useControlledForm: <TFieldValues extends FieldValues>({ defaultValues, controllerRef, resolver, validationSchema, mode, reValidateMode, }: UseControlledFormOptions<TFieldValues>) => {
|
|
1210
|
-
methods: UseFormReturn<TFieldValues>;
|
|
1210
|
+
methods: UseFormReturn<TFieldValues, any, TFieldValues>;
|
|
1211
1211
|
FormRoot: React__default.ForwardRefExoticComponent<Omit<FormProps<TFieldValues>, "methods" | "defaultValues" | "controllerRef"> & React__default.RefAttributes<FormController<TFieldValues>>>;
|
|
1212
1212
|
};
|
|
1213
1213
|
type FormComponent = <TFieldValues extends FieldValues>(props: FormProps<TFieldValues> & {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as yup from "yup";
|
|
5
|
+
import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogAction, AlertDialogCancel, AlertDialogTrigger, } from "@/components/AlertDialog/AlertDialog";
|
|
6
|
+
import { useControlledForm, Field } from "@/components/Form";
|
|
7
|
+
import { TextInput } from "@/components/TextInput/TextInput";
|
|
8
|
+
export const ConfirmDialog = ({ open, onOpenChange, title, description, confirmLabel = "Confirm", cancelLabel = "Cancel", onConfirm, onCancel, onClose, trigger, typeToConfirm, hideCancelButton = false, }) => {
|
|
9
|
+
const formId = React.useId();
|
|
10
|
+
const requiresInput = !!typeToConfirm;
|
|
11
|
+
const validationSchema = React.useMemo(() => yup.object({
|
|
12
|
+
confirmInput: yup
|
|
13
|
+
.string()
|
|
14
|
+
.required("This field is required.")
|
|
15
|
+
.test("type-to-confirm", `Please type '${typeToConfirm}' to proceed`, (value) => value === typeToConfirm),
|
|
16
|
+
}), [typeToConfirm]);
|
|
17
|
+
const { methods, FormRoot } = useControlledForm({
|
|
18
|
+
defaultValues: { confirmInput: "" },
|
|
19
|
+
validationSchema,
|
|
20
|
+
mode: "onTouched",
|
|
21
|
+
reValidateMode: "onChange",
|
|
22
|
+
});
|
|
23
|
+
const isFormValid = methods.formState.isValid;
|
|
24
|
+
const handleOpenChange = (nextOpen) => {
|
|
25
|
+
if (!nextOpen) {
|
|
26
|
+
methods.reset();
|
|
27
|
+
onClose === null || onClose === void 0 ? void 0 : onClose();
|
|
28
|
+
}
|
|
29
|
+
onOpenChange === null || onOpenChange === void 0 ? void 0 : onOpenChange(nextOpen);
|
|
30
|
+
};
|
|
31
|
+
const handleCancel = () => {
|
|
32
|
+
methods.reset();
|
|
33
|
+
onCancel === null || onCancel === void 0 ? void 0 : onCancel();
|
|
34
|
+
onClose === null || onClose === void 0 ? void 0 : onClose();
|
|
35
|
+
};
|
|
36
|
+
return (_jsxs(AlertDialog, { open: open, onOpenChange: handleOpenChange, children: [trigger && _jsx(AlertDialogTrigger, { asChild: true, children: trigger }), _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: title }), description && (_jsx(AlertDialogDescription, { children: description }))] }), requiresInput && (_jsxs(FormRoot, { id: formId, className: "flex flex-col gap-4 w-full", onSubmit: () => onConfirm === null || onConfirm === void 0 ? void 0 : onConfirm(), children: [_jsxs("p", { className: "typography-small1 text-text-contrast-max", children: ["Type \u201C", typeToConfirm, "\u201D to proceed."] }), _jsx(Field, { name: "confirmInput", component: TextInput, componentProps: {
|
|
37
|
+
label: "Type to confirm",
|
|
38
|
+
required: true,
|
|
39
|
+
hasClearIcon: true,
|
|
40
|
+
keepFooterSpace: true,
|
|
41
|
+
fullwidth: true,
|
|
42
|
+
} })] })), _jsxs(AlertDialogFooter, { children: [!hideCancelButton && (_jsx(AlertDialogCancel, { onClick: handleCancel, children: cancelLabel })), _jsx(AlertDialogAction, { type: "submit", form: requiresInput ? formId : undefined, disabled: requiresInput && !isFormValid, onClick: requiresInput ? undefined : () => onConfirm === null || onConfirm === void 0 ? void 0 : onConfirm(), children: confirmLabel })] })] })] }));
|
|
43
|
+
};
|
|
44
|
+
ConfirmDialog.displayName = "ConfirmDialog";
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useArgs } from "@storybook/preview-api";
|
|
3
|
+
import { ConfirmDialog } from "./ConfirmDialog";
|
|
4
|
+
import Button from "@/components/Button/Button";
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Patterns/ConfirmDialog",
|
|
7
|
+
component: ConfirmDialog,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: "fullscreen",
|
|
11
|
+
},
|
|
12
|
+
decorators: [
|
|
13
|
+
(Story) => (_jsx("div", { className: "p-5 flex w-full", children: _jsx(Story, {}) })),
|
|
14
|
+
],
|
|
15
|
+
argTypes: {
|
|
16
|
+
open: { control: "boolean" },
|
|
17
|
+
typeToConfirm: { control: "text" },
|
|
18
|
+
title: { control: "text" },
|
|
19
|
+
description: { control: "text" },
|
|
20
|
+
confirmLabel: { control: "text" },
|
|
21
|
+
cancelLabel: { control: "text" },
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
export default meta;
|
|
25
|
+
export const Default = {
|
|
26
|
+
args: {
|
|
27
|
+
open: false,
|
|
28
|
+
title: "Are you sure?",
|
|
29
|
+
description: "This action cannot be undone.",
|
|
30
|
+
confirmLabel: "Confirm",
|
|
31
|
+
cancelLabel: "Cancel",
|
|
32
|
+
},
|
|
33
|
+
render: (args) => {
|
|
34
|
+
const [{ open }, updateArgs] = useArgs();
|
|
35
|
+
return (_jsx(ConfirmDialog, Object.assign({}, args, { open: open, onOpenChange: (next) => updateArgs({ open: next }), onConfirm: () => updateArgs({ open: false }), onCancel: () => updateArgs({ open: false }), trigger: _jsx(Button, { fullwidth: false, onClick: () => updateArgs({ open: true }), children: "Open" }) })));
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
export const WithoutDescription = {
|
|
39
|
+
args: {
|
|
40
|
+
open: false,
|
|
41
|
+
title: "Delete item?",
|
|
42
|
+
confirmLabel: "Delete",
|
|
43
|
+
cancelLabel: "Cancel",
|
|
44
|
+
},
|
|
45
|
+
render: (args) => {
|
|
46
|
+
const [{ open }, updateArgs] = useArgs();
|
|
47
|
+
return (_jsx(ConfirmDialog, Object.assign({}, args, { open: open, onOpenChange: (next) => updateArgs({ open: next }), onConfirm: () => updateArgs({ open: false }), onCancel: () => updateArgs({ open: false }), trigger: _jsx(Button, { fullwidth: false, color: "error", onClick: () => updateArgs({ open: true }), children: "Delete" }) })));
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
export const WithoutCancelButton = {
|
|
51
|
+
args: {
|
|
52
|
+
open: false,
|
|
53
|
+
title: "Are you sure?",
|
|
54
|
+
description: "This action cannot be undone.",
|
|
55
|
+
confirmLabel: "Confirm",
|
|
56
|
+
hideCancelButton: true,
|
|
57
|
+
},
|
|
58
|
+
render: (args) => {
|
|
59
|
+
const [{ open }, updateArgs] = useArgs();
|
|
60
|
+
return (_jsx(ConfirmDialog, Object.assign({}, args, { open: open, onOpenChange: (next) => updateArgs({ open: next }), onConfirm: () => updateArgs({ open: false }), onCancel: () => updateArgs({ open: false }), trigger: _jsx(Button, { fullwidth: false, color: "error", onClick: () => updateArgs({ open: true }), children: "Confirm" }) })));
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
export const RequireConfirmText = {
|
|
64
|
+
args: {
|
|
65
|
+
open: false,
|
|
66
|
+
title: "Title",
|
|
67
|
+
description: "Subtitle description",
|
|
68
|
+
confirmLabel: "Confirm",
|
|
69
|
+
cancelLabel: "Cancel",
|
|
70
|
+
typeToConfirm: "confirm",
|
|
71
|
+
},
|
|
72
|
+
render: (args) => {
|
|
73
|
+
const [{ open }, updateArgs] = useArgs();
|
|
74
|
+
return (_jsx(ConfirmDialog, Object.assign({}, args, { open: open, onOpenChange: (next) => updateArgs({ open: next }), onConfirm: () => updateArgs({ open: false }), onCancel: () => updateArgs({ open: false }), trigger: _jsx(Button, { fullwidth: false, onClick: () => updateArgs({ open: true }), children: "Open (require confirm)" }) })));
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
export const FigmaDefaultOpen = {
|
|
78
|
+
args: {
|
|
79
|
+
open: false,
|
|
80
|
+
title: "Title",
|
|
81
|
+
description: "Subtitle description",
|
|
82
|
+
confirmLabel: "Confirm",
|
|
83
|
+
cancelLabel: "Cancel",
|
|
84
|
+
},
|
|
85
|
+
render: (args) => {
|
|
86
|
+
const [{ open }, updateArgs] = useArgs();
|
|
87
|
+
return (_jsx(ConfirmDialog, Object.assign({}, args, { open: open, onOpenChange: (next) => updateArgs({ open: next }), onConfirm: () => updateArgs({ open: false }), onCancel: () => updateArgs({ open: false }) })));
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
export const FigmaRequirePasswordDefaultOpen = {
|
|
91
|
+
args: {
|
|
92
|
+
open: false,
|
|
93
|
+
title: "Title",
|
|
94
|
+
description: "Subtitle description",
|
|
95
|
+
confirmLabel: "Confirm",
|
|
96
|
+
cancelLabel: "Cancel",
|
|
97
|
+
typeToConfirm: "confirm",
|
|
98
|
+
},
|
|
99
|
+
render: (args) => {
|
|
100
|
+
const [{ open }, updateArgs] = useArgs();
|
|
101
|
+
return (_jsx(ConfirmDialog, Object.assign({}, args, { open: open, onOpenChange: (next) => updateArgs({ open: next }), onConfirm: () => updateArgs({ open: false }), onCancel: () => updateArgs({ open: false }) })));
|
|
102
|
+
},
|
|
103
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogBody, DialogFooter, DialogTrigger, } from "@/components/Dialog/Dialog";
|
|
4
|
+
import Button from "@/components/Button/Button";
|
|
5
|
+
export const FormDialog = ({ open, onOpenChange, title, description, children, trigger, confirmAction, cancelAction, extraAction, scrollable = false, className, formId, }) => {
|
|
6
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
7
|
+
const hasFooter = confirmAction || cancelAction || extraAction;
|
|
8
|
+
return (_jsxs(Dialog, { open: open, onOpenChange: onOpenChange, children: [trigger && _jsx(DialogTrigger, { asChild: true, children: trigger }), _jsxs(DialogContent, { className: className, children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: title }), description && (_jsx(DialogDescription, { children: description }))] }), children && (_jsx(DialogBody, { scrollable: scrollable, children: children })), hasFooter && (_jsxs(DialogFooter, { className: extraAction ? "justify-between" : undefined, children: [extraAction && (_jsx(Button, { type: (_a = extraAction.type) !== null && _a !== void 0 ? _a : "button", variant: (_b = extraAction.variant) !== null && _b !== void 0 ? _b : "outline", color: (_c = extraAction.color) !== null && _c !== void 0 ? _c : "secondary", fullwidth: false, disabled: extraAction.disabled, isLoading: extraAction.isLoading, onClick: extraAction.onClick, children: extraAction.label })), _jsxs("div", { className: "flex items-center gap-4", children: [cancelAction && (_jsx(Button, { type: (_d = cancelAction.type) !== null && _d !== void 0 ? _d : "button", variant: (_e = cancelAction.variant) !== null && _e !== void 0 ? _e : "outline", color: (_f = cancelAction.color) !== null && _f !== void 0 ? _f : "primary", fullwidth: false, disabled: cancelAction.disabled, isLoading: cancelAction.isLoading, onClick: cancelAction.onClick, children: cancelAction.label })), confirmAction && (_jsx(Button, { type: formId ? "submit" : ((_g = confirmAction.type) !== null && _g !== void 0 ? _g : "button"), form: formId, variant: (_h = confirmAction.variant) !== null && _h !== void 0 ? _h : "solid", color: (_j = confirmAction.color) !== null && _j !== void 0 ? _j : "primary", fullwidth: false, disabled: confirmAction.disabled, isLoading: confirmAction.isLoading, onClick: confirmAction.onClick, children: confirmAction.label }))] })] }))] })] }));
|
|
9
|
+
};
|
|
10
|
+
FormDialog.displayName = "FormDialog";
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState } from "react";
|
|
3
|
+
import { useArgs } from "@storybook/preview-api";
|
|
4
|
+
import { FormDialog } from "./FormDialog";
|
|
5
|
+
import Button from "@/components/Button/Button";
|
|
6
|
+
import { TextInput } from "@/components/TextInput/TextInput";
|
|
7
|
+
import { useControlledForm, Field } from "@/components/Form";
|
|
8
|
+
import * as yup from "yup";
|
|
9
|
+
const meta = {
|
|
10
|
+
title: "Patterns/FormDialog",
|
|
11
|
+
component: FormDialog,
|
|
12
|
+
tags: ["autodocs"],
|
|
13
|
+
parameters: {
|
|
14
|
+
layout: "fullscreen",
|
|
15
|
+
},
|
|
16
|
+
decorators: [
|
|
17
|
+
(Story) => (_jsx("div", { className: "p-5 flex w-full", children: _jsx(Story, {}) })),
|
|
18
|
+
],
|
|
19
|
+
argTypes: {
|
|
20
|
+
open: { control: "boolean" },
|
|
21
|
+
title: { control: "text" },
|
|
22
|
+
description: { control: "text" },
|
|
23
|
+
scrollable: { control: "boolean" },
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
export default meta;
|
|
27
|
+
const ContentArea = () => (_jsx("div", { className: "flex items-center justify-center bg-ramps-secondary-150 h-[200px] w-full rounded-sm", children: _jsx("p", { className: "typography-body3 text-text-contrast-max", children: "Content - Form Area" }) }));
|
|
28
|
+
export const Default = {
|
|
29
|
+
args: {
|
|
30
|
+
open: false,
|
|
31
|
+
title: "Title",
|
|
32
|
+
description: "Subtitle description",
|
|
33
|
+
},
|
|
34
|
+
render: (args) => {
|
|
35
|
+
const [{ open }, updateArgs] = useArgs();
|
|
36
|
+
return (_jsx(FormDialog, Object.assign({}, args, { open: open, onOpenChange: (next) => updateArgs({ open: next }), cancelAction: {
|
|
37
|
+
label: "Cancel",
|
|
38
|
+
onClick: () => updateArgs({ open: false }),
|
|
39
|
+
}, confirmAction: { label: "Confirm", disabled: true }, trigger: _jsx(Button, { fullwidth: false, onClick: () => updateArgs({ open: true }), children: "Open" }), children: _jsx(ContentArea, {}) })));
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
export const WithExtraAction = {
|
|
43
|
+
args: {
|
|
44
|
+
open: false,
|
|
45
|
+
title: "Title",
|
|
46
|
+
description: "Subtitle description",
|
|
47
|
+
},
|
|
48
|
+
render: (args) => {
|
|
49
|
+
const [{ open }, updateArgs] = useArgs();
|
|
50
|
+
return (_jsx(FormDialog, Object.assign({}, args, { open: open, onOpenChange: (next) => updateArgs({ open: next }), extraAction: {
|
|
51
|
+
label: "Medium",
|
|
52
|
+
onClick: () => console.log("extra action"),
|
|
53
|
+
}, cancelAction: {
|
|
54
|
+
label: "Cancel",
|
|
55
|
+
onClick: () => updateArgs({ open: false }),
|
|
56
|
+
}, confirmAction: { label: "Confirm", disabled: true }, trigger: _jsx(Button, { fullwidth: false, onClick: () => updateArgs({ open: true }), children: "Open (with extra action)" }), children: _jsx(ContentArea, {}) })));
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
export const WithForm = {
|
|
60
|
+
args: {
|
|
61
|
+
open: false,
|
|
62
|
+
title: "Edit profile",
|
|
63
|
+
description: "Make changes to your profile here.",
|
|
64
|
+
},
|
|
65
|
+
render: (args) => {
|
|
66
|
+
const [{ open }, updateArgs] = useArgs();
|
|
67
|
+
const [name, setName] = useState("");
|
|
68
|
+
const isValid = name.trim().length > 0;
|
|
69
|
+
return (_jsx(FormDialog, Object.assign({}, args, { open: open, onOpenChange: (next) => {
|
|
70
|
+
if (!next)
|
|
71
|
+
setName("");
|
|
72
|
+
updateArgs({ open: next });
|
|
73
|
+
}, cancelAction: {
|
|
74
|
+
label: "Cancel",
|
|
75
|
+
onClick: () => {
|
|
76
|
+
setName("");
|
|
77
|
+
updateArgs({ open: false });
|
|
78
|
+
},
|
|
79
|
+
}, confirmAction: {
|
|
80
|
+
label: "Save changes",
|
|
81
|
+
disabled: !isValid,
|
|
82
|
+
onClick: () => {
|
|
83
|
+
console.log("save:", name);
|
|
84
|
+
updateArgs({ open: false });
|
|
85
|
+
},
|
|
86
|
+
}, trigger: _jsx(Button, { variant: "outline", fullwidth: false, onClick: () => updateArgs({ open: true }), children: "Edit profile" }), children: _jsx("div", { className: "flex flex-col gap-4", children: _jsx(TextInput, { label: "Display name", required: true, value: name, onChange: (e) => setName(e.target.value), fullwidth: true }) }) })));
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
export const FigmaFormDefaultOpen = {
|
|
90
|
+
args: {
|
|
91
|
+
open: true,
|
|
92
|
+
title: "Title",
|
|
93
|
+
description: "Subtitle description",
|
|
94
|
+
},
|
|
95
|
+
render: (args) => {
|
|
96
|
+
const [{ open }, updateArgs] = useArgs();
|
|
97
|
+
return (_jsx(FormDialog, Object.assign({}, args, { open: open, onOpenChange: (next) => updateArgs({ open: next }), cancelAction: {
|
|
98
|
+
label: "Cancel",
|
|
99
|
+
onClick: () => updateArgs({ open: false }),
|
|
100
|
+
}, confirmAction: { label: "Confirm", disabled: true }, trigger: _jsx(Button, { fullwidth: false, onClick: () => updateArgs({ open: true }), children: "Open" }), children: _jsx(ContentArea, {}) })));
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
export const FigmaFormWithActionDefaultOpen = {
|
|
104
|
+
args: {
|
|
105
|
+
open: true,
|
|
106
|
+
title: "Title",
|
|
107
|
+
description: "Subtitle description",
|
|
108
|
+
},
|
|
109
|
+
render: (args) => {
|
|
110
|
+
const [{ open }, updateArgs] = useArgs();
|
|
111
|
+
return (_jsx(FormDialog, Object.assign({}, args, { open: open, onOpenChange: (next) => updateArgs({ open: next }), extraAction: { label: "Medium" }, cancelAction: {
|
|
112
|
+
label: "Cancel",
|
|
113
|
+
onClick: () => updateArgs({ open: false }),
|
|
114
|
+
}, confirmAction: { label: "Confirm", disabled: true }, trigger: _jsx(Button, { fullwidth: false, onClick: () => updateArgs({ open: true }), children: "Open" }), children: _jsx(ContentArea, {}) })));
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
const editProfileSchema = yup.object({
|
|
118
|
+
displayName: yup.string().required("Display name is required."),
|
|
119
|
+
email: yup
|
|
120
|
+
.string()
|
|
121
|
+
.required("Email is required.")
|
|
122
|
+
.email("Must be a valid email."),
|
|
123
|
+
});
|
|
124
|
+
const EditProfileDialog = ({ open, onOpenChange }) => {
|
|
125
|
+
const formId = React.useId();
|
|
126
|
+
const { methods, FormRoot } = useControlledForm({
|
|
127
|
+
defaultValues: { displayName: "", email: "" },
|
|
128
|
+
validationSchema: editProfileSchema,
|
|
129
|
+
mode: "onTouched",
|
|
130
|
+
reValidateMode: "onChange",
|
|
131
|
+
});
|
|
132
|
+
const isValid = methods.formState.isValid;
|
|
133
|
+
const handleClose = () => {
|
|
134
|
+
methods.reset();
|
|
135
|
+
onOpenChange(false);
|
|
136
|
+
};
|
|
137
|
+
const handleSubmit = (values) => {
|
|
138
|
+
console.log("submitted:", values);
|
|
139
|
+
handleClose();
|
|
140
|
+
};
|
|
141
|
+
return (_jsx(FormDialog, { open: open, onOpenChange: (next) => {
|
|
142
|
+
if (!next)
|
|
143
|
+
handleClose();
|
|
144
|
+
else
|
|
145
|
+
onOpenChange(next);
|
|
146
|
+
}, title: "Edit profile", description: "Make changes to your profile here.", formId: formId, cancelAction: { label: "Cancel", onClick: handleClose }, confirmAction: { label: "Save changes", disabled: !isValid }, children: _jsxs(FormRoot, { id: formId, onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [_jsx(Field, { name: "displayName", component: TextInput, componentProps: {
|
|
147
|
+
label: "Display name",
|
|
148
|
+
required: true,
|
|
149
|
+
fullwidth: true,
|
|
150
|
+
} }), _jsx(Field, { name: "email", component: TextInput, componentProps: { label: "Email", required: true, fullwidth: true } })] }) }));
|
|
151
|
+
};
|
|
152
|
+
const EditProfileDialogWithLoading = ({ open, onOpenChange, }) => {
|
|
153
|
+
const formId = React.useId();
|
|
154
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
155
|
+
const { methods, FormRoot } = useControlledForm({
|
|
156
|
+
defaultValues: { displayName: "", email: "" },
|
|
157
|
+
validationSchema: editProfileSchema,
|
|
158
|
+
mode: "onTouched",
|
|
159
|
+
reValidateMode: "onChange",
|
|
160
|
+
});
|
|
161
|
+
const isValid = methods.formState.isValid;
|
|
162
|
+
const handleClose = () => {
|
|
163
|
+
methods.reset();
|
|
164
|
+
onOpenChange(false);
|
|
165
|
+
};
|
|
166
|
+
const handleSubmit = async (values) => {
|
|
167
|
+
setIsLoading(true);
|
|
168
|
+
try {
|
|
169
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
170
|
+
console.log("saved:", values);
|
|
171
|
+
handleClose();
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
setIsLoading(false);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
return (_jsx(FormDialog, { open: open, onOpenChange: (next) => {
|
|
178
|
+
if (!next && !isLoading)
|
|
179
|
+
handleClose();
|
|
180
|
+
else if (next)
|
|
181
|
+
onOpenChange(next);
|
|
182
|
+
}, title: "Edit profile", description: "Make changes to your profile here.", formId: formId, cancelAction: {
|
|
183
|
+
label: "Cancel",
|
|
184
|
+
onClick: handleClose,
|
|
185
|
+
disabled: isLoading,
|
|
186
|
+
}, confirmAction: {
|
|
187
|
+
label: "Save changes",
|
|
188
|
+
disabled: !isValid || isLoading,
|
|
189
|
+
isLoading,
|
|
190
|
+
}, children: _jsxs(FormRoot, { id: formId, onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [_jsx(Field, { name: "displayName", component: TextInput, componentProps: {
|
|
191
|
+
label: "Display name",
|
|
192
|
+
required: true,
|
|
193
|
+
fullwidth: true,
|
|
194
|
+
} }), _jsx(Field, { name: "email", component: TextInput, componentProps: { label: "Email", required: true, fullwidth: true } })] }) }));
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* Pattern A: useControlledForm
|
|
198
|
+
*
|
|
199
|
+
* Use when you need to:
|
|
200
|
+
* - access formState (isValid, isDirty) to control the confirm button
|
|
201
|
+
* - reset the form on close
|
|
202
|
+
* - trigger validation outside the form element
|
|
203
|
+
*/
|
|
204
|
+
export const WithRovulaForm = {
|
|
205
|
+
args: { open: false },
|
|
206
|
+
render: () => {
|
|
207
|
+
const [{ open }, updateArgs] = useArgs();
|
|
208
|
+
return (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", fullwidth: false, onClick: () => updateArgs({ open: true }), children: "Edit profile" }), _jsx(EditProfileDialog, { open: open, onOpenChange: (next) => updateArgs({ open: next }) })] }));
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
/**
|
|
212
|
+
* Pattern B: useControlledForm + async API call + loading state
|
|
213
|
+
*
|
|
214
|
+
* Use when the confirm action calls an API.
|
|
215
|
+
* isLoading disables + shows spinner on the confirm button while the request is in flight.
|
|
216
|
+
*/
|
|
217
|
+
export const WithRovulaFormAndApiLoading = {
|
|
218
|
+
args: { open: false },
|
|
219
|
+
render: () => {
|
|
220
|
+
const [{ open }, updateArgs] = useArgs();
|
|
221
|
+
return (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", fullwidth: false, onClick: () => updateArgs({ open: true }), children: "Edit profile (with loading)" }), _jsx(EditProfileDialogWithLoading, { open: open, onOpenChange: (next) => updateArgs({ open: next }) })] }));
|
|
222
|
+
},
|
|
223
|
+
};
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@ const AlertDialogOverlay = React.forwardRef<
|
|
|
19
19
|
<AlertDialogPrimitive.Overlay
|
|
20
20
|
className={cn(
|
|
21
21
|
"fixed inset-0 bg-modal-overlay z-50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
22
|
-
className
|
|
22
|
+
className,
|
|
23
23
|
)}
|
|
24
24
|
{...props}
|
|
25
25
|
ref={ref}
|
|
@@ -37,8 +37,8 @@ const AlertDialogContent = React.forwardRef<
|
|
|
37
37
|
<AlertDialogPrimitive.Content
|
|
38
38
|
ref={ref}
|
|
39
39
|
className={cn(
|
|
40
|
-
"fixed left-[50%] top-[50%] z-50 grid w-[calc(100%-32px)] max-w-[460px] translate-x-[-50%] translate-y-[-50%] gap-6 rounded-md bg-modal-surface px-6 py-8 text-text-contrast-max shadow-[0px_12px_24px_-4px_rgba(0,0,0,0.12)] duration-200 focus:outline-none focus-visible:outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",
|
|
41
|
-
className
|
|
40
|
+
"fixed left-[50%] top-[50%] z-50 grid w-[calc(100%-32px)] max-w-[460px] translate-x-[-50%] translate-y-[-50%] gap-6 rounded-md bg-modal-surface px-6 py-8 text-text-contrast-max shadow-[0px_12px_24px_-4px_rgba(0,0,0,0.12)] duration-200 focus:outline-none focus-visible:outline-none outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",
|
|
41
|
+
className,
|
|
42
42
|
)}
|
|
43
43
|
{...props}
|
|
44
44
|
/>
|
|
@@ -51,10 +51,7 @@ const AlertDialogHeader = ({
|
|
|
51
51
|
...props
|
|
52
52
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
53
53
|
<div
|
|
54
|
-
className={cn(
|
|
55
|
-
"flex flex-col items-center gap-2 text-center",
|
|
56
|
-
className
|
|
57
|
-
)}
|
|
54
|
+
className={cn("flex flex-col items-center gap-2 text-center", className)}
|
|
58
55
|
{...props}
|
|
59
56
|
/>
|
|
60
57
|
);
|
|
@@ -65,10 +62,7 @@ const AlertDialogFooter = ({
|
|
|
65
62
|
...props
|
|
66
63
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
67
64
|
<div
|
|
68
|
-
className={cn(
|
|
69
|
-
"flex flex-row items-center justify-center gap-4",
|
|
70
|
-
className
|
|
71
|
-
)}
|
|
65
|
+
className={cn("flex flex-row items-center justify-center gap-4", className)}
|
|
72
66
|
{...props}
|
|
73
67
|
/>
|
|
74
68
|
);
|
|
@@ -105,7 +99,11 @@ const AlertDialogAction = React.forwardRef<
|
|
|
105
99
|
>(({ className, ...props }, ref) => (
|
|
106
100
|
<AlertDialogPrimitive.Action
|
|
107
101
|
ref={ref}
|
|
108
|
-
className={cn(
|
|
102
|
+
className={cn(
|
|
103
|
+
buttonVariants({ fullwidth: false }),
|
|
104
|
+
"w-[100px] justify-center",
|
|
105
|
+
className,
|
|
106
|
+
)}
|
|
109
107
|
{...props}
|
|
110
108
|
/>
|
|
111
109
|
));
|
|
@@ -120,7 +118,7 @@ const AlertDialogCancel = React.forwardRef<
|
|
|
120
118
|
className={cn(
|
|
121
119
|
buttonVariants({ fullwidth: false, variant: "outline" }),
|
|
122
120
|
"w-[100px] justify-center",
|
|
123
|
-
className
|
|
121
|
+
className,
|
|
124
122
|
)}
|
|
125
123
|
{...props}
|
|
126
124
|
/>
|