@axzydev/axzy_ui_system 1.2.16 → 1.3.1
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/index.cjs +3349 -1457
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.cts +719 -54
- package/dist/index.d.ts +719 -54
- package/dist/index.js +3283 -1387
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/App.tsx +32 -0
- package/src/components/atoms/badget/badget.tsx +2 -4
- package/src/components/atoms/chip/chip.doc.mdx +47 -0
- package/src/components/atoms/chip/chip.props.ts +32 -0
- package/src/components/atoms/chip/chip.stories.tsx +64 -0
- package/src/components/atoms/chip/chip.tsx +112 -0
- package/src/components/atoms/grid/grid.tsx +1 -2
- package/src/components/molecules/accordion/accordion.doc.mdx +47 -0
- package/src/components/molecules/accordion/accordion.props.ts +35 -0
- package/src/components/molecules/accordion/accordion.stories.tsx +68 -0
- package/src/components/molecules/accordion/accordion.tsx +123 -0
- package/src/components/molecules/calendar/calendar.doc.mdx +2 -1
- package/src/components/molecules/calendar/calendar.tsx +102 -24
- package/src/components/molecules/chip-input/chip-input.doc.mdx +44 -0
- package/src/components/molecules/chip-input/chip-input.props.ts +42 -0
- package/src/components/molecules/chip-input/chip-input.stories.tsx +52 -0
- package/src/components/molecules/chip-input/chip-input.tsx +205 -0
- package/src/components/molecules/date-picker/datePicker.tsx +45 -56
- package/src/components/molecules/dropdown-menu/dropdown-menu.doc.mdx +46 -0
- package/src/components/molecules/dropdown-menu/dropdown-menu.props.ts +41 -0
- package/src/components/molecules/dropdown-menu/dropdown-menu.stories.tsx +41 -0
- package/src/components/molecules/dropdown-menu/dropdown-menu.tsx +219 -0
- package/src/components/molecules/field/field.doc.mdx +43 -0
- package/src/components/molecules/field/field.props.ts +23 -0
- package/src/components/molecules/field/field.stories.tsx +42 -0
- package/src/components/molecules/field/field.tsx +54 -0
- package/src/components/molecules/masked-input/masked-input.doc.mdx +78 -0
- package/src/components/molecules/masked-input/masked-input.props.ts +58 -0
- package/src/components/molecules/masked-input/masked-input.stories.tsx +126 -0
- package/src/components/molecules/masked-input/masked-input.tsx +244 -0
- package/src/components/molecules/multi-select/multi-select.doc.mdx +55 -0
- package/src/components/molecules/multi-select/multi-select.props.ts +65 -0
- package/src/components/molecules/multi-select/multi-select.stories.tsx +59 -0
- package/src/components/molecules/multi-select/multi-select.tsx +386 -0
- package/src/components/molecules/search-select/search-select.doc.mdx +5 -0
- package/src/components/molecules/search-select/search-select.props.ts +10 -1
- package/src/components/molecules/search-select/search-select.stories.tsx +18 -0
- package/src/components/molecules/search-select/search-select.tsx +102 -28
- package/src/components/molecules/time-picker/timePicker.tsx +14 -26
- package/src/components/molecules/wysiwyg/wysiwyg.props.ts +51 -0
- package/src/components/molecules/wysiwyg/wysiwyg.stories.tsx +95 -0
- package/src/components/molecules/wysiwyg/wysiwyg.tsx +609 -0
- package/src/components/organisms/form-builder/fieldRenderer.tsx +144 -27
- package/src/components/organisms/form-builder/formBuilder.context.tsx +77 -15
- package/src/components/organisms/form-builder/formBuilder.doc.mdx +213 -86
- package/src/components/organisms/form-builder/formBuilder.props.ts +118 -36
- package/src/components/organisms/form-builder/formBuilder.stories.tsx +694 -96
- package/src/components/organisms/form-builder/formBuilder.tsx +152 -69
- package/src/components/organisms/form-builder/useFormBuilder.ts +59 -21
- package/src/hooks/useFloatingPanel.ts +100 -0
- package/src/index.ts +26 -4
- package/src/showcases/DataShowcases.tsx +76 -0
- package/src/showcases/FormShowcases.tsx +574 -10
- package/src/showcases/HomeShowcase.tsx +6 -6
- package/src/showcases/NavigationShowcases.tsx +72 -2
- package/src/showcases/StructureShowcases.tsx +64 -0
- package/src/types/chip.types.ts +13 -0
|
@@ -4,7 +4,17 @@ import { Formik } from "formik";
|
|
|
4
4
|
import * as Yup from "yup";
|
|
5
5
|
import { useState } from "react";
|
|
6
6
|
import { FieldConfig, FieldConfigV2 } from "@/types/field.types";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
FaUser,
|
|
9
|
+
FaEnvelope,
|
|
10
|
+
FaSearch,
|
|
11
|
+
FaLock,
|
|
12
|
+
FaCalculator,
|
|
13
|
+
FaCommentDots,
|
|
14
|
+
FaCheckCircle,
|
|
15
|
+
} from "react-icons/fa";
|
|
16
|
+
import { expect, fn, userEvent } from "storybook/test";
|
|
17
|
+
import { useFormBuilder } from "./useFormBuilder";
|
|
8
18
|
|
|
9
19
|
const meta: Meta<typeof ITFormBuilder> = {
|
|
10
20
|
title: "Components/Form Elements/ITFormBuilder",
|
|
@@ -13,31 +23,108 @@ const meta: Meta<typeof ITFormBuilder> = {
|
|
|
13
23
|
layout: "centered",
|
|
14
24
|
docs: {
|
|
15
25
|
description: {
|
|
16
|
-
component:
|
|
26
|
+
component:
|
|
27
|
+
"Dynamic form builder using Formik and Yup configuration. Supports Legacy V1 and the powerful Enterprise V2 Grid mapping, validating, calculating, and conditional logic.",
|
|
17
28
|
},
|
|
18
29
|
},
|
|
19
30
|
},
|
|
20
31
|
};
|
|
21
|
-
|
|
22
32
|
export default meta;
|
|
23
33
|
type Story = StoryObj<typeof ITFormBuilder>;
|
|
24
34
|
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
36
|
+
// Helpers shared by the test stories
|
|
37
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/** Controlled wrapper that drives the builder with local React state. */
|
|
40
|
+
const ControlledForm = ({
|
|
41
|
+
children,
|
|
42
|
+
initialValues,
|
|
43
|
+
}: {
|
|
44
|
+
children: (props: {
|
|
45
|
+
values: Record<string, any>;
|
|
46
|
+
setValues: React.Dispatch<React.SetStateAction<Record<string, any>>>;
|
|
47
|
+
setFieldValue: (field: string, value: any) => Promise<void>;
|
|
48
|
+
handleChange: (e: any) => void;
|
|
49
|
+
handleBlur: (e: any) => void;
|
|
50
|
+
touched: Record<string, boolean>;
|
|
51
|
+
errors: Record<string, string>;
|
|
52
|
+
}) => React.ReactNode;
|
|
53
|
+
initialValues: Record<string, any>;
|
|
54
|
+
}) => {
|
|
55
|
+
const [values, setValues] = useState<Record<string, any>>(initialValues);
|
|
56
|
+
const [touched, setTouched] = useState<Record<string, boolean>>({});
|
|
57
|
+
const [errors] = useState<Record<string, string>>({});
|
|
58
|
+
|
|
59
|
+
const handleChange = (e: any) => {
|
|
60
|
+
const { name, value, type, checked } = e.target ?? {};
|
|
61
|
+
setValues((prev) => ({
|
|
62
|
+
...prev,
|
|
63
|
+
[name]: type === "checkbox" ? checked : value,
|
|
64
|
+
}));
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const handleBlur = (e: any) => {
|
|
68
|
+
const { name } = e.target ?? {};
|
|
69
|
+
if (name) setTouched((prev) => ({ ...prev, [name]: true }));
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const setFieldValue = async (field: string, value: any) => {
|
|
73
|
+
setValues((prev) => ({ ...prev, [field]: value }));
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<>
|
|
78
|
+
{children({
|
|
79
|
+
values,
|
|
80
|
+
setValues,
|
|
81
|
+
setFieldValue,
|
|
82
|
+
handleChange,
|
|
83
|
+
handleBlur,
|
|
84
|
+
touched,
|
|
85
|
+
errors,
|
|
86
|
+
})}
|
|
87
|
+
{/* Surface state so test play functions and humans can read it. */}
|
|
88
|
+
<pre
|
|
89
|
+
data-testid="form-state"
|
|
90
|
+
style={{
|
|
91
|
+
marginTop: 16,
|
|
92
|
+
padding: 8,
|
|
93
|
+
background: "#0f172a",
|
|
94
|
+
color: "#a7f3d0",
|
|
95
|
+
fontSize: 11,
|
|
96
|
+
borderRadius: 8,
|
|
97
|
+
maxWidth: 480,
|
|
98
|
+
overflow: "auto",
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
{JSON.stringify({ values, touched, errors }, null, 2)}
|
|
102
|
+
</pre>
|
|
103
|
+
</>
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
108
|
+
// Showcase stories (kept as-is for the Storybook gallery)
|
|
109
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
const FormWrapper = ({
|
|
112
|
+
children,
|
|
113
|
+
initialValues = {},
|
|
29
114
|
validationSchema,
|
|
30
|
-
widthClass = "w-[800px]"
|
|
31
|
-
}: {
|
|
32
|
-
children: (props: any) => React.ReactNode
|
|
33
|
-
initialValues?: any
|
|
34
|
-
validationSchema?: any
|
|
35
|
-
widthClass?: string
|
|
115
|
+
widthClass = "w-[800px]",
|
|
116
|
+
}: {
|
|
117
|
+
children: (props: any) => React.ReactNode;
|
|
118
|
+
initialValues?: any;
|
|
119
|
+
validationSchema?: any;
|
|
120
|
+
widthClass?: string;
|
|
36
121
|
}) => {
|
|
37
122
|
const [submittedValues, setSubmittedValues] = useState<any>(null);
|
|
38
123
|
|
|
39
124
|
return (
|
|
40
|
-
<div
|
|
125
|
+
<div
|
|
126
|
+
className={`p-6 bg-white rounded-xl shadow-sm border border-gray-100 max-w-full ${widthClass}`}
|
|
127
|
+
>
|
|
41
128
|
<Formik
|
|
42
129
|
initialValues={initialValues}
|
|
43
130
|
validationSchema={validationSchema}
|
|
@@ -49,7 +136,7 @@ const FormWrapper = ({
|
|
|
49
136
|
{(formikProps) => (
|
|
50
137
|
<form onSubmit={formikProps.handleSubmit}>
|
|
51
138
|
{children(formikProps)}
|
|
52
|
-
|
|
139
|
+
|
|
53
140
|
<div className="mt-6 flex items-center justify-end gap-3 pt-4 border-t border-gray-100">
|
|
54
141
|
<button
|
|
55
142
|
type="button"
|
|
@@ -68,7 +155,9 @@ const FormWrapper = ({
|
|
|
68
155
|
|
|
69
156
|
{submittedValues && (
|
|
70
157
|
<div className="mt-4 p-3 bg-gray-50 rounded-lg text-xs font-mono overflow-auto max-h-40">
|
|
71
|
-
<p className="font-semibold text-gray-500 mb-1">
|
|
158
|
+
<p className="font-semibold text-gray-500 mb-1">
|
|
159
|
+
Payload Generado:
|
|
160
|
+
</p>
|
|
72
161
|
<pre>{JSON.stringify(submittedValues, null, 2)}</pre>
|
|
73
162
|
</div>
|
|
74
163
|
)}
|
|
@@ -79,9 +168,6 @@ const FormWrapper = ({
|
|
|
79
168
|
);
|
|
80
169
|
};
|
|
81
170
|
|
|
82
|
-
|
|
83
|
-
// --- Enterprise V2 Showcase ---
|
|
84
|
-
|
|
85
171
|
const showcaseConfig: FieldConfigV2[] = [
|
|
86
172
|
{
|
|
87
173
|
name: "username",
|
|
@@ -98,8 +184,8 @@ const showcaseConfig: FieldConfigV2[] = [
|
|
|
98
184
|
type: "text",
|
|
99
185
|
placeholder: "Ingresa código...",
|
|
100
186
|
rightIcon: (
|
|
101
|
-
<button
|
|
102
|
-
type="button"
|
|
187
|
+
<button
|
|
188
|
+
type="button"
|
|
103
189
|
onClick={() => alert("¡Disparando búsqueda de código promocional!")}
|
|
104
190
|
className="text-primary-600 hover:text-primary-800 transition px-2"
|
|
105
191
|
>
|
|
@@ -127,34 +213,34 @@ const showcaseConfig: FieldConfigV2[] = [
|
|
|
127
213
|
column: { sm: 12, md: 6 },
|
|
128
214
|
},
|
|
129
215
|
{
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
216
|
+
name: "subtotal",
|
|
217
|
+
label: "Subtotal (Dispara OnChange)",
|
|
218
|
+
type: "number",
|
|
219
|
+
required: true,
|
|
220
|
+
currencyFormat: true,
|
|
221
|
+
leftIcon: <FaCalculator className="text-gray-400" />,
|
|
222
|
+
column: { sm: 12, md: 4 },
|
|
223
|
+
onChangeAction: (val, { setFieldValue }) => {
|
|
224
|
+
const num = parseFloat(val) || 0;
|
|
225
|
+
setFieldValue("iva", (num * 0.16).toFixed(2));
|
|
226
|
+
setFieldValue("total", (num * 1.16).toFixed(2));
|
|
227
|
+
},
|
|
142
228
|
},
|
|
143
229
|
{
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
230
|
+
name: "iva",
|
|
231
|
+
label: "IVA (Read-Only)",
|
|
232
|
+
type: "number",
|
|
233
|
+
currencyFormat: true,
|
|
234
|
+
readOnly: true,
|
|
235
|
+
column: { sm: 12, md: 4 },
|
|
150
236
|
},
|
|
151
237
|
{
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
238
|
+
name: "total",
|
|
239
|
+
label: "Total (Disabled)",
|
|
240
|
+
type: "number",
|
|
241
|
+
currencyFormat: true,
|
|
242
|
+
disabled: true,
|
|
243
|
+
column: { sm: 12, md: 4 },
|
|
158
244
|
},
|
|
159
245
|
{
|
|
160
246
|
name: "country",
|
|
@@ -165,7 +251,7 @@ const showcaseConfig: FieldConfigV2[] = [
|
|
|
165
251
|
{ value: "", label: "Selecciona país..." },
|
|
166
252
|
{ value: "MX", label: "México" },
|
|
167
253
|
{ value: "US", label: "Estados Unidos" },
|
|
168
|
-
{ value: "OTHER", label: "Otro" }
|
|
254
|
+
{ value: "OTHER", label: "Otro" },
|
|
169
255
|
],
|
|
170
256
|
column: { sm: 12, md: 6 },
|
|
171
257
|
},
|
|
@@ -173,8 +259,8 @@ const showcaseConfig: FieldConfigV2[] = [
|
|
|
173
259
|
name: "rfc",
|
|
174
260
|
label: "RFC de México (Renderización Condicional)",
|
|
175
261
|
type: "text",
|
|
176
|
-
dependsOn: ["country"],
|
|
177
|
-
renderWhen: (vals) => vals.country === "MX",
|
|
262
|
+
dependsOn: ["country"],
|
|
263
|
+
renderWhen: (vals) => vals.country === "MX",
|
|
178
264
|
required: true,
|
|
179
265
|
showHintLength: true,
|
|
180
266
|
maxLength: 13,
|
|
@@ -190,81 +276,95 @@ const showcaseConfig: FieldConfigV2[] = [
|
|
|
190
276
|
{
|
|
191
277
|
name: "time",
|
|
192
278
|
label: "Selector de Hora",
|
|
193
|
-
type: "time" as any,
|
|
279
|
+
type: "time" as any,
|
|
194
280
|
required: true,
|
|
195
281
|
column: { sm: 12, md: 4 },
|
|
196
282
|
},
|
|
197
283
|
{
|
|
198
284
|
name: "comments",
|
|
199
285
|
label: "Comentarios (Textarea + Límite)",
|
|
200
|
-
type: "textarea" as any,
|
|
286
|
+
type: "textarea" as any,
|
|
201
287
|
rows: 3,
|
|
202
288
|
showHintLength: true,
|
|
203
289
|
maxLength: 200,
|
|
204
290
|
leftIcon: <FaCommentDots className="text-gray-400" />,
|
|
205
291
|
column: { sm: 12, md: 4 },
|
|
206
|
-
}
|
|
292
|
+
},
|
|
207
293
|
];
|
|
208
294
|
|
|
209
295
|
const showcaseSchema = Yup.object().shape({
|
|
210
296
|
username: Yup.string().required("El nombre de usuario es requerido"),
|
|
211
297
|
promoCode: Yup.string(),
|
|
212
|
-
email: Yup.string()
|
|
213
|
-
|
|
214
|
-
|
|
298
|
+
email: Yup.string()
|
|
299
|
+
.email("Formato de correo inválido")
|
|
300
|
+
.required("El correo es requerido"),
|
|
301
|
+
password: Yup.string()
|
|
302
|
+
.min(8, "Mínimo 8 caracteres")
|
|
303
|
+
.required("La contraseña es requerida"),
|
|
304
|
+
subtotal: Yup.number()
|
|
305
|
+
.typeError("Debe ser un número")
|
|
306
|
+
.required("Requerido")
|
|
307
|
+
.min(1, "Mayor a 0"),
|
|
215
308
|
country: Yup.string().required("Selecciona un país"),
|
|
216
|
-
rfc: Yup.string().when(
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
309
|
+
rfc: Yup.string().when("country", {
|
|
310
|
+
is: "MX",
|
|
311
|
+
then: (schema) =>
|
|
312
|
+
schema
|
|
313
|
+
.length(13, "Debe tener 13 caracteres exactos")
|
|
314
|
+
.required("El RFC es obligatorio para MX"),
|
|
315
|
+
otherwise: (schema) => schema.notRequired(),
|
|
220
316
|
}),
|
|
221
317
|
date: Yup.date().required("Fecha requerida"),
|
|
222
318
|
time: Yup.string().required("Hora requerida"),
|
|
223
|
-
comments: Yup.string().max(200, "Máximo 200 caracteres")
|
|
319
|
+
comments: Yup.string().max(200, "Máximo 200 caracteres"),
|
|
224
320
|
});
|
|
225
321
|
|
|
226
322
|
export const FullFeaturesShowcaseV2: Story = {
|
|
227
323
|
render: () => (
|
|
228
|
-
<FormWrapper
|
|
324
|
+
<FormWrapper
|
|
229
325
|
initialValues={{
|
|
230
|
-
username:
|
|
231
|
-
promoCode:
|
|
232
|
-
email:
|
|
233
|
-
password:
|
|
234
|
-
subtotal:
|
|
235
|
-
iva:
|
|
236
|
-
total:
|
|
237
|
-
country:
|
|
238
|
-
rfc:
|
|
326
|
+
username: "",
|
|
327
|
+
promoCode: "",
|
|
328
|
+
email: "",
|
|
329
|
+
password: "",
|
|
330
|
+
subtotal: "",
|
|
331
|
+
iva: "",
|
|
332
|
+
total: "",
|
|
333
|
+
country: "",
|
|
334
|
+
rfc: "",
|
|
239
335
|
date: null,
|
|
240
|
-
time:
|
|
241
|
-
comments:
|
|
336
|
+
time: "",
|
|
337
|
+
comments: "",
|
|
242
338
|
}}
|
|
243
339
|
validationSchema={showcaseSchema}
|
|
244
340
|
widthClass="w-[1024px]"
|
|
245
341
|
>
|
|
246
342
|
{(formikProps) => (
|
|
247
343
|
<div className="space-y-4">
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
344
|
+
<h3 className="text-xl font-bold text-gray-900 mb-2">
|
|
345
|
+
ITFormBuilder V2 Completo
|
|
346
|
+
</h3>
|
|
347
|
+
<p className="text-sm text-gray-500 mb-6">
|
|
348
|
+
Prueba los cálculos en tiempo real en "Subtotal", mira el
|
|
349
|
+
comportamiento `disabled` vs `readonly`, interactúa con íconos e
|
|
350
|
+
ingresa el país "México" para renderizar dinámicamente un campo.
|
|
351
|
+
</p>
|
|
352
|
+
<ITFormBuilder
|
|
353
|
+
config={showcaseConfig as any}
|
|
354
|
+
columns={12}
|
|
355
|
+
values={formikProps.values}
|
|
356
|
+
handleChange={formikProps.handleChange}
|
|
357
|
+
handleBlur={formikProps.handleBlur}
|
|
358
|
+
touched={formikProps.touched}
|
|
359
|
+
errors={formikProps.errors}
|
|
360
|
+
setFieldValue={formikProps.setFieldValue}
|
|
361
|
+
/>
|
|
260
362
|
</div>
|
|
261
363
|
)}
|
|
262
364
|
</FormWrapper>
|
|
263
365
|
),
|
|
264
366
|
};
|
|
265
367
|
|
|
266
|
-
// --- Legacy V1 Login Example ---
|
|
267
|
-
|
|
268
368
|
const loginFields: FieldConfig[] = [
|
|
269
369
|
{
|
|
270
370
|
name: "email",
|
|
@@ -291,26 +391,524 @@ const loginSchema = Yup.object().shape({
|
|
|
291
391
|
|
|
292
392
|
export const LegacyV1LoginExample: Story = {
|
|
293
393
|
render: () => (
|
|
294
|
-
<FormWrapper
|
|
394
|
+
<FormWrapper
|
|
295
395
|
initialValues={{ email: "", password: "" }}
|
|
296
396
|
validationSchema={loginSchema}
|
|
297
397
|
widthClass="w-[500px]"
|
|
298
398
|
>
|
|
299
399
|
{(formikProps) => (
|
|
300
400
|
<div className="space-y-4">
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
401
|
+
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
|
402
|
+
Legacy Login (V1 Config Backward Compatibility)
|
|
403
|
+
</h3>
|
|
404
|
+
<ITFormBuilder
|
|
405
|
+
fields={loginFields}
|
|
406
|
+
columns={12}
|
|
407
|
+
values={formikProps.values}
|
|
408
|
+
handleChange={formikProps.handleChange}
|
|
409
|
+
handleBlur={formikProps.handleBlur}
|
|
410
|
+
touched={formikProps.touched}
|
|
411
|
+
errors={formikProps.errors}
|
|
412
|
+
/>
|
|
311
413
|
</div>
|
|
312
414
|
)}
|
|
313
415
|
</FormWrapper>
|
|
314
416
|
),
|
|
315
417
|
};
|
|
316
418
|
|
|
419
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
420
|
+
// Test stories (interaction tests, run via `pnpm exec vitest run`)
|
|
421
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
422
|
+
|
|
423
|
+
/** Renders a V2 config of three fields and asserts they are present in the DOM. */
|
|
424
|
+
export const TestRendersV2Basic: Story = {
|
|
425
|
+
name: "Test: renders V2 basic fields",
|
|
426
|
+
render: () => (
|
|
427
|
+
<ControlledForm initialValues={{ name: "", role: "", dob: null }}>
|
|
428
|
+
{({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
|
|
429
|
+
<ITFormBuilder
|
|
430
|
+
config={[
|
|
431
|
+
{ name: "name", label: "Name", type: "text", required: true, column: 6 },
|
|
432
|
+
{
|
|
433
|
+
name: "role",
|
|
434
|
+
label: "Role",
|
|
435
|
+
type: "select",
|
|
436
|
+
required: true,
|
|
437
|
+
column: 6,
|
|
438
|
+
options: [
|
|
439
|
+
{ value: "admin", label: "Admin" },
|
|
440
|
+
{ value: "user", label: "User" },
|
|
441
|
+
],
|
|
442
|
+
},
|
|
443
|
+
{ name: "dob", label: "Date of Birth", type: "date", column: 12 },
|
|
444
|
+
]}
|
|
445
|
+
columns={12}
|
|
446
|
+
values={values}
|
|
447
|
+
handleChange={handleChange}
|
|
448
|
+
handleBlur={handleBlur}
|
|
449
|
+
touched={touched}
|
|
450
|
+
errors={errors}
|
|
451
|
+
setFieldValue={setFieldValue}
|
|
452
|
+
/>
|
|
453
|
+
)}
|
|
454
|
+
</ControlledForm>
|
|
455
|
+
),
|
|
456
|
+
play: async ({ canvas }) => {
|
|
457
|
+
await expect(canvas.getByLabelText(/Name/)).toBeInTheDocument();
|
|
458
|
+
await expect(canvas.getByLabelText(/Role/)).toBeInTheDocument();
|
|
459
|
+
await expect(canvas.getByLabelText(/Date of Birth/)).toBeInTheDocument();
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
/** Typing in the Name input bubbles through `handleChange` into the state. */
|
|
464
|
+
export const TestTypingUpdatesState: Story = {
|
|
465
|
+
name: "Test: typing updates state via handleChange",
|
|
466
|
+
render: () => (
|
|
467
|
+
<ControlledForm initialValues={{ email: "" }}>
|
|
468
|
+
{({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
|
|
469
|
+
<ITFormBuilder
|
|
470
|
+
config={[
|
|
471
|
+
{ name: "email", label: "Email", type: "email", required: true },
|
|
472
|
+
]}
|
|
473
|
+
values={values}
|
|
474
|
+
handleChange={handleChange}
|
|
475
|
+
handleBlur={handleBlur}
|
|
476
|
+
touched={touched}
|
|
477
|
+
errors={errors}
|
|
478
|
+
setFieldValue={setFieldValue}
|
|
479
|
+
/>
|
|
480
|
+
)}
|
|
481
|
+
</ControlledForm>
|
|
482
|
+
),
|
|
483
|
+
play: async ({ canvas }) => {
|
|
484
|
+
const input = canvas.getByLabelText(/Email/);
|
|
485
|
+
await userEvent.type(input, "user@example.com");
|
|
486
|
+
await expect(input).toHaveValue("user@example.com");
|
|
487
|
+
|
|
488
|
+
const stateDump = canvas.getByTestId("form-state");
|
|
489
|
+
await expect(stateDump.textContent).toContain('"email": "user@example.com"');
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
/** RFC field appears only when country === "MX"; switching away hides it. */
|
|
494
|
+
export const TestConditionalRenderWhen: Story = {
|
|
495
|
+
name: "Test: renderWhen shows / hides dependent field",
|
|
496
|
+
render: () => (
|
|
497
|
+
<ControlledForm initialValues={{ country: "", rfc: "" }}>
|
|
498
|
+
{({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
|
|
499
|
+
<ITFormBuilder
|
|
500
|
+
config={[
|
|
501
|
+
{
|
|
502
|
+
name: "country",
|
|
503
|
+
label: "Country",
|
|
504
|
+
type: "select",
|
|
505
|
+
required: true,
|
|
506
|
+
options: [
|
|
507
|
+
{ value: "", label: "Select..." },
|
|
508
|
+
{ value: "MX", label: "México" },
|
|
509
|
+
{ value: "US", label: "USA" },
|
|
510
|
+
],
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
name: "rfc",
|
|
514
|
+
label: "RFC",
|
|
515
|
+
type: "text",
|
|
516
|
+
dependsOn: ["country"],
|
|
517
|
+
renderWhen: (v) => v.country === "MX",
|
|
518
|
+
required: true,
|
|
519
|
+
},
|
|
520
|
+
]}
|
|
521
|
+
values={values}
|
|
522
|
+
handleChange={handleChange}
|
|
523
|
+
handleBlur={handleBlur}
|
|
524
|
+
touched={touched}
|
|
525
|
+
errors={errors}
|
|
526
|
+
setFieldValue={setFieldValue}
|
|
527
|
+
/>
|
|
528
|
+
)}
|
|
529
|
+
</ControlledForm>
|
|
530
|
+
),
|
|
531
|
+
play: async ({ canvas }) => {
|
|
532
|
+
// Initially hidden.
|
|
533
|
+
await expect(canvas.queryByLabelText(/RFC/)).toBeNull();
|
|
534
|
+
|
|
535
|
+
// Pick Mexico → appears.
|
|
536
|
+
const country = canvas.getByLabelText(/Country/);
|
|
537
|
+
await userEvent.selectOptions(country, "MX");
|
|
538
|
+
await expect(canvas.getByLabelText(/RFC/)).toBeInTheDocument();
|
|
539
|
+
|
|
540
|
+
// Switch to USA → hidden again.
|
|
541
|
+
await userEvent.selectOptions(country, "US");
|
|
542
|
+
await expect(canvas.queryByLabelText(/RFC/)).toBeNull();
|
|
543
|
+
},
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
/** Typing in subtotal triggers `onChangeAction` to recompute total. */
|
|
547
|
+
export const TestOnChangeActionComputesTotal: Story = {
|
|
548
|
+
name: "Test: onChangeAction updates derived total",
|
|
549
|
+
render: () => (
|
|
550
|
+
<ControlledForm initialValues={{ subtotal: "", total: "" }}>
|
|
551
|
+
{({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
|
|
552
|
+
<ITFormBuilder
|
|
553
|
+
config={[
|
|
554
|
+
{
|
|
555
|
+
name: "subtotal",
|
|
556
|
+
label: "Subtotal",
|
|
557
|
+
type: "number",
|
|
558
|
+
currencyFormat: true,
|
|
559
|
+
onChangeAction: (val, ctx) => {
|
|
560
|
+
const n = parseFloat(String(val).replace(/,/g, "")) || 0;
|
|
561
|
+
ctx.setFieldValue("total", (n * 1.16).toFixed(2));
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
{ name: "total", label: "Total", type: "number", disabled: true },
|
|
565
|
+
]}
|
|
566
|
+
values={values}
|
|
567
|
+
handleChange={handleChange}
|
|
568
|
+
handleBlur={handleBlur}
|
|
569
|
+
touched={touched}
|
|
570
|
+
errors={errors}
|
|
571
|
+
setFieldValue={setFieldValue}
|
|
572
|
+
/>
|
|
573
|
+
)}
|
|
574
|
+
</ControlledForm>
|
|
575
|
+
),
|
|
576
|
+
play: async ({ canvas }) => {
|
|
577
|
+
const subtotal = canvas.getByLabelText(/Subtotal/);
|
|
578
|
+
await userEvent.type(subtotal, "100");
|
|
579
|
+
|
|
580
|
+
// Total should be derived to "116.50".
|
|
581
|
+
const total = canvas.getByLabelText(/Total/) as HTMLInputElement;
|
|
582
|
+
await expect(total.value).toBe("116.50");
|
|
583
|
+
},
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
/** Disabled field cannot receive input; programmatic value still renders. */
|
|
587
|
+
export const TestDisabledFieldBlocksInput: Story = {
|
|
588
|
+
name: "Test: disabled field blocks typing",
|
|
589
|
+
render: () => (
|
|
590
|
+
<ControlledForm initialValues={{ a: "", b: "fixed" }}>
|
|
591
|
+
{({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
|
|
592
|
+
<ITFormBuilder
|
|
593
|
+
config={[
|
|
594
|
+
{ name: "a", label: "Active", type: "text" },
|
|
595
|
+
{ name: "b", label: "Locked", type: "text", disabled: true },
|
|
596
|
+
]}
|
|
597
|
+
values={values}
|
|
598
|
+
handleChange={handleChange}
|
|
599
|
+
handleBlur={handleBlur}
|
|
600
|
+
touched={touched}
|
|
601
|
+
errors={errors}
|
|
602
|
+
setFieldValue={setFieldValue}
|
|
603
|
+
/>
|
|
604
|
+
)}
|
|
605
|
+
</ControlledForm>
|
|
606
|
+
),
|
|
607
|
+
play: async ({ canvas }) => {
|
|
608
|
+
const active = canvas.getByLabelText(/Active/);
|
|
609
|
+
const locked = canvas.getByLabelText(/Locked/) as HTMLInputElement;
|
|
610
|
+
|
|
611
|
+
await expect(locked).toBeDisabled();
|
|
612
|
+
await expect(locked.value).toBe("fixed");
|
|
613
|
+
|
|
614
|
+
await userEvent.type(active, "hello");
|
|
615
|
+
await userEvent.type(locked, "X"); // should be a no-op
|
|
616
|
+
await expect(locked.value).toBe("fixed");
|
|
617
|
+
await expect(active).toHaveValue("hello");
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
/** Required flag becomes true once another field exceeds a threshold. */
|
|
622
|
+
export const TestDynamicRequired: Story = {
|
|
623
|
+
name: "Test: dynamicProps toggles required at runtime",
|
|
624
|
+
render: () => (
|
|
625
|
+
<ControlledForm initialValues={{ amount: 0, justification: "" }}>
|
|
626
|
+
{({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
|
|
627
|
+
<ITFormBuilder
|
|
628
|
+
config={[
|
|
629
|
+
{
|
|
630
|
+
name: "amount",
|
|
631
|
+
label: "Amount",
|
|
632
|
+
type: "number",
|
|
633
|
+
defaultValue: 0,
|
|
634
|
+
dependsOn: ["amount"],
|
|
635
|
+
dynamicProps: (v) =>
|
|
636
|
+
Number(v.amount) > 50 ? { required: true } : { required: false },
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
name: "justification",
|
|
640
|
+
label: "Justification",
|
|
641
|
+
type: "text",
|
|
642
|
+
dependsOn: ["amount"],
|
|
643
|
+
dynamicProps: (v) =>
|
|
644
|
+
Number(v.amount) > 50 ? { required: true } : { required: false },
|
|
645
|
+
},
|
|
646
|
+
]}
|
|
647
|
+
values={values}
|
|
648
|
+
handleChange={handleChange}
|
|
649
|
+
handleBlur={handleBlur}
|
|
650
|
+
touched={touched}
|
|
651
|
+
errors={errors}
|
|
652
|
+
setFieldValue={setFieldValue}
|
|
653
|
+
/>
|
|
654
|
+
)}
|
|
655
|
+
</ControlledForm>
|
|
656
|
+
),
|
|
657
|
+
play: async ({ canvas }) => {
|
|
658
|
+
const amount = canvas.getByLabelText(/Amount/);
|
|
659
|
+
await expect(amount).not.toBeRequired();
|
|
660
|
+
|
|
661
|
+
await userEvent.clear(amount);
|
|
662
|
+
await userEvent.type(amount, "75");
|
|
663
|
+
|
|
664
|
+
const justification = canvas.getByLabelText(/Justification/);
|
|
665
|
+
await expect(justification).toBeRequired();
|
|
666
|
+
},
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
/** Async option loader renders a loading placeholder then the resolved list. */
|
|
670
|
+
export const TestAsyncOptionsLoad: Story = {
|
|
671
|
+
name: "Test: async options loader resolves and renders options",
|
|
672
|
+
render: () => {
|
|
673
|
+
const config: FieldConfigV2[] = [
|
|
674
|
+
{
|
|
675
|
+
name: "city",
|
|
676
|
+
label: "City",
|
|
677
|
+
type: "select",
|
|
678
|
+
options: async () => {
|
|
679
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
680
|
+
return [
|
|
681
|
+
{ value: "cdmx", label: "Ciudad de México" },
|
|
682
|
+
{ value: "gdl", label: "Guadalajara" },
|
|
683
|
+
];
|
|
684
|
+
},
|
|
685
|
+
},
|
|
686
|
+
];
|
|
687
|
+
return (
|
|
688
|
+
<ControlledForm initialValues={{ city: "" }}>
|
|
689
|
+
{({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
|
|
690
|
+
<ITFormBuilder
|
|
691
|
+
config={config}
|
|
692
|
+
values={values}
|
|
693
|
+
handleChange={handleChange}
|
|
694
|
+
handleBlur={handleBlur}
|
|
695
|
+
touched={touched}
|
|
696
|
+
errors={errors}
|
|
697
|
+
setFieldValue={setFieldValue}
|
|
698
|
+
/>
|
|
699
|
+
)}
|
|
700
|
+
</ControlledForm>
|
|
701
|
+
);
|
|
702
|
+
},
|
|
703
|
+
play: async ({ canvas }) => {
|
|
704
|
+
// Loading state shown until the loader settles.
|
|
705
|
+
await expect(await canvas.findByTestId("field-loading-city")).toBeInTheDocument();
|
|
706
|
+
|
|
707
|
+
// After settling, the search-select is rendered and accepts a value.
|
|
708
|
+
const select = await canvas.findByLabelText(/City/);
|
|
709
|
+
await userEvent.selectOptions(select, "gdl");
|
|
710
|
+
await expect(select).toHaveValue("gdl");
|
|
711
|
+
},
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
/** Section type renders the heading and nested fields. */
|
|
715
|
+
export const TestSectionRendersNestedFields: Story = {
|
|
716
|
+
name: "Test: section renders nested fields",
|
|
717
|
+
render: () => (
|
|
718
|
+
<ControlledForm initialValues={{ firstName: "", lastName: "" }}>
|
|
719
|
+
{({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
|
|
720
|
+
<ITFormBuilder
|
|
721
|
+
config={[
|
|
722
|
+
{
|
|
723
|
+
name: "identity",
|
|
724
|
+
label: "Personal Information",
|
|
725
|
+
type: "section",
|
|
726
|
+
fields: [
|
|
727
|
+
{ name: "firstName", label: "First Name", type: "text" },
|
|
728
|
+
{ name: "lastName", label: "Last Name", type: "text" },
|
|
729
|
+
],
|
|
730
|
+
},
|
|
731
|
+
]}
|
|
732
|
+
columns={12}
|
|
733
|
+
values={values}
|
|
734
|
+
handleChange={handleChange}
|
|
735
|
+
handleBlur={handleBlur}
|
|
736
|
+
touched={touched}
|
|
737
|
+
errors={errors}
|
|
738
|
+
setFieldValue={setFieldValue}
|
|
739
|
+
/>
|
|
740
|
+
)}
|
|
741
|
+
</ControlledForm>
|
|
742
|
+
),
|
|
743
|
+
play: async ({ canvas }) => {
|
|
744
|
+
await expect(
|
|
745
|
+
canvas.getByRole("heading", { name: /personal information/i }),
|
|
746
|
+
).toBeInTheDocument();
|
|
747
|
+
await expect(canvas.getByLabelText(/First Name/)).toBeInTheDocument();
|
|
748
|
+
await expect(canvas.getByLabelText(/Last Name/)).toBeInTheDocument();
|
|
749
|
+
},
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
/** Custom `type: "custom"` mounts the user-defined component with the value. */
|
|
753
|
+
export const TestCustomComponentReceivesValue: Story = {
|
|
754
|
+
name: "Test: custom component renders and updates value",
|
|
755
|
+
render: () => {
|
|
756
|
+
const ConfirmToggle = (props: any) => {
|
|
757
|
+
const checked = !!props.value;
|
|
758
|
+
return (
|
|
759
|
+
<label data-testid="confirm-toggle">
|
|
760
|
+
<input
|
|
761
|
+
type="checkbox"
|
|
762
|
+
checked={checked}
|
|
763
|
+
onChange={(e) => props.onChange?.(e.target.checked)}
|
|
764
|
+
/>
|
|
765
|
+
{props.label}
|
|
766
|
+
</label>
|
|
767
|
+
);
|
|
768
|
+
};
|
|
769
|
+
return (
|
|
770
|
+
<ControlledForm initialValues={{ confirm: false }}>
|
|
771
|
+
{({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
|
|
772
|
+
<ITFormBuilder
|
|
773
|
+
config={[
|
|
774
|
+
{
|
|
775
|
+
name: "confirm",
|
|
776
|
+
label: "Confirm subscription",
|
|
777
|
+
type: "custom",
|
|
778
|
+
component: ConfirmToggle,
|
|
779
|
+
defaultValue: false,
|
|
780
|
+
},
|
|
781
|
+
]}
|
|
782
|
+
values={values}
|
|
783
|
+
handleChange={handleChange}
|
|
784
|
+
handleBlur={handleBlur}
|
|
785
|
+
touched={touched}
|
|
786
|
+
errors={errors}
|
|
787
|
+
setFieldValue={setFieldValue}
|
|
788
|
+
/>
|
|
789
|
+
)}
|
|
790
|
+
</ControlledForm>
|
|
791
|
+
);
|
|
792
|
+
},
|
|
793
|
+
play: async ({ canvas }) => {
|
|
794
|
+
const checkbox = canvas.getByLabelText(/Confirm subscription/) as HTMLInputElement;
|
|
795
|
+
await expect(checkbox.checked).toBe(false);
|
|
796
|
+
|
|
797
|
+
await userEvent.click(checkbox);
|
|
798
|
+
await expect(checkbox.checked).toBe(true);
|
|
799
|
+
|
|
800
|
+
await expect(canvas.getByTestId("form-state").textContent).toContain(
|
|
801
|
+
'"confirm": true',
|
|
802
|
+
);
|
|
803
|
+
},
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
/** V1 legacy path still renders the supported input types. */
|
|
807
|
+
export const TestLegacyV1RendersBasic: Story = {
|
|
808
|
+
name: "Test: V1 legacy renders basic inputs",
|
|
809
|
+
render: () => (
|
|
810
|
+
<ControlledForm initialValues={{ email: "", password: "" }}>
|
|
811
|
+
{({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
|
|
812
|
+
<ITFormBuilder
|
|
813
|
+
fields={[
|
|
814
|
+
{ name: "email", label: "Email", type: "text", required: true, column: 6 },
|
|
815
|
+
{ name: "password", label: "Password", type: "password", required: true, column: 6 },
|
|
816
|
+
]}
|
|
817
|
+
columns={12}
|
|
818
|
+
values={values}
|
|
819
|
+
handleChange={handleChange}
|
|
820
|
+
handleBlur={handleBlur}
|
|
821
|
+
touched={touched}
|
|
822
|
+
errors={errors}
|
|
823
|
+
setFieldValue={setFieldValue}
|
|
824
|
+
/>
|
|
825
|
+
)}
|
|
826
|
+
</ControlledForm>
|
|
827
|
+
),
|
|
828
|
+
play: async ({ canvas }) => {
|
|
829
|
+
const email = canvas.getByLabelText(/Email/);
|
|
830
|
+
const password = canvas.getByLabelText(/Password/);
|
|
831
|
+
|
|
832
|
+
await userEvent.type(email, "a@b.co");
|
|
833
|
+
await userEvent.type(password, "secret123");
|
|
834
|
+
|
|
835
|
+
await expect(email).toHaveValue("a@b.co");
|
|
836
|
+
await expect(password).toHaveValue("secret123");
|
|
837
|
+
},
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
/** Custom button can read live progress via `useFormBuilder`. */
|
|
841
|
+
export const TestUseFormBuilderProgress: Story = {
|
|
842
|
+
name: "Test: useFormBuilder returns live progress",
|
|
843
|
+
render: () => {
|
|
844
|
+
const ProgressBadge = () => {
|
|
845
|
+
const { progress } = useFormBuilder();
|
|
846
|
+
return (
|
|
847
|
+
<span data-testid="progress-badge">
|
|
848
|
+
{progress}% <FaCheckCircle />
|
|
849
|
+
</span>
|
|
850
|
+
);
|
|
851
|
+
};
|
|
852
|
+
return (
|
|
853
|
+
<ControlledForm initialValues={{ a: "", b: "" }}>
|
|
854
|
+
{({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
|
|
855
|
+
<ITFormBuilder
|
|
856
|
+
config={[
|
|
857
|
+
{ name: "a", label: "Field A", type: "text", required: true },
|
|
858
|
+
{ name: "b", label: "Field B", type: "text", required: true },
|
|
859
|
+
]}
|
|
860
|
+
values={values}
|
|
861
|
+
handleChange={handleChange}
|
|
862
|
+
handleBlur={handleBlur}
|
|
863
|
+
touched={touched}
|
|
864
|
+
errors={errors}
|
|
865
|
+
setFieldValue={setFieldValue}
|
|
866
|
+
>
|
|
867
|
+
<ProgressBadge />
|
|
868
|
+
</ITFormBuilder>
|
|
869
|
+
)}
|
|
870
|
+
</ControlledForm>
|
|
871
|
+
);
|
|
872
|
+
},
|
|
873
|
+
play: async ({ canvas }) => {
|
|
874
|
+
const badge = canvas.getByTestId("progress-badge");
|
|
875
|
+
await expect(badge.textContent).toContain("0%");
|
|
876
|
+
|
|
877
|
+
await userEvent.type(canvas.getByLabelText(/Field A/), "hello");
|
|
878
|
+
|
|
879
|
+
await expect(badge.textContent).toContain("50%");
|
|
880
|
+
|
|
881
|
+
await userEvent.type(canvas.getByLabelText(/Field B/), "world");
|
|
882
|
+
|
|
883
|
+
await expect(badge.textContent).toContain("100%");
|
|
884
|
+
},
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
/** `fn()` is exposed for spy assertions; smoke-validates the props API. */
|
|
888
|
+
export const TestPropsApiAcceptsSpies: Story = {
|
|
889
|
+
name: "Test: handleChange / setFieldValue spies are called",
|
|
890
|
+
render: () => (
|
|
891
|
+
<ControlledForm initialValues={{ x: "" }}>
|
|
892
|
+
{({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => {
|
|
893
|
+
const handleChangeSpy = fn(handleChange);
|
|
894
|
+
const setFieldValueSpy = fn(setFieldValue);
|
|
895
|
+
return (
|
|
896
|
+
<ITFormBuilder
|
|
897
|
+
config={[{ name: "x", label: "X", type: "text" }]}
|
|
898
|
+
values={values}
|
|
899
|
+
handleChange={handleChangeSpy as any}
|
|
900
|
+
handleBlur={handleBlur}
|
|
901
|
+
touched={touched}
|
|
902
|
+
errors={errors}
|
|
903
|
+
setFieldValue={setFieldValueSpy}
|
|
904
|
+
/>
|
|
905
|
+
);
|
|
906
|
+
}}
|
|
907
|
+
</ControlledForm>
|
|
908
|
+
),
|
|
909
|
+
play: async ({ canvas }) => {
|
|
910
|
+
const input = canvas.getByLabelText(/^X/);
|
|
911
|
+
await userEvent.type(input, "z");
|
|
912
|
+
await expect(input).toHaveValue("z");
|
|
913
|
+
},
|
|
914
|
+
};
|