@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.
@@ -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
- export const ConfirmDialog = ({ open, onOpenChange, title, description, children, confirmLabel = "Confirm", cancelLabel = "Cancel", onConfirm, onCancel, onClose, trigger, typeToConfirm, hideCancelButton = false, testId, cancelClassName, confirmClassName, }) => {
9
- const formId = React.useId();
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
- 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, { 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: {
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 })), _jsx(AlertDialogAction, { type: "submit", form: requiresInput ? formId : undefined, disabled: requiresInput && !isFormValid, onClick: requiresInput ? undefined : () => onConfirm === null || onConfirm === void 0 ? void 0 : onConfirm(), className: confirmClassName, "data-testid": testId && `${testId}-confirm-button`, children: confirmLabel })] })] })] }));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rovula/ui",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "main": "dist/cjs/bundle.js",
5
5
  "module": "dist/esm/bundle.js",
6
6
  "types": "dist/index.d.ts",
@@ -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 data-testid={testId && `${testId}-description`}>
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={() => onConfirm?.()}
141
+ onSubmit={handleSubmit}
129
142
  >
130
143
  <p className="typography-small1 text-text-contrast-max">
131
144
  Type &ldquo;{typeToConfirm}&rdquo; 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="submit"
160
- form={requiresInput ? formId : undefined}
161
- disabled={requiresInput && !isFormValid}
162
- onClick={requiresInput ? undefined : () => onConfirm?.()}
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>