@indico-data/design-system 2.9.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.
Files changed (54) hide show
  1. package/lib/index.css +119 -8
  2. package/lib/index.d.ts +52 -8
  3. package/lib/index.esm.css +119 -8
  4. package/lib/index.esm.js +35 -19
  5. package/lib/index.esm.js.map +1 -1
  6. package/lib/index.js +36 -18
  7. package/lib/index.js.map +1 -1
  8. package/lib/src/components/forms/checkbox/Checkbox.d.ts +2 -1
  9. package/lib/src/components/forms/form/Form.d.ts +14 -0
  10. package/lib/src/components/forms/form/Form.stories.d.ts +8 -0
  11. package/lib/src/components/forms/input/Input.d.ts +5 -4
  12. package/lib/src/components/forms/passwordInput/PasswordInput.d.ts +17 -0
  13. package/lib/src/components/forms/passwordInput/PasswordInput.stories.d.ts +11 -0
  14. package/lib/src/components/forms/passwordInput/__tests__/PasswordInput.test.d.ts +1 -0
  15. package/lib/src/components/forms/passwordInput/index.d.ts +1 -0
  16. package/lib/src/components/forms/radio/Radio.d.ts +2 -1
  17. package/lib/src/components/forms/subcomponents/DisplayFormError.d.ts +5 -0
  18. package/lib/src/components/forms/textarea/Textarea.d.ts +4 -3
  19. package/lib/src/components/forms/toggle/Toggle.d.ts +2 -1
  20. package/lib/src/components/index.d.ts +2 -0
  21. package/lib/src/index.d.ts +2 -0
  22. package/package.json +5 -2
  23. package/src/components/forms/checkbox/Checkbox.stories.tsx +2 -2
  24. package/src/components/forms/checkbox/Checkbox.tsx +32 -41
  25. package/src/components/forms/form/Form.mdx +134 -0
  26. package/src/components/forms/form/Form.stories.tsx +413 -0
  27. package/src/components/forms/form/Form.tsx +64 -0
  28. package/src/components/forms/form/__tests__/Form.test.tsx +35 -0
  29. package/src/components/forms/form/index.ts +0 -0
  30. package/src/components/forms/form/styles/Form.scss +3 -0
  31. package/src/components/forms/input/Input.stories.tsx +0 -5
  32. package/src/components/forms/input/Input.tsx +67 -64
  33. package/src/components/forms/input/__tests__/Input.test.tsx +2 -13
  34. package/src/components/forms/input/styles/Input.scss +2 -8
  35. package/src/components/forms/passwordInput/PasswordInput.mdx +28 -0
  36. package/src/components/forms/passwordInput/PasswordInput.stories.tsx +268 -0
  37. package/src/components/forms/passwordInput/PasswordInput.tsx +86 -0
  38. package/src/components/forms/passwordInput/__tests__/PasswordInput.test.tsx +129 -0
  39. package/src/components/forms/passwordInput/index.ts +1 -0
  40. package/src/components/forms/passwordInput/styles/PasswordInput.scss +120 -0
  41. package/src/components/forms/radio/Radio.tsx +32 -35
  42. package/src/components/forms/subcomponents/DisplayFormError.tsx +7 -0
  43. package/src/components/forms/textarea/Textarea.stories.tsx +15 -21
  44. package/src/components/forms/textarea/Textarea.tsx +64 -62
  45. package/src/components/forms/textarea/__tests__/Textarea.test.tsx +1 -1
  46. package/src/components/forms/textarea/styles/Textarea.scss +1 -1
  47. package/src/components/forms/toggle/Toggle.tsx +30 -37
  48. package/src/components/index.ts +2 -0
  49. package/src/index.ts +2 -0
  50. package/src/styles/index.scss +2 -0
  51. package/lib/src/components/forms/subcomponents/ErrorList.d.ts +0 -6
  52. package/src/components/forms/subcomponents/ErrorList.tsx +0 -14
  53. package/src/components/forms/subcomponents/__tests__/ErrorList.test.tsx +0 -16
  54. /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
@@ -0,0 +1,3 @@
1
+ form {
2
+ width: 100%;
3
+ }
@@ -27,7 +27,6 @@ const meta: Meta = {
27
27
  summary: 'string',
28
28
  },
29
29
  },
30
- defaultValue: { summary: '' },
31
30
  },
32
31
  name: {
33
32
  control: 'text',
@@ -38,7 +37,6 @@ const meta: Meta = {
38
37
  summary: 'string',
39
38
  },
40
39
  },
41
- defaultValue: { summary: '' },
42
40
  },
43
41
  placeholder: {
44
42
  control: 'text',
@@ -49,7 +47,6 @@ const meta: Meta = {
49
47
  summary: 'string',
50
48
  },
51
49
  },
52
- defaultValue: { summary: '' },
53
50
  },
54
51
  value: {
55
52
  control: 'text',
@@ -60,7 +57,6 @@ const meta: Meta = {
60
57
  summary: 'string',
61
58
  },
62
59
  },
63
- defaultValue: { summary: '' },
64
60
  },
65
61
  isRequired: {
66
62
  control: 'boolean',
@@ -104,7 +100,6 @@ const meta: Meta = {
104
100
  summary: 'string',
105
101
  },
106
102
  },
107
- defaultValue: { summary: '' },
108
103
  },
109
104
  hasHiddenLabel: {
110
105
  control: 'boolean',