@indico-data/design-system 2.7.0 → 2.9.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 (39) hide show
  1. package/lib/index.css +195 -19
  2. package/lib/index.d.ts +14 -2
  3. package/lib/index.esm.css +195 -19
  4. package/lib/index.esm.js +7 -2
  5. package/lib/index.esm.js.map +1 -1
  6. package/lib/index.js +7 -1
  7. package/lib/index.js.map +1 -1
  8. package/lib/src/components/forms/textarea/Textarea.d.ts +22 -0
  9. package/lib/src/components/forms/textarea/Textarea.stories.d.ts +11 -0
  10. package/lib/src/components/forms/textarea/__tests__/Textarea.test.d.ts +1 -0
  11. package/lib/src/components/forms/textarea/index.d.ts +1 -0
  12. package/lib/src/components/forms/toggle/Toggle.d.ts +12 -0
  13. package/lib/src/components/forms/toggle/Toggle.stories.d.ts +6 -0
  14. package/lib/src/components/forms/toggle/__tests__/Toggle.test.d.ts +1 -0
  15. package/lib/src/components/forms/toggle/index.d.ts +1 -0
  16. package/lib/src/components/index.d.ts +1 -0
  17. package/lib/src/index.d.ts +1 -0
  18. package/package.json +1 -1
  19. package/src/components/forms/checkbox/Checkbox.mdx +6 -1
  20. package/src/components/forms/checkbox/Checkbox.stories.tsx +10 -4
  21. package/src/components/forms/checkbox/Checkbox.tsx +1 -1
  22. package/src/components/forms/checkbox/styles/Checkbox.scss +16 -16
  23. package/src/components/forms/input/Input.stories.tsx +70 -64
  24. package/src/components/forms/input/styles/Input.scss +10 -3
  25. package/src/components/forms/textarea/Textarea.mdx +19 -0
  26. package/src/components/forms/textarea/Textarea.stories.tsx +363 -0
  27. package/src/components/forms/textarea/Textarea.tsx +85 -0
  28. package/src/components/forms/textarea/__tests__/Textarea.test.tsx +103 -0
  29. package/src/components/forms/textarea/index.ts +1 -0
  30. package/src/components/forms/textarea/styles/Textarea.scss +102 -0
  31. package/src/components/forms/toggle/Toggle.mdx +15 -0
  32. package/src/components/forms/toggle/Toggle.stories.tsx +126 -0
  33. package/src/components/forms/toggle/Toggle.tsx +51 -0
  34. package/src/components/forms/toggle/__tests__/Toggle.test.tsx +19 -0
  35. package/src/components/forms/toggle/index.ts +1 -0
  36. package/src/components/forms/toggle/styles/Toggle.scss +72 -0
  37. package/src/components/index.ts +1 -0
  38. package/src/index.ts +1 -0
  39. package/src/styles/index.scss +2 -0
@@ -0,0 +1,363 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ import { Textarea, TextareaProps } from './Textarea';
3
+ import { SetStateAction, useEffect, useState } from 'react';
4
+
5
+ const meta: Meta = {
6
+ title: 'Forms/Textarea',
7
+ component: Textarea,
8
+ argTypes: {
9
+ onChange: {
10
+ control: false,
11
+ description: 'onChange event handler',
12
+ table: {
13
+ category: 'Callbacks',
14
+ type: {
15
+ summary: '(e: React.ChangeEvent<HTMLInputElement>) => void',
16
+ },
17
+ },
18
+ action: 'onChange',
19
+ },
20
+ label: {
21
+ control: 'text',
22
+ description: 'The label for the textarea field',
23
+ table: {
24
+ category: 'Props',
25
+ type: {
26
+ summary: 'string',
27
+ },
28
+ },
29
+ defaultValue: { summary: '' },
30
+ },
31
+ name: {
32
+ control: 'text',
33
+ description: 'The name for the textarea field',
34
+ table: {
35
+ category: 'Props',
36
+ type: {
37
+ summary: 'string',
38
+ },
39
+ },
40
+ defaultValue: { summary: '' },
41
+ },
42
+ placeholder: {
43
+ control: 'text',
44
+ description: 'The placeholder for the textarea field',
45
+ table: {
46
+ category: 'Props',
47
+ type: {
48
+ summary: 'string',
49
+ },
50
+ },
51
+ defaultValue: { summary: '' },
52
+ },
53
+ value: {
54
+ control: 'text',
55
+ description: 'The value for the textarea field',
56
+ table: {
57
+ category: 'Props',
58
+ type: {
59
+ summary: 'string',
60
+ },
61
+ },
62
+ defaultValue: { summary: '' },
63
+ },
64
+ isRequired: {
65
+ control: 'boolean',
66
+ description: 'Toggles the required astherisc on the label',
67
+ table: {
68
+ category: 'Props',
69
+ type: {
70
+ summary: 'boolean',
71
+ },
72
+ },
73
+ defaultValue: { summary: 'false' },
74
+ },
75
+ isDisabled: {
76
+ control: 'boolean',
77
+ description: 'Toggles the disabled state of the textarea field',
78
+ table: {
79
+ category: 'Props',
80
+ type: {
81
+ summary: 'boolean',
82
+ },
83
+ },
84
+ defaultValue: { summary: 'false' },
85
+ },
86
+ errorList: {
87
+ control: false,
88
+ description: 'An array of error messages',
89
+ table: {
90
+ category: 'Props',
91
+ type: {
92
+ summary: 'string[]',
93
+ },
94
+ },
95
+ defaultValue: { summary: '[]' },
96
+ },
97
+ helpText: {
98
+ control: 'text',
99
+ description: 'The help text for the textarea field',
100
+ table: {
101
+ category: 'Props',
102
+ type: {
103
+ summary: 'string',
104
+ },
105
+ },
106
+ defaultValue: { summary: '' },
107
+ },
108
+ hasHiddenLabel: {
109
+ control: 'boolean',
110
+ description: 'Hides the label visually (retains it for screen readers)',
111
+ table: {
112
+ category: 'Props',
113
+ type: {
114
+ summary: 'boolean',
115
+ },
116
+ },
117
+ defaultValue: { summary: 'false' },
118
+ },
119
+ autofocus: {
120
+ control: 'boolean',
121
+ description: ' Specifies that a text area should automatically get focus when the page loads',
122
+ table: {
123
+ category: 'Props',
124
+ type: {
125
+ summary: 'boolean',
126
+ },
127
+ },
128
+ defaultValue: { summary: 'false' },
129
+ },
130
+ rows: {
131
+ control: 'number',
132
+ description: 'The number of rows for the textarea field',
133
+ table: {
134
+ category: 'Props',
135
+ type: {
136
+ summary: 'number',
137
+ },
138
+ },
139
+ defaultValue: { summary: undefined },
140
+ },
141
+ cols: {
142
+ control: 'number',
143
+ description: 'Specifies the visible width of a text area',
144
+ table: {
145
+ category: 'Props',
146
+ type: {
147
+ summary: 'number',
148
+ },
149
+ },
150
+ defaultValue: { summary: undefined },
151
+ },
152
+ readonly: {
153
+ control: 'boolean',
154
+ description: 'Sets the textarea field to readonly',
155
+ table: {
156
+ category: 'Props',
157
+ type: {
158
+ summary: 'boolean',
159
+ },
160
+ },
161
+ defaultValue: { summary: 'false' },
162
+ },
163
+ wrap: {
164
+ control: 'text',
165
+ description: 'Sets the wrap attribute for the textarea field',
166
+ table: {
167
+ category: 'Props',
168
+ type: {
169
+ summary: 'string',
170
+ },
171
+ },
172
+ defaultValue: { summary: 'soft' },
173
+ },
174
+ form: {
175
+ control: 'text',
176
+ description: 'Specifies which form the text area belongs to',
177
+ table: {
178
+ category: 'Props',
179
+ type: {
180
+ summary: 'string',
181
+ },
182
+ },
183
+ defaultValue: { summary: '' },
184
+ },
185
+ maxLength: {
186
+ control: 'number',
187
+ description: 'Specifies the maximum number of characters allowed in the text area',
188
+ table: {
189
+ category: 'Props',
190
+ type: {
191
+ summary: 'number',
192
+ },
193
+ },
194
+ defaultValue: { summary: undefined },
195
+ },
196
+ ref: {
197
+ table: {
198
+ disable: true,
199
+ },
200
+ },
201
+ },
202
+ };
203
+
204
+ export default meta;
205
+
206
+ type Story = StoryObj<typeof Textarea>;
207
+
208
+ export const Default: Story = {
209
+ args: {
210
+ isRequired: true,
211
+ helpText: 'This Is Help Text',
212
+ label: 'Label Name',
213
+ name: 'textarea',
214
+ placeholder: 'Please enter a value',
215
+ hasHiddenLabel: false,
216
+ isDisabled: false,
217
+ errorList: [],
218
+ value: '',
219
+ readonly: false,
220
+ cols: 0,
221
+ rows: 0,
222
+ wrap: 'soft',
223
+ form: '',
224
+ maxLength: 200,
225
+ autofocus: false,
226
+ },
227
+ render: (args) => {
228
+ const [value, setValue] = useState(args.value);
229
+
230
+ useEffect(() => {
231
+ setValue(args.value);
232
+ }, [args.value]);
233
+
234
+ const handleChange = (e: { target: { value: SetStateAction<string> } }) => {
235
+ setValue(e.target.value);
236
+ };
237
+
238
+ return <Textarea {...args} value={value} onChange={handleChange} />;
239
+ },
240
+ };
241
+
242
+ export const Errors: Story = {
243
+ args: {
244
+ isRequired: true,
245
+ label: 'Label Name',
246
+ name: 'textarea',
247
+ placeholder: 'Please enter a value',
248
+ hasHiddenLabel: false,
249
+ isDisabled: false,
250
+ errorList: ['This Is An Error', 'This Is Another Error'],
251
+ value: '',
252
+ },
253
+ render: (args) => {
254
+ const [value, setValue] = useState(args.value);
255
+
256
+ useEffect(() => {
257
+ setValue(args.value);
258
+ }, [args.value]);
259
+
260
+ const handleChange = (e: { target: { value: SetStateAction<string> } }) => {
261
+ setValue(e.target.value);
262
+ };
263
+
264
+ return <Textarea {...args} value={value} onChange={handleChange} />;
265
+ },
266
+ };
267
+
268
+ export const HiddenLabel: Story = {
269
+ args: {
270
+ isRequired: true,
271
+ label: 'Label Name',
272
+ name: 'textarea',
273
+ placeholder: 'Please enter a value',
274
+ hasHiddenLabel: true,
275
+ isDisabled: false,
276
+ value: '',
277
+ },
278
+ render: (args) => {
279
+ const [value, setValue] = useState(args.value);
280
+
281
+ useEffect(() => {
282
+ setValue(args.value);
283
+ }, [args.value]);
284
+
285
+ const handleChange = (e: { target: { value: SetStateAction<string> } }) => {
286
+ setValue(e.target.value);
287
+ };
288
+
289
+ return <Textarea {...args} value={value} onChange={handleChange} />;
290
+ },
291
+ };
292
+
293
+ export const HelpText: Story = {
294
+ args: {
295
+ helpText: 'This Is Help Text',
296
+ label: 'Label Name',
297
+ name: 'textarea',
298
+ placeholder: 'Please enter a value',
299
+ isDisabled: false,
300
+ value: '',
301
+ },
302
+ render: (args) => {
303
+ const [value, setValue] = useState(args.value);
304
+
305
+ useEffect(() => {
306
+ setValue(args.value);
307
+ }, [args.value]);
308
+
309
+ const handleChange = (e: { target: { value: SetStateAction<string> } }) => {
310
+ setValue(e.target.value);
311
+ };
312
+
313
+ return <Textarea {...args} value={value} onChange={handleChange} />;
314
+ },
315
+ };
316
+
317
+ export const Disabled: Story = {
318
+ args: {
319
+ helpText: 'This Is Help Text',
320
+ label: 'Label Name',
321
+ name: 'textarea',
322
+ placeholder: 'Please enter a value',
323
+ isDisabled: true,
324
+ value: '',
325
+ },
326
+ render: (args) => {
327
+ const [value, setValue] = useState(args.value);
328
+
329
+ useEffect(() => {
330
+ setValue(args.value);
331
+ }, [args.value]);
332
+
333
+ const handleChange = (e: { target: { value: SetStateAction<string> } }) => {
334
+ setValue(e.target.value);
335
+ };
336
+
337
+ return <Textarea {...args} value={value} onChange={handleChange} />;
338
+ },
339
+ };
340
+
341
+ export const Readonly: Story = {
342
+ args: {
343
+ helpText: 'This Is Help Text',
344
+ label: 'Label Name',
345
+ name: 'textarea',
346
+ placeholder: 'Please enter a value',
347
+ value: '',
348
+ readonly: true,
349
+ },
350
+ render: (args) => {
351
+ const [value, setValue] = useState(args.value);
352
+
353
+ useEffect(() => {
354
+ setValue(args.value);
355
+ }, [args.value]);
356
+
357
+ const handleChange = (e: { target: { value: SetStateAction<string> } }) => {
358
+ setValue(e.target.value);
359
+ };
360
+
361
+ return <Textarea {...args} value={value} onChange={handleChange} />;
362
+ },
363
+ };
@@ -0,0 +1,85 @@
1
+ import React from 'react';
2
+ import { Label } from '../subcomponents/Label';
3
+ import { ErrorList } from '../subcomponents/ErrorList';
4
+
5
+ export interface TextareaProps {
6
+ ref?: React.LegacyRef<HTMLTextAreaElement>;
7
+ label: string;
8
+ name: string;
9
+ placeholder: string;
10
+ value: string;
11
+ onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
12
+ isRequired?: boolean;
13
+ isDisabled?: boolean;
14
+ errorList?: string[];
15
+ helpText?: string;
16
+ hasHiddenLabel?: boolean;
17
+ rows?: number;
18
+ cols?: number;
19
+ readonly?: boolean;
20
+ wrap?: 'hard' | 'soft';
21
+ form?: string;
22
+ maxLength?: number;
23
+ autofocus?: boolean;
24
+ }
25
+
26
+ export const Textarea = ({
27
+ ref,
28
+ label,
29
+ name,
30
+ placeholder,
31
+ value,
32
+ onChange,
33
+ isRequired,
34
+ isDisabled,
35
+ errorList,
36
+ helpText,
37
+ hasHiddenLabel,
38
+ rows,
39
+ cols,
40
+ readonly,
41
+ wrap,
42
+ form,
43
+ maxLength,
44
+ autofocus,
45
+ ...rest
46
+ }: TextareaProps) => {
47
+ const hasErrors = errorList && errorList.length > 0;
48
+
49
+ return (
50
+ <div className="form-control">
51
+ <Label label={label} name={name} isRequired={isRequired} hasHiddenLabel={hasHiddenLabel} />
52
+ <div className="textarea-wrapper">
53
+ <textarea
54
+ ref={ref}
55
+ rows={rows}
56
+ cols={cols}
57
+ autoFocus={autofocus}
58
+ wrap={wrap}
59
+ form={form}
60
+ value={value}
61
+ maxLength={maxLength}
62
+ readOnly={readonly}
63
+ data-testid={`form-textarea-${name}`}
64
+ name={name}
65
+ disabled={isDisabled}
66
+ required={isRequired}
67
+ placeholder={placeholder}
68
+ onChange={onChange}
69
+ className={`textarea ${hasErrors ? 'error' : ''}`}
70
+ aria-invalid={hasErrors}
71
+ aria-describedby={hasErrors || helpText ? `${name}-helper` : undefined}
72
+ aria-required={isRequired}
73
+ aria-label={label}
74
+ {...rest}
75
+ />
76
+ </div>
77
+ {hasErrors && <ErrorList errorList={errorList} name={name} />}
78
+ {helpText && (
79
+ <div data-testid={`${name}-help-text`} className="help-text" id={`${name}-helper`}>
80
+ {helpText}
81
+ </div>
82
+ )}
83
+ </div>
84
+ );
85
+ };
@@ -0,0 +1,103 @@
1
+ import { render, screen, act } from '@testing-library/react';
2
+ import { Textarea } from '@/components/forms/textarea/Textarea';
3
+ import { ChangeEvent } from 'react';
4
+ import userEvent from '@testing-library/user-event';
5
+
6
+ const handleOnChange = jest.fn();
7
+
8
+ describe('Textarea', () => {
9
+ it('renders the input field', () => {
10
+ render(
11
+ <Textarea
12
+ isRequired={true}
13
+ label="Enter your name"
14
+ helpText="In order to submit the form, this field is required."
15
+ name="name"
16
+ placeholder="Please enter a value"
17
+ value={''}
18
+ onChange={handleOnChange}
19
+ />,
20
+ );
21
+ expect(screen.getByText('Enter your name')).toBeInTheDocument();
22
+ });
23
+
24
+ it('adds the error class when errors exist', () => {
25
+ render(
26
+ <Textarea
27
+ isRequired={true}
28
+ errorList={['You require a username value.']}
29
+ label="Enter your name"
30
+ helpText="In order to submit the form, this field is required."
31
+ name="name"
32
+ placeholder="Please enter a value"
33
+ value={'test'}
34
+ onChange={handleOnChange}
35
+ />,
36
+ );
37
+ const textareaElement = screen.getByTestId('form-textarea-name');
38
+ expect(textareaElement).toHaveClass('error');
39
+ });
40
+ it('does not highlight the Textarea when no errors exist', () => {
41
+ render(
42
+ <Textarea
43
+ isRequired={true}
44
+ label="Enter your name"
45
+ helpText="In order to submit the form, this field is required."
46
+ name="name"
47
+ placeholder="Please enter a value"
48
+ value={'test'}
49
+ onChange={handleOnChange}
50
+ />,
51
+ );
52
+ const textareaElement = screen.getByTestId('form-textarea-name');
53
+ expect(textareaElement).not.toHaveClass('error');
54
+ });
55
+ it('renders help text when help text exists', () => {
56
+ render(
57
+ <Textarea
58
+ isRequired={true}
59
+ label="Enter your name"
60
+ helpText="In order to submit the form, this field is required."
61
+ name="name"
62
+ placeholder="Please enter a value"
63
+ value={'test'}
64
+ onChange={handleOnChange}
65
+ />,
66
+ );
67
+ const helpText = screen.getByText('In order to submit the form, this field is required.');
68
+ expect(helpText).toBeInTheDocument();
69
+ expect(helpText).toBeVisible();
70
+ });
71
+ it('does not render help text when help text does not exist', () => {
72
+ render(
73
+ <Textarea
74
+ isRequired={true}
75
+ label="Enter your name"
76
+ name="name"
77
+ placeholder="Please enter a value"
78
+ value={'test'}
79
+ onChange={handleOnChange}
80
+ />,
81
+ );
82
+ const helpText = screen.queryByTestId('name-help-text');
83
+ expect(helpText).not.toBeInTheDocument();
84
+ expect(helpText).toBeNull();
85
+ });
86
+
87
+ it('emits the value when user types', async () => {
88
+ const handleOnChange = jest.fn();
89
+ render(
90
+ <Textarea
91
+ isRequired={true}
92
+ label="Enter your name"
93
+ name="name"
94
+ placeholder="Please enter a value"
95
+ value={''}
96
+ onChange={handleOnChange}
97
+ />,
98
+ );
99
+ const textareaElement = screen.getByTestId('form-textarea-name');
100
+ await userEvent.type(textareaElement, 't');
101
+ expect(handleOnChange).toHaveBeenCalled();
102
+ });
103
+ });
@@ -0,0 +1 @@
1
+ export { Textarea } from './Textarea';
@@ -0,0 +1,102 @@
1
+ // Common Variables
2
+ :root,
3
+ :root [data-theme='light'],
4
+ :root [data-theme='dark'] {
5
+ // Typography
6
+ --pf-textarea-background-color: var(--pf-white-color);
7
+ --pf-textarea-border-color: var(--pf-gray-color);
8
+ --pf-textarea-text-color: var(--pf-gray-color);
9
+ --pf-textarea-placeholder-text-color: var(--pf-gray-color-300);
10
+ --pf-textarea-help-text-color: var(--pf-gray-color-400);
11
+ --pf-textarea-disabled-background-color: var(--pf-gray-color-100);
12
+ --pf-textarea-border-color: var(--pf-gray-color);
13
+ --pf-textarea-disabled-color: var(--pf-gray-color-400);
14
+
15
+ // textarea Radius
16
+ --pf-textarea-rounded: var(--pf-rounded);
17
+ }
18
+
19
+ // Dark Theme Specific Variables
20
+ :root [data-theme='dark'] {
21
+ --pf-textarea-background-color: var(--pf-primary-color);
22
+ --pf-textarea-border-color: var(--pf-gray-color-100);
23
+ --pf-textarea-text-color: var(--pf-gray-color-100);
24
+ --pf-textarea-placeholder-text-color: var(--pf-gray-color);
25
+ --pf-textarea-help-text-color: var(--pf-gray-color-200);
26
+ --pf-textarea-disabled-background-color: var(--pf-primary-color-200);
27
+ --pf-textarea-disabled-border-color: var(--pf-gray-color-300);
28
+ --pf-textarea-disabled-color: var(--pf-gray-color-400);
29
+ }
30
+
31
+ .textarea {
32
+ background-color: var(--pf-textarea-background-color);
33
+ border: 1px solid var(--pf-textarea-border-color);
34
+ border-radius: var(--pf-textarea-rounded);
35
+ color: var(--pf-textarea-text-color);
36
+ padding: 10px;
37
+ width: 100%;
38
+ box-sizing: border-box;
39
+ &::placeholder {
40
+ color: var(--pf-textarea-placeholder-text-color);
41
+ }
42
+
43
+ &:focus {
44
+ border-color: var(--pf-primary-color);
45
+ }
46
+
47
+ &.error {
48
+ border-color: var(--pf-error-color);
49
+ }
50
+
51
+ &.success {
52
+ border-color: var(--pf-success-color);
53
+ }
54
+
55
+ &.warning {
56
+ border-color: var(--pf-warning-color);
57
+ }
58
+
59
+ &.info {
60
+ border-color: var(--pf-info-color);
61
+ }
62
+
63
+ &:disabled {
64
+ background-color: var(--pf-textarea-disabled-background-color);
65
+ border-color: var(--pf-textarea-disabled-border-color);
66
+ color: var(--pf-textarea-disabled-color);
67
+ }
68
+ &--has-icon {
69
+ padding-left: var(--pf-padding-7);
70
+ }
71
+ }
72
+
73
+ .form-control {
74
+ .error-list {
75
+ list-style: none;
76
+ padding: 0;
77
+ margin: 0;
78
+ margin-top: var(--pf-margin-2);
79
+ margin-bottom: var(--pf-margin-2);
80
+ color: var(--pf-error-color);
81
+ }
82
+ .help-text {
83
+ margin-top: var(--pf-margin-2);
84
+ margin-bottom: var(--pf-margin-2);
85
+ color: var(--pf-textarea-help-text-color);
86
+ font-size: var(--pf-font-size-subtitle2);
87
+ }
88
+ .is-visually-hidden {
89
+ position: absolute;
90
+ width: 1px;
91
+ height: 1px;
92
+ padding: 0;
93
+ margin: -1px;
94
+ overflow: hidden;
95
+ clip: rect(0, 0, 0, 0);
96
+ white-space: nowrap;
97
+ border: 0;
98
+ }
99
+ .form-label {
100
+ margin-bottom: var(--pf-margin-2);
101
+ }
102
+ }
@@ -0,0 +1,15 @@
1
+ import { Canvas, Meta, Controls } from '@storybook/blocks';
2
+ import * as Toggle from './Toggle.stories';
3
+
4
+ <Meta title="Forms/Toggle" name="Toggle" />
5
+
6
+ # Toggle
7
+
8
+ Under the hood, Toggle component works the same as a regular Checkbox. Through CSS, we style the checkbox to resemble an on/off switch.
9
+
10
+ <Canvas of={Toggle.Default} source={{
11
+ }} />
12
+ <Controls of={Toggle.Default} />
13
+
14
+ ## Note
15
+ Because of storybook limitations, the "isChecked" prop is not working in the storybook. In addition, neither is the on click. Please refer to the [default](http://localhost:6006/?path=/story/forms-toggle--default) example on the forms section.