@indico-data/design-system 2.4.2 → 2.6.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/.storybook/preview.ts +4 -14
- package/lib/index.css +208 -0
- package/lib/index.d.ts +31 -3
- package/lib/index.esm.css +208 -0
- package/lib/index.esm.js +23 -1
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +24 -0
- package/lib/index.js.map +1 -1
- package/lib/src/components/forms/input/Input.d.ts +18 -0
- package/lib/src/components/forms/input/Input.stories.d.ts +12 -0
- package/lib/src/components/forms/input/__tests__/Input.test.d.ts +1 -0
- package/lib/src/components/forms/input/index.d.ts +1 -0
- package/lib/src/components/forms/radio/Radio.d.ts +11 -0
- package/lib/src/components/forms/radio/Radio.stories.d.ts +6 -0
- package/lib/src/components/forms/radio/__tests__/Radio.test.d.ts +1 -0
- package/lib/src/components/forms/radio/index.d.ts +1 -0
- package/lib/src/components/forms/subcomponents/ErrorList.d.ts +6 -0
- package/lib/src/components/forms/subcomponents/Label.d.ts +8 -0
- package/lib/src/components/forms/subcomponents/__tests__/ErrorList.test.d.ts +1 -0
- package/lib/src/components/forms/subcomponents/__tests__/Label.test.d.ts +1 -0
- package/lib/src/components/index.d.ts +2 -0
- package/lib/src/index.d.ts +2 -0
- package/package.json +1 -1
- package/src/components/forms/input/Input.mdx +19 -0
- package/src/components/forms/input/Input.stories.tsx +301 -0
- package/src/components/forms/input/Input.tsx +86 -0
- package/src/components/forms/input/__tests__/Input.test.tsx +213 -0
- package/src/components/forms/input/index.ts +1 -0
- package/src/components/forms/input/styles/Input.scss +112 -0
- package/src/components/forms/radio/Radio.mdx +83 -0
- package/src/components/forms/radio/Radio.stories.tsx +121 -0
- package/src/components/forms/radio/Radio.tsx +47 -0
- package/src/components/forms/radio/__tests__/Radio.test.tsx +35 -0
- package/src/components/forms/radio/index.ts +1 -0
- package/src/components/forms/radio/styles/Radio.scss +98 -0
- package/src/components/forms/subcomponents/ErrorList.tsx +14 -0
- package/src/components/forms/subcomponents/Label.tsx +20 -0
- package/src/components/forms/subcomponents/__tests__/ErrorList.test.tsx +16 -0
- package/src/components/forms/subcomponents/__tests__/Label.test.tsx +33 -0
- package/src/components/index.ts +2 -0
- package/src/index.ts +2 -0
- package/src/styles/_typography.scss +29 -11
- package/src/styles/index.scss +2 -0
- package/src/styles/storybook.scss +15 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { render, screen, act } from '@testing-library/react';
|
|
2
|
+
import { Input } from '@/components/forms/input/Input';
|
|
3
|
+
import { ChangeEvent } from 'react';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
5
|
+
|
|
6
|
+
const handleOnChange = jest.fn();
|
|
7
|
+
|
|
8
|
+
describe('Input', () => {
|
|
9
|
+
it('renders the input field', () => {
|
|
10
|
+
render(
|
|
11
|
+
<Input
|
|
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
|
+
iconName="user"
|
|
18
|
+
isClearable={true}
|
|
19
|
+
ref={undefined}
|
|
20
|
+
value={''}
|
|
21
|
+
onChange={handleOnChange}
|
|
22
|
+
/>,
|
|
23
|
+
);
|
|
24
|
+
expect(screen.getByText('Enter your name')).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
it('shows an x when the text is clearable', () => {
|
|
27
|
+
render(
|
|
28
|
+
<Input
|
|
29
|
+
isRequired={true}
|
|
30
|
+
label="Enter your name"
|
|
31
|
+
helpText="In order to submit the form, this field is required."
|
|
32
|
+
name="name"
|
|
33
|
+
placeholder="Please enter a value"
|
|
34
|
+
iconName="user"
|
|
35
|
+
isClearable={true}
|
|
36
|
+
ref={undefined}
|
|
37
|
+
value={''}
|
|
38
|
+
onChange={handleOnChange}
|
|
39
|
+
/>,
|
|
40
|
+
);
|
|
41
|
+
const icon = screen.getByTestId('name-clearable-icon');
|
|
42
|
+
expect(icon).toBeInTheDocument();
|
|
43
|
+
expect(icon).toBeVisible();
|
|
44
|
+
});
|
|
45
|
+
it('does not show an x when the text is not clearable', () => {
|
|
46
|
+
render(
|
|
47
|
+
<Input
|
|
48
|
+
isRequired={true}
|
|
49
|
+
label="Enter your name"
|
|
50
|
+
helpText="In order to submit the form, this field is required."
|
|
51
|
+
name="name"
|
|
52
|
+
placeholder="Please enter a value"
|
|
53
|
+
iconName="user"
|
|
54
|
+
isClearable={false}
|
|
55
|
+
ref={undefined}
|
|
56
|
+
value={''}
|
|
57
|
+
onChange={handleOnChange}
|
|
58
|
+
/>,
|
|
59
|
+
);
|
|
60
|
+
const icon = screen.queryByTestId('name-clearable-icon');
|
|
61
|
+
expect(icon).not.toBeInTheDocument();
|
|
62
|
+
expect(icon).toBe(null);
|
|
63
|
+
});
|
|
64
|
+
it('clicking on the x clears the value', async () => {
|
|
65
|
+
render(
|
|
66
|
+
<Input
|
|
67
|
+
isRequired={true}
|
|
68
|
+
label="Enter your name"
|
|
69
|
+
helpText="In order to submit the form, this field is required."
|
|
70
|
+
name="name"
|
|
71
|
+
placeholder="Please enter a value"
|
|
72
|
+
iconName="user"
|
|
73
|
+
isClearable={true}
|
|
74
|
+
ref={undefined}
|
|
75
|
+
value={'test'}
|
|
76
|
+
onChange={handleOnChange}
|
|
77
|
+
/>,
|
|
78
|
+
);
|
|
79
|
+
const input = screen.getByTestId('form-input-name');
|
|
80
|
+
const icon = screen.getByTestId('name-clearable-icon');
|
|
81
|
+
expect(input).toHaveValue('test');
|
|
82
|
+
await userEvent.click(icon);
|
|
83
|
+
expect(handleOnChange).toHaveBeenCalledWith({
|
|
84
|
+
target: { value: '' },
|
|
85
|
+
} as ChangeEvent<HTMLInputElement>);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('it renders an icon on the left when one exists', () => {
|
|
89
|
+
render(
|
|
90
|
+
<Input
|
|
91
|
+
isRequired={true}
|
|
92
|
+
label="Enter your name"
|
|
93
|
+
helpText="In order to submit the form, this field is required."
|
|
94
|
+
name="name"
|
|
95
|
+
placeholder="Please enter a value"
|
|
96
|
+
iconName="user"
|
|
97
|
+
isClearable={true}
|
|
98
|
+
ref={undefined}
|
|
99
|
+
value={'test'}
|
|
100
|
+
onChange={handleOnChange}
|
|
101
|
+
/>,
|
|
102
|
+
);
|
|
103
|
+
const icon = screen.getByTestId('name-embedded-icon');
|
|
104
|
+
expect(icon).toBeInTheDocument();
|
|
105
|
+
expect(icon).toBeVisible();
|
|
106
|
+
});
|
|
107
|
+
it('it does not render an embedded icon when one does not exist', () => {
|
|
108
|
+
render(
|
|
109
|
+
<Input
|
|
110
|
+
isRequired={true}
|
|
111
|
+
label="Enter your name"
|
|
112
|
+
helpText="In order to submit the form, this field is required."
|
|
113
|
+
name="name"
|
|
114
|
+
placeholder="Please enter a value"
|
|
115
|
+
isClearable={true}
|
|
116
|
+
ref={undefined}
|
|
117
|
+
value={'test'}
|
|
118
|
+
onChange={handleOnChange}
|
|
119
|
+
/>,
|
|
120
|
+
);
|
|
121
|
+
const icon = screen.queryByTestId('name-embedded-icon');
|
|
122
|
+
expect(icon).not.toBeInTheDocument();
|
|
123
|
+
expect(icon).toBe(null);
|
|
124
|
+
});
|
|
125
|
+
it('adds the error class when errors exist', () => {
|
|
126
|
+
render(
|
|
127
|
+
<Input
|
|
128
|
+
isRequired={true}
|
|
129
|
+
errorList={['You require a username value.']}
|
|
130
|
+
label="Enter your name"
|
|
131
|
+
helpText="In order to submit the form, this field is required."
|
|
132
|
+
name="name"
|
|
133
|
+
placeholder="Please enter a value"
|
|
134
|
+
isClearable={true}
|
|
135
|
+
ref={undefined}
|
|
136
|
+
value={'test'}
|
|
137
|
+
onChange={handleOnChange}
|
|
138
|
+
/>,
|
|
139
|
+
);
|
|
140
|
+
const input = screen.getByTestId('form-input-name');
|
|
141
|
+
expect(input).toHaveClass('error');
|
|
142
|
+
});
|
|
143
|
+
it('does not highlight the input when no errors exist', () => {
|
|
144
|
+
render(
|
|
145
|
+
<Input
|
|
146
|
+
isRequired={true}
|
|
147
|
+
label="Enter your name"
|
|
148
|
+
helpText="In order to submit the form, this field is required."
|
|
149
|
+
name="name"
|
|
150
|
+
placeholder="Please enter a value"
|
|
151
|
+
isClearable={true}
|
|
152
|
+
ref={undefined}
|
|
153
|
+
value={'test'}
|
|
154
|
+
onChange={handleOnChange}
|
|
155
|
+
/>,
|
|
156
|
+
);
|
|
157
|
+
const input = screen.getByTestId('form-input-name');
|
|
158
|
+
expect(input).not.toHaveClass('error');
|
|
159
|
+
});
|
|
160
|
+
it('renders help text when help text exists', () => {
|
|
161
|
+
render(
|
|
162
|
+
<Input
|
|
163
|
+
isRequired={true}
|
|
164
|
+
label="Enter your name"
|
|
165
|
+
helpText="In order to submit the form, this field is required."
|
|
166
|
+
name="name"
|
|
167
|
+
placeholder="Please enter a value"
|
|
168
|
+
isClearable={true}
|
|
169
|
+
ref={undefined}
|
|
170
|
+
value={'test'}
|
|
171
|
+
onChange={handleOnChange}
|
|
172
|
+
/>,
|
|
173
|
+
);
|
|
174
|
+
const helpText = screen.getByText('In order to submit the form, this field is required.');
|
|
175
|
+
expect(helpText).toBeInTheDocument();
|
|
176
|
+
expect(helpText).toBeVisible();
|
|
177
|
+
});
|
|
178
|
+
it('does not render help text when help text does not exist', () => {
|
|
179
|
+
render(
|
|
180
|
+
<Input
|
|
181
|
+
isRequired={true}
|
|
182
|
+
label="Enter your name"
|
|
183
|
+
name="name"
|
|
184
|
+
placeholder="Please enter a value"
|
|
185
|
+
isClearable={true}
|
|
186
|
+
ref={undefined}
|
|
187
|
+
value={'test'}
|
|
188
|
+
onChange={handleOnChange}
|
|
189
|
+
/>,
|
|
190
|
+
);
|
|
191
|
+
const helpText = screen.queryByTestId('name-help-text');
|
|
192
|
+
expect(helpText).not.toBeInTheDocument();
|
|
193
|
+
expect(helpText).toBeNull();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('emits the value when user types', async () => {
|
|
197
|
+
const handleOnChange = jest.fn();
|
|
198
|
+
render(
|
|
199
|
+
<Input
|
|
200
|
+
isRequired={true}
|
|
201
|
+
label="Enter your name"
|
|
202
|
+
name="name"
|
|
203
|
+
placeholder="Please enter a value"
|
|
204
|
+
ref={undefined}
|
|
205
|
+
value={''}
|
|
206
|
+
onChange={handleOnChange}
|
|
207
|
+
/>,
|
|
208
|
+
);
|
|
209
|
+
const input = screen.getByTestId('form-input-name');
|
|
210
|
+
await userEvent.type(input, 't');
|
|
211
|
+
expect(handleOnChange).toHaveBeenCalled();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Input } from './Input';
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Common Variables
|
|
2
|
+
:root,
|
|
3
|
+
:root [data-theme='light'],
|
|
4
|
+
:root [data-theme='dark'] {
|
|
5
|
+
// Typography
|
|
6
|
+
--pf-input-background-color: var(--pf-white-color);
|
|
7
|
+
--pf-input-border-color: var(--pf-gray-color);
|
|
8
|
+
--pf-input-text-color: var(--pf-gray-color);
|
|
9
|
+
--pf-input-placeholder-text-color: var(--pf-gray-color-300);
|
|
10
|
+
--pf-input-help-text-color: var(--pf-gray-color-400);
|
|
11
|
+
|
|
12
|
+
// input Radius
|
|
13
|
+
--pf-input-rounded: var(--pf-rounded);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Dark Theme Specific Variables
|
|
17
|
+
:root [data-theme='dark'] {
|
|
18
|
+
--pf-input-background-color: var(--pf-primary-color);
|
|
19
|
+
--pf-input-border-color: var(--pf-gray-color-100);
|
|
20
|
+
--pf-input-text-color: var(--pf-gray-color-100);
|
|
21
|
+
--pf-input-placeholder-text-color: var(--pf-gray-color);
|
|
22
|
+
--pf-input-help-text-color: var(--pf-gray-color-200);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.input {
|
|
26
|
+
background-color: var(--pf-input-background-color);
|
|
27
|
+
border: 1px solid var(--pf-input-border-color);
|
|
28
|
+
border-radius: var(--pf-input-rounded);
|
|
29
|
+
color: var(--pf-input-text-color);
|
|
30
|
+
padding: 10px;
|
|
31
|
+
width: 100%;
|
|
32
|
+
box-sizing: border-box;
|
|
33
|
+
height: 36px;
|
|
34
|
+
&::placeholder {
|
|
35
|
+
color: var(--pf-input-placeholder-text-color);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
&:focus {
|
|
39
|
+
border-color: var(--pf-primary-color);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
&.error {
|
|
43
|
+
border-color: var(--pf-error-color);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
&.success {
|
|
47
|
+
border-color: var(--pf-success-color);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
&.warning {
|
|
51
|
+
border-color: var(--pf-warning-color);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
&.info {
|
|
55
|
+
border-color: var(--pf-info-color);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
&:disabled {
|
|
59
|
+
background-color: var(--pf-gray-color-100);
|
|
60
|
+
border-color: var(--pf-gray-color-300);
|
|
61
|
+
color: var(--pf-gray-color-400);
|
|
62
|
+
}
|
|
63
|
+
&--has-icon {
|
|
64
|
+
padding-left: var(--pf-padding-7);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.form-control {
|
|
69
|
+
.error-list {
|
|
70
|
+
list-style: none;
|
|
71
|
+
padding: 0;
|
|
72
|
+
margin: 0;
|
|
73
|
+
margin-top: var(--pf-margin-2);
|
|
74
|
+
margin-bottom: var(--pf-margin-2);
|
|
75
|
+
color: var(--pf-error-color);
|
|
76
|
+
}
|
|
77
|
+
.help-text {
|
|
78
|
+
margin-top: var(--pf-margin-2);
|
|
79
|
+
margin-bottom: var(--pf-margin-2);
|
|
80
|
+
color: var(--pf-input-help-text-color);
|
|
81
|
+
font-size: var(--pf-font-size-subtitle2);
|
|
82
|
+
}
|
|
83
|
+
.input-wrapper {
|
|
84
|
+
position: relative;
|
|
85
|
+
.embedded-icon {
|
|
86
|
+
position: absolute;
|
|
87
|
+
top: 10px;
|
|
88
|
+
left: var(--pf-margin-2);
|
|
89
|
+
color: var(--pf-input-text-color);
|
|
90
|
+
}
|
|
91
|
+
.clearable-icon {
|
|
92
|
+
position: absolute;
|
|
93
|
+
top: var(--pf-margin-3);
|
|
94
|
+
right: var(--pf-margin-2);
|
|
95
|
+
color: var(--pf-input-text-color);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
.is-visually-hidden {
|
|
99
|
+
position: absolute;
|
|
100
|
+
width: 1px;
|
|
101
|
+
height: 1px;
|
|
102
|
+
padding: 0;
|
|
103
|
+
margin: -1px;
|
|
104
|
+
overflow: hidden;
|
|
105
|
+
clip: rect(0, 0, 0, 0);
|
|
106
|
+
white-space: nowrap;
|
|
107
|
+
border: 0;
|
|
108
|
+
}
|
|
109
|
+
.form-label {
|
|
110
|
+
margin-bottom: var(--pf-margin-2);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Canvas, Meta, Controls } from '@storybook/blocks';
|
|
2
|
+
import * as Radio from './Radio.stories';
|
|
3
|
+
|
|
4
|
+
<Meta title="Forms/Radio" name="Radio" />
|
|
5
|
+
|
|
6
|
+
# Radio
|
|
7
|
+
This component is intended to use as part of a group. You would apply multiple radio button components and they share the same ID field to form a group.
|
|
8
|
+
|
|
9
|
+
<Canvas of={Radio.Default} source={{
|
|
10
|
+
code: `
|
|
11
|
+
const radioList = [
|
|
12
|
+
{ id: 'one', label: 'Radio Label One', name: 'radio', value: 'radio' },
|
|
13
|
+
{ id: 'two', label: 'Radio Label Two', name: 'radio', value: 'radio' },
|
|
14
|
+
{ id: 'three', label: 'Radio Label Three', name: 'radio', value: 'radio' },
|
|
15
|
+
];
|
|
16
|
+
<h2 className="mb-2">Radio Buttons</h2>
|
|
17
|
+
{radioList.map((radio) => (
|
|
18
|
+
<Radio
|
|
19
|
+
key={radio.id}
|
|
20
|
+
onChange={handleChange}
|
|
21
|
+
id={radio.id}
|
|
22
|
+
label={radio.label}
|
|
23
|
+
name={radio.name}
|
|
24
|
+
value={radio.value}
|
|
25
|
+
isDisabled={args.isDisabled}
|
|
26
|
+
/>
|
|
27
|
+
))}
|
|
28
|
+
`,
|
|
29
|
+
}} />
|
|
30
|
+
<Controls of={Radio.Default} />
|
|
31
|
+
|
|
32
|
+
## Handling Required State
|
|
33
|
+
The radio component does not have a required state. If you need to enforce a required state. This will be managed in a parent component or wrapper element which handles the state. You will need to verify against a value that matches the ID group for the radio buttons and make sure it is not null.
|
|
34
|
+
|
|
35
|
+
### Rough code example
|
|
36
|
+
This is a temporary measure until we build our own component to manage this state.
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
const RadioGroupForm = () => {
|
|
40
|
+
const [selectedValue, setSelectedValue] = useState('');
|
|
41
|
+
const [isSubmittedWithoutSelection, setIsSubmittedWithoutSelection] = useState(false);
|
|
42
|
+
|
|
43
|
+
const handleSubmit = (e) => {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
if (!selectedValue) {
|
|
46
|
+
setIsSubmittedWithoutSelection(true);
|
|
47
|
+
} else {
|
|
48
|
+
setIsSubmittedWithoutSelection(false);
|
|
49
|
+
console.log('Form submitted with selected value:', selectedValue);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const radioList = [
|
|
54
|
+
{ id: 'one', label: 'Radio Label One', name: 'radio', value: 'radio' },
|
|
55
|
+
{ id: 'two', label: 'Radio Label Two', name: 'radio', value: 'radio' },
|
|
56
|
+
{ id: 'three', label: 'Radio Label Three', name: 'radio', value: 'radio' },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const handleChange = (e) => {
|
|
60
|
+
setSelectedValue(e.target.value);
|
|
61
|
+
if (isSubmittedWithoutSelection) {
|
|
62
|
+
setIsSubmittedWithoutSelection(false);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<form onSubmit={handleSubmit}>
|
|
68
|
+
{radioList.map((radio) => (
|
|
69
|
+
<Radio
|
|
70
|
+
key={radio.id}
|
|
71
|
+
onChange={handleChange}
|
|
72
|
+
id={radio.id}
|
|
73
|
+
label={radio.label}
|
|
74
|
+
name={radio.name}
|
|
75
|
+
value={radio.value}
|
|
76
|
+
isDisabled={args.isDisabled}
|
|
77
|
+
/>
|
|
78
|
+
))}
|
|
79
|
+
<button type="submit">Submit</button>
|
|
80
|
+
</form>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
```
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Radio, RadioProps } from './Radio';
|
|
3
|
+
import { SetStateAction, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
const meta: Meta = {
|
|
6
|
+
title: 'Forms/Radio',
|
|
7
|
+
component: Radio,
|
|
8
|
+
argTypes: {
|
|
9
|
+
ref: {
|
|
10
|
+
table: {
|
|
11
|
+
disable: true,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
onChange: {
|
|
15
|
+
control: false,
|
|
16
|
+
description: 'onChange event handler',
|
|
17
|
+
table: {
|
|
18
|
+
category: 'Callbacks',
|
|
19
|
+
type: {
|
|
20
|
+
summary: '(e: React.ChangeEvent<HTMLInputElement>) => void',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
action: 'onChange',
|
|
24
|
+
},
|
|
25
|
+
label: {
|
|
26
|
+
control: 'text',
|
|
27
|
+
description: 'The label for the Radio field',
|
|
28
|
+
table: {
|
|
29
|
+
category: 'Props',
|
|
30
|
+
type: {
|
|
31
|
+
summary: 'string',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
defaultValue: { summary: '' },
|
|
35
|
+
},
|
|
36
|
+
name: {
|
|
37
|
+
control: 'text',
|
|
38
|
+
description: 'The name for the Radio field',
|
|
39
|
+
table: {
|
|
40
|
+
category: 'Props',
|
|
41
|
+
type: {
|
|
42
|
+
summary: 'string',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
defaultValue: { summary: '' },
|
|
46
|
+
},
|
|
47
|
+
value: {
|
|
48
|
+
control: 'text',
|
|
49
|
+
description: 'This holds the value that will be emitted when the radio is selected',
|
|
50
|
+
table: {
|
|
51
|
+
category: 'Props',
|
|
52
|
+
type: {
|
|
53
|
+
summary: 'string',
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
defaultValue: { summary: null },
|
|
57
|
+
},
|
|
58
|
+
id: {
|
|
59
|
+
control: 'text',
|
|
60
|
+
description: 'This explains what button group this radio belongs to.',
|
|
61
|
+
table: {
|
|
62
|
+
category: 'Props',
|
|
63
|
+
type: {
|
|
64
|
+
summary: 'string',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
defaultValue: { summary: '' },
|
|
68
|
+
},
|
|
69
|
+
isDisabled: {
|
|
70
|
+
control: 'boolean',
|
|
71
|
+
description: 'Toggles the disabled state of the Radio field',
|
|
72
|
+
table: {
|
|
73
|
+
category: 'Props',
|
|
74
|
+
type: {
|
|
75
|
+
summary: 'boolean',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
defaultValue: { summary: false },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export default meta;
|
|
84
|
+
|
|
85
|
+
type Story = StoryObj<typeof Radio>;
|
|
86
|
+
|
|
87
|
+
const radioList = [
|
|
88
|
+
{ id: 'one', label: 'Radio Label One', name: 'radio', value: 'radio' },
|
|
89
|
+
{ id: 'two', label: 'Radio Label Two', name: 'radio', value: 'radio' },
|
|
90
|
+
{ id: 'three', label: 'Radio Label Three', name: 'radio', value: 'radio' },
|
|
91
|
+
];
|
|
92
|
+
export const Default: Story = {
|
|
93
|
+
args: {
|
|
94
|
+
id: 'one',
|
|
95
|
+
label: 'Radio Label',
|
|
96
|
+
name: 'radio',
|
|
97
|
+
value: 'radio',
|
|
98
|
+
isDisabled: false,
|
|
99
|
+
},
|
|
100
|
+
render: (args) => {
|
|
101
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
102
|
+
console.log(e.target.value);
|
|
103
|
+
};
|
|
104
|
+
return (
|
|
105
|
+
<div>
|
|
106
|
+
<h2 className="mb-2">Radio Buttons</h2>
|
|
107
|
+
{radioList.map((radio) => (
|
|
108
|
+
<Radio
|
|
109
|
+
key={radio.id}
|
|
110
|
+
onChange={handleChange}
|
|
111
|
+
id={radio.id}
|
|
112
|
+
label={radio.label}
|
|
113
|
+
name={radio.name}
|
|
114
|
+
value={radio.value}
|
|
115
|
+
isDisabled={args.isDisabled}
|
|
116
|
+
/>
|
|
117
|
+
))}
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
},
|
|
121
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export interface RadioProps {
|
|
4
|
+
ref?: React.LegacyRef<HTMLInputElement>;
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
name: string;
|
|
8
|
+
value?: string;
|
|
9
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
10
|
+
isDisabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const Radio = ({
|
|
14
|
+
ref,
|
|
15
|
+
id,
|
|
16
|
+
label,
|
|
17
|
+
name,
|
|
18
|
+
value,
|
|
19
|
+
onChange,
|
|
20
|
+
isDisabled,
|
|
21
|
+
...rest
|
|
22
|
+
}: RadioProps) => {
|
|
23
|
+
return (
|
|
24
|
+
<div className="form-control">
|
|
25
|
+
<div className="radio-wrapper">
|
|
26
|
+
<input
|
|
27
|
+
data-testid={`form-radio-input-${name}`}
|
|
28
|
+
{...rest}
|
|
29
|
+
className="radio-input"
|
|
30
|
+
type="radio"
|
|
31
|
+
id={id}
|
|
32
|
+
name={name}
|
|
33
|
+
value={value}
|
|
34
|
+
disabled={isDisabled}
|
|
35
|
+
ref={ref}
|
|
36
|
+
onChange={onChange}
|
|
37
|
+
tabIndex={0}
|
|
38
|
+
aria-describedby={id}
|
|
39
|
+
aria-label={label}
|
|
40
|
+
/>
|
|
41
|
+
<label htmlFor={id} className="radio-input-label" data-testid={`label-radio-input-${name}`}>
|
|
42
|
+
{label}
|
|
43
|
+
</label>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { render, screen, act } from '@testing-library/react';
|
|
2
|
+
import { Radio } from '@/components/forms/radio/Radio';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
|
|
5
|
+
const handleOnChange = jest.fn();
|
|
6
|
+
|
|
7
|
+
describe('radio', () => {
|
|
8
|
+
it('renders the radio input field', () => {
|
|
9
|
+
render(
|
|
10
|
+
<Radio
|
|
11
|
+
label="Option 1"
|
|
12
|
+
name="name"
|
|
13
|
+
ref={undefined}
|
|
14
|
+
onChange={handleOnChange}
|
|
15
|
+
id={'ButtonGroup'}
|
|
16
|
+
/>,
|
|
17
|
+
);
|
|
18
|
+
expect(screen.getByLabelText('Option 1')).toBeInTheDocument();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('calls the onChange function when the radio input is clicked', async () => {
|
|
22
|
+
render(
|
|
23
|
+
<Radio
|
|
24
|
+
label="Option 1"
|
|
25
|
+
name="name"
|
|
26
|
+
ref={undefined}
|
|
27
|
+
onChange={handleOnChange}
|
|
28
|
+
id={'ButtonGroup'}
|
|
29
|
+
/>,
|
|
30
|
+
);
|
|
31
|
+
expect(handleOnChange).toHaveBeenCalledTimes(0);
|
|
32
|
+
await userEvent.click(screen.getByLabelText('Option 1'));
|
|
33
|
+
expect(handleOnChange).toHaveBeenCalledTimes(1);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Radio } from './Radio';
|