@boostdev/design-system-components 0.1.18 → 1.0.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/AGENTS.md +65 -27
- package/README.md +64 -29
- package/dist/client.cjs +412 -241
- package/dist/client.css +1378 -1186
- package/dist/client.d.cts +43 -16
- package/dist/client.d.ts +43 -16
- package/dist/client.js +414 -245
- package/dist/index.cjs +412 -241
- package/dist/index.css +1378 -1186
- package/dist/index.d.cts +43 -16
- package/dist/index.d.ts +43 -16
- package/dist/index.js +414 -245
- package/package.json +9 -4
- package/src/components/interaction/Button/Button.mdx +2 -5
- package/src/components/interaction/Button/Button.module.css +74 -44
- package/src/components/interaction/Button/Button.stories.tsx +4 -4
- package/src/components/interaction/Button/Button.tsx +5 -5
- package/src/components/interaction/Button/index.ts +1 -0
- package/src/components/interaction/Command/Command.module.css +53 -38
- package/src/components/interaction/Command/Command.spec.tsx +24 -0
- package/src/components/interaction/Command/Command.tsx +5 -0
- package/src/components/interaction/Dialog/Dialog.mdx +2 -2
- package/src/components/interaction/Dialog/Dialog.module.css +20 -10
- package/src/components/interaction/Dialog/Dialog.spec.tsx +19 -0
- package/src/components/interaction/Dialog/Dialog.tsx +22 -0
- package/src/components/interaction/Drawer/Drawer.module.css +11 -11
- package/src/components/interaction/Drawer/Drawer.spec.tsx +30 -0
- package/src/components/interaction/Drawer/Drawer.tsx +7 -3
- package/src/components/interaction/DropdownMenu/DropdownMenu.module.css +24 -19
- package/src/components/interaction/DropdownMenu/DropdownMenu.stories.tsx +4 -4
- package/src/components/interaction/Popover/Popover.module.css +17 -12
- package/src/components/interaction/Popover/Popover.spec.tsx +25 -4
- package/src/components/interaction/Popover/Popover.stories.tsx +1 -1
- package/src/components/interaction/Popover/Popover.tsx +5 -2
- package/src/components/interaction/Rating/Rating.module.css +3 -3
- package/src/components/interaction/Toast/Toast.module.css +19 -19
- package/src/components/interaction/Toast/Toast.tsx +1 -1
- package/src/components/interaction/form/Checkbox/Checkbox.mdx +2 -2
- package/src/components/interaction/form/Checkbox/Checkbox.module.css +22 -18
- package/src/components/interaction/form/Checkbox/Checkbox.tsx +2 -2
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.module.css +29 -0
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.spec.tsx +87 -0
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.stories.tsx +62 -0
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.tsx +46 -0
- package/src/components/interaction/form/CheckboxGroup/index.ts +2 -0
- package/src/components/interaction/form/Combobox/Combobox.module.css +44 -34
- package/src/components/interaction/form/Combobox/Combobox.tsx +0 -1
- package/src/components/interaction/form/FileInput/FileInput.module.css +24 -16
- package/src/components/interaction/form/FileInput/FileInput.tsx +5 -2
- package/src/components/interaction/form/FormInput/FormInput.module.css +21 -16
- package/src/components/interaction/form/FormInput/FormInput.tsx +3 -0
- package/src/components/interaction/form/NumberInput/NumberInput.module.css +25 -19
- package/src/components/interaction/form/NumberInput/NumberInput.tsx +5 -2
- package/src/components/interaction/form/Radio/Radio.mdx +3 -3
- package/src/components/interaction/form/Radio/Radio.module.css +28 -24
- package/src/components/interaction/form/Radio/Radio.stories.tsx +1 -1
- package/src/components/interaction/form/Radio/Radio.tsx +2 -2
- package/src/components/interaction/form/RadioGroup/RadioGroup.module.css +29 -0
- package/src/components/interaction/form/RadioGroup/RadioGroup.spec.tsx +75 -0
- package/src/components/interaction/form/RadioGroup/RadioGroup.stories.tsx +64 -0
- package/src/components/interaction/form/RadioGroup/RadioGroup.tsx +46 -0
- package/src/components/interaction/form/RadioGroup/index.ts +2 -0
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.mdx +5 -5
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.module.css +26 -26
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.tsx +4 -0
- package/src/components/interaction/form/Select/Select.module.css +24 -19
- package/src/components/interaction/form/Select/Select.tsx +3 -0
- package/src/components/interaction/form/Slider/Slider.mdx +1 -1
- package/src/components/interaction/form/Slider/Slider.module.css +24 -24
- package/src/components/interaction/form/Slider/Slider.tsx +1 -0
- package/src/components/interaction/form/Switch/Switch.mdx +4 -4
- package/src/components/interaction/form/Switch/Switch.module.css +22 -16
- package/src/components/interaction/form/Switch/Switch.tsx +1 -1
- package/src/components/interaction/form/Textarea/Textarea.module.css +23 -18
- package/src/components/interaction/form/Textarea/Textarea.tsx +3 -0
- package/src/components/interaction/form/atoms/InputContainer.module.css +2 -2
- package/src/components/interaction/form/atoms/Label.module.css +2 -2
- package/src/components/interaction/form/atoms/Message.module.css +2 -2
- package/src/components/layout/ButtonGroup/ButtonGroup.module.css +2 -2
- package/src/components/layout/ButtonGroup/ButtonGroup.stories.tsx +2 -2
- package/src/components/layout/ButtonGroup/ButtonGroup.tsx +8 -3
- package/src/components/layout/ButtonGroup/index.ts +1 -0
- package/src/components/layout/Card/Card.module.css +30 -16
- package/src/components/layout/Card/Card.tsx +1 -1
- package/src/components/layout/IconWrapper/IconWrapper.mdx +2 -2
- package/src/components/layout/IconWrapper/IconWrapper.module.css +5 -5
- package/src/components/layout/IconWrapper/IconWrapper.tsx +4 -3
- package/src/components/layout/IconWrapper/index.ts +1 -0
- package/src/components/layout/SectionHeader/SectionHeader.module.css +23 -23
- package/src/components/layout/SectionHeader/SectionHeader.spec.tsx +2 -2
- package/src/components/layout/SectionHeader/SectionHeader.tsx +2 -2
- package/src/components/ui/Accordion/Accordion.module.css +23 -18
- package/src/components/ui/Alert/Alert.module.css +31 -27
- package/src/components/ui/Avatar/Avatar.module.css +6 -6
- package/src/components/ui/Avatar/Avatar.tsx +1 -1
- package/src/components/ui/Badge/Badge.mdx +2 -2
- package/src/components/ui/Badge/Badge.module.css +15 -15
- package/src/components/ui/Breadcrumb/Breadcrumb.module.css +11 -11
- package/src/components/ui/Calendar/Calendar.module.css +39 -29
- package/src/components/ui/Calendar/Calendar.tsx +6 -2
- package/src/components/ui/Carousel/Carousel.module.css +15 -10
- package/src/components/ui/Carousel/Carousel.stories.tsx +1 -1
- package/src/components/ui/Collapsible/Collapsible.mdx +6 -6
- package/src/components/ui/Collapsible/Collapsible.module.css +18 -18
- package/src/components/ui/DescriptionList/DescriptionList.module.css +8 -8
- package/src/components/ui/Link/Link.module.css +14 -14
- package/src/components/ui/Loading/Loading.module.css +8 -2
- package/src/components/ui/NotificationBanner/NotificationBanner.module.css +27 -23
- package/src/components/ui/Pagination/Pagination.module.css +22 -17
- package/src/components/ui/Progress/Progress.mdx +1 -1
- package/src/components/ui/Progress/Progress.module.css +10 -10
- package/src/components/ui/ProgressCircle/ProgressCircle.module.css +8 -8
- package/src/components/ui/Separator/Separator.module.css +1 -1
- package/src/components/ui/Separator/Separator.tsx +1 -1
- package/src/components/ui/Skeleton/Skeleton.module.css +5 -5
- package/src/components/ui/SkipLink/SkipLink.module.css +17 -12
- package/src/components/ui/Table/Table.module.css +35 -30
- package/src/components/ui/Table/Table.tsx +2 -1
- package/src/components/ui/Tabs/Tabs.module.css +17 -17
- package/src/components/ui/Tabs/Tabs.tsx +0 -1
- package/src/components/ui/Tooltip/Tooltip.module.css +12 -12
- package/src/components/ui/Tooltip/Tooltip.spec.tsx +3 -3
- package/src/components/ui/Tooltip/Tooltip.tsx +10 -2
- package/src/components/ui/Typography/Typography.module.css +18 -18
- package/src/css/bdc.css +61 -0
- package/src/css/index.css +5 -0
- package/src/index.ts +7 -0
- package/src/stories/DesignSystem/Borders.mdx +158 -0
- package/src/stories/DesignSystem/Colors.mdx +196 -0
- package/src/stories/DesignSystem/Elevation.mdx +127 -0
- package/src/stories/DesignSystem/Grid.mdx +140 -0
- package/src/stories/DesignSystem/Motion.mdx +96 -0
- package/src/stories/DesignSystem/Overview.mdx +99 -0
- package/src/stories/DesignSystem/Spacing.mdx +74 -0
- package/src/stories/DesignSystem/Typography.mdx +110 -0
- package/src/stories/Introduction.css +2 -2
- package/src/stories/Introduction.mdx +7 -7
|
@@ -63,6 +63,7 @@ export function NumberInput({
|
|
|
63
63
|
type="button"
|
|
64
64
|
className={css.stepper}
|
|
65
65
|
aria-label="Decrease"
|
|
66
|
+
aria-controls={uid}
|
|
66
67
|
disabled={disabled || (min !== undefined && currentValue <= min)}
|
|
67
68
|
onClick={() => adjust(-step)}
|
|
68
69
|
tabIndex={-1}
|
|
@@ -86,14 +87,16 @@ export function NumberInput({
|
|
|
86
87
|
className={cn(css.input, error ? css.inputError : undefined)}
|
|
87
88
|
onChange={e => {
|
|
88
89
|
const v = parseFloat(e.target.value);
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
const safe = isNaN(v) ? 0 : v;
|
|
91
|
+
if (!isControlled) setInternalValue(safe);
|
|
92
|
+
onChange?.(safe);
|
|
91
93
|
}}
|
|
92
94
|
/>
|
|
93
95
|
<button
|
|
94
96
|
type="button"
|
|
95
97
|
className={css.stepper}
|
|
96
98
|
aria-label="Increase"
|
|
99
|
+
aria-controls={uid}
|
|
97
100
|
disabled={disabled || (max !== undefined && currentValue >= max)}
|
|
98
101
|
onClick={() => adjust(step)}
|
|
99
102
|
tabIndex={-1}
|
|
@@ -46,9 +46,9 @@ Single-select option within a group. Renders a styled native `<input type="radio
|
|
|
46
46
|
<tr><th>Variable</th><th>Default</th><th>Description</th></tr>
|
|
47
47
|
</thead>
|
|
48
48
|
<tbody>
|
|
49
|
-
<tr><td>`--radio_color-active`</td><td>`var(--
|
|
50
|
-
<tr><td>`--radio_color-on-active`</td><td>`var(--
|
|
51
|
-
<tr><td>`--radio_description-color`</td><td>`var(--
|
|
49
|
+
<tr><td>`--radio_color-active`</td><td>`var(--bds-active)`</td><td>Selected fill and border colour</td></tr>
|
|
50
|
+
<tr><td>`--radio_color-on-active`</td><td>`var(--bds-on-active)`</td><td>Inner dot colour</td></tr>
|
|
51
|
+
<tr><td>`--radio_description-color`</td><td>`var(--bds-on-bg--subtle)`</td><td>Description text colour</td></tr>
|
|
52
52
|
</tbody>
|
|
53
53
|
</table>
|
|
54
54
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
@layer component {
|
|
2
2
|
.radioGroup {
|
|
3
|
-
--inputSize: calc(var(--space_s) * 2);
|
|
4
|
-
--labelPosition: calc(var(--inputSize) + var(--space_m));
|
|
3
|
+
--inputSize: calc(var(--bds-space_s) * 2);
|
|
4
|
+
--labelPosition: calc(var(--inputSize) + var(--bds-space_m));
|
|
5
5
|
|
|
6
6
|
display: flex;
|
|
7
7
|
flex-direction: column;
|
|
@@ -10,20 +10,20 @@
|
|
|
10
10
|
.inputWrapper {
|
|
11
11
|
display: flex;
|
|
12
12
|
align-items: flex-start;
|
|
13
|
-
gap: var(--space_xs);
|
|
13
|
+
gap: var(--bds-space_xs);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
.textWrapper {
|
|
17
17
|
display: flex;
|
|
18
18
|
flex-direction: column;
|
|
19
|
-
gap: var(--space_xxxs);
|
|
20
|
-
padding-block-start: var(--space_xxxs);
|
|
19
|
+
gap: var(--bds-space_xxxs);
|
|
20
|
+
padding-block-start: var(--bds-space_xxxs);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
.description {
|
|
24
|
-
font-size: var(--font_size--body--s);
|
|
25
|
-
color: var(--radio_description-color, var(--
|
|
26
|
-
line-height: var(--font_line-height--body);
|
|
24
|
+
font-size: var(--bds-font_size--body--s);
|
|
25
|
+
color: var(--radio_description-color, var(--bds-on-bg--subtle));
|
|
26
|
+
line-height: var(--bds-font_line-height--body);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
.radio {
|
|
@@ -31,19 +31,25 @@
|
|
|
31
31
|
margin-block-start: 0.2em;
|
|
32
32
|
width: var(--inputSize);
|
|
33
33
|
height: var(--inputSize);
|
|
34
|
-
color: var(--radio_color, var(--
|
|
35
|
-
border:
|
|
36
|
-
|
|
34
|
+
color: var(--radio_color, var(--bds-on-bg));
|
|
35
|
+
border: var(--radio_border, none);
|
|
36
|
+
--bdc_color: currentcolor;
|
|
37
|
+
--bdc_radius: 50%;
|
|
38
|
+
|
|
39
|
+
outline: var(--bdc_width) solid var(--bdc_color);
|
|
40
|
+
outline-offset: var(--bdc_offset);
|
|
41
|
+
border-radius: var(--radio_radius, var(--bdc_radius));
|
|
42
|
+
box-shadow: var(--radio_shadow, var(--bdc_shadow));
|
|
37
43
|
appearance: none;
|
|
38
|
-
background-color: var(--radio_color_bg, var(--
|
|
44
|
+
background-color: var(--radio_color_bg, var(--bds-bg));
|
|
39
45
|
cursor: pointer;
|
|
40
46
|
position: relative;
|
|
41
|
-
transition:
|
|
47
|
+
transition: --bdc_color 0.2s ease, background-color 0.2s ease;
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
.radio:checked {
|
|
45
|
-
background-color: var(--radio_color-active, var(--
|
|
46
|
-
|
|
51
|
+
background-color: var(--radio_color-active, var(--bds-active));
|
|
52
|
+
--bdc_color: var(--radio_color-active, var(--bds-active));
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
.radio:checked::after {
|
|
@@ -55,21 +61,19 @@
|
|
|
55
61
|
width: 0.4em;
|
|
56
62
|
height: 0.4em;
|
|
57
63
|
border-radius: 50%;
|
|
58
|
-
background-color: var(--radio_color-on-active, var(--
|
|
64
|
+
background-color: var(--radio_color-on-active, var(--bds-on-active));
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
.radio:focus {
|
|
62
|
-
|
|
63
|
-
border-color: var(--color_interactive);
|
|
64
|
-
box-shadow: 0 0 0 2px rgb(from var(--color_interactive) r g b / 20%);
|
|
68
|
+
--bdc_color: var(--bdc_color--focus);
|
|
65
69
|
}
|
|
66
70
|
|
|
67
|
-
.
|
|
68
|
-
|
|
71
|
+
.radio:focus-visible {
|
|
72
|
+
outline: var(--bds-outline_default);
|
|
73
|
+
outline-offset: var(--bds-outline_offset);
|
|
69
74
|
}
|
|
70
75
|
|
|
71
|
-
.radioError
|
|
72
|
-
|
|
73
|
-
box-shadow: 0 0 0 2px rgb(from var(--color_error) r g b / 20%);
|
|
76
|
+
.radioError {
|
|
77
|
+
--bdc_color: var(--bdc_color--error);
|
|
74
78
|
}
|
|
75
79
|
}
|
|
@@ -17,7 +17,7 @@ export const WithHint: Story = { args: { label: 'Option', name: 'choice4', value
|
|
|
17
17
|
export const Group: Story = {
|
|
18
18
|
render: () => (
|
|
19
19
|
<fieldset style={{ border: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
|
20
|
-
<legend style={{ fontWeight: 'var(--font_weight--semibold)', marginBottom: '0.5rem' }}>Pick one</legend>
|
|
20
|
+
<legend style={{ fontWeight: 'var(--bds-font_weight--semibold)', marginBottom: '0.5rem' }}>Pick one</legend>
|
|
21
21
|
<Radio label="Option A" name="group" value="a" description="Short description for option A." />
|
|
22
22
|
<Radio label="Option B" name="group" value="b" description="Short description for option B." />
|
|
23
23
|
<Radio label="Option C" name="group" value="c" />
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { InputHTMLAttributes, useId } from 'react';
|
|
1
|
+
import { InputHTMLAttributes, ReactNode, useId } from 'react';
|
|
2
2
|
import css from './Radio.module.css';
|
|
3
3
|
import { Message } from '../atoms/Message';
|
|
4
4
|
import { Label } from '../atoms/Label';
|
|
@@ -6,7 +6,7 @@ import { InputContainer } from '../atoms/InputContainer';
|
|
|
6
6
|
import { cn } from '@boostdev/design-system-foundation';
|
|
7
7
|
|
|
8
8
|
interface RadioProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
|
9
|
-
label:
|
|
9
|
+
label: ReactNode;
|
|
10
10
|
name: string;
|
|
11
11
|
description?: string;
|
|
12
12
|
error?: string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
@layer component {
|
|
2
|
+
.group {
|
|
3
|
+
border: none;
|
|
4
|
+
padding: 0;
|
|
5
|
+
margin: 0;
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
gap: var(--bds-space_xs);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.legend {
|
|
12
|
+
font-family: var(--bds-font_family--body);
|
|
13
|
+
font-size: var(--bds-font_size--body--s);
|
|
14
|
+
font-weight: var(--bds-font_weight--medium);
|
|
15
|
+
color: var(--bds-on-bg);
|
|
16
|
+
padding: 0;
|
|
17
|
+
margin-block-end: var(--bds-space_xs);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.required {
|
|
21
|
+
color: var(--bds-error);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.items {
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
gap: var(--bds-space_xs);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { Radio } from '../Radio';
|
|
3
|
+
import { RadioGroup } from './RadioGroup';
|
|
4
|
+
|
|
5
|
+
describe('RadioGroup', () => {
|
|
6
|
+
it('renders a fieldset with a legend', () => {
|
|
7
|
+
render(
|
|
8
|
+
<RadioGroup legend="Preferred contact">
|
|
9
|
+
<Radio label="Email" name="contact" value="email" />
|
|
10
|
+
</RadioGroup>
|
|
11
|
+
);
|
|
12
|
+
expect(screen.getByRole('group', { name: 'Preferred contact' })).toBeInTheDocument();
|
|
13
|
+
expect(screen.getByText('Preferred contact')).toBeInTheDocument();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('renders child radio inputs', () => {
|
|
17
|
+
render(
|
|
18
|
+
<RadioGroup legend="Size">
|
|
19
|
+
<Radio label="Small" name="size" value="small" />
|
|
20
|
+
<Radio label="Large" name="size" value="large" />
|
|
21
|
+
</RadioGroup>
|
|
22
|
+
);
|
|
23
|
+
expect(screen.getByRole('radio', { name: 'Small' })).toBeInTheDocument();
|
|
24
|
+
expect(screen.getByRole('radio', { name: 'Large' })).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('shows an error message', () => {
|
|
28
|
+
render(
|
|
29
|
+
<RadioGroup legend="Choice" error="Please select an option">
|
|
30
|
+
<Radio label="Option" name="opt" value="opt" />
|
|
31
|
+
</RadioGroup>
|
|
32
|
+
);
|
|
33
|
+
expect(screen.getByText('Please select an option')).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('shows a hint message', () => {
|
|
37
|
+
render(
|
|
38
|
+
<RadioGroup legend="Plan" hint="You can change this later">
|
|
39
|
+
<Radio label="Free" name="plan" value="free" />
|
|
40
|
+
</RadioGroup>
|
|
41
|
+
);
|
|
42
|
+
expect(screen.getByText('You can change this later')).toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('links error message via aria-describedby', () => {
|
|
46
|
+
render(
|
|
47
|
+
<RadioGroup legend="Required" error="Required field">
|
|
48
|
+
<Radio label="Option" name="opt" value="opt" />
|
|
49
|
+
</RadioGroup>
|
|
50
|
+
);
|
|
51
|
+
const fieldset = screen.getByRole('group');
|
|
52
|
+
const errorMsg = screen.getByText('Required field');
|
|
53
|
+
expect(fieldset).toHaveAttribute('aria-describedby');
|
|
54
|
+
expect(fieldset.getAttribute('aria-describedby')).toContain(errorMsg.id);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('marks fieldset as required when required prop is set', () => {
|
|
58
|
+
render(
|
|
59
|
+
<RadioGroup legend="Required" required>
|
|
60
|
+
<Radio label="Option" name="opt" value="opt" />
|
|
61
|
+
</RadioGroup>
|
|
62
|
+
);
|
|
63
|
+
expect(screen.getByRole('group')).toHaveAttribute('aria-required', 'true');
|
|
64
|
+
expect(screen.getByText('*')).toBeInTheDocument();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('disables the fieldset when disabled prop is set', () => {
|
|
68
|
+
render(
|
|
69
|
+
<RadioGroup legend="Disabled" disabled>
|
|
70
|
+
<Radio label="Option" name="opt" value="opt" />
|
|
71
|
+
</RadioGroup>
|
|
72
|
+
);
|
|
73
|
+
expect(screen.getByRole('group')).toBeDisabled();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Radio } from '../Radio';
|
|
3
|
+
import { RadioGroup } from './RadioGroup';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Form/RadioGroup',
|
|
7
|
+
component: RadioGroup,
|
|
8
|
+
} satisfies Meta<typeof RadioGroup>;
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj<typeof meta>;
|
|
12
|
+
|
|
13
|
+
export const Default: Story = {
|
|
14
|
+
args: { legend: 'Preferred contact' },
|
|
15
|
+
render: args => (
|
|
16
|
+
<RadioGroup {...args}>
|
|
17
|
+
<Radio label="Email" name="contact" value="email" />
|
|
18
|
+
<Radio label="Phone" name="contact" value="phone" />
|
|
19
|
+
<Radio label="Post" name="contact" value="post" />
|
|
20
|
+
</RadioGroup>
|
|
21
|
+
),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const WithError: Story = {
|
|
25
|
+
args: { legend: 'Plan', error: 'Please select a plan' },
|
|
26
|
+
render: args => (
|
|
27
|
+
<RadioGroup {...args}>
|
|
28
|
+
<Radio label="Free" name="plan" value="free" />
|
|
29
|
+
<Radio label="Pro" name="plan" value="pro" />
|
|
30
|
+
<Radio label="Enterprise" name="plan" value="enterprise" />
|
|
31
|
+
</RadioGroup>
|
|
32
|
+
),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const WithHint: Story = {
|
|
36
|
+
args: { legend: 'Billing cycle', hint: 'You can change this at any time' },
|
|
37
|
+
render: args => (
|
|
38
|
+
<RadioGroup {...args}>
|
|
39
|
+
<Radio label="Monthly" name="billing" value="monthly" />
|
|
40
|
+
<Radio label="Annually" name="billing" value="annually" description="Save 20%" />
|
|
41
|
+
</RadioGroup>
|
|
42
|
+
),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const Required: Story = {
|
|
46
|
+
args: { legend: 'Size', required: true },
|
|
47
|
+
render: args => (
|
|
48
|
+
<RadioGroup {...args}>
|
|
49
|
+
<Radio label="Small" name="size" value="small" />
|
|
50
|
+
<Radio label="Medium" name="size" value="medium" defaultChecked />
|
|
51
|
+
<Radio label="Large" name="size" value="large" />
|
|
52
|
+
</RadioGroup>
|
|
53
|
+
),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const Disabled: Story = {
|
|
57
|
+
args: { legend: 'Region', disabled: true },
|
|
58
|
+
render: args => (
|
|
59
|
+
<RadioGroup {...args}>
|
|
60
|
+
<Radio label="Europe" name="region" value="eu" defaultChecked />
|
|
61
|
+
<Radio label="North America" name="region" value="na" />
|
|
62
|
+
</RadioGroup>
|
|
63
|
+
),
|
|
64
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ReactNode, useId } from 'react';
|
|
2
|
+
import css from './RadioGroup.module.css';
|
|
3
|
+
import { Message } from '../atoms/Message';
|
|
4
|
+
import { cn } from '@boostdev/design-system-foundation';
|
|
5
|
+
|
|
6
|
+
export interface RadioGroupProps {
|
|
7
|
+
legend: string;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
error?: string;
|
|
10
|
+
hint?: string;
|
|
11
|
+
required?: boolean;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function RadioGroup({
|
|
17
|
+
legend,
|
|
18
|
+
children,
|
|
19
|
+
error,
|
|
20
|
+
hint,
|
|
21
|
+
required,
|
|
22
|
+
disabled,
|
|
23
|
+
className,
|
|
24
|
+
}: Readonly<RadioGroupProps>) {
|
|
25
|
+
const id = useId();
|
|
26
|
+
const hintId = id + 'hint';
|
|
27
|
+
const errorId = id + 'error';
|
|
28
|
+
const describedBy = [error && errorId, hint && hintId].filter(Boolean).join(' ') || undefined;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<fieldset
|
|
32
|
+
className={cn(css.group, className)}
|
|
33
|
+
disabled={disabled}
|
|
34
|
+
aria-required={required || undefined}
|
|
35
|
+
aria-describedby={describedBy}
|
|
36
|
+
>
|
|
37
|
+
<legend className={css.legend}>
|
|
38
|
+
{legend}
|
|
39
|
+
{required && <span className={css.required} aria-hidden="true"> *</span>}
|
|
40
|
+
</legend>
|
|
41
|
+
<div className={css.items}>{children}</div>
|
|
42
|
+
<Message inputId={id} type="error" message={error} />
|
|
43
|
+
<Message inputId={id} type="hint" message={hint} />
|
|
44
|
+
</fieldset>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -48,11 +48,11 @@ All options have **equal width** — the widest label determines the size for al
|
|
|
48
48
|
<tr><th>Variable</th><th>Default</th><th>Description</th></tr>
|
|
49
49
|
</thead>
|
|
50
50
|
<tbody>
|
|
51
|
-
<tr><td>`--control_track-bg`</td><td>`var(--
|
|
52
|
-
<tr><td>`--control_thumb-bg`</td><td>`var(--
|
|
53
|
-
<tr><td>`--control_label-color`</td><td>`var(--
|
|
54
|
-
<tr><td>`--control_label-color--active`</td><td>`var(--
|
|
55
|
-
<tr><td>`--control_label-color--hover`</td><td>`var(--
|
|
51
|
+
<tr><td>`--control_track-bg`</td><td>`var(--bds-bg--subtle)`</td><td>Track background</td></tr>
|
|
52
|
+
<tr><td>`--control_thumb-bg`</td><td>`var(--bds-bg)`</td><td>Thumb (active indicator) background</td></tr>
|
|
53
|
+
<tr><td>`--control_label-color`</td><td>`var(--bds-on-bg--subtle)`</td><td>Unselected label colour</td></tr>
|
|
54
|
+
<tr><td>`--control_label-color--active`</td><td>`var(--bds-on-bg)`</td><td>Selected label colour</td></tr>
|
|
55
|
+
<tr><td>`--control_label-color--hover`</td><td>`var(--bds-on-bg)`</td><td>Hovered (unselected) label colour</td></tr>
|
|
56
56
|
</tbody>
|
|
57
57
|
</table>
|
|
58
58
|
|
|
@@ -4,24 +4,24 @@
|
|
|
4
4
|
display: inline-grid;
|
|
5
5
|
grid-auto-flow: column;
|
|
6
6
|
grid-auto-columns: 1fr;
|
|
7
|
-
padding: var(--space_xxxs);
|
|
8
|
-
background-color: var(--control_track-bg, var(--
|
|
9
|
-
border-radius: var(--border_radius--m);
|
|
7
|
+
padding: var(--bds-space_xxxs);
|
|
8
|
+
background-color: var(--control_track-bg, var(--bds-bg--subtle));
|
|
9
|
+
border-radius: var(--bds-border_radius--m);
|
|
10
10
|
gap: 0;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
.thumb {
|
|
14
14
|
position: absolute;
|
|
15
|
-
top: var(--space_xxxs);
|
|
16
|
-
bottom: var(--space_xxxs);
|
|
17
|
-
left: var(--space_xxxs);
|
|
18
|
-
width: calc((100% - 2 * var(--space_xxxs)) / var(--control_count, 1));
|
|
19
|
-
border-radius: var(--border_radius--s);
|
|
20
|
-
background-color: var(--control_thumb-bg, var(--
|
|
21
|
-
box-shadow: var(--shadow_s);
|
|
15
|
+
top: var(--bds-space_xxxs);
|
|
16
|
+
bottom: var(--bds-space_xxxs);
|
|
17
|
+
left: var(--bds-space_xxxs);
|
|
18
|
+
width: calc((100% - 2 * var(--bds-space_xxxs)) / var(--control_count, 1));
|
|
19
|
+
border-radius: var(--bds-border_radius--s);
|
|
20
|
+
background-color: var(--control_thumb-bg, var(--bds-bg));
|
|
21
|
+
box-shadow: var(--bds-shadow_s);
|
|
22
22
|
pointer-events: none;
|
|
23
23
|
transform: translateX(calc(var(--control_selected-index, 0) * 100%));
|
|
24
|
-
transition: transform var(--animation_transition-duration) var(--animation_easing);
|
|
24
|
+
transition: transform var(--bds-animation_transition-duration) var(--bds-animation_easing);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
.item {
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
align-items: center;
|
|
32
32
|
justify-content: center;
|
|
33
33
|
cursor: pointer;
|
|
34
|
-
border-radius: var(--border_radius--s);
|
|
35
|
-
transition: var(--animation_transition);
|
|
34
|
+
border-radius: var(--bds-border_radius--s);
|
|
35
|
+
transition: var(--bds-animation_transition);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
.item.--disabled {
|
|
@@ -50,40 +50,40 @@
|
|
|
50
50
|
|
|
51
51
|
.label {
|
|
52
52
|
display: block;
|
|
53
|
-
padding: var(--space_xs) var(--space_m);
|
|
53
|
+
padding: var(--bds-space_xs) var(--bds-space_m);
|
|
54
54
|
white-space: nowrap;
|
|
55
|
-
font-size: var(--font_size--body);
|
|
56
|
-
line-height: var(--font_line-height--body);
|
|
57
|
-
color: var(--control_label-color, var(--
|
|
58
|
-
transition: color var(--animation_transition-duration) var(--animation_easing);
|
|
55
|
+
font-size: var(--bds-font_size--body);
|
|
56
|
+
line-height: var(--bds-font_line-height--body);
|
|
57
|
+
color: var(--control_label-color, var(--bds-on-bg--subtle));
|
|
58
|
+
transition: color var(--bds-animation_transition-duration) var(--bds-animation_easing);
|
|
59
59
|
pointer-events: none;
|
|
60
60
|
user-select: none;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
.control.--size_small .label {
|
|
64
|
-
padding: var(--space_xxs) var(--space_s);
|
|
65
|
-
font-size: var(--font_size--body--s);
|
|
64
|
+
padding: var(--bds-space_xxs) var(--bds-space_s);
|
|
65
|
+
font-size: var(--bds-font_size--body--s);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
.control.--size_large .label {
|
|
69
|
-
padding: var(--space_s) var(--space_m);
|
|
69
|
+
padding: var(--bds-space_s) var(--bds-space_m);
|
|
70
70
|
white-space: normal;
|
|
71
71
|
text-align: center;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
.item.--active .label {
|
|
75
|
-
color: var(--control_label-color--active, var(--
|
|
75
|
+
color: var(--control_label-color--active, var(--bds-on-bg));
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
.radio:focus-visible ~ .label {
|
|
79
|
-
outline: var(--outline_default);
|
|
80
|
-
outline-offset: calc(var(--outline_offset) * -1);
|
|
81
|
-
border-radius: var(--border_radius--s);
|
|
79
|
+
outline: var(--bds-outline_default);
|
|
80
|
+
outline-offset: calc(var(--bds-outline_offset) * -1);
|
|
81
|
+
border-radius: var(--bds-border_radius--s);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
@media (hover: hover) and (pointer: fine) {
|
|
85
85
|
.item:not(.--active, .--disabled):hover .label {
|
|
86
|
-
color: var(--control_label-color--hover, var(--
|
|
86
|
+
color: var(--control_label-color--hover, var(--bds-on-bg));
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
@@ -20,6 +20,7 @@ export interface SegmentedControlProps
|
|
|
20
20
|
disabled?: boolean;
|
|
21
21
|
size?: 'small' | 'medium' | 'large';
|
|
22
22
|
className?: string;
|
|
23
|
+
'aria-label'?: string;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export function SegmentedControl({
|
|
@@ -31,6 +32,7 @@ export function SegmentedControl({
|
|
|
31
32
|
disabled,
|
|
32
33
|
size = 'medium',
|
|
33
34
|
className,
|
|
35
|
+
'aria-label': ariaLabel,
|
|
34
36
|
...rest
|
|
35
37
|
}: Readonly<SegmentedControlProps>) {
|
|
36
38
|
const baseId = name + useId();
|
|
@@ -49,6 +51,8 @@ export function SegmentedControl({
|
|
|
49
51
|
|
|
50
52
|
return (
|
|
51
53
|
<div
|
|
54
|
+
role="group"
|
|
55
|
+
aria-label={ariaLabel}
|
|
52
56
|
className={cn(css.control, css[`--size_${size}`], className)}
|
|
53
57
|
style={{
|
|
54
58
|
'--control_count': options.length,
|
|
@@ -13,23 +13,32 @@
|
|
|
13
13
|
.select {
|
|
14
14
|
appearance: none;
|
|
15
15
|
width: 100%;
|
|
16
|
-
font-family: var(--font_family--body);
|
|
17
|
-
font-size: var(--font_size--body);
|
|
18
|
-
padding: var(--space_s);
|
|
19
|
-
padding-inline-end: var(--space_xxl);
|
|
20
|
-
border:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
font-family: var(--bds-font_family--body);
|
|
17
|
+
font-size: var(--bds-font_size--body);
|
|
18
|
+
padding: var(--bds-space_s);
|
|
19
|
+
padding-inline-end: var(--bds-space_xxl);
|
|
20
|
+
border: var(--select_border, none);
|
|
21
|
+
--bdc_color: currentcolor;
|
|
22
|
+
--bdc_radius: var(--bds-border_radius--xs);
|
|
23
|
+
|
|
24
|
+
outline: var(--bdc_width) solid var(--bdc_color);
|
|
25
|
+
outline-offset: var(--bdc_offset);
|
|
26
|
+
border-radius: var(--select_radius, var(--bdc_radius));
|
|
27
|
+
box-shadow: var(--select_shadow, var(--bdc_shadow));
|
|
28
|
+
background-color: var(--select_color_bg, var(--bds-bg));
|
|
29
|
+
color: var(--select_color, var(--bds-on-bg));
|
|
24
30
|
cursor: pointer;
|
|
25
|
-
transition: var(--animation_transition)
|
|
31
|
+
transition: --bdc_color var(--bds-animation_transition-duration) var(--bds-animation_easing),
|
|
32
|
+
background-color var(--bds-animation_transition-duration) var(--bds-animation_easing);
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
.select:focus {
|
|
29
|
-
--
|
|
36
|
+
--bdc_color: var(--bdc_color--focus);
|
|
37
|
+
}
|
|
30
38
|
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
.select:focus-visible {
|
|
40
|
+
outline: var(--bds-outline_default);
|
|
41
|
+
outline-offset: var(--bds-outline_offset);
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
.select:disabled {
|
|
@@ -38,20 +47,16 @@
|
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
.selectError {
|
|
41
|
-
--
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
.selectError:focus {
|
|
45
|
-
--select_focus-ring: 0 0 0 2px rgb(from var(--color_error) r g b / 20%);
|
|
50
|
+
--bdc_color: var(--bdc_color--error);
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
.chevron {
|
|
49
54
|
position: absolute;
|
|
50
|
-
inset-inline-end: var(--space_s);
|
|
55
|
+
inset-inline-end: var(--bds-space_s);
|
|
51
56
|
display: flex;
|
|
52
57
|
align-items: center;
|
|
53
58
|
pointer-events: none;
|
|
54
|
-
color: var(--select_color, var(--
|
|
59
|
+
color: var(--select_color, var(--bds-on-bg));
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
.chevron svg {
|
|
@@ -29,6 +29,7 @@ export function Select({
|
|
|
29
29
|
error,
|
|
30
30
|
hint,
|
|
31
31
|
className,
|
|
32
|
+
required,
|
|
32
33
|
...props
|
|
33
34
|
}: Readonly<SelectProps>) {
|
|
34
35
|
const id = name + useId();
|
|
@@ -44,7 +45,9 @@ export function Select({
|
|
|
44
45
|
id={id}
|
|
45
46
|
name={name}
|
|
46
47
|
aria-invalid={!!error}
|
|
48
|
+
aria-required={required || undefined}
|
|
47
49
|
aria-describedby={describedBy}
|
|
50
|
+
required={required}
|
|
48
51
|
className={cn(css.select, error ? css.selectError : undefined)}
|
|
49
52
|
{...props}
|
|
50
53
|
>
|
|
@@ -46,7 +46,7 @@ Range input for selecting a numeric value within a bounded range.
|
|
|
46
46
|
<tr><th>Variable</th><th>Default</th><th>Description</th></tr>
|
|
47
47
|
</thead>
|
|
48
48
|
<tbody>
|
|
49
|
-
<tr><td>`--slider_color-active`</td><td>`var(--
|
|
49
|
+
<tr><td>`--slider_color-active`</td><td>`var(--bds-active)`</td><td>Filled track and thumb colour</td></tr>
|
|
50
50
|
</tbody>
|
|
51
51
|
</table>
|
|
52
52
|
|