@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
|
@@ -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>(
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as yup from "yup";
|
|
5
|
+
import {
|
|
6
|
+
AlertDialog,
|
|
7
|
+
AlertDialogContent,
|
|
8
|
+
AlertDialogHeader,
|
|
9
|
+
AlertDialogTitle,
|
|
10
|
+
AlertDialogDescription,
|
|
11
|
+
AlertDialogFooter,
|
|
12
|
+
AlertDialogAction,
|
|
13
|
+
AlertDialogCancel,
|
|
14
|
+
AlertDialogTrigger,
|
|
15
|
+
} from "@/components/AlertDialog/AlertDialog";
|
|
16
|
+
import { useControlledForm, Field } from "@/components/Form";
|
|
17
|
+
import { TextInput } from "@/components/TextInput/TextInput";
|
|
18
|
+
|
|
19
|
+
export type ConfirmDialogProps = {
|
|
20
|
+
open?: boolean;
|
|
21
|
+
onOpenChange?: (open: boolean) => void;
|
|
22
|
+
title: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
confirmLabel?: string;
|
|
25
|
+
cancelLabel?: string;
|
|
26
|
+
onConfirm?: () => void;
|
|
27
|
+
onCancel?: () => void;
|
|
28
|
+
/** Fires whenever the dialog closes, regardless of how (cancel button, overlay, Escape). */
|
|
29
|
+
onClose?: () => void;
|
|
30
|
+
trigger?: React.ReactNode;
|
|
31
|
+
/**
|
|
32
|
+
* When provided, the user must type this exact text before the confirm button is enabled.
|
|
33
|
+
* e.g. typeToConfirm="confirm" or typeToConfirm="delete"
|
|
34
|
+
*/
|
|
35
|
+
typeToConfirm?: string;
|
|
36
|
+
/**
|
|
37
|
+
* When true, hides the cancel button — useful for info/error alerts that only need one action.
|
|
38
|
+
*/
|
|
39
|
+
hideCancelButton?: boolean;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type ConfirmFormValues = { confirmInput: string };
|
|
43
|
+
|
|
44
|
+
export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
|
|
45
|
+
open,
|
|
46
|
+
onOpenChange,
|
|
47
|
+
title,
|
|
48
|
+
description,
|
|
49
|
+
confirmLabel = "Confirm",
|
|
50
|
+
cancelLabel = "Cancel",
|
|
51
|
+
onConfirm,
|
|
52
|
+
onCancel,
|
|
53
|
+
onClose,
|
|
54
|
+
trigger,
|
|
55
|
+
typeToConfirm,
|
|
56
|
+
hideCancelButton = false,
|
|
57
|
+
}) => {
|
|
58
|
+
const formId = React.useId();
|
|
59
|
+
const requiresInput = !!typeToConfirm;
|
|
60
|
+
|
|
61
|
+
const validationSchema = React.useMemo(
|
|
62
|
+
() =>
|
|
63
|
+
yup.object({
|
|
64
|
+
confirmInput: yup
|
|
65
|
+
.string()
|
|
66
|
+
.required("This field is required.")
|
|
67
|
+
.test(
|
|
68
|
+
"type-to-confirm",
|
|
69
|
+
`Please type '${typeToConfirm}' to proceed`,
|
|
70
|
+
(value) => value === typeToConfirm,
|
|
71
|
+
),
|
|
72
|
+
}),
|
|
73
|
+
[typeToConfirm],
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const { methods, FormRoot } = useControlledForm<ConfirmFormValues>({
|
|
77
|
+
defaultValues: { confirmInput: "" },
|
|
78
|
+
validationSchema,
|
|
79
|
+
mode: "onTouched",
|
|
80
|
+
reValidateMode: "onChange",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const isFormValid = methods.formState.isValid;
|
|
84
|
+
|
|
85
|
+
const handleOpenChange = (nextOpen: boolean) => {
|
|
86
|
+
if (!nextOpen) {
|
|
87
|
+
methods.reset();
|
|
88
|
+
onClose?.();
|
|
89
|
+
}
|
|
90
|
+
onOpenChange?.(nextOpen);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const handleCancel = () => {
|
|
94
|
+
methods.reset();
|
|
95
|
+
onCancel?.();
|
|
96
|
+
onClose?.();
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<AlertDialog open={open} onOpenChange={handleOpenChange}>
|
|
101
|
+
{trigger && <AlertDialogTrigger asChild>{trigger}</AlertDialogTrigger>}
|
|
102
|
+
<AlertDialogContent>
|
|
103
|
+
<AlertDialogHeader>
|
|
104
|
+
<AlertDialogTitle>{title}</AlertDialogTitle>
|
|
105
|
+
{description && (
|
|
106
|
+
<AlertDialogDescription>{description}</AlertDialogDescription>
|
|
107
|
+
)}
|
|
108
|
+
</AlertDialogHeader>
|
|
109
|
+
|
|
110
|
+
{requiresInput && (
|
|
111
|
+
<FormRoot
|
|
112
|
+
id={formId}
|
|
113
|
+
className="flex flex-col gap-4 w-full"
|
|
114
|
+
onSubmit={() => onConfirm?.()}
|
|
115
|
+
>
|
|
116
|
+
<p className="typography-small1 text-text-contrast-max">
|
|
117
|
+
Type “{typeToConfirm}” to proceed.
|
|
118
|
+
</p>
|
|
119
|
+
<Field<ConfirmFormValues, "confirmInput">
|
|
120
|
+
name="confirmInput"
|
|
121
|
+
component={TextInput}
|
|
122
|
+
componentProps={{
|
|
123
|
+
label: "Type to confirm",
|
|
124
|
+
required: true,
|
|
125
|
+
hasClearIcon: true,
|
|
126
|
+
keepFooterSpace: true,
|
|
127
|
+
fullwidth: true,
|
|
128
|
+
}}
|
|
129
|
+
/>
|
|
130
|
+
</FormRoot>
|
|
131
|
+
)}
|
|
132
|
+
|
|
133
|
+
<AlertDialogFooter>
|
|
134
|
+
{!hideCancelButton && (
|
|
135
|
+
<AlertDialogCancel onClick={handleCancel}>
|
|
136
|
+
{cancelLabel}
|
|
137
|
+
</AlertDialogCancel>
|
|
138
|
+
)}
|
|
139
|
+
<AlertDialogAction
|
|
140
|
+
type="submit"
|
|
141
|
+
form={requiresInput ? formId : undefined}
|
|
142
|
+
disabled={requiresInput && !isFormValid}
|
|
143
|
+
onClick={requiresInput ? undefined : () => onConfirm?.()}
|
|
144
|
+
>
|
|
145
|
+
{confirmLabel}
|
|
146
|
+
</AlertDialogAction>
|
|
147
|
+
</AlertDialogFooter>
|
|
148
|
+
</AlertDialogContent>
|
|
149
|
+
</AlertDialog>
|
|
150
|
+
);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
ConfirmDialog.displayName = "ConfirmDialog";
|