@rovula/ui 0.1.14 → 0.1.16
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 +1545 -1545
- package/dist/cjs/bundle.js.map +1 -1
- package/dist/cjs/types/components/Form/Form.d.ts +1 -1
- package/dist/cjs/types/index.d.ts +2 -0
- package/dist/cjs/types/patterns/confirm-dialog/ConfirmDialog.d.ts +25 -0
- package/dist/cjs/types/patterns/confirm-dialog/ConfirmDialog.stories.d.ts +54 -0
- package/dist/cjs/types/patterns/form-dialog/FormDialog.d.ts +40 -0
- package/dist/cjs/types/patterns/form-dialog/FormDialog.stories.d.ts +63 -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 +1545 -1545
- package/dist/esm/bundle.js.map +1 -1
- package/dist/esm/types/components/Form/Form.d.ts +1 -1
- package/dist/esm/types/index.d.ts +2 -0
- package/dist/esm/types/patterns/confirm-dialog/ConfirmDialog.d.ts +25 -0
- package/dist/esm/types/patterns/confirm-dialog/ConfirmDialog.stories.d.ts +54 -0
- package/dist/esm/types/patterns/form-dialog/FormDialog.d.ts +40 -0
- package/dist/esm/types/patterns/form-dialog/FormDialog.stories.d.ts +63 -0
- package/dist/index.d.ts +67 -2
- package/dist/index.js +3 -0
- package/dist/patterns/confirm-dialog/ConfirmDialog.js +45 -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/index.ts +4 -0
- package/src/patterns/confirm-dialog/ConfirmDialog.stories.tsx +193 -0
- package/src/patterns/confirm-dialog/ConfirmDialog.tsx +164 -0
- package/src/patterns/form-dialog/FormDialog.stories.tsx +437 -0
- package/src/patterns/form-dialog/FormDialog.tsx +144 -0
|
@@ -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
|
/>
|
|
@@ -51,7 +51,7 @@ const DialogContent = React.forwardRef<
|
|
|
51
51
|
<DialogPrimitive.Content
|
|
52
52
|
ref={ref}
|
|
53
53
|
className={cn(
|
|
54
|
-
"fixed left-[50%] top-[50%] z-50 flex w-[calc(100%-32px)] max-w-[650px] translate-x-[-50%] translate-y-[-50%] flex-col gap-6 rounded-md bg-modal-surface p-8 text-text-g-contrast-medium 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%]",
|
|
54
|
+
"fixed left-[50%] top-[50%] z-50 flex w-[calc(100%-32px)] max-w-[650px] translate-x-[-50%] translate-y-[-50%] flex-col gap-6 rounded-md bg-modal-surface p-8 text-text-g-contrast-medium 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%]",
|
|
55
55
|
className,
|
|
56
56
|
)}
|
|
57
57
|
{...props}
|
|
@@ -103,10 +103,7 @@ const DialogFooter = ({
|
|
|
103
103
|
...props
|
|
104
104
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
105
105
|
<div
|
|
106
|
-
className={cn(
|
|
107
|
-
"flex flex-row items-center justify-end gap-4",
|
|
108
|
-
className,
|
|
109
|
-
)}
|
|
106
|
+
className={cn("flex flex-row items-center justify-end gap-4", className)}
|
|
110
107
|
{...props}
|
|
111
108
|
/>
|
|
112
109
|
);
|
|
@@ -133,10 +130,7 @@ const DialogDescription = React.forwardRef<
|
|
|
133
130
|
>(({ className, ...props }, ref) => (
|
|
134
131
|
<DialogPrimitive.Description
|
|
135
132
|
ref={ref}
|
|
136
|
-
className={cn(
|
|
137
|
-
"typography-body3 text-text-contrast-max",
|
|
138
|
-
className,
|
|
139
|
-
)}
|
|
133
|
+
className={cn("typography-body3 text-text-contrast-max", className)}
|
|
140
134
|
{...props}
|
|
141
135
|
/>
|
|
142
136
|
));
|
|
@@ -110,11 +110,26 @@ export const useControlledForm = <TFieldValues extends FieldValues>({
|
|
|
110
110
|
reValidateMode,
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
// Keep FormRoot component reference stable across re-renders.
|
|
114
|
+
// createControlledForm creates a new component type (via forwardRef) on every call —
|
|
115
|
+
// if FormRoot changes reference, React unmounts + remounts the entire form tree,
|
|
116
|
+
// causing inputs to lose focus whenever parent state changes (e.g. isValid).
|
|
117
|
+
const stableRef = React.useRef<ReturnType<
|
|
118
|
+
typeof createControlledForm<TFieldValues>
|
|
119
|
+
> | null>(null);
|
|
120
|
+
|
|
121
|
+
if (!stableRef.current) {
|
|
122
|
+
stableRef.current = createControlledForm<TFieldValues>({
|
|
123
|
+
methods,
|
|
124
|
+
defaultValues,
|
|
125
|
+
controllerRef,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
114
130
|
methods,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
});
|
|
131
|
+
FormRoot: stableRef.current.FormRoot,
|
|
132
|
+
};
|
|
118
133
|
};
|
|
119
134
|
|
|
120
135
|
const FormInner = <TFieldValues extends FieldValues>(
|
package/src/index.ts
CHANGED
|
@@ -52,6 +52,10 @@ export * from "./components/FocusedScrollView/FocusedScrollView";
|
|
|
52
52
|
export * from "./components/RadioGroup/RadioGroup";
|
|
53
53
|
export * from "./components/Form";
|
|
54
54
|
|
|
55
|
+
// Patterns
|
|
56
|
+
export * from "./patterns/confirm-dialog/ConfirmDialog";
|
|
57
|
+
export * from "./patterns/form-dialog/FormDialog";
|
|
58
|
+
|
|
55
59
|
// Export component types
|
|
56
60
|
export type { ButtonProps } from "./components/Button/Button";
|
|
57
61
|
export type { InputProps } from "./components/TextInput/TextInput";
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { useArgs } from "@storybook/preview-api";
|
|
4
|
+
import { ConfirmDialog } from "./ConfirmDialog";
|
|
5
|
+
import Button from "@/components/Button/Button";
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: "Patterns/ConfirmDialog",
|
|
9
|
+
component: ConfirmDialog,
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: "fullscreen",
|
|
13
|
+
},
|
|
14
|
+
decorators: [
|
|
15
|
+
(Story) => (
|
|
16
|
+
<div className="p-5 flex w-full">
|
|
17
|
+
<Story />
|
|
18
|
+
</div>
|
|
19
|
+
),
|
|
20
|
+
],
|
|
21
|
+
argTypes: {
|
|
22
|
+
open: { control: "boolean" },
|
|
23
|
+
typeToConfirm: { control: "text" },
|
|
24
|
+
title: { control: "text" },
|
|
25
|
+
description: { control: "text" },
|
|
26
|
+
confirmLabel: { control: "text" },
|
|
27
|
+
cancelLabel: { control: "text" },
|
|
28
|
+
},
|
|
29
|
+
} satisfies Meta<typeof ConfirmDialog>;
|
|
30
|
+
|
|
31
|
+
export default meta;
|
|
32
|
+
type Story = StoryObj<typeof ConfirmDialog>;
|
|
33
|
+
|
|
34
|
+
export const Default: Story = {
|
|
35
|
+
args: {
|
|
36
|
+
open: false,
|
|
37
|
+
title: "Are you sure?",
|
|
38
|
+
description: "This action cannot be undone.",
|
|
39
|
+
confirmLabel: "Confirm",
|
|
40
|
+
cancelLabel: "Cancel",
|
|
41
|
+
},
|
|
42
|
+
render: (args) => {
|
|
43
|
+
const [{ open }, updateArgs] = useArgs();
|
|
44
|
+
return (
|
|
45
|
+
<ConfirmDialog
|
|
46
|
+
{...args}
|
|
47
|
+
open={open}
|
|
48
|
+
onOpenChange={(next) => updateArgs({ open: next })}
|
|
49
|
+
onConfirm={() => updateArgs({ open: false })}
|
|
50
|
+
onCancel={() => updateArgs({ open: false })}
|
|
51
|
+
trigger={
|
|
52
|
+
<Button fullwidth={false} onClick={() => updateArgs({ open: true })}>
|
|
53
|
+
Open
|
|
54
|
+
</Button>
|
|
55
|
+
}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const WithoutDescription: Story = {
|
|
62
|
+
args: {
|
|
63
|
+
open: false,
|
|
64
|
+
title: "Delete item?",
|
|
65
|
+
confirmLabel: "Delete",
|
|
66
|
+
cancelLabel: "Cancel",
|
|
67
|
+
},
|
|
68
|
+
render: (args) => {
|
|
69
|
+
const [{ open }, updateArgs] = useArgs();
|
|
70
|
+
return (
|
|
71
|
+
<ConfirmDialog
|
|
72
|
+
{...args}
|
|
73
|
+
open={open}
|
|
74
|
+
onOpenChange={(next) => updateArgs({ open: next })}
|
|
75
|
+
onConfirm={() => updateArgs({ open: false })}
|
|
76
|
+
onCancel={() => updateArgs({ open: false })}
|
|
77
|
+
trigger={
|
|
78
|
+
<Button
|
|
79
|
+
fullwidth={false}
|
|
80
|
+
color="error"
|
|
81
|
+
onClick={() => updateArgs({ open: true })}
|
|
82
|
+
>
|
|
83
|
+
Delete
|
|
84
|
+
</Button>
|
|
85
|
+
}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const WithoutCancelButton: Story = {
|
|
92
|
+
args: {
|
|
93
|
+
open: false,
|
|
94
|
+
title: "Are you sure?",
|
|
95
|
+
description: "This action cannot be undone.",
|
|
96
|
+
confirmLabel: "Confirm",
|
|
97
|
+
hideCancelButton: true,
|
|
98
|
+
},
|
|
99
|
+
render: (args) => {
|
|
100
|
+
const [{ open }, updateArgs] = useArgs();
|
|
101
|
+
return (
|
|
102
|
+
<ConfirmDialog
|
|
103
|
+
{...args}
|
|
104
|
+
open={open}
|
|
105
|
+
onOpenChange={(next) => updateArgs({ open: next })}
|
|
106
|
+
onConfirm={() => updateArgs({ open: false })}
|
|
107
|
+
onCancel={() => updateArgs({ open: false })}
|
|
108
|
+
trigger={
|
|
109
|
+
<Button
|
|
110
|
+
fullwidth={false}
|
|
111
|
+
color="error"
|
|
112
|
+
onClick={() => updateArgs({ open: true })}
|
|
113
|
+
>
|
|
114
|
+
Confirm
|
|
115
|
+
</Button>
|
|
116
|
+
}
|
|
117
|
+
/>
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const RequireConfirmText: Story = {
|
|
123
|
+
args: {
|
|
124
|
+
open: false,
|
|
125
|
+
title: "Title",
|
|
126
|
+
description: "Subtitle description",
|
|
127
|
+
confirmLabel: "Confirm",
|
|
128
|
+
cancelLabel: "Cancel",
|
|
129
|
+
typeToConfirm: "confirm",
|
|
130
|
+
},
|
|
131
|
+
render: (args) => {
|
|
132
|
+
const [{ open }, updateArgs] = useArgs();
|
|
133
|
+
return (
|
|
134
|
+
<ConfirmDialog
|
|
135
|
+
{...args}
|
|
136
|
+
open={open}
|
|
137
|
+
onOpenChange={(next) => updateArgs({ open: next })}
|
|
138
|
+
onConfirm={() => updateArgs({ open: false })}
|
|
139
|
+
onCancel={() => updateArgs({ open: false })}
|
|
140
|
+
trigger={
|
|
141
|
+
<Button fullwidth={false} onClick={() => updateArgs({ open: true })}>
|
|
142
|
+
Open (require confirm)
|
|
143
|
+
</Button>
|
|
144
|
+
}
|
|
145
|
+
/>
|
|
146
|
+
);
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const FigmaDefaultOpen: Story = {
|
|
151
|
+
args: {
|
|
152
|
+
open: false,
|
|
153
|
+
title: "Title",
|
|
154
|
+
description: "Subtitle description",
|
|
155
|
+
confirmLabel: "Confirm",
|
|
156
|
+
cancelLabel: "Cancel",
|
|
157
|
+
},
|
|
158
|
+
render: (args) => {
|
|
159
|
+
const [{ open }, updateArgs] = useArgs();
|
|
160
|
+
return (
|
|
161
|
+
<ConfirmDialog
|
|
162
|
+
{...args}
|
|
163
|
+
open={open}
|
|
164
|
+
onOpenChange={(next) => updateArgs({ open: next })}
|
|
165
|
+
onConfirm={() => updateArgs({ open: false })}
|
|
166
|
+
onCancel={() => updateArgs({ open: false })}
|
|
167
|
+
/>
|
|
168
|
+
);
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export const FigmaRequirePasswordDefaultOpen: Story = {
|
|
173
|
+
args: {
|
|
174
|
+
open: false,
|
|
175
|
+
title: "Title",
|
|
176
|
+
description: "Subtitle description",
|
|
177
|
+
confirmLabel: "Confirm",
|
|
178
|
+
cancelLabel: "Cancel",
|
|
179
|
+
typeToConfirm: "confirm",
|
|
180
|
+
},
|
|
181
|
+
render: (args) => {
|
|
182
|
+
const [{ open }, updateArgs] = useArgs();
|
|
183
|
+
return (
|
|
184
|
+
<ConfirmDialog
|
|
185
|
+
{...args}
|
|
186
|
+
open={open}
|
|
187
|
+
onOpenChange={(next) => updateArgs({ open: next })}
|
|
188
|
+
onConfirm={() => updateArgs({ open: false })}
|
|
189
|
+
onCancel={() => updateArgs({ open: false })}
|
|
190
|
+
/>
|
|
191
|
+
);
|
|
192
|
+
},
|
|
193
|
+
};
|