@indico-data/design-system 2.10.0 → 2.11.0
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/lib/index.css +7 -8
- package/lib/index.d.ts +18 -13
- package/lib/index.esm.css +7 -8
- package/lib/index.esm.js +28 -28
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +28 -28
- package/lib/index.js.map +1 -1
- package/lib/src/components/forms/checkbox/Checkbox.d.ts +2 -1
- package/lib/src/components/forms/form/Form.d.ts +14 -0
- package/lib/src/components/forms/form/Form.stories.d.ts +8 -0
- package/lib/src/components/forms/input/Input.d.ts +4 -4
- package/lib/src/components/forms/passwordInput/PasswordInput.d.ts +4 -3
- package/lib/src/components/forms/radio/Radio.d.ts +2 -1
- package/lib/src/components/forms/subcomponents/DisplayFormError.d.ts +5 -0
- package/lib/src/components/forms/textarea/Textarea.d.ts +4 -3
- package/lib/src/components/forms/toggle/Toggle.d.ts +2 -1
- package/package.json +5 -2
- package/src/components/forms/checkbox/Checkbox.stories.tsx +2 -2
- package/src/components/forms/checkbox/Checkbox.tsx +32 -41
- package/src/components/forms/form/Form.mdx +134 -0
- package/src/components/forms/form/Form.stories.tsx +413 -0
- package/src/components/forms/form/Form.tsx +64 -0
- package/src/components/forms/form/__tests__/Form.test.tsx +35 -0
- package/src/components/forms/form/index.ts +0 -0
- package/src/components/forms/form/styles/Form.scss +3 -0
- package/src/components/forms/input/Input.tsx +66 -65
- package/src/components/forms/input/__tests__/Input.test.tsx +2 -13
- package/src/components/forms/input/styles/Input.scss +1 -8
- package/src/components/forms/passwordInput/PasswordInput.stories.tsx +11 -12
- package/src/components/forms/passwordInput/PasswordInput.tsx +63 -59
- package/src/components/forms/passwordInput/__tests__/PasswordInput.test.tsx +1 -1
- package/src/components/forms/radio/Radio.tsx +32 -35
- package/src/components/forms/subcomponents/DisplayFormError.tsx +7 -0
- package/src/components/forms/textarea/Textarea.stories.tsx +15 -21
- package/src/components/forms/textarea/Textarea.tsx +64 -62
- package/src/components/forms/textarea/__tests__/Textarea.test.tsx +1 -1
- package/src/components/forms/textarea/styles/Textarea.scss +1 -1
- package/src/components/forms/toggle/Toggle.tsx +30 -37
- package/src/styles/index.scss +1 -0
- package/lib/src/components/forms/subcomponents/ErrorList.d.ts +0 -6
- package/src/components/forms/subcomponents/ErrorList.tsx +0 -14
- package/src/components/forms/subcomponents/__tests__/ErrorList.test.tsx +0 -16
- /package/lib/src/components/forms/{subcomponents/__tests__/ErrorList.test.d.ts → form/__tests__/Form.test.d.ts} +0 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Form } from './Form';
|
|
3
|
+
import { Input } from '../input/Input';
|
|
4
|
+
import { useForm, Controller, SubmitHandler } from 'react-hook-form';
|
|
5
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
6
|
+
import * as z from 'zod';
|
|
7
|
+
import { Button } from '../../button/Button';
|
|
8
|
+
import { PasswordInput } from '../passwordInput';
|
|
9
|
+
import { Textarea } from '../textarea';
|
|
10
|
+
import { Col, Row, Container } from '../../grid';
|
|
11
|
+
import { Checkbox } from '../checkbox';
|
|
12
|
+
import { Radio } from '../radio';
|
|
13
|
+
import { Toggle } from '../toggle';
|
|
14
|
+
|
|
15
|
+
const meta: Meta = {
|
|
16
|
+
title: 'Forms/Form',
|
|
17
|
+
component: Form,
|
|
18
|
+
argTypes: {
|
|
19
|
+
onSubmit: {
|
|
20
|
+
control: false,
|
|
21
|
+
table: {
|
|
22
|
+
category: 'Callbacks',
|
|
23
|
+
type: {
|
|
24
|
+
summary: '() => void',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
children: {
|
|
29
|
+
control: false,
|
|
30
|
+
table: {
|
|
31
|
+
category: 'Props',
|
|
32
|
+
type: {
|
|
33
|
+
summary: 'React.ReactNode',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
className: {
|
|
38
|
+
control: false,
|
|
39
|
+
table: {
|
|
40
|
+
category: 'Props',
|
|
41
|
+
type: {
|
|
42
|
+
summary: 'string',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
action: {
|
|
47
|
+
control: false,
|
|
48
|
+
table: {
|
|
49
|
+
category: 'Props',
|
|
50
|
+
type: {
|
|
51
|
+
summary: 'string',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
method: {
|
|
56
|
+
control: 'select',
|
|
57
|
+
options: ['get', 'post', 'dialog', 'delete', 'put'],
|
|
58
|
+
table: {
|
|
59
|
+
category: 'Props',
|
|
60
|
+
type: {
|
|
61
|
+
summary: 'get | post | dialog | delete | put',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
defaultValue: { summary: 'post' },
|
|
65
|
+
},
|
|
66
|
+
target: {
|
|
67
|
+
control: 'select',
|
|
68
|
+
options: ['_blank', '_self', '_parent', '_top'],
|
|
69
|
+
table: {
|
|
70
|
+
category: 'Props',
|
|
71
|
+
type: {
|
|
72
|
+
summary: '_blank | _self | _parent | _top',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
defaultValue: { summary: '_self' },
|
|
76
|
+
},
|
|
77
|
+
autocomplete: {
|
|
78
|
+
control: 'select',
|
|
79
|
+
options: ['on', 'off'],
|
|
80
|
+
table: {
|
|
81
|
+
category: 'Props',
|
|
82
|
+
type: {
|
|
83
|
+
summary: 'on | off',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
defaultValue: { summary: 'on' },
|
|
87
|
+
},
|
|
88
|
+
noValidate: {
|
|
89
|
+
control: false,
|
|
90
|
+
table: {
|
|
91
|
+
category: 'Props',
|
|
92
|
+
type: {
|
|
93
|
+
summary: 'boolean',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
defaultValue: { summary: false },
|
|
97
|
+
},
|
|
98
|
+
enctype: {
|
|
99
|
+
control: 'select',
|
|
100
|
+
options: ['application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain'],
|
|
101
|
+
table: {
|
|
102
|
+
category: 'Props',
|
|
103
|
+
type: {
|
|
104
|
+
summary: 'application/x-www-form-urlencoded | multipart/form-data | text/plain',
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
defaultValue: { summary: 'application/x-www-form-urlencoded' },
|
|
108
|
+
},
|
|
109
|
+
rel: {
|
|
110
|
+
control: false,
|
|
111
|
+
table: {
|
|
112
|
+
category: 'Props',
|
|
113
|
+
type: {
|
|
114
|
+
summary: 'string',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export default meta;
|
|
122
|
+
|
|
123
|
+
type Story = StoryObj<typeof Form>;
|
|
124
|
+
|
|
125
|
+
export const Controlled: Story = {
|
|
126
|
+
args: {},
|
|
127
|
+
render: (args) => {
|
|
128
|
+
const { control, handleSubmit } = useForm({
|
|
129
|
+
defaultValues: {
|
|
130
|
+
firstName: 'test',
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
interface FormValues {
|
|
135
|
+
firstName: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const onSubmit: SubmitHandler<FormValues> = (data) => {
|
|
139
|
+
console.log(data);
|
|
140
|
+
};
|
|
141
|
+
return (
|
|
142
|
+
<Container>
|
|
143
|
+
<Row>
|
|
144
|
+
<Col sm={4}>
|
|
145
|
+
<Form {...args} onSubmit={() => handleSubmit(onSubmit)}>
|
|
146
|
+
<Controller
|
|
147
|
+
name="firstName"
|
|
148
|
+
control={control}
|
|
149
|
+
render={({ field }) => <Input label={'Label'} placeholder={'PH'} {...field} />}
|
|
150
|
+
/>
|
|
151
|
+
<input type="submit" />
|
|
152
|
+
</Form>
|
|
153
|
+
</Col>
|
|
154
|
+
<Col sm={4}></Col>
|
|
155
|
+
</Row>
|
|
156
|
+
</Container>
|
|
157
|
+
);
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export const Uncontrolled: Story = {
|
|
162
|
+
args: {},
|
|
163
|
+
render: (args) => {
|
|
164
|
+
const schema = z
|
|
165
|
+
.object({
|
|
166
|
+
firstName: z.string().min(1, 'First name is required').max(10),
|
|
167
|
+
lastName: z.string().min(1, 'Last name is required').max(10),
|
|
168
|
+
email: z.string().min(1, 'Email is required').email(), // todo -- leave a note on this min1
|
|
169
|
+
password: z
|
|
170
|
+
.string()
|
|
171
|
+
.min(1, 'Password is required')
|
|
172
|
+
.max(100)
|
|
173
|
+
.regex(
|
|
174
|
+
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
|
|
175
|
+
'Password must be at least 8 characters long and include at least one uppercase letter, one lowercase letter, one number, and one special character (@, $, !, %, *, ?, &).',
|
|
176
|
+
),
|
|
177
|
+
confirmPassword: z.string().min(1, 'Password is required').max(100),
|
|
178
|
+
description: z.string().max(255).optional(),
|
|
179
|
+
terms: z.boolean(),
|
|
180
|
+
rpgClass: z.enum(['Rogue', 'Warrior', 'Mage']),
|
|
181
|
+
toggle: z.boolean(),
|
|
182
|
+
})
|
|
183
|
+
.refine((data) => data.password === data.confirmPassword, {
|
|
184
|
+
message: "Passwords don't match",
|
|
185
|
+
path: ['confirmPassword'],
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
interface FormValues {
|
|
189
|
+
firstName: string;
|
|
190
|
+
lastName: string;
|
|
191
|
+
email: string;
|
|
192
|
+
password: string;
|
|
193
|
+
confirmPassword: string;
|
|
194
|
+
description: string;
|
|
195
|
+
terms: boolean;
|
|
196
|
+
rpgClass: string;
|
|
197
|
+
toggle: boolean;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const {
|
|
201
|
+
register,
|
|
202
|
+
handleSubmit,
|
|
203
|
+
formState: { errors },
|
|
204
|
+
} = useForm<FormValues>({ mode: 'onChange', resolver: zodResolver(schema) });
|
|
205
|
+
|
|
206
|
+
const onSubmit: SubmitHandler<FormValues> = (data) => data;
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<Container>
|
|
210
|
+
<Row>
|
|
211
|
+
<Col offset={{ sm: 4 }} sm={4}>
|
|
212
|
+
<Form {...args} onSubmit={() => handleSubmit(onSubmit)}>
|
|
213
|
+
<Input
|
|
214
|
+
placeholder={'Please enter your first name.'}
|
|
215
|
+
label="First name"
|
|
216
|
+
isRequired
|
|
217
|
+
errorMessage={errors.firstName ? errors.firstName.message?.toString() : ''}
|
|
218
|
+
{...register('firstName')}
|
|
219
|
+
/>
|
|
220
|
+
<Input
|
|
221
|
+
placeholder={'Please enter your last name.'}
|
|
222
|
+
label="Last name"
|
|
223
|
+
isRequired
|
|
224
|
+
errorMessage={errors.lastName ? errors.lastName.message?.toString() : ''}
|
|
225
|
+
{...register('lastName')}
|
|
226
|
+
/>
|
|
227
|
+
<Input
|
|
228
|
+
placeholder={'Please enter your email.'}
|
|
229
|
+
label="Email"
|
|
230
|
+
isRequired
|
|
231
|
+
errorMessage={errors.email ? errors.email.message?.toString() : ''}
|
|
232
|
+
{...register('email')}
|
|
233
|
+
/>
|
|
234
|
+
<PasswordInput
|
|
235
|
+
placeholder={'Please enter your password.'}
|
|
236
|
+
label="Password"
|
|
237
|
+
isRequired
|
|
238
|
+
errorMessage={errors.password ? errors.password.message?.toString() : ''}
|
|
239
|
+
{...register('password')}
|
|
240
|
+
/>
|
|
241
|
+
<PasswordInput
|
|
242
|
+
placeholder={'Please enter your password again.'}
|
|
243
|
+
label="Confirm Password"
|
|
244
|
+
isRequired
|
|
245
|
+
errorMessage={
|
|
246
|
+
errors.confirmPassword ? errors.confirmPassword.message?.toString() : ''
|
|
247
|
+
}
|
|
248
|
+
{...register('confirmPassword')}
|
|
249
|
+
/>
|
|
250
|
+
|
|
251
|
+
<Textarea
|
|
252
|
+
label="Description"
|
|
253
|
+
placeholder="Please enter a description"
|
|
254
|
+
{...register('description')}
|
|
255
|
+
/>
|
|
256
|
+
|
|
257
|
+
<div className="radio-group mb-5">
|
|
258
|
+
<h2 className="mb-4">Select a class</h2>
|
|
259
|
+
<Radio label="Rogue" value="Rogue" id="rogue" {...register('rpgClass')} />
|
|
260
|
+
<Radio label="Warrior" value="Warruir" id="warrior" {...register('rpgClass')} />
|
|
261
|
+
<Radio label="Mage" value="Mage" id="mage" {...register('rpgClass')} />
|
|
262
|
+
<Radio label="Monk" value="Monk" id="monk" {...register('rpgClass')} />
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<Checkbox label="Do you accept the terms?" id={'one'} {...register('terms')} />
|
|
266
|
+
|
|
267
|
+
<Toggle label="Party On?" id={'Toggle'} {...register('toggle')} />
|
|
268
|
+
|
|
269
|
+
<Button type="submit" ariaLabel={'Submit Form'}>
|
|
270
|
+
Submit Form
|
|
271
|
+
</Button>
|
|
272
|
+
</Form>
|
|
273
|
+
</Col>
|
|
274
|
+
</Row>
|
|
275
|
+
</Container>
|
|
276
|
+
);
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
export const UncontrolledPreFilled: Story = {
|
|
281
|
+
args: {},
|
|
282
|
+
render: (args) => {
|
|
283
|
+
const schema = z
|
|
284
|
+
.object({
|
|
285
|
+
firstName: z.string().min(1, 'First name is required').max(10),
|
|
286
|
+
lastName: z.string().min(1, 'Last name is required').max(10),
|
|
287
|
+
email: z.string().min(1, 'Email is required').email(), // todo -- leave a note on this min1
|
|
288
|
+
password: z
|
|
289
|
+
.string()
|
|
290
|
+
.min(1, 'Password is required')
|
|
291
|
+
.max(100)
|
|
292
|
+
.regex(
|
|
293
|
+
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
|
|
294
|
+
'Password must be at least 8 characters long and include at least one uppercase letter, one lowercase letter, one number, and one special character (@, $, !, %, *, ?, &).',
|
|
295
|
+
),
|
|
296
|
+
confirmPassword: z.string().min(1, 'Password is required').max(100),
|
|
297
|
+
description: z.string().max(255).optional(),
|
|
298
|
+
terms: z.boolean(),
|
|
299
|
+
rpgClass: z.enum(['Rogue', 'Warrior', 'Mage']),
|
|
300
|
+
toggle: z.boolean(),
|
|
301
|
+
})
|
|
302
|
+
.refine((data) => data.password === data.confirmPassword, {
|
|
303
|
+
message: "Passwords don't match",
|
|
304
|
+
path: ['confirmPassword'],
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const {
|
|
308
|
+
register,
|
|
309
|
+
handleSubmit,
|
|
310
|
+
formState: { errors },
|
|
311
|
+
} = useForm<any>({ mode: 'onChange', resolver: zodResolver(schema) });
|
|
312
|
+
interface FormValues {
|
|
313
|
+
firstName: string;
|
|
314
|
+
lastName: string;
|
|
315
|
+
email: string;
|
|
316
|
+
password: string;
|
|
317
|
+
confirmPassword: string;
|
|
318
|
+
description: string;
|
|
319
|
+
terms: boolean;
|
|
320
|
+
rpgClass: string;
|
|
321
|
+
toggle: boolean;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const onSubmit: SubmitHandler<FormValues> = (data) => data;
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<Container>
|
|
328
|
+
<Row>
|
|
329
|
+
<Col offset={{ sm: 3 }} sm={4}>
|
|
330
|
+
<Form {...args} onSubmit={() => handleSubmit(onSubmit)}>
|
|
331
|
+
<Input
|
|
332
|
+
placeholder={'Please enter your first name.'}
|
|
333
|
+
label="First name"
|
|
334
|
+
isRequired
|
|
335
|
+
errorMessage={errors.firstName ? errors.firstName.message?.toString() : ''}
|
|
336
|
+
{...register('firstName')}
|
|
337
|
+
defaultValue="John"
|
|
338
|
+
/>
|
|
339
|
+
<Input
|
|
340
|
+
placeholder={'Please enter your last name.'}
|
|
341
|
+
label="Last name"
|
|
342
|
+
isRequired
|
|
343
|
+
errorMessage={errors.lastName ? errors.lastName.message?.toString() : ''}
|
|
344
|
+
{...register('lastName')}
|
|
345
|
+
defaultValue="Rambo"
|
|
346
|
+
/>
|
|
347
|
+
<Input
|
|
348
|
+
placeholder={'Please enter your email.'}
|
|
349
|
+
label="Email"
|
|
350
|
+
isRequired
|
|
351
|
+
errorMessage={errors.email ? errors.email.message?.toString() : ''}
|
|
352
|
+
{...register('email')}
|
|
353
|
+
defaultValue="john.rambo@indico.io"
|
|
354
|
+
/>
|
|
355
|
+
<PasswordInput
|
|
356
|
+
placeholder={'Please enter your password.'}
|
|
357
|
+
label="Password"
|
|
358
|
+
isRequired
|
|
359
|
+
errorMessage={errors.password ? errors.password.message?.toString() : ''}
|
|
360
|
+
{...register('password')}
|
|
361
|
+
defaultValue="Password1!"
|
|
362
|
+
/>
|
|
363
|
+
<PasswordInput
|
|
364
|
+
placeholder={'Please enter your password again.'}
|
|
365
|
+
label="Confirm Password"
|
|
366
|
+
isRequired
|
|
367
|
+
errorMessage={
|
|
368
|
+
errors.confirmPassword ? errors.confirmPassword.message?.toString() : ''
|
|
369
|
+
}
|
|
370
|
+
{...register('confirmPassword')}
|
|
371
|
+
defaultValue="Password1!"
|
|
372
|
+
/>
|
|
373
|
+
|
|
374
|
+
<Textarea
|
|
375
|
+
label="Description"
|
|
376
|
+
placeholder="Please enter a description"
|
|
377
|
+
{...register('description')}
|
|
378
|
+
defaultValue="I'm Batman!"
|
|
379
|
+
/>
|
|
380
|
+
|
|
381
|
+
<div className="radio-group mb-5">
|
|
382
|
+
<h2 className="mb-4">Select a class</h2>
|
|
383
|
+
<Radio label="Rogue" value="Rogue" id="rogue" {...register('rpgClass')} />
|
|
384
|
+
<Radio
|
|
385
|
+
label="Warrior"
|
|
386
|
+
value="Warruir"
|
|
387
|
+
id="warrior"
|
|
388
|
+
{...register('rpgClass')}
|
|
389
|
+
defaultChecked
|
|
390
|
+
/>
|
|
391
|
+
<Radio label="Mage" value="Mage" id="mage" {...register('rpgClass')} />
|
|
392
|
+
<Radio label="Monk" value="Monk" id="monk" {...register('rpgClass')} />
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
<Checkbox
|
|
396
|
+
label="Do you accept the terms?"
|
|
397
|
+
id={'one'}
|
|
398
|
+
{...register('terms')}
|
|
399
|
+
defaultChecked
|
|
400
|
+
/>
|
|
401
|
+
|
|
402
|
+
<Toggle label="Party On?" id={'Toggle'} {...register('toggle')} defaultChecked />
|
|
403
|
+
|
|
404
|
+
<Button type="submit" ariaLabel={'Submit Form'}>
|
|
405
|
+
Submit Form
|
|
406
|
+
</Button>
|
|
407
|
+
</Form>
|
|
408
|
+
</Col>
|
|
409
|
+
</Row>
|
|
410
|
+
</Container>
|
|
411
|
+
);
|
|
412
|
+
},
|
|
413
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export interface FormProps {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
className?: string;
|
|
6
|
+
action?: string;
|
|
7
|
+
method?: 'get' | 'post' | 'dialog' | 'delete' | 'put';
|
|
8
|
+
target?: '_blank' | '_self' | '_parent' | '_top';
|
|
9
|
+
autocomplete?: 'on' | 'off';
|
|
10
|
+
noValidate?: boolean;
|
|
11
|
+
enctype?: 'application/x-www-form-urlencoded' | 'multipart/form-data' | 'text/plain';
|
|
12
|
+
rel?: string;
|
|
13
|
+
onSubmit?: (formObject: Record<string, string>) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const Form = ({
|
|
17
|
+
children,
|
|
18
|
+
onSubmit,
|
|
19
|
+
action,
|
|
20
|
+
method,
|
|
21
|
+
target,
|
|
22
|
+
autocomplete,
|
|
23
|
+
noValidate,
|
|
24
|
+
enctype,
|
|
25
|
+
rel,
|
|
26
|
+
...rest
|
|
27
|
+
}: FormProps) => {
|
|
28
|
+
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
29
|
+
e.preventDefault(); // Prevent the default form submission
|
|
30
|
+
|
|
31
|
+
// Create a FormData object, passing in the form (e.currentTarget)
|
|
32
|
+
const formData = new FormData(e.currentTarget);
|
|
33
|
+
|
|
34
|
+
// Convert FormData into a plain object
|
|
35
|
+
const formObject = Array.from(formData.entries()).reduce<Record<string, string>>(
|
|
36
|
+
(obj, [key, value]) => {
|
|
37
|
+
obj[key] = value.toString();
|
|
38
|
+
return obj;
|
|
39
|
+
},
|
|
40
|
+
{},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Call the onSubmit prop, if provided, passing the form object instead of the event
|
|
44
|
+
if (onSubmit) {
|
|
45
|
+
onSubmit(formObject); // Modified to pass formObject instead of e
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<form
|
|
51
|
+
onSubmit={handleSubmit}
|
|
52
|
+
action={action}
|
|
53
|
+
method={method}
|
|
54
|
+
target={target}
|
|
55
|
+
autoComplete={autocomplete}
|
|
56
|
+
noValidate={noValidate}
|
|
57
|
+
encType={enctype}
|
|
58
|
+
rel={rel}
|
|
59
|
+
{...rest}
|
|
60
|
+
>
|
|
61
|
+
{children}
|
|
62
|
+
</form>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { Form } from '@/components/forms/form/Form';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { Input } from '../../input';
|
|
5
|
+
import { ChangeEvent } from 'react';
|
|
6
|
+
import { Button } from '../../../button';
|
|
7
|
+
|
|
8
|
+
const onSubmit = jest.fn();
|
|
9
|
+
|
|
10
|
+
describe('form', () => {
|
|
11
|
+
it('renders the form field', async () => {
|
|
12
|
+
render(
|
|
13
|
+
<Form onSubmit={onSubmit}>
|
|
14
|
+
<Input
|
|
15
|
+
placeholder={'Please enter your first name.'}
|
|
16
|
+
label="First name"
|
|
17
|
+
isRequired
|
|
18
|
+
name="first name"
|
|
19
|
+
value="first name"
|
|
20
|
+
onChange={function (e: ChangeEvent<HTMLInputElement>): void {
|
|
21
|
+
throw new Error('Function not implemented.');
|
|
22
|
+
}}
|
|
23
|
+
/>
|
|
24
|
+
<Button type="submit" data-testid="submit" ariaLabel={'submit'}>
|
|
25
|
+
Submit
|
|
26
|
+
</Button>
|
|
27
|
+
</Form>,
|
|
28
|
+
);
|
|
29
|
+
const submitButton = screen.getByTestId('submit');
|
|
30
|
+
expect(submitButton).toBeInTheDocument();
|
|
31
|
+
await userEvent.click(submitButton);
|
|
32
|
+
expect(onSubmit).toHaveBeenCalledTimes(1);
|
|
33
|
+
expect(onSubmit).toHaveBeenCalledWith({ 'first name': 'first name' });
|
|
34
|
+
});
|
|
35
|
+
});
|
|
File without changes
|
|
@@ -2,87 +2,88 @@ import React from 'react';
|
|
|
2
2
|
import { Icon } from '@/components/icons';
|
|
3
3
|
import { IconName } from '@/types';
|
|
4
4
|
import { Label } from '../subcomponents/Label';
|
|
5
|
-
import {
|
|
5
|
+
import { DisplayFormError } from '../subcomponents/DisplayFormError';
|
|
6
6
|
|
|
7
7
|
export interface InputProps {
|
|
8
|
-
ref?: React.LegacyRef<HTMLInputElement>;
|
|
9
8
|
label: string;
|
|
10
9
|
name: string;
|
|
10
|
+
value?: string | undefined;
|
|
11
11
|
placeholder: string;
|
|
12
|
-
value: string;
|
|
13
12
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
14
13
|
isRequired?: boolean;
|
|
15
14
|
isDisabled?: boolean;
|
|
16
|
-
|
|
15
|
+
errorMessage?: string | undefined;
|
|
17
16
|
helpText?: string;
|
|
18
17
|
hasHiddenLabel?: boolean;
|
|
19
18
|
iconName?: IconName;
|
|
20
19
|
isClearable?: boolean;
|
|
21
20
|
className?: string;
|
|
21
|
+
defaultValue?: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export const Input = (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
24
|
+
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
25
|
+
(
|
|
26
|
+
{
|
|
27
|
+
label,
|
|
28
|
+
name,
|
|
29
|
+
placeholder,
|
|
30
|
+
onChange,
|
|
31
|
+
isRequired,
|
|
32
|
+
isDisabled,
|
|
33
|
+
isClearable,
|
|
34
|
+
errorMessage,
|
|
35
|
+
helpText,
|
|
36
|
+
iconName,
|
|
37
|
+
hasHiddenLabel,
|
|
38
|
+
className,
|
|
39
|
+
...rest
|
|
40
|
+
},
|
|
41
|
+
ref,
|
|
42
|
+
) => {
|
|
43
|
+
const hasErrors = errorMessage && errorMessage.length > 0;
|
|
44
|
+
const handleClear = () => {
|
|
45
|
+
onChange({ target: { value: '' } } as React.ChangeEvent<HTMLInputElement>);
|
|
46
|
+
};
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
aria-label={label}
|
|
68
|
-
{...rest}
|
|
69
|
-
/>
|
|
70
|
-
{isClearable && (
|
|
71
|
-
<Icon
|
|
72
|
-
name="x-close"
|
|
73
|
-
data-testid={`${name}-clearable-icon`}
|
|
74
|
-
size="sm"
|
|
75
|
-
onClick={handleClear}
|
|
76
|
-
className="clearable-icon"
|
|
48
|
+
return (
|
|
49
|
+
<div className="form-control">
|
|
50
|
+
<Label label={label} name={name} isRequired={isRequired} hasHiddenLabel={hasHiddenLabel} />
|
|
51
|
+
<div className="input-wrapper">
|
|
52
|
+
{iconName && (
|
|
53
|
+
<Icon name={iconName} data-testid={`${name}-embedded-icon`} className="embedded-icon" />
|
|
54
|
+
)}
|
|
55
|
+
<input
|
|
56
|
+
ref={ref}
|
|
57
|
+
data-testid={`form-input-${name}`}
|
|
58
|
+
name={name}
|
|
59
|
+
type="text"
|
|
60
|
+
disabled={isDisabled}
|
|
61
|
+
placeholder={placeholder}
|
|
62
|
+
onChange={onChange}
|
|
63
|
+
className={`input ${hasErrors ? 'error' : ''} ${iconName ? 'input--has-icon' : ''} ${className}`}
|
|
64
|
+
aria-invalid={hasErrors ? true : undefined}
|
|
65
|
+
aria-describedby={hasErrors || helpText ? `${name}-helper` : undefined}
|
|
66
|
+
aria-required={isRequired}
|
|
67
|
+
aria-label={label}
|
|
68
|
+
{...rest}
|
|
77
69
|
/>
|
|
70
|
+
{isClearable && (
|
|
71
|
+
<Icon
|
|
72
|
+
name="x-close"
|
|
73
|
+
data-testid={`${name}-clearable-icon`}
|
|
74
|
+
size="sm"
|
|
75
|
+
onClick={handleClear}
|
|
76
|
+
className="clearable-icon"
|
|
77
|
+
/>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
{hasErrors && <DisplayFormError message={errorMessage} />}
|
|
81
|
+
{helpText && (
|
|
82
|
+
<div data-testid={`${name}-help-text`} className="help-text" id={`${name}-helper`}>
|
|
83
|
+
{helpText}
|
|
84
|
+
</div>
|
|
78
85
|
)}
|
|
79
86
|
</div>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
{helpText}
|
|
84
|
-
</div>
|
|
85
|
-
)}
|
|
86
|
-
</div>
|
|
87
|
-
);
|
|
88
|
-
};
|
|
87
|
+
);
|
|
88
|
+
},
|
|
89
|
+
);
|