@boostdev/design-system-components 1.1.2 → 1.2.1
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 +38 -14
- package/dist/client.cjs +85 -103
- package/dist/client.css +610 -601
- package/dist/client.d.cts +13 -16
- package/dist/client.d.ts +13 -16
- package/dist/client.js +95 -113
- package/dist/index.cjs +85 -103
- package/dist/index.css +610 -601
- package/dist/index.d.cts +13 -16
- package/dist/index.d.ts +13 -16
- package/dist/index.js +95 -113
- package/dist/native/index.cjs +9 -8
- package/dist/native/index.d.cts +1 -1
- package/dist/native/index.d.ts +1 -1
- package/dist/native/index.js +9 -8
- package/dist/web-components/{chunk-K5CE6HSB.js → chunk-2FGATTGT.js} +79 -17
- package/dist/web-components/{chunk-O4UFI2RX.js → chunk-3REOIRDW.js} +13 -11
- package/dist/web-components/{chunk-5IPHEONG.js → chunk-6I2DBFQ7.js} +1 -0
- package/dist/web-components/{chunk-VM3YHGQ2.js → chunk-AJSXNDAP.js} +1 -2
- package/dist/web-components/{chunk-LSC4N56Q.js → chunk-BX3IPVOB.js} +1 -2
- package/dist/web-components/{chunk-CAPI25CU.js → chunk-DZRSJGPB.js} +3 -3
- package/dist/web-components/{chunk-VSS5ITNE.js → chunk-EBJM3VD4.js} +1 -3
- package/dist/web-components/{chunk-O7I63SQX.js → chunk-HHHRF2PS.js} +6 -12
- package/dist/web-components/{chunk-OTX4COSV.js → chunk-JUKB3BUP.js} +3 -4
- package/dist/web-components/{chunk-IQGLIQSO.js → chunk-N6IMUOY4.js} +2 -5
- package/dist/web-components/{chunk-CJ5ZOO42.js → chunk-OCODKRVZ.js} +68 -44
- package/dist/web-components/{chunk-QDRH46TF.js → chunk-QLZ2362S.js} +1 -3
- package/dist/web-components/{chunk-IK3PTFZ4.js → chunk-T6AETZRP.js} +1 -3
- package/dist/web-components/{chunk-4BX6AWXX.js → chunk-X3FKVHLK.js} +2 -4
- package/dist/web-components/{chunk-A67LCT7C.js → chunk-XNA6WTXG.js} +3 -4
- package/dist/web-components/{chunk-XIENXR7H.js → chunk-YRXCVKHV.js} +1 -3
- package/dist/web-components/{chunk-6T3RKUKG.js → chunk-ZTC6GRP7.js} +1 -2
- package/dist/web-components/globals.js +1 -1
- package/dist/web-components/index.d.ts +571 -1
- package/dist/web-components/index.js +1515 -18
- package/dist/web-components/interaction/bds-accordion.js +1 -1
- package/dist/web-components/interaction/bds-button.d.ts +2 -2
- package/dist/web-components/interaction/bds-button.js +1 -1
- package/dist/web-components/interaction/bds-collapsible.js +1 -1
- package/dist/web-components/interaction/bds-dialog.js +1 -1
- package/dist/web-components/interaction/bds-drawer.js +1 -1
- package/dist/web-components/interaction/form/bds-checkbox.js +1 -1
- package/dist/web-components/interaction/form/bds-combobox.js +1 -1
- package/dist/web-components/interaction/form/bds-number-input.js +1 -1
- package/dist/web-components/interaction/form/bds-radio.js +1 -1
- package/dist/web-components/interaction/form/bds-segmented-control.d.ts +20 -3
- package/dist/web-components/interaction/form/bds-segmented-control.js +1 -1
- package/dist/web-components/interaction/form/bds-select.js +1 -1
- package/dist/web-components/interaction/form/bds-switch.js +1 -1
- package/dist/web-components/interaction/form/bds-textarea.js +1 -1
- package/dist/web-components/ui/bds-alert.js +1 -1
- package/dist/web-components/ui/bds-card.js +1 -1
- package/dist/web-components/ui/bds-notification-banner.js +1 -1
- package/package.json +2 -2
- package/src/components/interaction/Button/Button.module.css +12 -11
- package/src/components/interaction/Button/Button.native.tsx +10 -9
- package/src/components/interaction/Button/Button.tsx +1 -1
- package/src/components/interaction/Command/Command.module.css +3 -7
- package/src/components/interaction/Dialog/Dialog.module.css +7 -5
- package/src/components/interaction/Drawer/Drawer.module.css +48 -25
- package/src/components/interaction/Drawer/Drawer.tsx +21 -17
- package/src/components/interaction/DropdownMenu/DropdownMenu.module.css +1 -3
- package/src/components/interaction/Popover/Popover.module.css +1 -3
- package/src/components/interaction/form/Checkbox/Checkbox.module.css +1 -3
- package/src/components/interaction/form/Combobox/Combobox.module.css +2 -6
- package/src/components/interaction/form/FormInput/FormInput.module.css +1 -3
- package/src/components/interaction/form/NumberInput/NumberInput.module.css +16 -4
- package/src/components/interaction/form/Radio/Radio.module.css +6 -16
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.mdx +74 -25
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.module.css +65 -36
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.spec.tsx +127 -56
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.stories.tsx +89 -72
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.tsx +40 -62
- package/src/components/interaction/form/SegmentedControl/index.ts +1 -1
- package/src/components/interaction/form/Select/Select.module.css +2 -4
- package/src/components/interaction/form/Switch/Switch.module.css +5 -7
- package/src/components/interaction/form/Switch/Switch.native.spec.tsx +9 -9
- package/src/components/interaction/form/Switch/Switch.native.tsx +2 -2
- package/src/components/interaction/form/Textarea/Textarea.module.css +1 -3
- package/src/components/interaction/form/atoms/InputContainer.stories.tsx +64 -0
- package/src/components/interaction/form/atoms/Label.stories.tsx +33 -0
- package/src/components/interaction/form/atoms/Message.stories.tsx +33 -0
- package/src/components/layout/Card/Card.module.css +1 -3
- package/src/components/ui/Accordion/Accordion.module.css +1 -2
- package/src/components/ui/Alert/Alert.module.css +1 -2
- package/src/components/ui/Calendar/Calendar.module.css +2 -5
- package/src/components/ui/Carousel/Carousel.module.css +1 -3
- package/src/components/ui/NotificationBanner/NotificationBanner.module.css +1 -2
- package/src/components/ui/Pagination/Pagination.module.css +1 -2
- package/src/components/ui/SkipLink/SkipLink.module.css +1 -2
- package/src/components/ui/Table/Table.module.css +1 -2
- package/src/css/bdc.css +16 -11
- package/src/index.ts +1 -1
- package/src/stories/Introduction.mdx +2 -1
- package/src/web-components/globals.ts +3 -3
- package/src/web-components/index.ts +12 -0
- package/src/web-components/interaction/BdsAccordion.mdx +103 -0
- package/src/web-components/interaction/BdsAccordion.stories.tsx +94 -0
- package/src/web-components/interaction/BdsCollapsible.mdx +116 -0
- package/src/web-components/interaction/BdsCollapsible.stories.tsx +68 -0
- package/src/web-components/interaction/BdsDialog.mdx +110 -0
- package/src/web-components/interaction/BdsDialog.stories.tsx +157 -0
- package/src/web-components/interaction/BdsDrawer.mdx +108 -0
- package/src/web-components/interaction/BdsDrawer.stories.tsx +196 -0
- package/src/web-components/interaction/BdsDropdownMenu.mdx +108 -0
- package/src/web-components/interaction/BdsDropdownMenu.stories.tsx +91 -0
- package/src/web-components/interaction/BdsSkipLink.mdx +90 -0
- package/src/web-components/interaction/BdsSkipLink.stories.tsx +68 -0
- package/src/web-components/interaction/BdsTabs.mdx +117 -0
- package/src/web-components/interaction/BdsTabs.stories.tsx +94 -0
- package/src/web-components/interaction/BdsTooltip.mdx +104 -0
- package/src/web-components/interaction/BdsTooltip.stories.tsx +87 -0
- package/src/web-components/interaction/bds-accordion.ts +1 -2
- package/src/web-components/interaction/bds-button.ts +15 -13
- package/src/web-components/interaction/bds-collapsible.ts +1 -0
- package/src/web-components/interaction/bds-dialog.ts +2 -5
- package/src/web-components/interaction/bds-drawer.ts +70 -44
- package/src/web-components/interaction/bds-dropdown-menu-item.ts +124 -0
- package/src/web-components/interaction/bds-dropdown-menu.spec.ts +102 -0
- package/src/web-components/interaction/bds-dropdown-menu.ts +200 -0
- package/src/web-components/interaction/form/BdsCheckbox.mdx +76 -0
- package/src/web-components/interaction/form/BdsCheckbox.stories.tsx +72 -0
- package/src/web-components/interaction/form/BdsCheckboxGroup.mdx +70 -0
- package/src/web-components/interaction/form/BdsCheckboxGroup.stories.tsx +81 -0
- package/src/web-components/interaction/form/BdsCombobox.mdx +71 -0
- package/src/web-components/interaction/form/BdsCombobox.stories.tsx +93 -0
- package/src/web-components/interaction/form/BdsFileInput.mdx +79 -0
- package/src/web-components/interaction/form/BdsFileInput.stories.tsx +69 -0
- package/src/web-components/interaction/form/BdsFormInput.mdx +98 -0
- package/src/web-components/interaction/form/BdsFormInput.stories.tsx +139 -0
- package/src/web-components/interaction/form/BdsNumberInput.mdx +70 -0
- package/src/web-components/interaction/form/BdsNumberInput.stories.tsx +68 -0
- package/src/web-components/interaction/form/BdsRadio.mdx +72 -0
- package/src/web-components/interaction/form/BdsRadio.stories.tsx +58 -0
- package/src/web-components/interaction/form/BdsRadioGroup.mdx +73 -0
- package/src/web-components/interaction/form/BdsRadioGroup.stories.tsx +88 -0
- package/src/web-components/interaction/form/BdsSegmentedControl.mdx +95 -0
- package/src/web-components/interaction/form/BdsSegmentedControl.stories.tsx +102 -0
- package/src/web-components/interaction/form/BdsSelect.mdx +76 -0
- package/src/web-components/interaction/form/BdsSelect.stories.tsx +84 -0
- package/src/web-components/interaction/form/BdsSlider.mdx +77 -0
- package/src/web-components/interaction/form/BdsSlider.stories.tsx +68 -0
- package/src/web-components/interaction/form/BdsSwitch.mdx +72 -0
- package/src/web-components/interaction/form/BdsSwitch.stories.tsx +58 -0
- package/src/web-components/interaction/form/BdsTextarea.mdx +73 -0
- package/src/web-components/interaction/form/BdsTextarea.stories.tsx +70 -0
- package/src/web-components/interaction/form/bds-checkbox-group.spec.ts +55 -0
- package/src/web-components/interaction/form/bds-checkbox-group.ts +117 -0
- package/src/web-components/interaction/form/bds-checkbox.ts +1 -3
- package/src/web-components/interaction/form/bds-combobox.ts +1 -3
- package/src/web-components/interaction/form/bds-form-input.spec.ts +83 -0
- package/src/web-components/interaction/form/bds-form-input.ts +268 -0
- package/src/web-components/interaction/form/bds-number-input.ts +3 -4
- package/src/web-components/interaction/form/bds-radio-group.spec.ts +62 -0
- package/src/web-components/interaction/form/bds-radio-group.ts +142 -0
- package/src/web-components/interaction/form/bds-radio.ts +6 -12
- package/src/web-components/interaction/form/bds-segmented-control.ts +96 -20
- package/src/web-components/interaction/form/bds-select.ts +2 -4
- package/src/web-components/interaction/form/bds-switch.ts +3 -4
- package/src/web-components/interaction/form/bds-textarea.ts +1 -3
- package/src/web-components/ui/BdsAvatar.mdx +76 -0
- package/src/web-components/ui/BdsAvatar.stories.tsx +48 -0
- package/src/web-components/ui/BdsBreadcrumb.mdx +66 -0
- package/src/web-components/ui/BdsBreadcrumb.stories.tsx +49 -0
- package/src/web-components/ui/BdsButtonGroup.mdx +69 -0
- package/src/web-components/ui/BdsButtonGroup.stories.tsx +66 -0
- package/src/web-components/ui/BdsCalendar.mdx +93 -0
- package/src/web-components/ui/BdsCalendar.stories.tsx +63 -0
- package/src/web-components/ui/BdsCard.mdx +88 -0
- package/src/web-components/ui/BdsCard.stories.tsx +67 -0
- package/src/web-components/ui/BdsCarousel.mdx +83 -0
- package/src/web-components/ui/BdsCarousel.stories.tsx +96 -0
- package/src/web-components/ui/BdsDescriptionList.mdx +64 -0
- package/src/web-components/ui/BdsDescriptionList.stories.tsx +64 -0
- package/src/web-components/ui/BdsIconWrapper.mdx +70 -0
- package/src/web-components/ui/BdsIconWrapper.stories.tsx +81 -0
- package/src/web-components/ui/BdsLink.mdx +79 -0
- package/src/web-components/ui/BdsLink.stories.tsx +63 -0
- package/src/web-components/ui/BdsLoading.mdx +58 -0
- package/src/web-components/ui/BdsLoading.stories.tsx +35 -0
- package/src/web-components/ui/BdsNotificationBanner.mdx +85 -0
- package/src/web-components/ui/BdsNotificationBanner.stories.tsx +84 -0
- package/src/web-components/ui/BdsPagination.mdx +74 -0
- package/src/web-components/ui/BdsPagination.stories.tsx +36 -0
- package/src/web-components/ui/BdsProgress.mdx +70 -0
- package/src/web-components/ui/BdsProgress.stories.tsx +96 -0
- package/src/web-components/ui/BdsProgressCircle.mdx +70 -0
- package/src/web-components/ui/BdsProgressCircle.stories.tsx +57 -0
- package/src/web-components/ui/BdsRating.mdx +61 -0
- package/src/web-components/ui/BdsRating.stories.tsx +45 -0
- package/src/web-components/ui/BdsSectionHeader.mdx +72 -0
- package/src/web-components/ui/BdsSectionHeader.stories.tsx +54 -0
- package/src/web-components/ui/BdsSeparator.mdx +77 -0
- package/src/web-components/ui/BdsSeparator.stories.tsx +55 -0
- package/src/web-components/ui/BdsSkeleton.mdx +61 -0
- package/src/web-components/ui/BdsSkeleton.stories.tsx +50 -0
- package/src/web-components/ui/BdsTable.mdx +81 -0
- package/src/web-components/ui/BdsTable.stories.tsx +83 -0
- package/src/web-components/ui/BdsTypography.mdx +74 -0
- package/src/web-components/ui/BdsTypography.stories.tsx +52 -0
- package/src/web-components/ui/bds-alert.ts +1 -2
- package/src/web-components/ui/bds-button-group.spec.ts +40 -0
- package/src/web-components/ui/bds-button-group.ts +78 -0
- package/src/web-components/ui/bds-calendar.spec.ts +91 -0
- package/src/web-components/ui/bds-calendar.ts +427 -0
- package/src/web-components/ui/bds-card.ts +1 -3
- package/src/web-components/ui/bds-carousel.spec.ts +64 -0
- package/src/web-components/ui/bds-carousel.ts +296 -0
- package/src/web-components/ui/bds-notification-banner.ts +1 -2
- package/src/web-components/ui/bds-pagination.spec.ts +67 -0
- package/src/web-components/ui/bds-pagination.ts +197 -0
- package/src/web-components/ui/bds-table.spec.ts +45 -0
- package/src/web-components/ui/bds-table.ts +96 -0
|
@@ -37,44 +37,65 @@ export class BdsDrawer extends LitElement {
|
|
|
37
37
|
|
|
38
38
|
.drawer {
|
|
39
39
|
position: fixed;
|
|
40
|
-
inset: 0;
|
|
40
|
+
inset-block: 0;
|
|
41
|
+
inset-inline: auto 0;
|
|
42
|
+
inline-size: min(28rem, 90vw);
|
|
43
|
+
max-inline-size: 100vw;
|
|
44
|
+
block-size: 100dvh;
|
|
45
|
+
max-block-size: 100dvh;
|
|
41
46
|
margin: 0;
|
|
42
47
|
padding: 0;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-direction: column;
|
|
50
|
+
background-color: var(--drawer_color, var(--bds-color_bg));
|
|
51
|
+
color: var(--drawer_on-color, var(--bds-color_on-bg));
|
|
47
52
|
border: none;
|
|
48
|
-
|
|
53
|
+
box-shadow: var(--bds-shadow_xl);
|
|
49
54
|
overflow: hidden;
|
|
55
|
+
translate: 100% 0;
|
|
56
|
+
transition:
|
|
57
|
+
translate var(--bds-animation_transition-duration) var(--bds-animation_easing),
|
|
58
|
+
display var(--bds-animation_transition-duration) var(--bds-animation_easing) allow-discrete,
|
|
59
|
+
overlay var(--bds-animation_transition-duration) var(--bds-animation_easing) allow-discrete;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
:host([side='left']) .drawer {
|
|
63
|
+
inset-inline: 0 auto;
|
|
64
|
+
translate: -100% 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.drawer[open] {
|
|
68
|
+
translate: 0 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@starting-style {
|
|
72
|
+
.drawer[open] {
|
|
73
|
+
translate: 100% 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
:host([side='left']) .drawer[open] {
|
|
77
|
+
translate: -100% 0;
|
|
78
|
+
}
|
|
50
79
|
}
|
|
51
80
|
|
|
52
81
|
.drawer::backdrop {
|
|
53
82
|
background-color: var(--color_backdrop, rgb(0 0 0 / 50%));
|
|
54
83
|
backdrop-filter: blur(3px);
|
|
84
|
+
opacity: 0;
|
|
85
|
+
transition:
|
|
86
|
+
opacity var(--bds-animation_transition-duration) var(--bds-animation_easing),
|
|
87
|
+
display var(--bds-animation_transition-duration) allow-discrete,
|
|
88
|
+
overlay var(--bds-animation_transition-duration) allow-discrete;
|
|
55
89
|
}
|
|
56
90
|
|
|
57
|
-
.
|
|
58
|
-
|
|
59
|
-
inset-block: 0;
|
|
60
|
-
display: flex;
|
|
61
|
-
flex-direction: column;
|
|
62
|
-
inline-size: min(28rem, 90vw);
|
|
63
|
-
block-size: 100%;
|
|
64
|
-
background-color: var(--drawer_color, var(--bds-color_bg));
|
|
65
|
-
color: var(--drawer_on-color, var(--bds-color_on-bg));
|
|
66
|
-
box-shadow: var(--bds-shadow_xl);
|
|
67
|
-
overflow: hidden;
|
|
68
|
-
transition: var(--bds-animation_transition);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
:host([side='right']) .panel {
|
|
72
|
-
inset-inline-end: 0;
|
|
91
|
+
.drawer[open]::backdrop {
|
|
92
|
+
opacity: 1;
|
|
73
93
|
}
|
|
74
94
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
95
|
+
@starting-style {
|
|
96
|
+
.drawer[open]::backdrop {
|
|
97
|
+
opacity: 0;
|
|
98
|
+
}
|
|
78
99
|
}
|
|
79
100
|
|
|
80
101
|
.header {
|
|
@@ -128,7 +149,8 @@ export class BdsDrawer extends LitElement {
|
|
|
128
149
|
}
|
|
129
150
|
|
|
130
151
|
@media (prefers-reduced-motion: reduce) {
|
|
131
|
-
.
|
|
152
|
+
.drawer,
|
|
153
|
+
.drawer::backdrop {
|
|
132
154
|
transition: none;
|
|
133
155
|
}
|
|
134
156
|
}
|
|
@@ -202,7 +224,13 @@ export class BdsDrawer extends LitElement {
|
|
|
202
224
|
}
|
|
203
225
|
|
|
204
226
|
private _handleBackdropClick(e: MouseEvent) {
|
|
205
|
-
|
|
227
|
+
const dialog = this._dialog;
|
|
228
|
+
if (!dialog) return;
|
|
229
|
+
const rect = dialog.getBoundingClientRect();
|
|
230
|
+
const outside =
|
|
231
|
+
e.clientX < rect.left || e.clientX > rect.right ||
|
|
232
|
+
e.clientY < rect.top || e.clientY > rect.bottom;
|
|
233
|
+
if (outside) this._closeDrawer();
|
|
206
234
|
}
|
|
207
235
|
|
|
208
236
|
private _handleCancel(e: Event) {
|
|
@@ -234,23 +262,21 @@ export class BdsDrawer extends LitElement {
|
|
|
234
262
|
@cancel=${this._handleCancel}
|
|
235
263
|
@keydown=${this._handleKeydown}
|
|
236
264
|
>
|
|
237
|
-
<div class="
|
|
238
|
-
<
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
>
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
<
|
|
252
|
-
<slot></slot>
|
|
253
|
-
</div>
|
|
265
|
+
<div class="header">
|
|
266
|
+
<span class="title"><slot name="title"></slot></span>
|
|
267
|
+
<button
|
|
268
|
+
type="button"
|
|
269
|
+
class="close-button"
|
|
270
|
+
aria-label="Close drawer"
|
|
271
|
+
@click=${this._closeDrawer}
|
|
272
|
+
>
|
|
273
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
274
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M18 6L6 18M6 6l12 12" />
|
|
275
|
+
</svg>
|
|
276
|
+
</button>
|
|
277
|
+
</div>
|
|
278
|
+
<div class="body">
|
|
279
|
+
<slot></slot>
|
|
254
280
|
</div>
|
|
255
281
|
</dialog>
|
|
256
282
|
`;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { LitElement, css, html } from 'lit';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `<bds-dropdown-menu-item>` — a single item inside a `<bds-dropdown-menu>`.
|
|
5
|
+
*
|
|
6
|
+
* Attributes:
|
|
7
|
+
* value — value associated with this item
|
|
8
|
+
* disabled — disables this item
|
|
9
|
+
* destructive — renders the item in a destructive (red) style
|
|
10
|
+
*
|
|
11
|
+
* Slots:
|
|
12
|
+
* (default) — item label text
|
|
13
|
+
*
|
|
14
|
+
* Events:
|
|
15
|
+
* bds-select — fired when the item is clicked or activated; `detail: { value: string }`
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* <bds-dropdown-menu-item value="delete" destructive>Delete</bds-dropdown-menu-item>
|
|
19
|
+
*/
|
|
20
|
+
export class BdsDropdownMenuItem extends LitElement {
|
|
21
|
+
static styles = css`
|
|
22
|
+
:host {
|
|
23
|
+
display: block;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
:host([disabled]) {
|
|
27
|
+
opacity: 0.5;
|
|
28
|
+
pointer-events: none;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.item {
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
gap: var(--bds-space_xs);
|
|
35
|
+
inline-size: 100%;
|
|
36
|
+
padding: var(--bds-space_xs) var(--bds-space_s);
|
|
37
|
+
font-size: var(--bds-font_size--body);
|
|
38
|
+
font-family: var(--bds-font_family--body);
|
|
39
|
+
color: var(--bds-color_on-bg);
|
|
40
|
+
background: none;
|
|
41
|
+
border: none;
|
|
42
|
+
text-align: start;
|
|
43
|
+
cursor: pointer;
|
|
44
|
+
border-radius: var(--bds-border_radius--xs);
|
|
45
|
+
transition: var(--bds-animation_transition);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
:host([destructive]) .item {
|
|
49
|
+
color: var(--bds-color_error);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@media (hover: hover) and (pointer: fine) {
|
|
53
|
+
.item:hover {
|
|
54
|
+
background-color: var(--bds-color_bg--subtle);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
:host([destructive]) .item:hover {
|
|
58
|
+
background-color: color-mix(in srgb, var(--bds-color_error) 10%, transparent);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.item:focus-visible {
|
|
63
|
+
outline: var(--bds-outline_default);
|
|
64
|
+
outline-offset: var(--bds-outline_offset);
|
|
65
|
+
}
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
static properties = {
|
|
69
|
+
value: { type: String, reflect: true },
|
|
70
|
+
disabled: { type: Boolean, reflect: true },
|
|
71
|
+
destructive: { type: Boolean, reflect: true },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
declare value: string;
|
|
75
|
+
declare disabled: boolean;
|
|
76
|
+
declare destructive: boolean;
|
|
77
|
+
|
|
78
|
+
constructor() {
|
|
79
|
+
super();
|
|
80
|
+
this.value = '';
|
|
81
|
+
this.disabled = false;
|
|
82
|
+
this.destructive = false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private _handleClick() {
|
|
86
|
+
if (this.disabled) return;
|
|
87
|
+
this.dispatchEvent(new CustomEvent('bds-select', {
|
|
88
|
+
detail: { value: this.value },
|
|
89
|
+
bubbles: true,
|
|
90
|
+
composed: true,
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private _handleKeydown(e: KeyboardEvent) {
|
|
95
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
this._handleClick();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
render() {
|
|
102
|
+
return html`
|
|
103
|
+
<button
|
|
104
|
+
class="item"
|
|
105
|
+
type="button"
|
|
106
|
+
role="menuitem"
|
|
107
|
+
tabindex=${this.disabled ? '-1' : '0'}
|
|
108
|
+
?disabled=${this.disabled}
|
|
109
|
+
@click=${this._handleClick}
|
|
110
|
+
@keydown=${this._handleKeydown}
|
|
111
|
+
>
|
|
112
|
+
<slot></slot>
|
|
113
|
+
</button>
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
customElements.define('bds-dropdown-menu-item', BdsDropdownMenuItem);
|
|
119
|
+
|
|
120
|
+
declare global {
|
|
121
|
+
interface HTMLElementTagNameMap {
|
|
122
|
+
'bds-dropdown-menu-item': BdsDropdownMenuItem;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { fixture, cleanup } from '../test/helpers';
|
|
2
|
+
import { BdsDropdownMenu } from './bds-dropdown-menu';
|
|
3
|
+
import { BdsDropdownMenuItem } from './bds-dropdown-menu-item';
|
|
4
|
+
|
|
5
|
+
describe('bds-dropdown-menu', () => {
|
|
6
|
+
it('is registered as a custom element', () => {
|
|
7
|
+
expect(customElements.get('bds-dropdown-menu')).toBe(BdsDropdownMenu);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('is closed by default', async () => {
|
|
11
|
+
const el = await fixture('<bds-dropdown-menu></bds-dropdown-menu>') as BdsDropdownMenu;
|
|
12
|
+
expect(el.open).toBe(false);
|
|
13
|
+
cleanup(el);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('open attribute is absent when closed', async () => {
|
|
17
|
+
const el = await fixture('<bds-dropdown-menu></bds-dropdown-menu>') as BdsDropdownMenu;
|
|
18
|
+
expect(el.open).toBe(false);
|
|
19
|
+
expect(el.hasAttribute('open')).toBe(false);
|
|
20
|
+
cleanup(el);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('fires bds-open when _open() is called', async () => {
|
|
24
|
+
const el = await fixture('<bds-dropdown-menu></bds-dropdown-menu>') as BdsDropdownMenu;
|
|
25
|
+
const events: string[] = [];
|
|
26
|
+
el.addEventListener('bds-open', () => events.push('open'));
|
|
27
|
+
el._open();
|
|
28
|
+
expect(events).toContain('open');
|
|
29
|
+
cleanup(el);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('fires bds-close when _close() is called', async () => {
|
|
33
|
+
const el = await fixture('<bds-dropdown-menu></bds-dropdown-menu>') as BdsDropdownMenu;
|
|
34
|
+
el._open();
|
|
35
|
+
const events: string[] = [];
|
|
36
|
+
el.addEventListener('bds-close', () => events.push('close'));
|
|
37
|
+
el._close();
|
|
38
|
+
expect(events).toContain('close');
|
|
39
|
+
cleanup(el);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('does not fire bds-open if already open (state guard)', async () => {
|
|
43
|
+
const el = await fixture('<bds-dropdown-menu></bds-dropdown-menu>') as BdsDropdownMenu;
|
|
44
|
+
el._open();
|
|
45
|
+
const events: string[] = [];
|
|
46
|
+
el.addEventListener('bds-open', () => events.push('open'));
|
|
47
|
+
el._open(); // call again
|
|
48
|
+
expect(events.length).toBe(0);
|
|
49
|
+
cleanup(el);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('does not fire bds-close if already closed (state guard)', async () => {
|
|
53
|
+
const el = await fixture('<bds-dropdown-menu></bds-dropdown-menu>') as BdsDropdownMenu;
|
|
54
|
+
const events: string[] = [];
|
|
55
|
+
el.addEventListener('bds-close', () => events.push('close'));
|
|
56
|
+
el._close(); // already closed
|
|
57
|
+
expect(events.length).toBe(0);
|
|
58
|
+
cleanup(el);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('closes on Escape key', async () => {
|
|
62
|
+
const el = await fixture('<bds-dropdown-menu></bds-dropdown-menu>') as BdsDropdownMenu;
|
|
63
|
+
el._open();
|
|
64
|
+
expect(el.open).toBe(true);
|
|
65
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
66
|
+
expect(el.open).toBe(false);
|
|
67
|
+
cleanup(el);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('bds-dropdown-menu-item', () => {
|
|
72
|
+
it('is registered as a custom element', () => {
|
|
73
|
+
expect(customElements.get('bds-dropdown-menu-item')).toBe(BdsDropdownMenuItem);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('renders a button with role="menuitem"', async () => {
|
|
77
|
+
const el = await fixture('<bds-dropdown-menu-item value="edit">Edit</bds-dropdown-menu-item>');
|
|
78
|
+
const btn = el.shadowRoot!.querySelector('button')!;
|
|
79
|
+
expect(btn.getAttribute('role')).toBe('menuitem');
|
|
80
|
+
cleanup(el);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('fires bds-select on click', async () => {
|
|
84
|
+
const el = await fixture('<bds-dropdown-menu-item value="edit">Edit</bds-dropdown-menu-item>');
|
|
85
|
+
const events: string[] = [];
|
|
86
|
+
el.addEventListener('bds-select', (e: Event) => {
|
|
87
|
+
events.push((e as CustomEvent).detail.value);
|
|
88
|
+
});
|
|
89
|
+
el.shadowRoot!.querySelector<HTMLButtonElement>('button')!.click();
|
|
90
|
+
expect(events).toEqual(['edit']);
|
|
91
|
+
cleanup(el);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('does not fire bds-select when disabled', async () => {
|
|
95
|
+
const el = await fixture('<bds-dropdown-menu-item value="edit" disabled>Edit</bds-dropdown-menu-item>');
|
|
96
|
+
const events: string[] = [];
|
|
97
|
+
el.addEventListener('bds-select', () => events.push('selected'));
|
|
98
|
+
el.shadowRoot!.querySelector<HTMLButtonElement>('button')!.click();
|
|
99
|
+
expect(events.length).toBe(0);
|
|
100
|
+
cleanup(el);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { LitElement, css, html } from 'lit';
|
|
2
|
+
import type { BdsDropdownMenuItem } from './bds-dropdown-menu-item';
|
|
3
|
+
|
|
4
|
+
export type DropdownMenuPlacement = 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `<bds-dropdown-menu>` — anchored dropdown menu with keyboard navigation.
|
|
8
|
+
*
|
|
9
|
+
* Uses the native Popover API for the floating panel (light-dismiss, top-layer).
|
|
10
|
+
* The trigger slot element controls open/close. The component handles keyboard
|
|
11
|
+
* navigation (Arrow keys, Escape, Tab) and focus management.
|
|
12
|
+
*
|
|
13
|
+
* Attributes:
|
|
14
|
+
* open — controls open/closed state (reflected)
|
|
15
|
+
* placement — menu position relative to trigger (default: `"bottom-start"`)
|
|
16
|
+
*
|
|
17
|
+
* Slots:
|
|
18
|
+
* trigger — the activating element (button or similar)
|
|
19
|
+
* (default) — `<bds-dropdown-menu-item>` elements
|
|
20
|
+
*
|
|
21
|
+
* Events:
|
|
22
|
+
* bds-open — fired when the menu opens
|
|
23
|
+
* bds-close — fired when the menu closes
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* <bds-dropdown-menu>
|
|
27
|
+
* <button slot="trigger">Options</button>
|
|
28
|
+
* <bds-dropdown-menu-item value="edit">Edit</bds-dropdown-menu-item>
|
|
29
|
+
* <bds-dropdown-menu-item value="delete" destructive>Delete</bds-dropdown-menu-item>
|
|
30
|
+
* </bds-dropdown-menu>
|
|
31
|
+
*/
|
|
32
|
+
export class BdsDropdownMenu extends LitElement {
|
|
33
|
+
static styles = css`
|
|
34
|
+
:host {
|
|
35
|
+
display: inline-block;
|
|
36
|
+
position: relative;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.panel {
|
|
40
|
+
position: absolute;
|
|
41
|
+
inset-block-start: calc(100% + var(--bds-space_xxs));
|
|
42
|
+
inset-inline-start: 0;
|
|
43
|
+
min-inline-size: 10rem;
|
|
44
|
+
background-color: var(--bds-color_bg);
|
|
45
|
+
border-radius: var(--bds-border_radius--s);
|
|
46
|
+
box-shadow: var(--bds-shadow_m);
|
|
47
|
+
padding: var(--bds-space_xxs);
|
|
48
|
+
z-index: var(--bds-z-index_dropdown, 200);
|
|
49
|
+
display: none;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
:host([placement='bottom-end']) .panel {
|
|
54
|
+
inset-inline-start: auto;
|
|
55
|
+
inset-inline-end: 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
:host([placement='top-start']) .panel {
|
|
59
|
+
inset-block-start: auto;
|
|
60
|
+
inset-block-end: calc(100% + var(--bds-space_xxs));
|
|
61
|
+
inset-inline-start: 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
:host([placement='top-end']) .panel {
|
|
65
|
+
inset-block-start: auto;
|
|
66
|
+
inset-block-end: calc(100% + var(--bds-space_xxs));
|
|
67
|
+
inset-inline-start: auto;
|
|
68
|
+
inset-inline-end: 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
:host([open]) .panel {
|
|
72
|
+
display: flex;
|
|
73
|
+
}
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
static properties = {
|
|
77
|
+
open: { type: Boolean, reflect: true },
|
|
78
|
+
placement: { type: String, reflect: true },
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
declare open: boolean;
|
|
82
|
+
declare placement: DropdownMenuPlacement;
|
|
83
|
+
|
|
84
|
+
constructor() {
|
|
85
|
+
super();
|
|
86
|
+
this.open = false;
|
|
87
|
+
this.placement = 'bottom-start';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
connectedCallback() {
|
|
91
|
+
super.connectedCallback();
|
|
92
|
+
document.addEventListener('click', this._handleOutsideClick);
|
|
93
|
+
this.addEventListener('keydown', this._handleKeydown);
|
|
94
|
+
this.addEventListener('bds-select', this._handleItemSelect);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
disconnectedCallback() {
|
|
98
|
+
super.disconnectedCallback();
|
|
99
|
+
document.removeEventListener('click', this._handleOutsideClick);
|
|
100
|
+
this.removeEventListener('keydown', this._handleKeydown);
|
|
101
|
+
this.removeEventListener('bds-select', this._handleItemSelect);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private get _items(): BdsDropdownMenuItem[] {
|
|
105
|
+
return [...this.querySelectorAll<BdsDropdownMenuItem>('bds-dropdown-menu-item')].filter(
|
|
106
|
+
item => !item.disabled,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private get _trigger(): HTMLElement | null {
|
|
111
|
+
const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('slot[name="trigger"]');
|
|
112
|
+
return (slot?.assignedElements()[0] as HTMLElement) ?? null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
_open() {
|
|
116
|
+
if (this.open) return;
|
|
117
|
+
this.open = true;
|
|
118
|
+
this.dispatchEvent(new CustomEvent('bds-open', { bubbles: true, composed: true }));
|
|
119
|
+
// Focus first item after render
|
|
120
|
+
this.updateComplete.then(() => {
|
|
121
|
+
this._items[0]?.shadowRoot?.querySelector<HTMLElement>('button')?.focus();
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
_close() {
|
|
126
|
+
if (!this.open) return;
|
|
127
|
+
this.open = false;
|
|
128
|
+
this.dispatchEvent(new CustomEvent('bds-close', { bubbles: true, composed: true }));
|
|
129
|
+
this._trigger?.focus();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private _handleTriggerClick = (e: Event) => {
|
|
133
|
+
e.stopPropagation();
|
|
134
|
+
if (this.open) { this._close(); } else { this._open(); }
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
private _handleOutsideClick = (e: Event) => {
|
|
138
|
+
if (this.open && !this.contains(e.target as Node)) {
|
|
139
|
+
this._close();
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
private _handleKeydown = (e: KeyboardEvent) => {
|
|
144
|
+
if (!this.open) return;
|
|
145
|
+
|
|
146
|
+
const items = this._items;
|
|
147
|
+
const focused = items.findIndex(item =>
|
|
148
|
+
item.shadowRoot?.activeElement !== null &&
|
|
149
|
+
item.shadowRoot?.querySelector('button') === item.shadowRoot?.activeElement,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (e.key === 'Escape') {
|
|
153
|
+
e.preventDefault();
|
|
154
|
+
this._close();
|
|
155
|
+
} else if (e.key === 'ArrowDown') {
|
|
156
|
+
e.preventDefault();
|
|
157
|
+
const next = (focused + 1) % items.length;
|
|
158
|
+
items[next]?.shadowRoot?.querySelector<HTMLElement>('button')?.focus();
|
|
159
|
+
} else if (e.key === 'ArrowUp') {
|
|
160
|
+
e.preventDefault();
|
|
161
|
+
const prev = (focused - 1 + items.length) % items.length;
|
|
162
|
+
items[prev]?.shadowRoot?.querySelector<HTMLElement>('button')?.focus();
|
|
163
|
+
} else if (e.key === 'Tab') {
|
|
164
|
+
this._close();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
private _handleItemSelect = () => {
|
|
169
|
+
this._close();
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
private _onTriggerSlotChange() {
|
|
173
|
+
const trigger = this._trigger;
|
|
174
|
+
if (trigger) {
|
|
175
|
+
trigger.removeEventListener('click', this._handleTriggerClick);
|
|
176
|
+
trigger.addEventListener('click', this._handleTriggerClick);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
render() {
|
|
181
|
+
return html`
|
|
182
|
+
<slot name="trigger" @slotchange=${this._onTriggerSlotChange}></slot>
|
|
183
|
+
<div
|
|
184
|
+
class="panel"
|
|
185
|
+
role="menu"
|
|
186
|
+
aria-hidden=${!this.open ? 'true' : 'false'}
|
|
187
|
+
>
|
|
188
|
+
<slot></slot>
|
|
189
|
+
</div>
|
|
190
|
+
`;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
customElements.define('bds-dropdown-menu', BdsDropdownMenu);
|
|
195
|
+
|
|
196
|
+
declare global {
|
|
197
|
+
interface HTMLElementTagNameMap {
|
|
198
|
+
'bds-dropdown-menu': BdsDropdownMenu;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
|
|
2
|
+
import * as Stories from './BdsCheckbox.stories';
|
|
3
|
+
|
|
4
|
+
<Meta of={Stories} />
|
|
5
|
+
|
|
6
|
+
# <bds-checkbox>
|
|
7
|
+
|
|
8
|
+
Form-associated checkbox custom element. Participates in HTML forms via the Form-Associated Custom Elements API — native browser validation (required, etc.) works without any wrapper.
|
|
9
|
+
|
|
10
|
+
> **Status: experimental** — API may change before stable release.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import '@boostdev/components/web-components';
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## When to use
|
|
19
|
+
|
|
20
|
+
Use `<bds-checkbox>` whenever you need a single boolean toggle that participates in a form. For grouping multiple checkboxes under a shared label, wrap them in `<bds-checkbox-group>`.
|
|
21
|
+
|
|
22
|
+
## Examples
|
|
23
|
+
|
|
24
|
+
### Default
|
|
25
|
+
<Canvas of={Stories.Default} />
|
|
26
|
+
|
|
27
|
+
### Checked
|
|
28
|
+
<Canvas of={Stories.Checked} />
|
|
29
|
+
|
|
30
|
+
### Required
|
|
31
|
+
<Canvas of={Stories.Required} />
|
|
32
|
+
|
|
33
|
+
### Disabled
|
|
34
|
+
<Canvas of={Stories.Disabled} />
|
|
35
|
+
|
|
36
|
+
### Visual group (without group wrapper)
|
|
37
|
+
<Canvas of={Stories.WithGroup} />
|
|
38
|
+
|
|
39
|
+
## Props
|
|
40
|
+
|
|
41
|
+
<ArgTypes of={Stories} />
|
|
42
|
+
|
|
43
|
+
## Attributes
|
|
44
|
+
|
|
45
|
+
| Attribute | Type | Default | Description |
|
|
46
|
+
|------------|---------|---------|--------------------------------------------------|
|
|
47
|
+
| `name` | string | `""` | Form field name |
|
|
48
|
+
| `value` | string | `"on"` | Value submitted when checked |
|
|
49
|
+
| `checked` | boolean | `false` | Whether the checkbox is checked |
|
|
50
|
+
| `required` | boolean | `false` | Marks the field as required |
|
|
51
|
+
| `disabled` | boolean | `false` | Disables the checkbox |
|
|
52
|
+
|
|
53
|
+
## Slots
|
|
54
|
+
|
|
55
|
+
| Slot | Description |
|
|
56
|
+
|-------------|--------------------|
|
|
57
|
+
| `(default)` | Label text content |
|
|
58
|
+
|
|
59
|
+
## Events
|
|
60
|
+
|
|
61
|
+
| Event | Detail | Description |
|
|
62
|
+
|----------|--------|-------------------------------------|
|
|
63
|
+
| `change` | — | Native change event forwarded from the inner `<input>` |
|
|
64
|
+
|
|
65
|
+
## Usage in plain HTML
|
|
66
|
+
|
|
67
|
+
```html
|
|
68
|
+
<bds-checkbox name="agree" value="yes" required>I agree to the terms</bds-checkbox>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Accessibility
|
|
72
|
+
|
|
73
|
+
- The inner `<input type="checkbox">` is associated with a `<label>` via matching `id`/`for` attributes
|
|
74
|
+
- `required` triggers native browser validation
|
|
75
|
+
- `disabled` sets `pointer-events: none` and reduces opacity
|
|
76
|
+
- Use `<bds-checkbox-group>` for accessible grouping with `<fieldset>`/`<legend>`
|