@rovula/ui 0.1.25 → 0.1.27
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 +1 -1
- package/dist/cjs/bundle.js.map +1 -1
- package/dist/cjs/types/patterns/confirm-dialog/ConfirmDialog.d.ts +2 -0
- package/dist/cjs/types/patterns/confirm-dialog/ConfirmDialog.stories.d.ts +3 -0
- package/dist/esm/bundle.js +1 -1
- package/dist/esm/bundle.js.map +1 -1
- package/dist/esm/types/patterns/confirm-dialog/ConfirmDialog.d.ts +2 -0
- package/dist/esm/types/patterns/confirm-dialog/ConfirmDialog.stories.d.ts +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/patterns/confirm-dialog/ConfirmDialog.js +22 -6
- package/dist/patterns/confirm-dialog/ConfirmDialog.stories.js +33 -0
- package/package.json +1 -1
- package/src/patterns/confirm-dialog/ConfirmDialog.stories.tsx +71 -0
- package/src/patterns/confirm-dialog/ConfirmDialog.tsx +33 -10
|
@@ -21,6 +21,8 @@ export type ConfirmDialogProps = {
|
|
|
21
21
|
* When true, hides the cancel button — useful for info/error alerts that only need one action.
|
|
22
22
|
*/
|
|
23
23
|
hideCancelButton?: boolean;
|
|
24
|
+
/** When true, shows a loading spinner on the confirm button and disables all interactive controls. */
|
|
25
|
+
isLoading?: boolean;
|
|
24
26
|
testId?: string;
|
|
25
27
|
cancelClassName?: string;
|
|
26
28
|
confirmClassName?: string;
|
|
@@ -22,6 +22,7 @@ declare const meta: {
|
|
|
22
22
|
trigger?: React.ReactNode;
|
|
23
23
|
typeToConfirm?: string | undefined;
|
|
24
24
|
hideCancelButton?: boolean | undefined;
|
|
25
|
+
isLoading?: boolean | undefined;
|
|
25
26
|
testId?: string | undefined;
|
|
26
27
|
cancelClassName?: string | undefined;
|
|
27
28
|
confirmClassName?: string | undefined;
|
|
@@ -55,3 +56,5 @@ export declare const WithoutCancelButton: Story;
|
|
|
55
56
|
export declare const RequireConfirmText: Story;
|
|
56
57
|
export declare const FigmaDefaultOpen: Story;
|
|
57
58
|
export declare const FigmaRequirePasswordDefaultOpen: Story;
|
|
59
|
+
export declare const WithLoading: Story;
|
|
60
|
+
export declare const WithLoadingAndTypeToConfirm: Story;
|
package/dist/index.d.ts
CHANGED
|
@@ -1318,6 +1318,8 @@ type ConfirmDialogProps = {
|
|
|
1318
1318
|
* When true, hides the cancel button — useful for info/error alerts that only need one action.
|
|
1319
1319
|
*/
|
|
1320
1320
|
hideCancelButton?: boolean;
|
|
1321
|
+
/** When true, shows a loading spinner on the confirm button and disables all interactive controls. */
|
|
1322
|
+
isLoading?: boolean;
|
|
1321
1323
|
testId?: string;
|
|
1322
1324
|
cancelClassName?: string;
|
|
1323
1325
|
confirmClassName?: string;
|
|
@@ -5,8 +5,8 @@ import * as yup from "yup";
|
|
|
5
5
|
import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogAction, AlertDialogCancel, AlertDialogTrigger, } from "@/components/AlertDialog/AlertDialog";
|
|
6
6
|
import { useControlledForm, Field } from "@/components/Form";
|
|
7
7
|
import { TextInput } from "@/components/TextInput/TextInput";
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import Loading from "@/components/Loading/Loading";
|
|
9
|
+
export const ConfirmDialog = ({ open, onOpenChange, title, description, children, confirmLabel = "Confirm", cancelLabel = "Cancel", onConfirm, onCancel, onClose, trigger, typeToConfirm, hideCancelButton = false, isLoading = false, testId, cancelClassName, confirmClassName, }) => {
|
|
10
10
|
const requiresInput = !!typeToConfirm;
|
|
11
11
|
const validationSchema = React.useMemo(() => yup.object({
|
|
12
12
|
confirmInput: yup
|
|
@@ -21,25 +21,41 @@ export const ConfirmDialog = ({ open, onOpenChange, title, description, children
|
|
|
21
21
|
reValidateMode: "onChange",
|
|
22
22
|
});
|
|
23
23
|
const isFormValid = methods.formState.isValid;
|
|
24
|
+
React.useEffect(() => {
|
|
25
|
+
if (!open) {
|
|
26
|
+
methods.reset();
|
|
27
|
+
}
|
|
28
|
+
}, [open]);
|
|
24
29
|
const handleOpenChange = (nextOpen) => {
|
|
30
|
+
if (isLoading)
|
|
31
|
+
return;
|
|
25
32
|
if (!nextOpen) {
|
|
26
|
-
methods.reset();
|
|
27
33
|
onClose === null || onClose === void 0 ? void 0 : onClose();
|
|
28
34
|
}
|
|
29
35
|
onOpenChange === null || onOpenChange === void 0 ? void 0 : onOpenChange(nextOpen);
|
|
30
36
|
};
|
|
31
37
|
const handleCancel = () => {
|
|
32
|
-
methods.reset();
|
|
33
38
|
onCancel === null || onCancel === void 0 ? void 0 : onCancel();
|
|
34
39
|
onClose === null || onClose === void 0 ? void 0 : onClose();
|
|
35
40
|
};
|
|
36
|
-
|
|
41
|
+
const handleSubmit = () => {
|
|
42
|
+
onConfirm === null || onConfirm === void 0 ? void 0 : onConfirm();
|
|
43
|
+
};
|
|
44
|
+
return (_jsxs(AlertDialog, { open: open, onOpenChange: handleOpenChange, children: [trigger && _jsx(AlertDialogTrigger, { asChild: true, children: trigger }), _jsxs(AlertDialogContent, { "data-testid": testId, children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { "data-testid": testId && `${testId}-title`, children: title }), description && (_jsx(AlertDialogDescription, { "data-testid": testId && `${testId}-description`, children: description }))] }), children, requiresInput && (_jsxs(FormRoot, { className: "flex flex-col gap-4 w-full", onSubmit: handleSubmit, 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
45
|
label: "Type to confirm",
|
|
38
46
|
required: true,
|
|
39
47
|
hasClearIcon: true,
|
|
40
48
|
keepFooterSpace: true,
|
|
41
49
|
fullwidth: true,
|
|
42
50
|
testId: testId && `${testId}-type-to-confirm-input`,
|
|
43
|
-
} })] })), _jsxs(AlertDialogFooter, { children: [!hideCancelButton && (_jsx(AlertDialogCancel, { className: cancelClassName, "data-testid": testId && `${testId}-cancel-button`, onClick: handleCancel, children: cancelLabel })),
|
|
51
|
+
}, disabled: isLoading })] })), _jsxs(AlertDialogFooter, { children: [!hideCancelButton && (_jsx(AlertDialogCancel, { className: cancelClassName, "data-testid": testId && `${testId}-cancel-button`, onClick: handleCancel, disabled: isLoading, children: cancelLabel })), _jsxs(AlertDialogAction, { type: "button", disabled: isLoading || (requiresInput && !isFormValid), onClick: (e) => {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
if (requiresInput) {
|
|
54
|
+
methods.handleSubmit(handleSubmit)();
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
onConfirm === null || onConfirm === void 0 ? void 0 : onConfirm();
|
|
58
|
+
}
|
|
59
|
+
}, className: confirmClassName, "data-testid": testId && `${testId}-confirm-button`, children: [isLoading && _jsx(Loading, {}), confirmLabel] })] })] })] }));
|
|
44
60
|
};
|
|
45
61
|
ConfirmDialog.displayName = "ConfirmDialog";
|
|
@@ -101,3 +101,36 @@ export const FigmaRequirePasswordDefaultOpen = {
|
|
|
101
101
|
return (_jsx(ConfirmDialog, Object.assign({}, args, { open: open, onOpenChange: (next) => updateArgs({ open: next }), onConfirm: () => updateArgs({ open: false }), onCancel: () => updateArgs({ open: false }) })));
|
|
102
102
|
},
|
|
103
103
|
};
|
|
104
|
+
export const WithLoading = {
|
|
105
|
+
args: {
|
|
106
|
+
open: false,
|
|
107
|
+
title: "Delete project?",
|
|
108
|
+
description: "This action cannot be undone.",
|
|
109
|
+
confirmLabel: "Delete",
|
|
110
|
+
cancelLabel: "Cancel",
|
|
111
|
+
},
|
|
112
|
+
render: (args) => {
|
|
113
|
+
const [{ open, isLoading }, updateArgs] = useArgs();
|
|
114
|
+
return (_jsx(ConfirmDialog, Object.assign({}, args, { open: open, isLoading: isLoading, onOpenChange: (next) => updateArgs({ open: next }), onConfirm: () => {
|
|
115
|
+
updateArgs({ isLoading: true });
|
|
116
|
+
setTimeout(() => updateArgs({ isLoading: false, open: false }), 2000);
|
|
117
|
+
}, onCancel: () => updateArgs({ open: false }), trigger: _jsx(Button, { fullwidth: false, color: "error", onClick: () => updateArgs({ open: true, isLoading: false }), children: "Delete" }) })));
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
export const WithLoadingAndTypeToConfirm = {
|
|
121
|
+
args: {
|
|
122
|
+
open: false,
|
|
123
|
+
title: "Delete project?",
|
|
124
|
+
description: "This action cannot be undone.",
|
|
125
|
+
confirmLabel: "Delete",
|
|
126
|
+
cancelLabel: "Cancel",
|
|
127
|
+
typeToConfirm: "delete",
|
|
128
|
+
},
|
|
129
|
+
render: (args) => {
|
|
130
|
+
const [{ open, isLoading }, updateArgs] = useArgs();
|
|
131
|
+
return (_jsx(ConfirmDialog, Object.assign({}, args, { open: open, isLoading: isLoading, onOpenChange: (next) => updateArgs({ open: next }), onConfirm: () => {
|
|
132
|
+
updateArgs({ isLoading: true });
|
|
133
|
+
setTimeout(() => updateArgs({ isLoading: false, open: false }), 5000);
|
|
134
|
+
}, onCancel: () => updateArgs({ open: false }), trigger: _jsx(Button, { fullwidth: false, color: "error", onClick: () => updateArgs({ open: true, isLoading: false }), children: "Delete (type to confirm)" }) })));
|
|
135
|
+
},
|
|
136
|
+
};
|
package/package.json
CHANGED
|
@@ -191,3 +191,74 @@ export const FigmaRequirePasswordDefaultOpen: Story = {
|
|
|
191
191
|
);
|
|
192
192
|
},
|
|
193
193
|
};
|
|
194
|
+
|
|
195
|
+
export const WithLoading: Story = {
|
|
196
|
+
args: {
|
|
197
|
+
open: false,
|
|
198
|
+
title: "Delete project?",
|
|
199
|
+
description: "This action cannot be undone.",
|
|
200
|
+
confirmLabel: "Delete",
|
|
201
|
+
cancelLabel: "Cancel",
|
|
202
|
+
},
|
|
203
|
+
render: (args) => {
|
|
204
|
+
const [{ open, isLoading }, updateArgs] = useArgs();
|
|
205
|
+
return (
|
|
206
|
+
<ConfirmDialog
|
|
207
|
+
{...args}
|
|
208
|
+
open={open}
|
|
209
|
+
isLoading={isLoading}
|
|
210
|
+
onOpenChange={(next) => updateArgs({ open: next })}
|
|
211
|
+
onConfirm={() => {
|
|
212
|
+
updateArgs({ isLoading: true });
|
|
213
|
+
setTimeout(() => updateArgs({ isLoading: false, open: false }), 2000);
|
|
214
|
+
}}
|
|
215
|
+
onCancel={() => updateArgs({ open: false })}
|
|
216
|
+
trigger={
|
|
217
|
+
<Button
|
|
218
|
+
fullwidth={false}
|
|
219
|
+
color="error"
|
|
220
|
+
onClick={() => updateArgs({ open: true, isLoading: false })}
|
|
221
|
+
>
|
|
222
|
+
Delete
|
|
223
|
+
</Button>
|
|
224
|
+
}
|
|
225
|
+
/>
|
|
226
|
+
);
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
export const WithLoadingAndTypeToConfirm: Story = {
|
|
231
|
+
args: {
|
|
232
|
+
open: false,
|
|
233
|
+
title: "Delete project?",
|
|
234
|
+
description: "This action cannot be undone.",
|
|
235
|
+
confirmLabel: "Delete",
|
|
236
|
+
cancelLabel: "Cancel",
|
|
237
|
+
typeToConfirm: "delete",
|
|
238
|
+
},
|
|
239
|
+
render: (args) => {
|
|
240
|
+
const [{ open, isLoading }, updateArgs] = useArgs();
|
|
241
|
+
return (
|
|
242
|
+
<ConfirmDialog
|
|
243
|
+
{...args}
|
|
244
|
+
open={open}
|
|
245
|
+
isLoading={isLoading}
|
|
246
|
+
onOpenChange={(next) => updateArgs({ open: next })}
|
|
247
|
+
onConfirm={() => {
|
|
248
|
+
updateArgs({ isLoading: true });
|
|
249
|
+
setTimeout(() => updateArgs({ isLoading: false, open: false }), 5000);
|
|
250
|
+
}}
|
|
251
|
+
onCancel={() => updateArgs({ open: false })}
|
|
252
|
+
trigger={
|
|
253
|
+
<Button
|
|
254
|
+
fullwidth={false}
|
|
255
|
+
color="error"
|
|
256
|
+
onClick={() => updateArgs({ open: true, isLoading: false })}
|
|
257
|
+
>
|
|
258
|
+
Delete (type to confirm)
|
|
259
|
+
</Button>
|
|
260
|
+
}
|
|
261
|
+
/>
|
|
262
|
+
);
|
|
263
|
+
},
|
|
264
|
+
};
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "@/components/AlertDialog/AlertDialog";
|
|
16
16
|
import { useControlledForm, Field } from "@/components/Form";
|
|
17
17
|
import { TextInput } from "@/components/TextInput/TextInput";
|
|
18
|
+
import Loading from "@/components/Loading/Loading";
|
|
18
19
|
|
|
19
20
|
export type ConfirmDialogProps = {
|
|
20
21
|
open?: boolean;
|
|
@@ -38,6 +39,8 @@ export type ConfirmDialogProps = {
|
|
|
38
39
|
* When true, hides the cancel button — useful for info/error alerts that only need one action.
|
|
39
40
|
*/
|
|
40
41
|
hideCancelButton?: boolean;
|
|
42
|
+
/** When true, shows a loading spinner on the confirm button and disables all interactive controls. */
|
|
43
|
+
isLoading?: boolean;
|
|
41
44
|
testId?: string;
|
|
42
45
|
cancelClassName?: string;
|
|
43
46
|
confirmClassName?: string;
|
|
@@ -59,11 +62,11 @@ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
|
|
|
59
62
|
trigger,
|
|
60
63
|
typeToConfirm,
|
|
61
64
|
hideCancelButton = false,
|
|
65
|
+
isLoading = false,
|
|
62
66
|
testId,
|
|
63
67
|
cancelClassName,
|
|
64
68
|
confirmClassName,
|
|
65
69
|
}) => {
|
|
66
|
-
const formId = React.useId();
|
|
67
70
|
const requiresInput = !!typeToConfirm;
|
|
68
71
|
|
|
69
72
|
const validationSchema = React.useMemo(
|
|
@@ -90,20 +93,29 @@ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
|
|
|
90
93
|
|
|
91
94
|
const isFormValid = methods.formState.isValid;
|
|
92
95
|
|
|
96
|
+
React.useEffect(() => {
|
|
97
|
+
if (!open) {
|
|
98
|
+
methods.reset();
|
|
99
|
+
}
|
|
100
|
+
}, [open]);
|
|
101
|
+
|
|
93
102
|
const handleOpenChange = (nextOpen: boolean) => {
|
|
103
|
+
if (isLoading) return;
|
|
94
104
|
if (!nextOpen) {
|
|
95
|
-
methods.reset();
|
|
96
105
|
onClose?.();
|
|
97
106
|
}
|
|
98
107
|
onOpenChange?.(nextOpen);
|
|
99
108
|
};
|
|
100
109
|
|
|
101
110
|
const handleCancel = () => {
|
|
102
|
-
methods.reset();
|
|
103
111
|
onCancel?.();
|
|
104
112
|
onClose?.();
|
|
105
113
|
};
|
|
106
114
|
|
|
115
|
+
const handleSubmit = () => {
|
|
116
|
+
onConfirm?.();
|
|
117
|
+
};
|
|
118
|
+
|
|
107
119
|
return (
|
|
108
120
|
<AlertDialog open={open} onOpenChange={handleOpenChange}>
|
|
109
121
|
{trigger && <AlertDialogTrigger asChild>{trigger}</AlertDialogTrigger>}
|
|
@@ -113,7 +125,9 @@ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
|
|
|
113
125
|
{title}
|
|
114
126
|
</AlertDialogTitle>
|
|
115
127
|
{description && (
|
|
116
|
-
<AlertDialogDescription
|
|
128
|
+
<AlertDialogDescription
|
|
129
|
+
data-testid={testId && `${testId}-description`}
|
|
130
|
+
>
|
|
117
131
|
{description}
|
|
118
132
|
</AlertDialogDescription>
|
|
119
133
|
)}
|
|
@@ -123,9 +137,8 @@ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
|
|
|
123
137
|
|
|
124
138
|
{requiresInput && (
|
|
125
139
|
<FormRoot
|
|
126
|
-
id={formId}
|
|
127
140
|
className="flex flex-col gap-4 w-full"
|
|
128
|
-
onSubmit={
|
|
141
|
+
onSubmit={handleSubmit}
|
|
129
142
|
>
|
|
130
143
|
<p className="typography-small1 text-text-contrast-max">
|
|
131
144
|
Type “{typeToConfirm}” to proceed.
|
|
@@ -141,6 +154,7 @@ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
|
|
|
141
154
|
fullwidth: true,
|
|
142
155
|
testId: testId && `${testId}-type-to-confirm-input`,
|
|
143
156
|
}}
|
|
157
|
+
disabled={isLoading}
|
|
144
158
|
/>
|
|
145
159
|
</FormRoot>
|
|
146
160
|
)}
|
|
@@ -151,18 +165,27 @@ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
|
|
|
151
165
|
className={cancelClassName}
|
|
152
166
|
data-testid={testId && `${testId}-cancel-button`}
|
|
153
167
|
onClick={handleCancel}
|
|
168
|
+
disabled={isLoading}
|
|
154
169
|
>
|
|
155
170
|
{cancelLabel}
|
|
156
171
|
</AlertDialogCancel>
|
|
157
172
|
)}
|
|
158
173
|
<AlertDialogAction
|
|
159
|
-
type="
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
174
|
+
type="button"
|
|
175
|
+
disabled={isLoading || (requiresInput && !isFormValid)}
|
|
176
|
+
onClick={(e) => {
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
|
|
179
|
+
if (requiresInput) {
|
|
180
|
+
methods.handleSubmit(handleSubmit)();
|
|
181
|
+
} else {
|
|
182
|
+
onConfirm?.();
|
|
183
|
+
}
|
|
184
|
+
}}
|
|
163
185
|
className={confirmClassName}
|
|
164
186
|
data-testid={testId && `${testId}-confirm-button`}
|
|
165
187
|
>
|
|
188
|
+
{isLoading && <Loading />}
|
|
166
189
|
{confirmLabel}
|
|
167
190
|
</AlertDialogAction>
|
|
168
191
|
</AlertDialogFooter>
|