@boostdev/design-system-components 2.15.0 → 2.16.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 +19 -14
- package/dist/client.cjs +271 -205
- package/dist/client.css +801 -642
- package/dist/client.d.cts +19 -4
- package/dist/client.d.ts +19 -4
- package/dist/client.js +206 -139
- package/dist/index.cjs +271 -205
- package/dist/index.css +801 -642
- package/dist/index.d.cts +19 -4
- package/dist/index.d.ts +19 -4
- package/dist/index.js +206 -139
- package/dist/web-components/chunk-2DHW2WJZ.js +154 -0
- package/dist/web-components/{chunk-LDIPCTRN.js → chunk-2MJTO2IW.js} +48 -2
- package/dist/web-components/{chunk-3WAV6MDN.js → chunk-3FSGP35V.js} +1 -0
- package/dist/web-components/{chunk-KA3VPKMY.js → chunk-4MB2ICSK.js} +1 -0
- package/dist/web-components/{chunk-IDSM74LM.js → chunk-6XL5P76D.js} +3 -3
- package/dist/web-components/{chunk-P24JYC5V.js → chunk-7VXRMCAL.js} +10 -4
- package/dist/web-components/{chunk-CUKQ2L5B.js → chunk-AGI6YQRC.js} +9 -16
- package/dist/web-components/chunk-BDRV452N.js +51 -0
- package/dist/web-components/{chunk-3AY2YKCM.js → chunk-CKCPV347.js} +1 -0
- package/dist/web-components/{chunk-HBLJ4SLY.js → chunk-DD3OUXNG.js} +5 -5
- package/dist/web-components/{chunk-RVF7W653.js → chunk-DGOCPKHI.js} +30 -10
- package/dist/web-components/{chunk-2OLVQUBU.js → chunk-EYCW7TKB.js} +8 -15
- package/dist/web-components/{chunk-VANAKB6D.js → chunk-GPJWLAKU.js} +33 -16
- package/dist/web-components/{chunk-VED2DFJ6.js → chunk-GQNQBEYR.js} +8 -15
- package/dist/web-components/{chunk-WFVKU4GX.js → chunk-IKAJLROF.js} +49 -4
- package/dist/web-components/{chunk-RSZ3BWBK.js → chunk-KSK4RZZQ.js} +30 -7
- package/dist/web-components/{chunk-FEDABOR5.js → chunk-LHFUXZ52.js} +7 -15
- package/dist/web-components/{chunk-M3ECULRH.js → chunk-RBF7WPLI.js} +10 -8
- package/dist/web-components/{chunk-6G22I2CK.js → chunk-RZH5IVB5.js} +31 -1
- package/dist/web-components/chunk-SRKQ6GST.js +40 -0
- package/dist/web-components/{chunk-5HUAFCU6.js → chunk-TJV7UIXC.js} +7 -0
- package/dist/web-components/{chunk-J3HVUIUH.js → chunk-XI2U6U26.js} +8 -15
- package/dist/web-components/{chunk-JH6LQKLK.js → chunk-XJU7KITO.js} +8 -15
- package/dist/web-components/{chunk-JV5QT2OU.js → chunk-YLDQOESI.js} +27 -13
- package/dist/web-components/globals.js +1 -1
- package/dist/web-components/index.d.ts +1 -0
- package/dist/web-components/index.js +33 -28
- package/dist/web-components/interaction/bds-dropdown-menu-group.d.ts +41 -0
- package/dist/web-components/interaction/bds-dropdown-menu-group.js +7 -0
- package/dist/web-components/interaction/bds-dropdown-menu-item.d.ts +14 -1
- package/dist/web-components/interaction/bds-dropdown-menu-item.js +1 -1
- package/dist/web-components/interaction/bds-dropdown-menu.d.ts +2 -1
- package/dist/web-components/interaction/bds-dropdown-menu.js +1 -1
- package/dist/web-components/interaction/bds-tab.d.ts +5 -0
- package/dist/web-components/interaction/bds-tab.js +1 -1
- package/dist/web-components/interaction/bds-tooltip.js +1 -1
- package/dist/web-components/interaction/form/bds-checkbox-group.d.ts +6 -0
- package/dist/web-components/interaction/form/bds-checkbox-group.js +2 -1
- package/dist/web-components/interaction/form/bds-checkbox.js +1 -1
- package/dist/web-components/interaction/form/bds-combobox.js +2 -1
- package/dist/web-components/interaction/form/bds-form-input.js +2 -1
- package/dist/web-components/interaction/form/bds-number-input.js +2 -1
- package/dist/web-components/interaction/form/bds-radio-group.d.ts +5 -0
- package/dist/web-components/interaction/form/bds-radio-group.js +2 -1
- package/dist/web-components/interaction/form/bds-radio.js +1 -1
- package/dist/web-components/interaction/form/bds-select.js +2 -1
- package/dist/web-components/interaction/form/bds-slider.js +2 -1
- package/dist/web-components/interaction/form/bds-switch.js +1 -1
- package/dist/web-components/interaction/form/bds-textarea.js +2 -1
- package/dist/web-components/ui/bds-alert.d.ts +8 -1
- package/dist/web-components/ui/bds-alert.js +1 -1
- package/dist/web-components/ui/bds-avatar.d.ts +11 -0
- package/dist/web-components/ui/bds-avatar.js +1 -1
- package/dist/web-components/ui/bds-notification-banner.d.ts +14 -1
- package/dist/web-components/ui/bds-notification-banner.js +1 -1
- package/dist/web-components/ui/bds-section-header.d.ts +6 -0
- package/dist/web-components/ui/bds-section-header.js +1 -1
- package/dist/web-components/ui/bds-separator.js +1 -1
- package/dist/web-components/ui/bds-table.js +1 -1
- package/package.json +6 -2
- package/src/components/interaction/DropdownMenu/DropdownMenu.mdx +35 -4
- package/src/components/interaction/DropdownMenu/DropdownMenu.module.css +58 -10
- package/src/components/interaction/DropdownMenu/DropdownMenu.spec.tsx +114 -1
- package/src/components/interaction/DropdownMenu/DropdownMenu.stories.tsx +25 -0
- package/src/components/interaction/DropdownMenu/DropdownMenu.tsx +69 -17
- package/src/components/interaction/form/Checkbox/Checkbox.module.css +1 -0
- package/src/components/interaction/form/Checkbox/Checkbox.spec.tsx +7 -0
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.mdx +1 -0
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.module.css +7 -0
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.spec.tsx +22 -0
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.tsx +1 -1
- package/src/components/interaction/form/Combobox/Combobox.module.css +1 -0
- package/src/components/interaction/form/Combobox/Combobox.spec.tsx +7 -0
- package/src/components/interaction/form/FormInput/FormInput.mdx +9 -0
- package/src/components/interaction/form/FormInput/FormInput.module.css +1 -0
- package/src/components/interaction/form/FormInput/FormInput.spec.tsx +7 -0
- package/src/components/interaction/form/NumberInput/NumberInput.module.css +1 -0
- package/src/components/interaction/form/NumberInput/NumberInput.spec.tsx +7 -0
- package/src/components/interaction/form/Radio/Radio.module.css +1 -0
- package/src/components/interaction/form/Radio/Radio.spec.tsx +7 -0
- package/src/components/interaction/form/RadioGroup/RadioGroup.mdx +1 -0
- package/src/components/interaction/form/RadioGroup/RadioGroup.module.css +7 -0
- package/src/components/interaction/form/RadioGroup/RadioGroup.spec.tsx +22 -0
- package/src/components/interaction/form/RadioGroup/RadioGroup.tsx +1 -1
- package/src/components/interaction/form/Select/Select.module.css +1 -0
- package/src/components/interaction/form/Select/Select.spec.tsx +7 -0
- package/src/components/interaction/form/Switch/Switch.module.css +1 -0
- package/src/components/interaction/form/Switch/Switch.spec.tsx +7 -0
- package/src/components/interaction/form/Textarea/Textarea.module.css +1 -0
- package/src/components/interaction/form/Textarea/Textarea.spec.tsx +7 -0
- package/src/components/interaction/form/atoms/Message.module.css +14 -3
- package/src/components/interaction/form/atoms/Message.spec.tsx +22 -0
- package/src/components/interaction/form/atoms/Message.tsx +5 -0
- package/src/components/layout/SectionHeader/SectionHeader.mdx +18 -5
- package/src/components/layout/SectionHeader/SectionHeader.module.css +35 -14
- package/src/components/layout/SectionHeader/SectionHeader.spec.tsx +44 -0
- package/src/components/layout/SectionHeader/SectionHeader.stories.tsx +5 -0
- package/src/components/layout/SectionHeader/SectionHeader.tsx +3 -1
- package/src/components/ui/Alert/Alert.mdx +11 -0
- package/src/components/ui/Alert/Alert.module.css +30 -1
- package/src/components/ui/Alert/Alert.spec.tsx +21 -0
- package/src/components/ui/Alert/Alert.stories.tsx +20 -0
- package/src/components/ui/Alert/Alert.tsx +8 -1
- package/src/components/ui/Avatar/Avatar.mdx +22 -2
- package/src/components/ui/Avatar/Avatar.module.css +29 -2
- package/src/components/ui/Avatar/Avatar.spec.tsx +26 -0
- package/src/components/ui/Avatar/Avatar.stories.tsx +23 -0
- package/src/components/ui/Avatar/Avatar.tsx +4 -2
- package/src/components/ui/Avatar/avatarTone.spec.ts +41 -0
- package/src/components/ui/Avatar/avatarTone.ts +22 -0
- package/src/components/ui/Collapsible/Collapsible.module.css +1 -2
- package/src/components/ui/NotificationBanner/NotificationBanner.mdx +14 -0
- package/src/components/ui/NotificationBanner/NotificationBanner.module.css +31 -1
- package/src/components/ui/NotificationBanner/NotificationBanner.spec.tsx +29 -0
- package/src/components/ui/NotificationBanner/NotificationBanner.stories.tsx +25 -0
- package/src/components/ui/NotificationBanner/NotificationBanner.tsx +15 -1
- package/src/components/ui/Separator/Separator.mdx +1 -0
- package/src/components/ui/Separator/Separator.module.css +6 -0
- package/src/components/ui/Separator/Separator.spec.tsx +19 -0
- package/src/components/ui/Separator/Separator.stories.tsx +15 -0
- package/src/components/ui/Table/Table.mdx +4 -1
- package/src/components/ui/Table/Table.module.css +7 -7
- package/src/components/ui/Table/Table.spec.tsx +11 -0
- package/src/components/ui/Tabs/Tabs.mdx +11 -5
- package/src/components/ui/Tabs/Tabs.module.css +23 -5
- package/src/components/ui/Tabs/Tabs.spec.tsx +39 -0
- package/src/components/ui/Tabs/Tabs.tsx +2 -1
- package/src/components/ui/Tabs/getNodeText.spec.tsx +23 -0
- package/src/components/ui/Tabs/getNodeText.ts +13 -0
- package/src/components/ui/Tooltip/Tooltip.mdx +6 -3
- package/src/components/ui/Tooltip/Tooltip.module.css +6 -2
- package/src/components/ui/Tooltip/Tooltip.spec.tsx +10 -0
- package/src/components/ui/Tooltip/Tooltip.stories.tsx +13 -0
- package/src/css/bdc.css +5 -2
- package/src/css/bdc.spec.ts +65 -0
- package/src/stories/DesignSystem/Borders.mdx +17 -9
- package/src/web-components/globals.ts +10 -4
- package/src/web-components/index.ts +1 -0
- package/src/web-components/interaction/BdsAccordion.mdx +1 -1
- package/src/web-components/interaction/BdsAccordion.stories.tsx +1 -1
- package/src/web-components/interaction/BdsButton.mdx +1 -1
- package/src/web-components/interaction/BdsButton.stories.tsx +1 -1
- package/src/web-components/interaction/BdsCollapsible.mdx +1 -1
- package/src/web-components/interaction/BdsCollapsible.stories.tsx +1 -1
- package/src/web-components/interaction/BdsDialog.mdx +1 -1
- package/src/web-components/interaction/BdsDialog.stories.tsx +1 -1
- package/src/web-components/interaction/BdsDrawer.mdx +1 -1
- package/src/web-components/interaction/BdsDrawer.stories.tsx +1 -1
- package/src/web-components/interaction/BdsDropdownMenu.mdx +62 -6
- package/src/web-components/interaction/BdsDropdownMenu.stories.tsx +32 -7
- package/src/web-components/interaction/BdsPopover.mdx +1 -1
- package/src/web-components/interaction/BdsPopover.stories.tsx +1 -1
- package/src/web-components/interaction/BdsSkipLink.mdx +1 -1
- package/src/web-components/interaction/BdsSkipLink.stories.tsx +1 -1
- package/src/web-components/interaction/BdsTabs.mdx +10 -1
- package/src/web-components/interaction/BdsTabs.stories.tsx +1 -1
- package/src/web-components/interaction/BdsToastProvider.mdx +1 -1
- package/src/web-components/interaction/BdsToastProvider.stories.tsx +1 -1
- package/src/web-components/interaction/BdsTooltip.mdx +13 -5
- package/src/web-components/interaction/BdsTooltip.stories.tsx +12 -12
- package/src/web-components/interaction/bds-dropdown-menu-group.ts +72 -0
- package/src/web-components/interaction/bds-dropdown-menu-item.ts +65 -17
- package/src/web-components/interaction/bds-dropdown-menu.spec.ts +135 -0
- package/src/web-components/interaction/bds-dropdown-menu.ts +5 -4
- package/src/web-components/interaction/bds-tab.spec.ts +34 -2
- package/src/web-components/interaction/bds-tab.ts +31 -6
- package/src/web-components/interaction/bds-tooltip.spec.ts +16 -0
- package/src/web-components/interaction/bds-tooltip.ts +10 -8
- package/src/web-components/interaction/form/BdsCheckbox.mdx +5 -1
- package/src/web-components/interaction/form/BdsCheckbox.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsCheckboxGroup.mdx +5 -1
- package/src/web-components/interaction/form/BdsCheckboxGroup.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsCombobox.mdx +5 -1
- package/src/web-components/interaction/form/BdsCombobox.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsFieldGroup.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsFileInput.mdx +1 -1
- package/src/web-components/interaction/form/BdsFileInput.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsFormInput.mdx +8 -1
- package/src/web-components/interaction/form/BdsFormInput.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsNumberInput.mdx +5 -1
- package/src/web-components/interaction/form/BdsNumberInput.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsRadio.mdx +5 -1
- package/src/web-components/interaction/form/BdsRadio.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsRadioGroup.mdx +5 -1
- package/src/web-components/interaction/form/BdsRadioGroup.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsSegmentedControl.mdx +1 -1
- package/src/web-components/interaction/form/BdsSegmentedControl.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsSelect.mdx +5 -1
- package/src/web-components/interaction/form/BdsSelect.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsSlider.mdx +5 -1
- package/src/web-components/interaction/form/BdsSlider.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsSwitch.mdx +5 -1
- package/src/web-components/interaction/form/BdsSwitch.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsTextarea.mdx +5 -1
- package/src/web-components/interaction/form/BdsTextarea.stories.tsx +1 -1
- package/src/web-components/interaction/form/bds-checkbox-group.spec.ts +32 -3
- package/src/web-components/interaction/form/bds-checkbox-group.ts +33 -17
- package/src/web-components/interaction/form/bds-checkbox.ts +1 -0
- package/src/web-components/interaction/form/bds-combobox.ts +6 -16
- package/src/web-components/interaction/form/bds-form-input.ts +6 -16
- package/src/web-components/interaction/form/bds-number-input.ts +6 -16
- package/src/web-components/interaction/form/bds-radio-group.spec.ts +32 -3
- package/src/web-components/interaction/form/bds-radio-group.ts +26 -14
- package/src/web-components/interaction/form/bds-radio.ts +1 -0
- package/src/web-components/interaction/form/bds-select.ts +6 -16
- package/src/web-components/interaction/form/bds-slider.ts +5 -16
- package/src/web-components/interaction/form/bds-switch.ts +1 -0
- package/src/web-components/interaction/form/bds-textarea.ts +6 -16
- package/src/web-components/interaction/form/field-message.spec.ts +62 -0
- package/src/web-components/interaction/form/field-message.ts +55 -0
- package/src/web-components/layout/BdsGrid.mdx +1 -1
- package/src/web-components/layout/BdsGrid.stories.tsx +1 -1
- package/src/web-components/layout/BdsGridItem.mdx +1 -1
- package/src/web-components/layout/BdsGridItem.stories.tsx +1 -1
- package/src/web-components/ui/BdsAlert.mdx +19 -1
- package/src/web-components/ui/BdsAlert.stories.tsx +29 -3
- package/src/web-components/ui/BdsAvatar.mdx +20 -3
- package/src/web-components/ui/BdsAvatar.stories.tsx +25 -2
- package/src/web-components/ui/BdsBadge.mdx +1 -1
- package/src/web-components/ui/BdsBadge.stories.tsx +1 -1
- package/src/web-components/ui/BdsBreadcrumb.mdx +1 -1
- package/src/web-components/ui/BdsBreadcrumb.stories.tsx +1 -1
- package/src/web-components/ui/BdsButtonContainer.mdx +1 -1
- package/src/web-components/ui/BdsButtonContainer.stories.tsx +1 -1
- package/src/web-components/ui/BdsCalendar.mdx +1 -1
- package/src/web-components/ui/BdsCalendar.stories.tsx +1 -1
- package/src/web-components/ui/BdsCalendarRange.mdx +1 -1
- package/src/web-components/ui/BdsCalendarRange.stories.tsx +1 -1
- package/src/web-components/ui/BdsCard.mdx +1 -1
- package/src/web-components/ui/BdsCard.stories.tsx +1 -1
- package/src/web-components/ui/BdsCarousel.mdx +1 -1
- package/src/web-components/ui/BdsCarousel.stories.tsx +1 -1
- package/src/web-components/ui/BdsDescriptionList.mdx +1 -1
- package/src/web-components/ui/BdsDescriptionList.stories.tsx +1 -1
- package/src/web-components/ui/BdsIconWrapper.mdx +1 -1
- package/src/web-components/ui/BdsIconWrapper.stories.tsx +1 -1
- package/src/web-components/ui/BdsLink.mdx +1 -1
- package/src/web-components/ui/BdsLink.stories.tsx +1 -1
- package/src/web-components/ui/BdsLoading.mdx +1 -1
- package/src/web-components/ui/BdsLoading.stories.tsx +1 -1
- package/src/web-components/ui/BdsNotificationBanner.mdx +10 -1
- package/src/web-components/ui/BdsNotificationBanner.stories.tsx +29 -2
- package/src/web-components/ui/BdsPagination.mdx +1 -1
- package/src/web-components/ui/BdsPagination.stories.tsx +1 -1
- package/src/web-components/ui/BdsProgress.mdx +1 -1
- package/src/web-components/ui/BdsProgress.stories.tsx +1 -1
- package/src/web-components/ui/BdsProgressCircle.mdx +1 -1
- package/src/web-components/ui/BdsProgressCircle.stories.tsx +1 -1
- package/src/web-components/ui/BdsRating.mdx +1 -1
- package/src/web-components/ui/BdsRating.stories.tsx +1 -1
- package/src/web-components/ui/BdsSectionHeader.mdx +18 -6
- package/src/web-components/ui/BdsSectionHeader.stories.tsx +10 -2
- package/src/web-components/ui/BdsSeparator.mdx +2 -1
- package/src/web-components/ui/BdsSeparator.stories.tsx +13 -1
- package/src/web-components/ui/BdsSkeleton.mdx +1 -1
- package/src/web-components/ui/BdsSkeleton.stories.tsx +1 -1
- package/src/web-components/ui/BdsTable.mdx +5 -1
- package/src/web-components/ui/BdsTable.stories.tsx +1 -1
- package/src/web-components/ui/BdsTypography.mdx +1 -1
- package/src/web-components/ui/BdsTypography.stories.tsx +1 -1
- package/src/web-components/ui/bds-alert.spec.ts +27 -0
- package/src/web-components/ui/bds-alert.ts +34 -1
- package/src/web-components/ui/bds-avatar.spec.ts +47 -0
- package/src/web-components/ui/bds-avatar.ts +36 -3
- package/src/web-components/ui/bds-notification-banner.spec.ts +45 -0
- package/src/web-components/ui/bds-notification-banner.ts +53 -1
- package/src/web-components/ui/bds-section-header.spec.ts +36 -3
- package/src/web-components/ui/bds-section-header.ts +32 -10
- package/src/web-components/ui/bds-separator.spec.ts +8 -0
- package/src/web-components/ui/bds-separator.ts +7 -0
- package/src/web-components/ui/bds-table.spec.ts +11 -0
- package/src/web-components/ui/bds-table.ts +5 -5
- package/dist/web-components/chunk-QZAIRJ75.js +0 -111
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boostdev/design-system-components",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.0",
|
|
4
4
|
"description": "BoostDev React component library: accessible, token-driven components built on @boostdev/design-system-foundation",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"React",
|
|
@@ -171,6 +171,10 @@
|
|
|
171
171
|
"types": "./dist/web-components/interaction/bds-dropdown-menu-item.d.ts",
|
|
172
172
|
"import": "./dist/web-components/interaction/bds-dropdown-menu-item.js"
|
|
173
173
|
},
|
|
174
|
+
"./web-components/interaction/bds-dropdown-menu-group": {
|
|
175
|
+
"types": "./dist/web-components/interaction/bds-dropdown-menu-group.d.ts",
|
|
176
|
+
"import": "./dist/web-components/interaction/bds-dropdown-menu-group.js"
|
|
177
|
+
},
|
|
174
178
|
"./web-components/interaction/bds-popover": {
|
|
175
179
|
"types": "./dist/web-components/interaction/bds-popover.d.ts",
|
|
176
180
|
"import": "./dist/web-components/interaction/bds-popover.js"
|
|
@@ -338,7 +342,7 @@
|
|
|
338
342
|
"changelog:unreleased": "pnpm exec git-cliff --unreleased"
|
|
339
343
|
},
|
|
340
344
|
"devDependencies": {
|
|
341
|
-
"@boostdev/design-system-foundation": "^2.
|
|
345
|
+
"@boostdev/design-system-foundation": "^2.4.0",
|
|
342
346
|
"@boostdev/design-system-react-native": "0.2.0",
|
|
343
347
|
"@modelcontextprotocol/sdk": "^1.27.0",
|
|
344
348
|
"@storybook/addon-docs": "8.6.14",
|
|
@@ -5,7 +5,7 @@ import * as Stories from './DropdownMenu.stories';
|
|
|
5
5
|
|
|
6
6
|
# DropdownMenu
|
|
7
7
|
|
|
8
|
-
Floating menu with a list of selectable actions, triggered by a button. Supports disabled items and
|
|
8
|
+
Floating menu with a list of selectable actions, triggered by a button. Supports disabled items, separators, destructive items, shortcut hints and labelled groups.
|
|
9
9
|
|
|
10
10
|
## When to use
|
|
11
11
|
- Overflow actions in a toolbar or table row
|
|
@@ -26,6 +26,12 @@ Floating menu with a list of selectable actions, triggered by a button. Supports
|
|
|
26
26
|
### With disabled item
|
|
27
27
|
<Canvas of={Stories.WithDisabledItem} />
|
|
28
28
|
|
|
29
|
+
### With groups
|
|
30
|
+
<Canvas of={Stories.WithGroups} />
|
|
31
|
+
|
|
32
|
+
### With destructive item
|
|
33
|
+
<Canvas of={Stories.WithDestructiveItem} />
|
|
34
|
+
|
|
29
35
|
### Aligned end
|
|
30
36
|
<Canvas of={Stories.AlignedEnd} />
|
|
31
37
|
|
|
@@ -45,14 +51,37 @@ Floating menu with a list of selectable actions, triggered by a button. Supports
|
|
|
45
51
|
<tr><td>`--dropdown_radius`</td><td>`var(--bdc-outline_radius)`</td><td>Menu container corner radius</td></tr>
|
|
46
52
|
<tr><td>`--dropdown_color`</td><td>`var(--bds-color_bg)`</td><td>Menu background colour</td></tr>
|
|
47
53
|
<tr><td>`--dropdown_on-color`</td><td>`var(--bds-color_on-bg)`</td><td>Menu text colour</td></tr>
|
|
48
|
-
<tr><td>`--dropdown_shadow`</td><td>`var(--bds-
|
|
54
|
+
<tr><td>`--dropdown_shadow`</td><td>`var(--bds-shadow_l)`</td><td>Menu box shadow</td></tr>
|
|
49
55
|
<tr><td>`--dropdown_separator-color`</td><td>`var(--bds-color_on-bg--as-surface)`</td><td>Separator line colour — the non-text member of the colour set (≥ 3:1)</td></tr>
|
|
50
|
-
<tr><td>`--dropdown_item-bg--hover`</td><td>`var(--bds-
|
|
51
|
-
<tr><td>`--dropdown_item-color--hover`</td><td>`var(--bds-
|
|
56
|
+
<tr><td>`--dropdown_item-bg--hover`</td><td>`var(--bds-color_primary--subtle)`</td><td>Menu item background on hover and focus</td></tr>
|
|
57
|
+
<tr><td>`--dropdown_item-color--hover`</td><td>`var(--bds-color_on-primary--subtle)`</td><td>Menu item text colour on hover and focus</td></tr>
|
|
58
|
+
<tr><td>`--dropdown_padding`</td><td>`var(--bds-space_xxs)`</td><td>Menu padding on all sides</td></tr>
|
|
59
|
+
<tr><td>`--dropdown_border-color`</td><td>`var(--bds-color_on-bg--as-surface)`</td><td>Menu outline colour</td></tr>
|
|
60
|
+
<tr><td>`--dropdown_item-padding`</td><td>`var(--bds-space_xs) var(--bds-space_s)`</td><td>Item padding</td></tr>
|
|
61
|
+
<tr><td>`--dropdown_item-radius`</td><td>`var(--bds-border_radius--xs)`</td><td>Item corner radius</td></tr>
|
|
62
|
+
<tr><td>`--dropdown_item-font-size`</td><td>`var(--bds-font_size--body)`</td><td>Item font size</td></tr>
|
|
63
|
+
<tr><td>`--dropdown_item-opacity--disabled`</td><td>`0.4`</td><td>Disabled item opacity</td></tr>
|
|
64
|
+
<tr><td>`--dropdown_item-color--destructive`</td><td>`var(--bds-color_error_on-bg)`</td><td>Destructive item text colour</td></tr>
|
|
65
|
+
<tr><td>`--dropdown_item-bg--destructive-hover`</td><td>`var(--bds-color_error--subtle)`</td><td>Destructive item background on hover and focus</td></tr>
|
|
66
|
+
<tr><td>`--dropdown_item-color--destructive-hover`</td><td>`var(--bds-color_on-error--subtle)`</td><td>Destructive item text colour on hover and focus</td></tr>
|
|
67
|
+
<tr><td>`--dropdown_group-label-font-size`</td><td>`var(--bds-font_size--body--s)`</td><td>Group label font size</td></tr>
|
|
68
|
+
<tr><td>`--dropdown_group-label-font-weight`</td><td>`var(--bds-font_weight--bold)`</td><td>Group label font weight</td></tr>
|
|
69
|
+
<tr><td>`--dropdown_group-label-color`</td><td>`var(--bds-color_on-bg--muted)`</td><td>Group label colour</td></tr>
|
|
70
|
+
<tr><td>`--dropdown_group-label-padding`</td><td>`var(--bds-space_xs) var(--bds-space_s) var(--bds-space_xxs)`</td><td>Group label padding</td></tr>
|
|
71
|
+
<tr><td>`--dropdown_shortcut-color`</td><td>`var(--bds-color_on-bg--muted)`</td><td>Shortcut hint colour</td></tr>
|
|
72
|
+
<tr><td>`--dropdown_shortcut-font-size`</td><td>`var(--bds-font_size--body--s)`</td><td>Shortcut hint font size</td></tr>
|
|
52
73
|
</tbody>
|
|
53
74
|
</table>
|
|
54
75
|
|
|
55
76
|
|
|
77
|
+
## Item options
|
|
78
|
+
|
|
79
|
+
| Option | Type | Description |
|
|
80
|
+
|--------|------|-------------|
|
|
81
|
+
| `destructive` | boolean | Renders the item in the error colour |
|
|
82
|
+
| `shortcut` | string | Display-only shortcut hint at the inline end; rendered `aria-hidden`, the consumer wires the actual key |
|
|
83
|
+
| `group` | string | Consecutive items with the same `group` render inside a `role="group"` named by a visible label |
|
|
84
|
+
|
|
56
85
|
## Accessibility
|
|
57
86
|
|
|
58
87
|
- Implements ARIA menu pattern: `role="menu"`, `role="menuitem"`
|
|
@@ -60,3 +89,5 @@ Floating menu with a list of selectable actions, triggered by a button. Supports
|
|
|
60
89
|
- Disabled items have `aria-disabled="true"` and are skipped during keyboard navigation
|
|
61
90
|
- Opening the menu focuses the first enabled item (disabled items are skipped as the initial target)
|
|
62
91
|
- Closing the menu via `Escape` or item activation returns focus to the trigger; closing via `Tab` lets focus move naturally to the next element
|
|
92
|
+
- Grouped items sit inside `role="group"` with `aria-labelledby` pointing at the visible group label; the label is not focusable and arrow keys skip it
|
|
93
|
+
- Shortcut hints are `aria-hidden` so they do not become part of the item name
|
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
inset-block-start: calc(100% + var(--bds-space_xxs));
|
|
10
10
|
z-index: var(--bds-z-index_dropdown);
|
|
11
11
|
min-inline-size: 10rem;
|
|
12
|
-
padding: var(--bds-space_xxs)
|
|
12
|
+
padding: var(--dropdown_padding, var(--bds-space_xxs));
|
|
13
13
|
border: var(--dropdown_border, none);
|
|
14
|
-
--bdc_color:
|
|
14
|
+
--bdc_color: var(--dropdown_border-color, var(--bds-color_on-bg--as-surface, var(--bds-color_on-bg--border, var(--bds-color_on-bg))));
|
|
15
15
|
--bdc-outline_radius: var(--bds-border_radius--s);
|
|
16
16
|
|
|
17
17
|
border-radius: var(--dropdown_radius, var(--bdc-outline_radius));
|
|
18
18
|
background-color: var(--dropdown_color, var(--bds-color_bg--strong, var(--bds-color_bg)));
|
|
19
19
|
color: var(--dropdown_on-color, var(--bds-color_on-bg));
|
|
20
|
-
box-shadow: inset 0 0 0 var(--bdc-outline_width) var(--bdc_color), var(--dropdown_shadow, var(--bds-
|
|
20
|
+
box-shadow: inset 0 0 0 var(--bdc-outline_width) var(--bdc_color), var(--dropdown_shadow, var(--bds-shadow_l));
|
|
21
21
|
list-style: none;
|
|
22
22
|
margin: 0;
|
|
23
23
|
}
|
|
@@ -29,10 +29,29 @@
|
|
|
29
29
|
.--placement_bottom-start { inset-inline-start: 0; }
|
|
30
30
|
.--placement_bottom-end { inset-inline-end: 0; }
|
|
31
31
|
|
|
32
|
+
.separatorRow {
|
|
33
|
+
padding-block: var(--bds-space_xxs);
|
|
34
|
+
}
|
|
35
|
+
|
|
32
36
|
.separator {
|
|
33
37
|
border: none;
|
|
34
38
|
border-block-start: 1px solid var(--dropdown_separator-color, var(--bds-color_on-bg--as-surface, var(--bds-color_on-bg--border, var(--bds-color_on-bg))));
|
|
35
|
-
margin:
|
|
39
|
+
margin: 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.group {
|
|
43
|
+
margin: 0;
|
|
44
|
+
padding: 0;
|
|
45
|
+
list-style: none;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.groupLabel {
|
|
49
|
+
display: block;
|
|
50
|
+
padding: var(--dropdown_group-label-padding, var(--bds-space_xs) var(--bds-space_s) var(--bds-space_xxs));
|
|
51
|
+
font-family: var(--bds-font_family--body);
|
|
52
|
+
font-size: var(--dropdown_group-label-font-size, var(--bds-font_size--body--s));
|
|
53
|
+
font-weight: var(--dropdown_group-label-font-weight, var(--bds-font_weight--bold));
|
|
54
|
+
color: var(--dropdown_group-label-color, var(--bds-color_on-bg--muted, var(--bds-color_on-bg)));
|
|
36
55
|
}
|
|
37
56
|
|
|
38
57
|
.item {
|
|
@@ -41,9 +60,10 @@
|
|
|
41
60
|
align-items: center;
|
|
42
61
|
gap: var(--bds-space_xs);
|
|
43
62
|
inline-size: 100%;
|
|
44
|
-
padding: var(--bds-space_xs) var(--bds-
|
|
63
|
+
padding: var(--dropdown_item-padding, var(--bds-space_xs) var(--bds-space_s));
|
|
64
|
+
border-radius: var(--dropdown_item-radius, var(--bds-border_radius--xs));
|
|
45
65
|
font-family: var(--bds-font_family--body);
|
|
46
|
-
font-size: var(--bds-font_size--body);
|
|
66
|
+
font-size: var(--dropdown_item-font-size, var(--bds-font_size--body));
|
|
47
67
|
color: currentcolor;
|
|
48
68
|
cursor: pointer;
|
|
49
69
|
box-sizing: border-box;
|
|
@@ -51,23 +71,51 @@
|
|
|
51
71
|
}
|
|
52
72
|
|
|
53
73
|
.item:disabled {
|
|
54
|
-
opacity: 0.4;
|
|
74
|
+
opacity: var(--dropdown_item-opacity--disabled, 0.4);
|
|
55
75
|
cursor: not-allowed;
|
|
56
76
|
}
|
|
57
77
|
|
|
58
78
|
.item:focus-visible {
|
|
59
79
|
outline: var(--bds-outline_default);
|
|
60
80
|
outline-offset: calc(var(--bds-outline_offset) * -1);
|
|
61
|
-
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.item:not(:disabled):focus-visible {
|
|
84
|
+
background-color: var(--dropdown_item-bg--hover, var(--bds-color_primary--subtle, var(--bds-color_blue--subtle)));
|
|
85
|
+
color: var(--dropdown_item-color--hover, var(--bds-color_on-primary--subtle, var(--bds-color_on-blue--subtle)));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.--destructive {
|
|
89
|
+
color: var(--dropdown_item-color--destructive, var(--bds-color_error_on-bg));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.--destructive:not(:disabled):focus-visible {
|
|
93
|
+
background-color: var(--dropdown_item-bg--destructive-hover, var(--bds-color_error--subtle));
|
|
94
|
+
color: var(--dropdown_item-color--destructive-hover, var(--bds-color_on-error--subtle));
|
|
62
95
|
}
|
|
63
96
|
|
|
64
97
|
@media (hover: hover) and (pointer: fine) {
|
|
65
98
|
.item:not(:disabled):hover {
|
|
66
|
-
background-color: var(--dropdown_item-bg--hover, var(--bds-
|
|
67
|
-
color: var(--dropdown_item-color--hover, var(--bds-
|
|
99
|
+
background-color: var(--dropdown_item-bg--hover, var(--bds-color_primary--subtle, var(--bds-color_blue--subtle)));
|
|
100
|
+
color: var(--dropdown_item-color--hover, var(--bds-color_on-primary--subtle, var(--bds-color_on-blue--subtle)));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.--destructive:not(:disabled):hover {
|
|
104
|
+
background-color: var(--dropdown_item-bg--destructive-hover, var(--bds-color_error--subtle));
|
|
105
|
+
color: var(--dropdown_item-color--destructive-hover, var(--bds-color_on-error--subtle));
|
|
68
106
|
}
|
|
69
107
|
}
|
|
70
108
|
|
|
109
|
+
.label {
|
|
110
|
+
flex: 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.shortcut {
|
|
114
|
+
flex-shrink: 0;
|
|
115
|
+
color: var(--dropdown_shortcut-color, var(--bds-color_on-bg--muted, var(--bds-color_on-bg)));
|
|
116
|
+
font-size: var(--dropdown_shortcut-font-size, var(--bds-font_size--body--s));
|
|
117
|
+
}
|
|
118
|
+
|
|
71
119
|
.icon {
|
|
72
120
|
display: flex;
|
|
73
121
|
align-items: center;
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { render, screen, within } from '@testing-library/react';
|
|
2
4
|
import userEvent from '@testing-library/user-event';
|
|
3
5
|
import { DropdownMenu } from './DropdownMenu';
|
|
4
6
|
|
|
7
|
+
const cssSource = readFileSync(
|
|
8
|
+
resolve(process.cwd(), 'src/components/interaction/DropdownMenu/DropdownMenu.module.css'),
|
|
9
|
+
'utf8',
|
|
10
|
+
);
|
|
11
|
+
|
|
5
12
|
const items = [
|
|
6
13
|
{ id: 'edit', label: 'Edit', onClick: vi.fn() },
|
|
7
14
|
{ id: 'duplicate', label: 'Duplicate', onClick: vi.fn() },
|
|
@@ -105,4 +112,110 @@ describe('DropdownMenu', () => {
|
|
|
105
112
|
await user.click(screen.getByRole('button', { name: 'Actions' }));
|
|
106
113
|
expect(screen.getByRole('menuitem', { name: 'Edit' })).toHaveFocus();
|
|
107
114
|
});
|
|
115
|
+
|
|
116
|
+
describe('groups, destructive and shortcut', () => {
|
|
117
|
+
const grouped = [
|
|
118
|
+
{ id: 'edit', label: 'Edit', group: 'Edit' },
|
|
119
|
+
{ id: 'duplicate', label: 'Duplicate', shortcut: '⌘D', group: 'Edit' },
|
|
120
|
+
{ id: 'delete', label: 'Delete', destructive: true, group: 'Danger' },
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
it('renders a labelled group for consecutive items with the same group', async () => {
|
|
124
|
+
const user = userEvent.setup();
|
|
125
|
+
render(<DropdownMenu trigger={<button type="button">Actions</button>} items={grouped} />);
|
|
126
|
+
await user.click(screen.getByRole('button', { name: 'Actions' }));
|
|
127
|
+
const group = screen.getByRole('group', { name: 'Edit' });
|
|
128
|
+
expect(within(group).getAllByRole('menuitem')).toHaveLength(2);
|
|
129
|
+
expect(screen.getByRole('group', { name: 'Danger' })).toBeInTheDocument();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('moves focus from the last item of a group to the first of the next', async () => {
|
|
133
|
+
const user = userEvent.setup();
|
|
134
|
+
render(<DropdownMenu trigger={<button type="button">Actions</button>} items={grouped} />);
|
|
135
|
+
await user.click(screen.getByRole('button', { name: 'Actions' }));
|
|
136
|
+
await user.keyboard('{ArrowDown}');
|
|
137
|
+
expect(screen.getByRole('menuitem', { name: 'Duplicate' })).toHaveFocus();
|
|
138
|
+
await user.keyboard('{ArrowDown}');
|
|
139
|
+
expect(screen.getByRole('menuitem', { name: 'Delete' })).toHaveFocus();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('does not render a group for items without a group', async () => {
|
|
143
|
+
const user = userEvent.setup();
|
|
144
|
+
render(<DropdownMenu trigger={<button type="button">Actions</button>} items={items} />);
|
|
145
|
+
await user.click(screen.getByRole('button', { name: 'Actions' }));
|
|
146
|
+
expect(screen.queryByRole('group')).not.toBeInTheDocument();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('marks a destructive item with the destructive class', async () => {
|
|
150
|
+
const user = userEvent.setup();
|
|
151
|
+
render(<DropdownMenu trigger={<button type="button">Actions</button>} items={grouped} />);
|
|
152
|
+
await user.click(screen.getByRole('button', { name: 'Actions' }));
|
|
153
|
+
expect(screen.getByRole('menuitem', { name: 'Delete' }).className).toMatch(/destructive/);
|
|
154
|
+
expect(screen.getByRole('menuitem', { name: 'Edit' }).className).not.toMatch(/destructive/);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('renders the shortcut aria-hidden and keeps it out of the item name', async () => {
|
|
158
|
+
const user = userEvent.setup();
|
|
159
|
+
render(<DropdownMenu trigger={<button type="button">Actions</button>} items={grouped} />);
|
|
160
|
+
await user.click(screen.getByRole('button', { name: 'Actions' }));
|
|
161
|
+
expect(screen.getByText('⌘D')).toHaveAttribute('aria-hidden', 'true');
|
|
162
|
+
expect(screen.getByRole('menuitem', { name: 'Duplicate' })).toBeInTheDocument();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('renders a separator before an item with separator set', async () => {
|
|
166
|
+
const user = userEvent.setup();
|
|
167
|
+
render(
|
|
168
|
+
<DropdownMenu
|
|
169
|
+
trigger={<button type="button">Actions</button>}
|
|
170
|
+
items={[{ id: 'a', label: 'A' }, { id: 'b', label: 'B', separator: true }]}
|
|
171
|
+
/>
|
|
172
|
+
);
|
|
173
|
+
await user.click(screen.getByRole('button', { name: 'Actions' }));
|
|
174
|
+
expect(screen.getByRole('separator')).toBeInTheDocument();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('renders the separator of a group\'s first item outside the group', async () => {
|
|
178
|
+
const user = userEvent.setup();
|
|
179
|
+
render(
|
|
180
|
+
<DropdownMenu
|
|
181
|
+
trigger={<button type="button">Actions</button>}
|
|
182
|
+
items={[
|
|
183
|
+
{ id: 'a', label: 'A', group: 'One' },
|
|
184
|
+
{ id: 'b', label: 'B', group: 'Two', separator: true },
|
|
185
|
+
{ id: 'c', label: 'C', group: 'Two', separator: true },
|
|
186
|
+
]}
|
|
187
|
+
/>
|
|
188
|
+
);
|
|
189
|
+
await user.click(screen.getByRole('button', { name: 'Actions' }));
|
|
190
|
+
const groups = screen.getAllByRole('group');
|
|
191
|
+
const separators = screen.getAllByRole('separator');
|
|
192
|
+
expect(separators).toHaveLength(2);
|
|
193
|
+
expect(groups[1]).not.toContainElement(separators[0]);
|
|
194
|
+
expect(groups[1]).toContainElement(separators[1]);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('stylesheet hooks', () => {
|
|
199
|
+
it.each([
|
|
200
|
+
'--dropdown_padding',
|
|
201
|
+
'--dropdown_border-color',
|
|
202
|
+
'--dropdown_item-padding',
|
|
203
|
+
'--dropdown_item-radius',
|
|
204
|
+
'--dropdown_item-font-size',
|
|
205
|
+
'--dropdown_item-bg--hover',
|
|
206
|
+
'--dropdown_item-color--hover',
|
|
207
|
+
'--dropdown_item-opacity--disabled',
|
|
208
|
+
'--dropdown_item-color--destructive',
|
|
209
|
+
'--dropdown_item-bg--destructive-hover',
|
|
210
|
+
'--dropdown_item-color--destructive-hover',
|
|
211
|
+
'--dropdown_group-label-font-size',
|
|
212
|
+
'--dropdown_group-label-font-weight',
|
|
213
|
+
'--dropdown_group-label-color',
|
|
214
|
+
'--dropdown_group-label-padding',
|
|
215
|
+
'--dropdown_shortcut-color',
|
|
216
|
+
'--dropdown_shortcut-font-size',
|
|
217
|
+
])('reads %s through var()', hook => {
|
|
218
|
+
expect(cssSource).toContain(`var(${hook},`);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
108
221
|
});
|
|
@@ -66,3 +66,28 @@ export const AlignedEnd: Story = {
|
|
|
66
66
|
/>
|
|
67
67
|
),
|
|
68
68
|
};
|
|
69
|
+
|
|
70
|
+
export const WithGroups: Story = {
|
|
71
|
+
render: () => (
|
|
72
|
+
<DropdownMenu
|
|
73
|
+
trigger={<Button variant="outline">Acties ▾</Button>}
|
|
74
|
+
items={[
|
|
75
|
+
{ id: 'edit', label: 'Bewerken', icon: <span>✎</span>, shortcut: '⌘E', group: 'Bewerken' },
|
|
76
|
+
{ id: 'duplicate', label: 'Dupliceren', shortcut: '⌘D', group: 'Bewerken' },
|
|
77
|
+
{ id: 'delete', label: 'Verwijderen', destructive: true, group: 'Gevaarlijk' },
|
|
78
|
+
]}
|
|
79
|
+
/>
|
|
80
|
+
),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const WithDestructiveItem: Story = {
|
|
84
|
+
render: () => (
|
|
85
|
+
<DropdownMenu
|
|
86
|
+
trigger={<Button variant="outline">Beheer ▾</Button>}
|
|
87
|
+
items={[
|
|
88
|
+
{ id: 'edit', label: 'Bewerken' },
|
|
89
|
+
{ id: 'delete', label: 'Verwijderen', destructive: true, separator: true },
|
|
90
|
+
]}
|
|
91
|
+
/>
|
|
92
|
+
),
|
|
93
|
+
};
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
HTMLAttributes,
|
|
3
3
|
KeyboardEvent,
|
|
4
4
|
ReactElement,
|
|
5
|
+
Fragment,
|
|
5
6
|
cloneElement,
|
|
6
7
|
isValidElement,
|
|
7
8
|
useEffect,
|
|
@@ -20,8 +21,29 @@ export interface DropdownMenuItem {
|
|
|
20
21
|
disabled?: boolean;
|
|
21
22
|
icon?: ReactElement;
|
|
22
23
|
separator?: boolean;
|
|
24
|
+
/** Renders the item in the error colour, for actions that delete or discard. */
|
|
25
|
+
destructive?: boolean;
|
|
26
|
+
/** Keyboard shortcut as display text. Decorative (`aria-hidden`); the consumer wires the actual key. */
|
|
27
|
+
shortcut?: string;
|
|
28
|
+
/** Consecutive items with the same group render inside a labelled `role="group"`. */
|
|
29
|
+
group?: string;
|
|
23
30
|
}
|
|
24
31
|
|
|
32
|
+
interface IndexedItem {
|
|
33
|
+
item: DropdownMenuItem;
|
|
34
|
+
index: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Splits items into runs of consecutive items sharing the same `group`. */
|
|
38
|
+
const toSegments = (items: DropdownMenuItem[]): IndexedItem[][] =>
|
|
39
|
+
items.reduce<IndexedItem[][]>((segments, item, index) => {
|
|
40
|
+
const last = segments[segments.length - 1];
|
|
41
|
+
const startsNewSegment = !last || item.group === undefined || last[0].item.group !== item.group;
|
|
42
|
+
return startsNewSegment
|
|
43
|
+
? [...segments, [{ item, index }]]
|
|
44
|
+
: [...segments.slice(0, -1), [...last, { item, index }]];
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
25
47
|
export interface DropdownMenuProps extends WithClassName, HTMLAttributes<HTMLDivElement> {
|
|
26
48
|
trigger: ReactElement;
|
|
27
49
|
items: DropdownMenuItem[];
|
|
@@ -125,23 +147,53 @@ export function DropdownMenu({
|
|
|
125
147
|
role="menu"
|
|
126
148
|
className={cn(css.menu, css[`--placement_${placement}`])}
|
|
127
149
|
>
|
|
128
|
-
{items.map(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
>
|
|
140
|
-
{item.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
150
|
+
{toSegments(items).map(segment => {
|
|
151
|
+
const { item: first } = segment[0];
|
|
152
|
+
const groupLabel = first.group;
|
|
153
|
+
const renderSeparator = () => (
|
|
154
|
+
<li role="presentation" className={css.separatorRow}>
|
|
155
|
+
<hr className={css.separator} role="separator" />
|
|
156
|
+
</li>
|
|
157
|
+
);
|
|
158
|
+
/* The first item's separator sits before the group, so the group holds only its label and items. */
|
|
159
|
+
const leadingSeparator = groupLabel !== undefined && first.separator;
|
|
160
|
+
const rows = segment.map(({ item, index }, position) => (
|
|
161
|
+
<Fragment key={item.id}>
|
|
162
|
+
{item.separator && !(leadingSeparator && position === 0) && renderSeparator()}
|
|
163
|
+
<li role="presentation">
|
|
164
|
+
<button
|
|
165
|
+
ref={el => { itemRefs.current[index] = el; }}
|
|
166
|
+
type="button"
|
|
167
|
+
role="menuitem"
|
|
168
|
+
disabled={item.disabled}
|
|
169
|
+
className={cn(css.item, item.destructive && css['--destructive'])}
|
|
170
|
+
onClick={() => { item.onClick?.(); close(true); }}
|
|
171
|
+
onKeyDown={e => handleItemKeyDown(e, index)}
|
|
172
|
+
>
|
|
173
|
+
{item.icon && <span className={css.icon} aria-hidden="true">{item.icon}</span>}
|
|
174
|
+
<span className={css.label}>{item.label}</span>
|
|
175
|
+
{item.shortcut && <span className={css.shortcut} aria-hidden="true">{item.shortcut}</span>}
|
|
176
|
+
</button>
|
|
177
|
+
</li>
|
|
178
|
+
</Fragment>
|
|
179
|
+
));
|
|
180
|
+
|
|
181
|
+
return groupLabel === undefined ? (
|
|
182
|
+
rows
|
|
183
|
+
) : (
|
|
184
|
+
<Fragment key={`group-${first.id}`}>
|
|
185
|
+
{leadingSeparator && renderSeparator()}
|
|
186
|
+
<li role="presentation">
|
|
187
|
+
<ul role="group" aria-labelledby={`${menuId}-group-${first.id}`} className={css.group}>
|
|
188
|
+
<li role="presentation" id={`${menuId}-group-${first.id}`} className={css.groupLabel}>
|
|
189
|
+
{groupLabel}
|
|
190
|
+
</li>
|
|
191
|
+
{rows}
|
|
192
|
+
</ul>
|
|
193
|
+
</li>
|
|
194
|
+
</Fragment>
|
|
195
|
+
);
|
|
196
|
+
})}
|
|
145
197
|
</ul>
|
|
146
198
|
)}
|
|
147
199
|
</div>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
1
3
|
import { render, screen } from '@testing-library/react';
|
|
2
4
|
import userEvent from '@testing-library/user-event';
|
|
3
5
|
import { Checkbox } from './Checkbox';
|
|
@@ -42,4 +44,9 @@ describe('Checkbox', () => {
|
|
|
42
44
|
const checkbox = screen.getByLabelText('Accept terms');
|
|
43
45
|
expect(checkbox.id).not.toBe('consumer-supplied-id');
|
|
44
46
|
});
|
|
47
|
+
|
|
48
|
+
it('widens the outline in the error state', () => {
|
|
49
|
+
const css = readFileSync(resolve(process.cwd(), 'src/components/interaction/form/Checkbox/Checkbox.module.css'), 'utf8');
|
|
50
|
+
expect(css).toMatch(/\.checkboxError \{[^}]*--bdc-outline_width: var\(--bdc-outline_width--error, 2px\)/);
|
|
51
|
+
});
|
|
45
52
|
});
|
|
@@ -57,3 +57,4 @@ All public override tokens. Set on the CheckboxGroup or any ancestor; they casca
|
|
|
57
57
|
- `disabled` on the group sets the HTML `disabled` attribute on the `<fieldset>`, which disables all child inputs
|
|
58
58
|
- When `required` is true, the legend shows a visible `*` (hidden from assistive tech via `aria-hidden`) plus a visually-hidden "(required)" span, so the fieldset's accessible name resolves to `"<legend> (required)"` (e.g. `"Contact preferences (required)"`). `aria-required` is intentionally **not** set on the `<fieldset>` — it is not supported ARIA on a `role=group` element and would be ignored by assistive tech
|
|
59
59
|
- Error and hint messages are linked via `aria-describedby`
|
|
60
|
+
- With `error`, the child controls show the error ring (`--bdc-outline_width--error`, `--bdc_color--error`) and the message gets a warning icon. Message hooks (`--formMessage_*`) are listed on FormInput
|
|
@@ -42,4 +42,11 @@
|
|
|
42
42
|
flex-direction: column;
|
|
43
43
|
gap: var(--checkboxGroup_items-gap, var(--bds-space_xs));
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
/* Error ring on the controls inside; state rules on the control itself
|
|
47
|
+
(checked, focus) still win because they have higher specificity. */
|
|
48
|
+
.groupError input {
|
|
49
|
+
--bdc_color: var(--bdc_color--error);
|
|
50
|
+
--bdc-outline_width: var(--bdc-outline_width--error, 2px);
|
|
51
|
+
}
|
|
45
52
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
1
3
|
import { render, screen } from '@testing-library/react';
|
|
2
4
|
import { Checkbox } from '../Checkbox';
|
|
3
5
|
import { CheckboxGroup } from './CheckboxGroup';
|
|
@@ -114,4 +116,24 @@ describe('CheckboxGroup', () => {
|
|
|
114
116
|
);
|
|
115
117
|
expect(screen.getByRole('group')).toBeDisabled();
|
|
116
118
|
});
|
|
119
|
+
|
|
120
|
+
it('marks the group with the error class so the controls inside show the error ring', () => {
|
|
121
|
+
render(
|
|
122
|
+
<CheckboxGroup legend="Pick" error="Required">
|
|
123
|
+
<Checkbox label="One" name="one" value="1" />
|
|
124
|
+
</CheckboxGroup>
|
|
125
|
+
);
|
|
126
|
+
expect(screen.getByRole('group', { name: 'Pick' }).className).toContain('groupError');
|
|
127
|
+
const css = readFileSync(resolve(process.cwd(), 'src/components/interaction/form/CheckboxGroup/CheckboxGroup.module.css'), 'utf8');
|
|
128
|
+
expect(css).toMatch(/\.groupError input \{[^}]*--bdc_color: var\(--bdc_color--error\)[^}]*--bdc-outline_width: var\(--bdc-outline_width--error, 2px\)/);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('does not mark the group without an error', () => {
|
|
132
|
+
render(
|
|
133
|
+
<CheckboxGroup legend="Pick">
|
|
134
|
+
<Checkbox label="One" name="one" value="1" />
|
|
135
|
+
</CheckboxGroup>
|
|
136
|
+
);
|
|
137
|
+
expect(screen.getByRole('group', { name: 'Pick' }).className).not.toContain('groupError');
|
|
138
|
+
});
|
|
117
139
|
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
1
3
|
import { render, screen } from '@testing-library/react';
|
|
2
4
|
import userEvent from '@testing-library/user-event';
|
|
3
5
|
import { Combobox } from './Combobox';
|
|
@@ -125,4 +127,9 @@ describe('Combobox', () => {
|
|
|
125
127
|
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
|
|
126
128
|
expect(combobox).not.toHaveAttribute('aria-activedescendant');
|
|
127
129
|
});
|
|
130
|
+
|
|
131
|
+
it('widens the outline in the error state', () => {
|
|
132
|
+
const css = readFileSync(resolve(process.cwd(), 'src/components/interaction/form/Combobox/Combobox.module.css'), 'utf8');
|
|
133
|
+
expect(css).toMatch(/\.inputError \{[^}]*--bdc-outline_width: var\(--bdc-outline_width--error, 2px\)/);
|
|
134
|
+
});
|
|
128
135
|
});
|
|
@@ -43,9 +43,18 @@ Text input field with label, optional hint, and error state. The label and input
|
|
|
43
43
|
<tr><td>`--formInput_border`</td><td>`none`</td><td>Border</td></tr>
|
|
44
44
|
<tr><td>`--formInput_radius`</td><td>`var(--bds-border_radius--full)`</td><td>Corner radius</td></tr>
|
|
45
45
|
<tr><td>`--formInput_shadow`</td><td>`var(--bdc-outline_shadow)`</td><td>Box shadow</td></tr>
|
|
46
|
+
<tr><td>`--formMessage_font-size`</td><td>`var(--bds-font_size--body--s)`</td><td>Font size of the error and hint message (shared by all form controls)</td></tr>
|
|
47
|
+
<tr><td>`--formMessage_gap`</td><td>`var(--bds-space_xxs)`</td><td>Gap between the error icon and the error text</td></tr>
|
|
48
|
+
<tr><td>`--formMessage_icon-size`</td><td>`1.15em`</td><td>Size of the error icon</td></tr>
|
|
49
|
+
<tr><td>`--formMessage_color--error`</td><td>`var(--bds-color_error_on-bg)`</td><td>Error message colour</td></tr>
|
|
50
|
+
<tr><td>`--formMessage_font-weight--error`</td><td>`var(--bds-font_weight--semibold)`</td><td>Error message weight</td></tr>
|
|
51
|
+
<tr><td>`--formMessage_color--hint`</td><td>`inherit`</td><td>Hint colour</td></tr>
|
|
52
|
+
<tr><td>`--formMessage_font-style--hint`</td><td>`italic`</td><td>Hint font style</td></tr>
|
|
46
53
|
</tbody>
|
|
47
54
|
</table>
|
|
48
55
|
|
|
56
|
+
In the error state the control draws a `--bdc-outline_width--error` (2px) ring in `--bdc_color--error`, and the message gets a warning icon.
|
|
57
|
+
|
|
49
58
|
`--input_*` (e.g. `--input_border`, `--input_radius`, `--input_shadow`, `--input_color`, `--input_color_bg`) are deprecated aliases for the `--formInput_*` tokens above — they still work but `--formInput_*` is the documented, forward-compatible name.
|
|
50
59
|
|
|
51
60
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
1
3
|
import { render, screen } from '@testing-library/react';
|
|
2
4
|
import userEvent from '@testing-library/user-event';
|
|
3
5
|
import { FormInput } from './FormInput';
|
|
@@ -60,4 +62,9 @@ describe('FormInput', () => {
|
|
|
60
62
|
render(<FormInput label="Email" name="email" ariaLabel="Deprecated label" aria-label="Native label" />);
|
|
61
63
|
expect(screen.getByRole('textbox')).toHaveAccessibleName('Deprecated label');
|
|
62
64
|
});
|
|
65
|
+
|
|
66
|
+
it('widens the outline in the error state', () => {
|
|
67
|
+
const css = readFileSync(resolve(process.cwd(), 'src/components/interaction/form/FormInput/FormInput.module.css'), 'utf8');
|
|
68
|
+
expect(css).toMatch(/\.inputError \{[^}]*--bdc-outline_width: var\(--bdc-outline_width--error, 2px\)/);
|
|
69
|
+
});
|
|
63
70
|
});
|