@indico-data/design-system 2.10.0 → 2.12.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 +99 -8
- package/lib/index.d.ts +47 -15
- package/lib/index.esm.css +99 -8
- package/lib/index.esm.js +61 -28
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +62 -27
- 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/form/index.d.ts +1 -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/select/Select.d.ts +6 -0
- package/lib/src/components/forms/select/Select.stories.d.ts +7 -0
- package/lib/src/components/forms/select/__tests__/Select.test.d.ts +1 -0
- package/lib/src/components/forms/select/index.d.ts +1 -0
- package/lib/src/components/forms/select/types.d.ts +6 -0
- 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/lib/src/components/index.d.ts +2 -0
- package/lib/src/index.d.ts +2 -0
- package/lib/src/types.d.ts +2 -0
- 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 +1 -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/select/Select.stories.tsx +118 -0
- package/src/components/forms/select/Select.tsx +43 -0
- package/src/components/forms/select/__tests__/Select.test.tsx +67 -0
- package/src/components/forms/select/index.ts +1 -0
- package/src/components/forms/select/styles/Select.scss +120 -0
- package/src/components/forms/select/types.ts +6 -0
- 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/components/index.ts +2 -0
- package/src/index.ts +2 -0
- package/src/styles/index.scss +3 -1
- package/src/types.ts +3 -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
|
@@ -17,7 +17,6 @@ describe('Input', () => {
|
|
|
17
17
|
iconName="user"
|
|
18
18
|
isClearable={true}
|
|
19
19
|
ref={undefined}
|
|
20
|
-
value={''}
|
|
21
20
|
onChange={handleOnChange}
|
|
22
21
|
/>,
|
|
23
22
|
);
|
|
@@ -34,7 +33,6 @@ describe('Input', () => {
|
|
|
34
33
|
iconName="user"
|
|
35
34
|
isClearable={true}
|
|
36
35
|
ref={undefined}
|
|
37
|
-
value={''}
|
|
38
36
|
onChange={handleOnChange}
|
|
39
37
|
/>,
|
|
40
38
|
);
|
|
@@ -53,7 +51,6 @@ describe('Input', () => {
|
|
|
53
51
|
iconName="user"
|
|
54
52
|
isClearable={false}
|
|
55
53
|
ref={undefined}
|
|
56
|
-
value={''}
|
|
57
54
|
onChange={handleOnChange}
|
|
58
55
|
/>,
|
|
59
56
|
);
|
|
@@ -71,9 +68,8 @@ describe('Input', () => {
|
|
|
71
68
|
placeholder="Please enter a value"
|
|
72
69
|
iconName="user"
|
|
73
70
|
isClearable={true}
|
|
74
|
-
ref={undefined}
|
|
75
|
-
value={'test'}
|
|
76
71
|
onChange={handleOnChange}
|
|
72
|
+
value="test"
|
|
77
73
|
/>,
|
|
78
74
|
);
|
|
79
75
|
const input = screen.getByTestId('form-input-name');
|
|
@@ -95,8 +91,6 @@ describe('Input', () => {
|
|
|
95
91
|
placeholder="Please enter a value"
|
|
96
92
|
iconName="user"
|
|
97
93
|
isClearable={true}
|
|
98
|
-
ref={undefined}
|
|
99
|
-
value={'test'}
|
|
100
94
|
onChange={handleOnChange}
|
|
101
95
|
/>,
|
|
102
96
|
);
|
|
@@ -113,8 +107,6 @@ describe('Input', () => {
|
|
|
113
107
|
name="name"
|
|
114
108
|
placeholder="Please enter a value"
|
|
115
109
|
isClearable={true}
|
|
116
|
-
ref={undefined}
|
|
117
|
-
value={'test'}
|
|
118
110
|
onChange={handleOnChange}
|
|
119
111
|
/>,
|
|
120
112
|
);
|
|
@@ -126,13 +118,12 @@ describe('Input', () => {
|
|
|
126
118
|
render(
|
|
127
119
|
<Input
|
|
128
120
|
isRequired={true}
|
|
129
|
-
|
|
121
|
+
errorMessage="You require a username value."
|
|
130
122
|
label="Enter your name"
|
|
131
123
|
helpText="In order to submit the form, this field is required."
|
|
132
124
|
name="name"
|
|
133
125
|
placeholder="Please enter a value"
|
|
134
126
|
isClearable={true}
|
|
135
|
-
ref={undefined}
|
|
136
127
|
value={'test'}
|
|
137
128
|
onChange={handleOnChange}
|
|
138
129
|
/>,
|
|
@@ -201,8 +192,6 @@ describe('Input', () => {
|
|
|
201
192
|
label="Enter your name"
|
|
202
193
|
name="name"
|
|
203
194
|
placeholder="Please enter a value"
|
|
204
|
-
ref={undefined}
|
|
205
|
-
value={''}
|
|
206
195
|
onChange={handleOnChange}
|
|
207
196
|
/>,
|
|
208
197
|
);
|
|
@@ -73,14 +73,7 @@
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
.form-control {
|
|
76
|
-
|
|
77
|
-
list-style: none;
|
|
78
|
-
padding: 0;
|
|
79
|
-
margin: 0;
|
|
80
|
-
margin-top: var(--pf-margin-2);
|
|
81
|
-
margin-bottom: var(--pf-margin-2);
|
|
82
|
-
color: var(--pf-error-color);
|
|
83
|
-
}
|
|
76
|
+
margin-bottom: var(--pf-margin-3);
|
|
84
77
|
.help-text {
|
|
85
78
|
margin-top: var(--pf-margin-2);
|
|
86
79
|
margin-bottom: var(--pf-margin-2);
|
|
@@ -91,16 +91,16 @@ const meta: Meta = {
|
|
|
91
91
|
},
|
|
92
92
|
defaultValue: { summary: 'false' },
|
|
93
93
|
},
|
|
94
|
-
|
|
94
|
+
errorMessage: {
|
|
95
95
|
control: false,
|
|
96
96
|
description: 'An array of error messages',
|
|
97
97
|
table: {
|
|
98
98
|
category: 'Props',
|
|
99
99
|
type: {
|
|
100
|
-
summary: 'string
|
|
100
|
+
summary: 'string',
|
|
101
101
|
},
|
|
102
102
|
},
|
|
103
|
-
defaultValue: { summary:
|
|
103
|
+
defaultValue: { summary: undefined },
|
|
104
104
|
},
|
|
105
105
|
helpText: {
|
|
106
106
|
control: 'text',
|
|
@@ -151,14 +151,13 @@ export const Default: Story = {
|
|
|
151
151
|
hasHiddenLabel: false,
|
|
152
152
|
hasShowPassword: true,
|
|
153
153
|
isDisabled: false,
|
|
154
|
-
|
|
155
|
-
value: '',
|
|
154
|
+
errorMessage: '',
|
|
156
155
|
},
|
|
157
156
|
render: (args) => {
|
|
158
157
|
const [value, setValue] = useState('');
|
|
159
158
|
|
|
160
159
|
useEffect(() => {
|
|
161
|
-
setValue(args.value);
|
|
160
|
+
setValue(args.value || '');
|
|
162
161
|
}, [args.value]);
|
|
163
162
|
|
|
164
163
|
const handleChange = (e: { target: { value: SetStateAction<string> } }) => {
|
|
@@ -171,13 +170,13 @@ export const Default: Story = {
|
|
|
171
170
|
export const Errors: Story = {
|
|
172
171
|
args: {
|
|
173
172
|
...defaultArgs,
|
|
174
|
-
|
|
173
|
+
errorMessage: 'You require a password value.',
|
|
175
174
|
},
|
|
176
175
|
render: (args) => {
|
|
177
176
|
const [value, setValue] = useState('');
|
|
178
177
|
|
|
179
178
|
useEffect(() => {
|
|
180
|
-
setValue(args.value);
|
|
179
|
+
setValue(args.value || '');
|
|
181
180
|
}, [args.value]);
|
|
182
181
|
|
|
183
182
|
const handleChange = (e: { target: { value: SetStateAction<string> } }) => {
|
|
@@ -197,7 +196,7 @@ export const HiddenLabel: Story = {
|
|
|
197
196
|
const [value, setValue] = useState('');
|
|
198
197
|
|
|
199
198
|
useEffect(() => {
|
|
200
|
-
setValue(args.value);
|
|
199
|
+
setValue(args.value || '');
|
|
201
200
|
}, [args.value]);
|
|
202
201
|
|
|
203
202
|
const handleChange = (e: { target: { value: SetStateAction<string> } }) => {
|
|
@@ -217,7 +216,7 @@ export const HelpText: Story = {
|
|
|
217
216
|
const [value, setValue] = useState('');
|
|
218
217
|
|
|
219
218
|
useEffect(() => {
|
|
220
|
-
setValue(args.value);
|
|
219
|
+
setValue(args.value || '');
|
|
221
220
|
}, [args.value]);
|
|
222
221
|
|
|
223
222
|
const handleChange = (e: { target: { value: SetStateAction<string> } }) => {
|
|
@@ -237,7 +236,7 @@ export const Required: Story = {
|
|
|
237
236
|
const [value, setValue] = useState('');
|
|
238
237
|
|
|
239
238
|
useEffect(() => {
|
|
240
|
-
setValue(args.value);
|
|
239
|
+
setValue(args.value || '');
|
|
241
240
|
}, [args.value]);
|
|
242
241
|
|
|
243
242
|
const handleChange = (e: { target: { value: SetStateAction<string> } }) => {
|
|
@@ -257,7 +256,7 @@ export const NoTogglePasswordVisibility: Story = {
|
|
|
257
256
|
const [value, setValue] = useState('');
|
|
258
257
|
|
|
259
258
|
useEffect(() => {
|
|
260
|
-
setValue(args.value);
|
|
259
|
+
setValue(args.value || '');
|
|
261
260
|
}, [args.value]);
|
|
262
261
|
|
|
263
262
|
const handleChange = (e: { target: { value: SetStateAction<string> } }) => {
|
|
@@ -1,82 +1,86 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import { Icon } from '@/components/icons';
|
|
3
3
|
import { Label } from '../subcomponents/Label';
|
|
4
|
-
import {
|
|
4
|
+
import { DisplayFormError } from '../subcomponents/DisplayFormError';
|
|
5
5
|
|
|
6
6
|
export interface PasswordInputProps {
|
|
7
7
|
ref?: React.LegacyRef<HTMLInputElement>;
|
|
8
8
|
label: string;
|
|
9
|
+
value?: string | undefined;
|
|
9
10
|
name: string;
|
|
10
11
|
placeholder: string;
|
|
11
|
-
value: string;
|
|
12
12
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
13
13
|
isRequired?: boolean;
|
|
14
14
|
isDisabled?: boolean;
|
|
15
|
-
|
|
15
|
+
errorMessage?: string | undefined;
|
|
16
16
|
helpText?: string;
|
|
17
17
|
hasHiddenLabel?: boolean;
|
|
18
18
|
hasShowPassword?: boolean;
|
|
19
|
+
defaultValue?: string;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
export const PasswordInput = (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
22
|
+
export const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
|
|
23
|
+
(
|
|
24
|
+
{
|
|
25
|
+
label,
|
|
26
|
+
name,
|
|
27
|
+
placeholder,
|
|
28
|
+
onChange,
|
|
29
|
+
isRequired,
|
|
30
|
+
isDisabled,
|
|
31
|
+
errorMessage,
|
|
32
|
+
helpText,
|
|
33
|
+
hasHiddenLabel,
|
|
34
|
+
hasShowPassword = true,
|
|
35
|
+
...rest
|
|
36
|
+
},
|
|
37
|
+
ref,
|
|
38
|
+
) => {
|
|
39
|
+
const hasErrors = errorMessage && errorMessage.length > 0;
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
const handleShowPassword = () => {
|
|
44
|
+
setShowPassword((prevShowPassword) => !prevShowPassword);
|
|
45
|
+
};
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
{...rest}
|
|
63
|
-
/>
|
|
64
|
-
{hasShowPassword && (
|
|
65
|
-
<Icon
|
|
66
|
-
name={showPassword ? 'fa-eye-slash' : 'eye'}
|
|
67
|
-
data-testid={`${name}-${showPassword ? 'hide' : 'show'}-password-icon`}
|
|
68
|
-
size="md"
|
|
69
|
-
onClick={handleShowPassword}
|
|
70
|
-
className="toggle-show-password-icon"
|
|
47
|
+
return (
|
|
48
|
+
<div className="form-control">
|
|
49
|
+
<Label label={label} name={name} isRequired={isRequired} hasHiddenLabel={hasHiddenLabel} />
|
|
50
|
+
<div className="password-input-wrapper">
|
|
51
|
+
<Icon name="lock" data-testid={`${name}-embedded-icon`} className="embedded-icon" />
|
|
52
|
+
<input
|
|
53
|
+
ref={ref}
|
|
54
|
+
data-testid={`form-password-input-${name}`}
|
|
55
|
+
name={name}
|
|
56
|
+
type={showPassword ? 'text' : 'password'}
|
|
57
|
+
disabled={isDisabled}
|
|
58
|
+
placeholder={placeholder}
|
|
59
|
+
onChange={onChange}
|
|
60
|
+
className={`password-input ${hasErrors ? 'error' : ''} password-input--has-icon`}
|
|
61
|
+
aria-invalid={hasErrors ? 'true' : 'false'}
|
|
62
|
+
aria-describedby={hasErrors || helpText ? `${name}-helper` : undefined}
|
|
63
|
+
aria-required={isRequired}
|
|
64
|
+
aria-label={label}
|
|
65
|
+
{...rest}
|
|
71
66
|
/>
|
|
67
|
+
{hasShowPassword && (
|
|
68
|
+
<Icon
|
|
69
|
+
name={showPassword ? 'fa-eye-slash' : 'eye'}
|
|
70
|
+
data-testid={`${name}-${showPassword ? 'hide' : 'show'}-password-icon`}
|
|
71
|
+
size="md"
|
|
72
|
+
onClick={handleShowPassword}
|
|
73
|
+
className="toggle-show-password-icon"
|
|
74
|
+
/>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
{hasErrors && <DisplayFormError message={errorMessage} />}
|
|
78
|
+
{helpText && (
|
|
79
|
+
<div data-testid={`${name}-help-text`} className="help-text" id={`${name}-helper`}>
|
|
80
|
+
{helpText}
|
|
81
|
+
</div>
|
|
72
82
|
)}
|
|
73
83
|
</div>
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
{helpText}
|
|
78
|
-
</div>
|
|
79
|
-
)}
|
|
80
|
-
</div>
|
|
81
|
-
);
|
|
82
|
-
};
|
|
84
|
+
);
|
|
85
|
+
},
|
|
86
|
+
);
|
|
@@ -48,7 +48,7 @@ describe('Input', () => {
|
|
|
48
48
|
render(
|
|
49
49
|
<PasswordInput
|
|
50
50
|
isRequired
|
|
51
|
-
|
|
51
|
+
errorMessage="You require a username value."
|
|
52
52
|
label="Enter your name"
|
|
53
53
|
helpText="In order to submit the form, this field is required."
|
|
54
54
|
name="name"
|
|
@@ -8,40 +8,37 @@ export interface RadioProps {
|
|
|
8
8
|
value?: string;
|
|
9
9
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
10
10
|
isDisabled?: boolean;
|
|
11
|
+
defaultChecked?: boolean;
|
|
11
12
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
/>
|
|
41
|
-
<label htmlFor={id} className="radio-input-label" data-testid={`label-radio-input-${name}`}>
|
|
42
|
-
{label}
|
|
43
|
-
</label>
|
|
13
|
+
export const Radio = React.forwardRef<HTMLInputElement, RadioProps>(
|
|
14
|
+
({ id, label, name, value, onChange, isDisabled, ...rest }, ref) => {
|
|
15
|
+
return (
|
|
16
|
+
<div className="form-control">
|
|
17
|
+
<div className="radio-wrapper">
|
|
18
|
+
<input
|
|
19
|
+
data-testid={`form-radio-input-${name}`}
|
|
20
|
+
className="radio-input"
|
|
21
|
+
type="radio"
|
|
22
|
+
id={id}
|
|
23
|
+
name={name}
|
|
24
|
+
value={value}
|
|
25
|
+
disabled={isDisabled}
|
|
26
|
+
ref={ref}
|
|
27
|
+
onChange={onChange}
|
|
28
|
+
tabIndex={0}
|
|
29
|
+
aria-describedby={id}
|
|
30
|
+
aria-label={label}
|
|
31
|
+
{...rest}
|
|
32
|
+
/>
|
|
33
|
+
<label
|
|
34
|
+
htmlFor={id}
|
|
35
|
+
className="radio-input-label"
|
|
36
|
+
data-testid={`label-radio-input-${name}`}
|
|
37
|
+
>
|
|
38
|
+
{label}
|
|
39
|
+
</label>
|
|
40
|
+
</div>
|
|
44
41
|
</div>
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
);
|
|
43
|
+
},
|
|
44
|
+
);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Select, SelectProps } from './Select';
|
|
3
|
+
import { SelectOption } from './types';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<SelectProps<SelectOption>> = {
|
|
6
|
+
title: 'Forms/Select',
|
|
7
|
+
component: Select,
|
|
8
|
+
argTypes: {
|
|
9
|
+
options: {
|
|
10
|
+
control: 'object',
|
|
11
|
+
description: 'Options for the select component',
|
|
12
|
+
table: {
|
|
13
|
+
category: 'Props',
|
|
14
|
+
type: {
|
|
15
|
+
summary: '{ value: string, label: string, detail?: string }[]',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
defaultValue: { summary: '[]' },
|
|
19
|
+
},
|
|
20
|
+
isDisabled: {
|
|
21
|
+
control: 'boolean',
|
|
22
|
+
description: 'Toggles the disabled state of the select component',
|
|
23
|
+
table: {
|
|
24
|
+
category: 'Props',
|
|
25
|
+
type: {
|
|
26
|
+
summary: 'boolean',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
defaultValue: { summary: 'false' },
|
|
30
|
+
},
|
|
31
|
+
isLoading: {
|
|
32
|
+
control: 'boolean',
|
|
33
|
+
description: 'Toggles the loading state of the select component',
|
|
34
|
+
table: {
|
|
35
|
+
category: 'Props',
|
|
36
|
+
type: {
|
|
37
|
+
summary: 'boolean',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
defaultValue: { summary: 'false' },
|
|
41
|
+
},
|
|
42
|
+
isClearable: {
|
|
43
|
+
control: 'boolean',
|
|
44
|
+
description: 'Enables the clearable feature of the select component',
|
|
45
|
+
table: {
|
|
46
|
+
category: 'Props',
|
|
47
|
+
type: {
|
|
48
|
+
summary: 'boolean',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
defaultValue: { summary: 'false' },
|
|
52
|
+
},
|
|
53
|
+
isSearchable: {
|
|
54
|
+
control: 'boolean',
|
|
55
|
+
description: 'Enables the searchable feature of the select component',
|
|
56
|
+
table: {
|
|
57
|
+
category: 'Props',
|
|
58
|
+
type: {
|
|
59
|
+
summary: 'boolean',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
defaultValue: { summary: 'true' },
|
|
63
|
+
},
|
|
64
|
+
placeholder: {
|
|
65
|
+
control: 'text',
|
|
66
|
+
description: 'The placeholder text for the select component',
|
|
67
|
+
table: {
|
|
68
|
+
category: 'Props',
|
|
69
|
+
type: {
|
|
70
|
+
summary: 'string',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
defaultValue: { summary: 'Select...' },
|
|
74
|
+
},
|
|
75
|
+
className: {
|
|
76
|
+
control: 'text',
|
|
77
|
+
description: 'Additional CSS class for the select component',
|
|
78
|
+
table: {
|
|
79
|
+
category: 'Props',
|
|
80
|
+
type: {
|
|
81
|
+
summary: 'string',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
defaultValue: { summary: '' },
|
|
85
|
+
},
|
|
86
|
+
onChange: {
|
|
87
|
+
control: false,
|
|
88
|
+
description: 'Event handler for when the selected value changes',
|
|
89
|
+
table: {
|
|
90
|
+
category: 'Callbacks',
|
|
91
|
+
type: {
|
|
92
|
+
summary:
|
|
93
|
+
'(newValue: SingleValue<SelectOption> | MultiValue<SelectOption>, actionMeta: ActionMeta<SelectOption>) => void',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
action: 'onChange',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export default meta;
|
|
102
|
+
|
|
103
|
+
type Story = StoryObj<SelectProps<SelectOption>>;
|
|
104
|
+
|
|
105
|
+
export const Default: Story = {
|
|
106
|
+
args: {
|
|
107
|
+
options: [
|
|
108
|
+
{ value: 'option1', label: 'Option 1', detail: '123 Count' },
|
|
109
|
+
{ value: 'option2', label: 'Option 2', detail: '456 Count' },
|
|
110
|
+
{ value: 'option3', label: 'Option 3', detail: '789 Count' },
|
|
111
|
+
],
|
|
112
|
+
placeholder: 'Select an option...',
|
|
113
|
+
isClearable: false,
|
|
114
|
+
isSearchable: true,
|
|
115
|
+
isDisabled: false,
|
|
116
|
+
isLoading: false,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import ReactSelect, { Props as ReactSelectProps, components, OptionProps } from 'react-select';
|
|
4
|
+
import { SelectOption } from './types';
|
|
5
|
+
|
|
6
|
+
export interface SelectProps<OptionType extends SelectOption> extends ReactSelectProps<OptionType> {
|
|
7
|
+
options: OptionType[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const OptionComponent = <OptionType extends SelectOption>({
|
|
11
|
+
...props
|
|
12
|
+
}: OptionProps<OptionType>) => {
|
|
13
|
+
return (
|
|
14
|
+
<components.Option {...props}>
|
|
15
|
+
<div className="select__items">
|
|
16
|
+
<div className="select__item-value">{props?.data?.label}</div>
|
|
17
|
+
{props?.data?.detail && <div className="select__item-detail">{props?.data?.detail}</div>}
|
|
18
|
+
</div>
|
|
19
|
+
</components.Option>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const Select = <OptionType extends SelectOption>({
|
|
24
|
+
classNamePrefix = 'select',
|
|
25
|
+
className,
|
|
26
|
+
components: customComponents,
|
|
27
|
+
...props
|
|
28
|
+
}: SelectProps<OptionType>) => {
|
|
29
|
+
const defaultComponents = {
|
|
30
|
+
Option: OptionComponent as React.ComponentType<OptionProps<OptionType>>,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const mergedComponents = { ...defaultComponents, ...customComponents };
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<ReactSelect
|
|
37
|
+
classNamePrefix={classNamePrefix}
|
|
38
|
+
className={classNames('select-wrapper', className)}
|
|
39
|
+
components={mergedComponents}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
|
+
|
|
3
|
+
import { Select } from '../Select';
|
|
4
|
+
import { SelectOption } from '../types';
|
|
5
|
+
import { OptionProps, components } from 'react-select';
|
|
6
|
+
|
|
7
|
+
const options: SelectOption[] = [
|
|
8
|
+
{ value: 'option1', label: 'Option 1' },
|
|
9
|
+
{ value: 'option2', label: 'Option 2' },
|
|
10
|
+
{ value: 'option3', label: 'Option 3' },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
describe('Select Component', () => {
|
|
14
|
+
it('displays options correctly', () => {
|
|
15
|
+
render(<Select options={options} menuIsOpen />);
|
|
16
|
+
options.forEach((option) => {
|
|
17
|
+
expect(screen.getByText(option.label)).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('handles onChange event', () => {
|
|
22
|
+
const handleChange = jest.fn();
|
|
23
|
+
render(<Select options={options} menuIsOpen onChange={handleChange} />);
|
|
24
|
+
const option = screen.getByText('Option 1');
|
|
25
|
+
fireEvent.click(option);
|
|
26
|
+
expect(handleChange).toHaveBeenCalledWith(options[0], expect.any(Object));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('applies custom class names', () => {
|
|
30
|
+
const customClassName = 'custom-select-class';
|
|
31
|
+
render(<Select options={options} className={customClassName} />);
|
|
32
|
+
const selectWrapper = screen.getByRole('combobox').closest('.select-wrapper');
|
|
33
|
+
expect(selectWrapper).toHaveClass(customClassName);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('applies custom class name prefix', () => {
|
|
37
|
+
const customClassNamePrefix = 'custom-prefix';
|
|
38
|
+
render(<Select options={options} classNamePrefix={customClassNamePrefix} />);
|
|
39
|
+
const selectWrapper = screen.getByRole('combobox');
|
|
40
|
+
expect(selectWrapper).toHaveClass(`${customClassNamePrefix}__input`);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('displays detail when present', () => {
|
|
44
|
+
const optionsWithDetail = options.map((option, index) => ({
|
|
45
|
+
...option,
|
|
46
|
+
detail: `${index + 1}23 Count`,
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
render(<Select options={optionsWithDetail} menuIsOpen />);
|
|
50
|
+
optionsWithDetail.forEach((option) => {
|
|
51
|
+
expect(screen.getByText(option.detail)).toBeInTheDocument();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('uses custom components', () => {
|
|
56
|
+
const oneOption = options.slice(0, 1);
|
|
57
|
+
const CustomOptionComponent = (props: OptionProps<SelectOption>) => (
|
|
58
|
+
<div data-testid="custom-option">{props.data.label}</div>
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
render(
|
|
62
|
+
<Select options={oneOption} components={{ Option: CustomOptionComponent }} menuIsOpen />,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(screen.getByTestId('custom-option')).toHaveTextContent(oneOption[0].label);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Select } from './Select';
|