@agregio-solutions/design-system 2.1.0 → 2.2.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/dist/design-system.cjs +185 -104
- package/dist/design-system.js +2589 -2463
- package/dist/packages/components/DataTable/doc.md +11 -11
- package/dist/packages/components/PageLayout/doc.md +11 -11
- package/dist/packages/components/Radio/Radio.d.ts +7 -0
- package/dist/packages/components/Radio/doc.md +4 -0
- package/dist/packages/components/RadioGroup/RadioGroup.d.ts +92 -0
- package/dist/packages/components/RadioGroup/doc.md +569 -0
- package/dist/packages/components/RadioGroupItem/RadioGroupItem.d.ts +27 -0
- package/dist/packages/index.d.ts +3 -1
- package/package.json +1 -1
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
# RadioGroup
|
|
2
|
+
|
|
3
|
+
## Props
|
|
4
|
+
|
|
5
|
+
The complete Props documentation with JS doc for this component is available at this path:
|
|
6
|
+
|
|
7
|
+
node_modules/@agregio-solutions/design-system/dist/packages/components/RadioGroup/RadioGroup.d.ts
|
|
8
|
+
|
|
9
|
+
## Example usage
|
|
10
|
+
|
|
11
|
+
Here are the Storybook Stories.
|
|
12
|
+
|
|
13
|
+
Base stories:
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { Meta, StoryObj } from "@storybook/react-vite";
|
|
17
|
+
import { expect, fn, within } from "storybook/test";
|
|
18
|
+
import RadioGroup from "./RadioGroup";
|
|
19
|
+
import RadioGroupItem from "../RadioGroupItem/RadioGroupItem";
|
|
20
|
+
import { ICON_NAMES_ARRAY } from "../Icon/Icon";
|
|
21
|
+
|
|
22
|
+
const meta: Meta<typeof RadioGroup> = {
|
|
23
|
+
component: RadioGroup,
|
|
24
|
+
argTypes: {
|
|
25
|
+
direction: {
|
|
26
|
+
control: "select",
|
|
27
|
+
options: ["vertical", "horizontal"],
|
|
28
|
+
},
|
|
29
|
+
orientation: {
|
|
30
|
+
control: "select",
|
|
31
|
+
options: ["vertical", "horizontal"],
|
|
32
|
+
},
|
|
33
|
+
labelIconRight: {
|
|
34
|
+
control: "select",
|
|
35
|
+
options: ["", ...ICON_NAMES_ARRAY],
|
|
36
|
+
},
|
|
37
|
+
helperTextIcon: {
|
|
38
|
+
control: "select",
|
|
39
|
+
options: ["", ...ICON_NAMES_ARRAY],
|
|
40
|
+
},
|
|
41
|
+
errorHelperTextIcon: {
|
|
42
|
+
control: "select",
|
|
43
|
+
options: ["", ...ICON_NAMES_ARRAY],
|
|
44
|
+
},
|
|
45
|
+
children: { control: false },
|
|
46
|
+
value: { control: false },
|
|
47
|
+
},
|
|
48
|
+
parameters: {
|
|
49
|
+
layout: "centered",
|
|
50
|
+
},
|
|
51
|
+
render: (args) => (
|
|
52
|
+
<RadioGroup {...args}>
|
|
53
|
+
<RadioGroupItem value="solar" label="Solar" />
|
|
54
|
+
<RadioGroupItem value="wind" label="Wind" />
|
|
55
|
+
<RadioGroupItem value="hydro" label="Hydro" />
|
|
56
|
+
</RadioGroup>
|
|
57
|
+
),
|
|
58
|
+
};
|
|
59
|
+
export default meta;
|
|
60
|
+
|
|
61
|
+
type Story = StoryObj<typeof RadioGroup>;
|
|
62
|
+
|
|
63
|
+
export const Playground: Story = {
|
|
64
|
+
args: {
|
|
65
|
+
label: "Energy source",
|
|
66
|
+
onChange: fn(),
|
|
67
|
+
},
|
|
68
|
+
play: async ({ canvasElement }) => {
|
|
69
|
+
const canvas = within(canvasElement);
|
|
70
|
+
const group = canvas.getByRole("radiogroup", { name: "Energy source" });
|
|
71
|
+
await expect(within(group).getAllByRole("radio")).toHaveLength(3);
|
|
72
|
+
await expect(
|
|
73
|
+
canvas.getByRole("radio", { name: "Solar" }),
|
|
74
|
+
).not.toBeChecked();
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const ShouldPreselectDefaultValue: Story = {
|
|
79
|
+
args: {
|
|
80
|
+
...Playground.args,
|
|
81
|
+
defaultValue: "wind",
|
|
82
|
+
},
|
|
83
|
+
play: async ({ canvasElement }) => {
|
|
84
|
+
const canvas = within(canvasElement);
|
|
85
|
+
await expect(canvas.getByRole("radio", { name: "Wind" })).toBeChecked();
|
|
86
|
+
await expect(
|
|
87
|
+
canvas.getByRole("radio", { name: "Solar" }),
|
|
88
|
+
).not.toBeChecked();
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const ShouldDisableAllRadios: Story = {
|
|
93
|
+
args: {
|
|
94
|
+
...Playground.args,
|
|
95
|
+
defaultValue: "wind",
|
|
96
|
+
isDisabled: true,
|
|
97
|
+
},
|
|
98
|
+
play: async ({ canvasElement }) => {
|
|
99
|
+
const canvas = within(canvasElement);
|
|
100
|
+
for (const radio of canvas.getAllByRole("radio")) {
|
|
101
|
+
await expect(radio).toBeDisabled();
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const ShouldDisableOneRadio: Story = {
|
|
107
|
+
args: Playground.args,
|
|
108
|
+
render: (args) => (
|
|
109
|
+
<RadioGroup {...args}>
|
|
110
|
+
<RadioGroupItem value="solar" label="Solar" />
|
|
111
|
+
<RadioGroupItem value="wind" label="Wind" isDisabled />
|
|
112
|
+
<RadioGroupItem value="hydro" label="Hydro" />
|
|
113
|
+
</RadioGroup>
|
|
114
|
+
),
|
|
115
|
+
play: async ({ canvasElement }) => {
|
|
116
|
+
const canvas = within(canvasElement);
|
|
117
|
+
await expect(canvas.getByRole("radio", { name: "Wind" })).toBeDisabled();
|
|
118
|
+
await expect(canvas.getByRole("radio", { name: "Solar" })).toBeEnabled();
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const ShouldDisplayReadOnly: Story = {
|
|
123
|
+
args: {
|
|
124
|
+
...Playground.args,
|
|
125
|
+
defaultValue: "wind",
|
|
126
|
+
isReadOnly: true,
|
|
127
|
+
},
|
|
128
|
+
play: async ({ canvasElement }) => {
|
|
129
|
+
const canvas = within(canvasElement);
|
|
130
|
+
await expect(canvas.getByRole("radio", { name: "Wind" })).toBeChecked();
|
|
131
|
+
await expect(canvas.getByText("Wind").closest("label")).toHaveAttribute(
|
|
132
|
+
"data-readonly",
|
|
133
|
+
"true",
|
|
134
|
+
);
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const ShouldDisplayHorizontally: Story = {
|
|
139
|
+
args: {
|
|
140
|
+
...Playground.args,
|
|
141
|
+
direction: "horizontal",
|
|
142
|
+
},
|
|
143
|
+
play: async ({ canvasElement }) => {
|
|
144
|
+
const canvas = within(canvasElement);
|
|
145
|
+
await expect(canvas.getByRole("radiogroup")).toHaveAttribute(
|
|
146
|
+
"data-orientation",
|
|
147
|
+
"horizontal",
|
|
148
|
+
);
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export const ShouldDisplayLabelDecorations: Story = {
|
|
153
|
+
args: {
|
|
154
|
+
...Playground.args,
|
|
155
|
+
required: true,
|
|
156
|
+
labelIconRight: "help_outline",
|
|
157
|
+
labelIconRightTooltip: "Additional information",
|
|
158
|
+
helperText: "Main source of the site",
|
|
159
|
+
helperTextIcon: "help_outline",
|
|
160
|
+
},
|
|
161
|
+
play: async ({ canvasElement }) => {
|
|
162
|
+
const canvas = within(canvasElement);
|
|
163
|
+
await canvas.findByText("Main source of the site");
|
|
164
|
+
await canvas.findByTitle("Required");
|
|
165
|
+
for (const radio of canvas.getAllByRole("radio")) {
|
|
166
|
+
await expect(radio).toBeRequired();
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export const ShouldDisplayError: Story = {
|
|
172
|
+
args: {
|
|
173
|
+
...Playground.args,
|
|
174
|
+
errorHelperText: "Select an energy source",
|
|
175
|
+
errorHelperTextIcon: "error_outline",
|
|
176
|
+
},
|
|
177
|
+
play: async ({ canvasElement }) => {
|
|
178
|
+
const canvas = within(canvasElement);
|
|
179
|
+
await expect(canvas.getByRole("alert")).toHaveTextContent(
|
|
180
|
+
"Select an energy source",
|
|
181
|
+
);
|
|
182
|
+
await expect(canvas.getByRole("radiogroup")).toHaveAttribute(
|
|
183
|
+
"data-invalid",
|
|
184
|
+
"true",
|
|
185
|
+
);
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export const ShouldDescribeARadio: Story = {
|
|
190
|
+
args: Playground.args,
|
|
191
|
+
render: (args) => (
|
|
192
|
+
<RadioGroup {...args}>
|
|
193
|
+
<RadioGroupItem
|
|
194
|
+
value="solar"
|
|
195
|
+
label="Solar"
|
|
196
|
+
description="Production during daytime only"
|
|
197
|
+
/>
|
|
198
|
+
<RadioGroupItem value="wind" label="Wind" />
|
|
199
|
+
</RadioGroup>
|
|
200
|
+
),
|
|
201
|
+
play: async ({ canvasElement }) => {
|
|
202
|
+
const canvas = within(canvasElement);
|
|
203
|
+
await expect(
|
|
204
|
+
canvas.getByRole("radio", { name: "Solar" }),
|
|
205
|
+
).toHaveAccessibleDescription("Production during daytime only");
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export const ShouldWrapLongText: Story = {
|
|
210
|
+
args: Playground.args,
|
|
211
|
+
render: (args) => (
|
|
212
|
+
<div style={{ width: 200 }}>
|
|
213
|
+
<RadioGroup {...args}>
|
|
214
|
+
<RadioGroupItem
|
|
215
|
+
value="long"
|
|
216
|
+
label="This is a very long text that should wrap on several lines"
|
|
217
|
+
/>
|
|
218
|
+
<RadioGroupItem value="short" label="Short" />
|
|
219
|
+
</RadioGroup>
|
|
220
|
+
</div>
|
|
221
|
+
),
|
|
222
|
+
play: async ({ canvasElement }) => {
|
|
223
|
+
const canvas = within(canvasElement);
|
|
224
|
+
await canvas.findByText(
|
|
225
|
+
"This is a very long text that should wrap on several lines",
|
|
226
|
+
);
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## How to test this component
|
|
232
|
+
|
|
233
|
+
Here are some more advanced stories with more testing coverage and examples that you can read to understand how to test this component.
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
import { Meta, StoryObj } from "@storybook/react-vite";
|
|
237
|
+
import { expect, fn, userEvent, within } from "storybook/test";
|
|
238
|
+
import { useState } from "react";
|
|
239
|
+
import { Controller, useForm, useWatch } from "react-hook-form";
|
|
240
|
+
import RadioGroup from "../RadioGroup";
|
|
241
|
+
import RadioGroupItem from "../../RadioGroupItem/RadioGroupItem";
|
|
242
|
+
import * as RadioGroupStories from "../RadioGroup.stories";
|
|
243
|
+
|
|
244
|
+
const meta: Meta<typeof RadioGroup> = {
|
|
245
|
+
...RadioGroupStories.default,
|
|
246
|
+
component: RadioGroup,
|
|
247
|
+
parameters: {
|
|
248
|
+
...RadioGroupStories.default.parameters,
|
|
249
|
+
chromatic: { disableSnapshot: true },
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
export default meta;
|
|
253
|
+
|
|
254
|
+
type Story = StoryObj<typeof RadioGroup>;
|
|
255
|
+
|
|
256
|
+
const ENERGY_SOURCES = [
|
|
257
|
+
{ value: "solar", label: "Solar" },
|
|
258
|
+
{ value: "wind", label: "Wind" },
|
|
259
|
+
{ value: "hydro", label: "Hydro" },
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
const EnergyItems = () =>
|
|
263
|
+
ENERGY_SOURCES.map((source) => (
|
|
264
|
+
<RadioGroupItem key={source.value} {...source} />
|
|
265
|
+
));
|
|
266
|
+
|
|
267
|
+
const ControlledExample = ({
|
|
268
|
+
onChange,
|
|
269
|
+
}: {
|
|
270
|
+
onChange?: (value: string) => void;
|
|
271
|
+
}) => {
|
|
272
|
+
const [selected, setSelected] = useState<string | null>(null);
|
|
273
|
+
return (
|
|
274
|
+
<div>
|
|
275
|
+
<div>Debug selected: {selected}</div>
|
|
276
|
+
<RadioGroup
|
|
277
|
+
label="Energy source"
|
|
278
|
+
value={selected}
|
|
279
|
+
onChange={(value) => {
|
|
280
|
+
setSelected(value);
|
|
281
|
+
onChange?.(value);
|
|
282
|
+
}}
|
|
283
|
+
>
|
|
284
|
+
<EnergyItems />
|
|
285
|
+
</RadioGroup>
|
|
286
|
+
</div>
|
|
287
|
+
);
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
export const ExampleControlled: Story = {
|
|
291
|
+
args: { onChange: fn() },
|
|
292
|
+
render: (args) => <ControlledExample onChange={args.onChange} />,
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
export const ShouldUpdateControlledValue: Story = {
|
|
296
|
+
...ExampleControlled,
|
|
297
|
+
play: async ({ canvasElement, args }) => {
|
|
298
|
+
const canvas = within(canvasElement);
|
|
299
|
+
const user = userEvent.setup();
|
|
300
|
+
|
|
301
|
+
await user.click(canvas.getByText("Wind"));
|
|
302
|
+
await expect(canvas.getByRole("radio", { name: "Wind" })).toBeChecked();
|
|
303
|
+
await canvas.findByText("Debug selected: wind");
|
|
304
|
+
await expect(args.onChange).toHaveBeenCalledWith("wind");
|
|
305
|
+
|
|
306
|
+
await user.click(canvas.getByText("Hydro"));
|
|
307
|
+
await expect(canvas.getByRole("radio", { name: "Wind" })).not.toBeChecked();
|
|
308
|
+
await canvas.findByText("Debug selected: hydro");
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
export const ShouldSelectWhenUncontrolled: Story = {
|
|
313
|
+
args: { label: "Energy source", defaultValue: "solar", onChange: fn() },
|
|
314
|
+
play: async ({ canvasElement, args }) => {
|
|
315
|
+
const canvas = within(canvasElement);
|
|
316
|
+
const user = userEvent.setup();
|
|
317
|
+
|
|
318
|
+
await expect(canvas.getByRole("radio", { name: "Solar" })).toBeChecked();
|
|
319
|
+
await user.click(canvas.getByText("Hydro"));
|
|
320
|
+
await expect(canvas.getByRole("radio", { name: "Hydro" })).toBeChecked();
|
|
321
|
+
await expect(
|
|
322
|
+
canvas.getByRole("radio", { name: "Solar" }),
|
|
323
|
+
).not.toBeChecked();
|
|
324
|
+
await expect(args.onChange).toHaveBeenCalledWith("hydro");
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
export const ShouldNavigateWithKeyboard: Story = {
|
|
329
|
+
args: { label: "Energy source", defaultValue: "solar" },
|
|
330
|
+
render: (args) => (
|
|
331
|
+
<div>
|
|
332
|
+
<RadioGroup {...args}>
|
|
333
|
+
<RadioGroupItem value="solar" label="Solar" />
|
|
334
|
+
<RadioGroupItem value="wind" label="Wind" isDisabled />
|
|
335
|
+
<RadioGroupItem value="hydro" label="Hydro" />
|
|
336
|
+
</RadioGroup>
|
|
337
|
+
<button type="button">After</button>
|
|
338
|
+
</div>
|
|
339
|
+
),
|
|
340
|
+
play: async ({ canvasElement }) => {
|
|
341
|
+
const canvas = within(canvasElement);
|
|
342
|
+
const user = userEvent.setup();
|
|
343
|
+
const solar = canvas.getByRole("radio", { name: "Solar" });
|
|
344
|
+
const hydro = canvas.getByRole("radio", { name: "Hydro" });
|
|
345
|
+
|
|
346
|
+
await user.tab();
|
|
347
|
+
await expect(solar).toHaveFocus();
|
|
348
|
+
await expect(canvas.getByText("Solar").closest("label")).toHaveAttribute(
|
|
349
|
+
"data-focus-visible",
|
|
350
|
+
"true",
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
await user.keyboard("{ArrowDown}");
|
|
354
|
+
await expect(hydro).toHaveFocus();
|
|
355
|
+
await expect(hydro).toBeChecked();
|
|
356
|
+
|
|
357
|
+
await user.tab();
|
|
358
|
+
await expect(canvas.getByRole("button", { name: "After" })).toHaveFocus();
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
export const ShouldNameARadioWithoutVisibleLabel: Story = {
|
|
363
|
+
args: RadioGroupStories.Playground.args,
|
|
364
|
+
render: (args) => (
|
|
365
|
+
<RadioGroup {...args}>
|
|
366
|
+
<RadioGroupItem value="solar" aria-label="Solar" />
|
|
367
|
+
<RadioGroupItem value="wind" aria-label="Wind" />
|
|
368
|
+
</RadioGroup>
|
|
369
|
+
),
|
|
370
|
+
play: async ({ canvasElement }) => {
|
|
371
|
+
const canvas = within(canvasElement);
|
|
372
|
+
await expect(canvas.getByRole("radio", { name: "Solar" })).toBeVisible();
|
|
373
|
+
await expect(canvas.queryByText("Solar")).not.toBeInTheDocument();
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
export const ShouldNotChangeWhenReadOnly: Story = {
|
|
378
|
+
args: {
|
|
379
|
+
label: "Energy source",
|
|
380
|
+
defaultValue: "solar",
|
|
381
|
+
isReadOnly: true,
|
|
382
|
+
onChange: fn(),
|
|
383
|
+
},
|
|
384
|
+
play: async ({ canvasElement, args }) => {
|
|
385
|
+
const canvas = within(canvasElement);
|
|
386
|
+
const user = userEvent.setup();
|
|
387
|
+
|
|
388
|
+
await user.click(canvas.getByText("Wind"));
|
|
389
|
+
await expect(canvas.getByRole("radio", { name: "Solar" })).toBeChecked();
|
|
390
|
+
await expect(canvas.getByRole("radio", { name: "Wind" })).not.toBeChecked();
|
|
391
|
+
await expect(args.onChange).not.toHaveBeenCalled();
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const ReactHookFormExample = () => {
|
|
396
|
+
const { control } = useForm({ defaultValues: { energy: "wind" } });
|
|
397
|
+
const energy = useWatch({ control, name: "energy" });
|
|
398
|
+
|
|
399
|
+
return (
|
|
400
|
+
<form>
|
|
401
|
+
<div>Debug selected: {energy}</div>
|
|
402
|
+
<Controller
|
|
403
|
+
control={control}
|
|
404
|
+
name="energy"
|
|
405
|
+
render={({ field }) => (
|
|
406
|
+
<RadioGroup
|
|
407
|
+
ref={field.ref}
|
|
408
|
+
label="Energy source"
|
|
409
|
+
name={field.name}
|
|
410
|
+
value={field.value}
|
|
411
|
+
onChange={field.onChange}
|
|
412
|
+
>
|
|
413
|
+
<EnergyItems />
|
|
414
|
+
</RadioGroup>
|
|
415
|
+
)}
|
|
416
|
+
/>
|
|
417
|
+
</form>
|
|
418
|
+
);
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
export const ExampleWithReactHookForm: Story = {
|
|
422
|
+
render: () => <ReactHookFormExample />,
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
export const ShouldWorkWithReactHookForm: Story = {
|
|
426
|
+
...ExampleWithReactHookForm,
|
|
427
|
+
play: async ({ canvasElement }) => {
|
|
428
|
+
const canvas = within(canvasElement);
|
|
429
|
+
const user = userEvent.setup();
|
|
430
|
+
|
|
431
|
+
await canvas.findByText("Debug selected: wind");
|
|
432
|
+
await expect(canvas.getByRole("radio", { name: "Wind" })).toBeChecked();
|
|
433
|
+
await user.click(canvas.getByText("Solar"));
|
|
434
|
+
await canvas.findByText("Debug selected: solar");
|
|
435
|
+
await expect(canvas.getByRole("radio", { name: "Wind" })).not.toBeChecked();
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const ReactHookFormRequiredExample = () => {
|
|
440
|
+
const { control, handleSubmit } = useForm<{ energy: string | null }>({
|
|
441
|
+
defaultValues: { energy: null },
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
return (
|
|
445
|
+
<form onSubmit={handleSubmit(() => {})}>
|
|
446
|
+
<Controller
|
|
447
|
+
control={control}
|
|
448
|
+
name="energy"
|
|
449
|
+
rules={{ required: "Select an energy source" }}
|
|
450
|
+
render={({ field, fieldState }) => (
|
|
451
|
+
<RadioGroup
|
|
452
|
+
ref={field.ref}
|
|
453
|
+
label="Energy source"
|
|
454
|
+
name={field.name}
|
|
455
|
+
value={field.value}
|
|
456
|
+
onChange={field.onChange}
|
|
457
|
+
errorHelperText={fieldState.error?.message}
|
|
458
|
+
>
|
|
459
|
+
<EnergyItems />
|
|
460
|
+
</RadioGroup>
|
|
461
|
+
)}
|
|
462
|
+
/>
|
|
463
|
+
<button type="submit">Submit</button>
|
|
464
|
+
</form>
|
|
465
|
+
);
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
export const ShouldFocusFirstRadioOnReactHookFormError: Story = {
|
|
469
|
+
render: () => <ReactHookFormRequiredExample />,
|
|
470
|
+
play: async ({ canvasElement }) => {
|
|
471
|
+
const canvas = within(canvasElement);
|
|
472
|
+
const user = userEvent.setup();
|
|
473
|
+
|
|
474
|
+
await user.click(canvas.getByRole("button", { name: "Submit" }));
|
|
475
|
+
await canvas.findByText("Select an energy source");
|
|
476
|
+
await expect(canvas.getByRole("radio", { name: "Solar" })).toHaveFocus();
|
|
477
|
+
},
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const onNativeSubmit = fn();
|
|
481
|
+
|
|
482
|
+
export const ShouldSubmitValueInNativeForm: Story = {
|
|
483
|
+
beforeEach: () => {
|
|
484
|
+
onNativeSubmit.mockClear();
|
|
485
|
+
},
|
|
486
|
+
render: () => (
|
|
487
|
+
<form
|
|
488
|
+
onSubmit={(event) => {
|
|
489
|
+
event.preventDefault();
|
|
490
|
+
onNativeSubmit(new FormData(event.currentTarget).get("energy"));
|
|
491
|
+
}}
|
|
492
|
+
>
|
|
493
|
+
<RadioGroup label="Energy source" name="energy">
|
|
494
|
+
<EnergyItems />
|
|
495
|
+
</RadioGroup>
|
|
496
|
+
<button type="submit">Submit</button>
|
|
497
|
+
</form>
|
|
498
|
+
),
|
|
499
|
+
play: async ({ canvasElement }) => {
|
|
500
|
+
const canvas = within(canvasElement);
|
|
501
|
+
const user = userEvent.setup();
|
|
502
|
+
|
|
503
|
+
await user.click(canvas.getByText("Hydro"));
|
|
504
|
+
await user.click(canvas.getByRole("button", { name: "Submit" }));
|
|
505
|
+
await expect(onNativeSubmit).toHaveBeenCalledWith("hydro");
|
|
506
|
+
},
|
|
507
|
+
};
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
## Developer notes
|
|
511
|
+
|
|
512
|
+
Here are the notes available for the developer on the built Storybook, you can read them to understand the component and how to use it.
|
|
513
|
+
|
|
514
|
+
```mdx
|
|
515
|
+
import {
|
|
516
|
+
Canvas,
|
|
517
|
+
Meta,
|
|
518
|
+
Stories,
|
|
519
|
+
Controls,
|
|
520
|
+
Source,
|
|
521
|
+
} from "@storybook/addon-docs/blocks";
|
|
522
|
+
|
|
523
|
+
import * as RadioGroup from "./RadioGroup.stories";
|
|
524
|
+
import * as RadioGroupTests from "./tests/RadioGroup.stories";
|
|
525
|
+
|
|
526
|
+
<Meta of={RadioGroup} />
|
|
527
|
+
|
|
528
|
+
# RadioGroup
|
|
529
|
+
|
|
530
|
+
Lets the user pick one option among several. Each option is a `RadioGroupItem` with a unique `value`.
|
|
531
|
+
The group carries the state (`value` / `defaultValue` / `onChange`), the label, and the helper and error texts.
|
|
532
|
+
|
|
533
|
+
It is built on React Aria: the group is announced as a `radiogroup` with its label, it is a single tab stop,
|
|
534
|
+
and arrow keys move the selection.
|
|
535
|
+
|
|
536
|
+
<Canvas of={RadioGroup.Playground} />
|
|
537
|
+
|
|
538
|
+
## Controlled example
|
|
539
|
+
|
|
540
|
+
<Source of={RadioGroupTests.ExampleControlled} type="code" dark />
|
|
541
|
+
|
|
542
|
+
## With react-hook-form
|
|
543
|
+
|
|
544
|
+
Use a `Controller` (or `useController`): `register()` only works with a native input.
|
|
545
|
+
Pass `field.ref` to the group's `ref`: on validation error, react-hook-form then focuses the radio holding the tab stop
|
|
546
|
+
(the selected one, otherwise the first enabled one).
|
|
547
|
+
|
|
548
|
+
<Source of={RadioGroupTests.ExampleWithReactHookForm} type="code" dark />
|
|
549
|
+
|
|
550
|
+
## Migrating from `Radio`
|
|
551
|
+
|
|
552
|
+
`Radio` is deprecated. To migrate a group of `Radio`s:
|
|
553
|
+
|
|
554
|
+
- Wrap all the radios in one `RadioGroup` and move the group title into its `label` (or `aria-label`).
|
|
555
|
+
- Replace each `Radio` by a `RadioGroupItem` with a `value`: it is now mandatory, even when you used to rely on `checked`.
|
|
556
|
+
- Replace `checked={selected === x}` + `onChange` on each radio by `value={selected}` + `onChange={setSelected}` on the group.
|
|
557
|
+
`onChange` receives the value, not an event.
|
|
558
|
+
- Replace `{...register("field")}` by a `Controller`.
|
|
559
|
+
- Replace `disabled` by `isDisabled`. A radio without visible text takes an `aria-label`.
|
|
560
|
+
|
|
561
|
+
Keep `Radio` for now when the radios can't sit in one block (spread across table cells) or when clicking
|
|
562
|
+
the selected radio must deselect it: a radio group can't be emptied by the user.
|
|
563
|
+
|
|
564
|
+
### RadioGroup props
|
|
565
|
+
|
|
566
|
+
<Controls of={RadioGroup.Playground} />
|
|
567
|
+
|
|
568
|
+
<Stories />
|
|
569
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
export type Props = {
|
|
3
|
+
/**
|
|
4
|
+
* The value of the radio, returned by the `onChange` of the parent `RadioGroup`.
|
|
5
|
+
* Also used as the native `value` attribute when submitting an HTML form.
|
|
6
|
+
*/
|
|
7
|
+
value: string;
|
|
8
|
+
/**
|
|
9
|
+
* The text to display next to the radio.
|
|
10
|
+
* If you give a ReactNode or no label at all, also provide an `aria-label`.
|
|
11
|
+
*/
|
|
12
|
+
label?: string | React.ReactNode;
|
|
13
|
+
/**
|
|
14
|
+
* Accessible name of the radio, required when there is no string `label`.
|
|
15
|
+
*/
|
|
16
|
+
"aria-label"?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Additional text displayed below the label, linked to the radio with `aria-describedby`.
|
|
19
|
+
*/
|
|
20
|
+
description?: React.ReactNode;
|
|
21
|
+
/**
|
|
22
|
+
* Whether the radio is disabled.
|
|
23
|
+
*/
|
|
24
|
+
isDisabled?: boolean;
|
|
25
|
+
};
|
|
26
|
+
declare const RadioGroupItem: React.ForwardRefExoticComponent<Props & React.RefAttributes<HTMLDivElement>>;
|
|
27
|
+
export default RadioGroupItem;
|
package/dist/packages/index.d.ts
CHANGED
|
@@ -43,6 +43,8 @@ import { default as PageLayout } from './components/PageLayout/PageLayout';
|
|
|
43
43
|
import { default as Pagination } from './components/Pagination/Pagination';
|
|
44
44
|
import { default as Popover, PopoverTrigger } from './components/Popover/Popover';
|
|
45
45
|
import { default as Radio } from './components/Radio/Radio';
|
|
46
|
+
import { default as RadioGroup } from './components/RadioGroup/RadioGroup';
|
|
47
|
+
import { default as RadioGroupItem } from './components/RadioGroupItem/RadioGroupItem';
|
|
46
48
|
import { default as SearchBar } from './components/SearchBar/SearchBar';
|
|
47
49
|
import { default as SegmentedControl } from './components/SegmentedControl/SegmentedControl';
|
|
48
50
|
import { default as SegmentedControlButton } from './components/SegmentedControl/SegmentedControlButton/SegmentedControlButton';
|
|
@@ -73,5 +75,5 @@ import { default as RouterProvider } from './components/RouterProvider/RouterPro
|
|
|
73
75
|
import { tokens } from './ds-design-tokens/index';
|
|
74
76
|
import { I18nProvider } from 'react-aria-components';
|
|
75
77
|
import { toast } from 'sonner';
|
|
76
|
-
export { Accordion, Badge, Breadcrumbs, Button, Calendar, CalendarCell, Card, ChartLegend, ChartTooltip, Checkbox, CheckboxGroup, Chip, Combobox, DataTable, DataTableCell, DataTableHeader, DataTableRoot, DataTableRow, DatePicker, DateRangePicker, Drawer, DrawerContent, DrawerActions, DrawerForm, Dropdown, DropdownListItem, EmptyState, FileUpload, Filter, Header, I18nProvider, Icon, Label, LinearProgressBar, Link, List, Loader, Menu, MenuItem, Message, Modal, ModalContent, ModalActions, ModalForm, Navigation, NavigationItem, Notifications, NotificationCard, NumberField, PageLayout, Pagination, Popover, PopoverTrigger, Radio, RouterProvider, SearchBar, SegmentedControl, SegmentedControlButton, SegmentedControlList, SegmentedControlPanel, Select, SelectItem, Skeleton, Slider, Stepper, Step, Switch, Tab, TabList, TabPanel, Tabs, Tag, TextInput, TimeField, TimePicker, Timeline, toast, Toaster, ToggleButton, ToggleButtonGroup, Tooltip, TooltipTrigger, tokens, YearMonthPicker, };
|
|
78
|
+
export { Accordion, Badge, Breadcrumbs, Button, Calendar, CalendarCell, Card, ChartLegend, ChartTooltip, Checkbox, CheckboxGroup, Chip, Combobox, DataTable, DataTableCell, DataTableHeader, DataTableRoot, DataTableRow, DatePicker, DateRangePicker, Drawer, DrawerContent, DrawerActions, DrawerForm, Dropdown, DropdownListItem, EmptyState, FileUpload, Filter, Header, I18nProvider, Icon, Label, LinearProgressBar, Link, List, Loader, Menu, MenuItem, Message, Modal, ModalContent, ModalActions, ModalForm, Navigation, NavigationItem, Notifications, NotificationCard, NumberField, PageLayout, Pagination, Popover, PopoverTrigger, Radio, RadioGroup, RadioGroupItem, RouterProvider, SearchBar, SegmentedControl, SegmentedControlButton, SegmentedControlList, SegmentedControlPanel, Select, SelectItem, Skeleton, Slider, Stepper, Step, Switch, Tab, TabList, TabPanel, Tabs, Tag, TextInput, TimeField, TimePicker, Timeline, toast, Toaster, ToggleButton, ToggleButtonGroup, Tooltip, TooltipTrigger, tokens, YearMonthPicker, };
|
|
77
79
|
export type { TypeTableColumnWithFilter, ComboboxMultipleProps };
|
package/package.json
CHANGED