@boostdev/design-system-components 1.2.0 → 1.2.2
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 +46 -4
- package/README.md +15 -15
- package/dist/client.cjs +149 -124
- package/dist/client.css +503 -525
- package/dist/client.d.cts +71 -96
- package/dist/client.d.ts +71 -96
- package/dist/client.js +149 -124
- package/dist/index.cjs +149 -124
- package/dist/index.css +503 -525
- package/dist/index.d.cts +71 -96
- package/dist/index.d.ts +71 -96
- package/dist/index.js +149 -124
- package/dist/web-components/{chunk-6MH5UWUD.js → chunk-2FGATTGT.js} +5 -0
- package/dist/web-components/{chunk-5IPHEONG.js → chunk-6I2DBFQ7.js} +1 -0
- package/dist/web-components/{chunk-DI46Q2EA.js → chunk-OCODKRVZ.js} +57 -82
- package/dist/web-components/index.d.ts +571 -1
- package/dist/web-components/index.js +1501 -4
- package/dist/web-components/interaction/bds-collapsible.js +1 -1
- package/dist/web-components/interaction/bds-drawer.js +1 -1
- package/dist/web-components/interaction/form/bds-segmented-control.js +1 -1
- package/package.json +1 -1
- package/src/components/interaction/Command/Command.tsx +4 -3
- package/src/components/interaction/Dialog/Dialog.tsx +4 -5
- package/src/components/interaction/Drawer/Drawer.module.css +37 -62
- package/src/components/interaction/Drawer/Drawer.spec.tsx +3 -3
- package/src/components/interaction/Drawer/Drawer.tsx +25 -24
- package/src/components/interaction/DropdownMenu/DropdownMenu.tsx +4 -2
- package/src/components/interaction/Popover/Popover.tsx +4 -3
- package/src/components/interaction/Rating/Rating.tsx +4 -2
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.tsx +4 -6
- package/src/components/interaction/form/Combobox/Combobox.tsx +4 -2
- package/src/components/interaction/form/FileInput/FileInput.tsx +4 -3
- package/src/components/interaction/form/NumberInput/NumberInput.tsx +4 -3
- package/src/components/interaction/form/RadioGroup/RadioGroup.tsx +4 -6
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.mdx +8 -4
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.module.css +13 -6
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.tsx +4 -6
- package/src/components/interaction/form/atoms/InputContainer.tsx +2 -2
- package/src/components/layout/ButtonGroup/ButtonGroup.tsx +4 -6
- package/src/components/layout/Card/Card.tsx +4 -10
- package/src/components/layout/IconWrapper/IconWrapper.tsx +4 -5
- package/src/components/layout/SectionHeader/SectionHeader.tsx +5 -4
- package/src/components/ui/Accordion/Accordion.tsx +4 -3
- package/src/components/ui/Alert/Alert.tsx +4 -3
- package/src/components/ui/Avatar/Avatar.tsx +5 -3
- package/src/components/ui/Badge/Badge.tsx +4 -5
- package/src/components/ui/Breadcrumb/Breadcrumb.tsx +4 -3
- package/src/components/ui/Calendar/Calendar.tsx +4 -4
- package/src/components/ui/Carousel/Carousel.tsx +4 -4
- package/src/components/ui/DescriptionList/DescriptionList.tsx +4 -4
- package/src/components/ui/Loading/Loading.tsx +4 -3
- package/src/components/ui/NotificationBanner/NotificationBanner.tsx +4 -2
- package/src/components/ui/Pagination/Pagination.tsx +4 -2
- package/src/components/ui/Progress/Progress.tsx +4 -2
- package/src/components/ui/ProgressCircle/ProgressCircle.tsx +4 -1
- package/src/components/ui/Separator/Separator.tsx +5 -3
- package/src/components/ui/Skeleton/Skeleton.tsx +4 -3
- package/src/components/ui/SkipLink/SkipLink.tsx +4 -5
- package/src/components/ui/Tabs/Tabs.tsx +4 -4
- package/src/components/ui/Tooltip/Tooltip.tsx +4 -2
- package/src/components/ui/Typography/Typography.tsx +4 -5
- package/src/stories/DesignSystem/Grid.mdx +2 -0
- package/src/stories/Introduction.mdx +2 -1
- package/src/web-components/index.ts +12 -0
- package/src/web-components/interaction/BdsAccordion.mdx +80 -28
- package/src/web-components/interaction/BdsAccordion.stories.tsx +67 -58
- package/src/web-components/interaction/BdsCollapsible.mdx +93 -23
- package/src/web-components/interaction/BdsCollapsible.stories.tsx +29 -48
- package/src/web-components/interaction/BdsDialog.mdx +88 -27
- package/src/web-components/interaction/BdsDialog.stories.tsx +129 -47
- package/src/web-components/interaction/BdsDrawer.mdx +85 -27
- package/src/web-components/interaction/BdsDrawer.stories.tsx +161 -31
- 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 +72 -16
- package/src/web-components/interaction/BdsSkipLink.stories.tsx +47 -34
- package/src/web-components/interaction/BdsTabs.mdx +97 -27
- package/src/web-components/interaction/BdsTabs.stories.tsx +69 -55
- package/src/web-components/interaction/BdsTooltip.mdx +84 -18
- package/src/web-components/interaction/BdsTooltip.stories.tsx +49 -30
- package/src/web-components/interaction/bds-collapsible.ts +1 -0
- package/src/web-components/interaction/bds-drawer.ts +59 -82
- 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 +57 -20
- package/src/web-components/interaction/form/BdsCheckbox.stories.tsx +35 -36
- 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 +52 -33
- package/src/web-components/interaction/form/BdsCombobox.stories.tsx +55 -53
- package/src/web-components/interaction/form/BdsFileInput.mdx +59 -24
- package/src/web-components/interaction/form/BdsFileInput.stories.tsx +38 -53
- 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 +51 -22
- package/src/web-components/interaction/form/BdsNumberInput.stories.tsx +24 -46
- package/src/web-components/interaction/form/BdsRadio.mdx +53 -22
- package/src/web-components/interaction/form/BdsRadio.stories.tsx +19 -51
- 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 +76 -23
- package/src/web-components/interaction/form/BdsSegmentedControl.stories.tsx +64 -54
- package/src/web-components/interaction/form/BdsSelect.mdx +59 -21
- package/src/web-components/interaction/form/BdsSelect.stories.tsx +47 -56
- package/src/web-components/interaction/form/BdsSlider.mdx +55 -27
- package/src/web-components/interaction/form/BdsSlider.stories.tsx +23 -44
- package/src/web-components/interaction/form/BdsSwitch.mdx +53 -20
- package/src/web-components/interaction/form/BdsSwitch.stories.tsx +22 -37
- package/src/web-components/interaction/form/BdsTextarea.mdx +54 -22
- package/src/web-components/interaction/form/BdsTextarea.stories.tsx +35 -44
- 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-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-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-segmented-control.ts +5 -0
- package/src/web-components/ui/BdsAvatar.mdx +29 -20
- package/src/web-components/ui/BdsAvatar.stories.tsx +14 -28
- package/src/web-components/ui/BdsBreadcrumb.mdx +22 -19
- package/src/web-components/ui/BdsBreadcrumb.stories.tsx +11 -21
- 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 +30 -25
- package/src/web-components/ui/BdsCard.stories.tsx +19 -42
- 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 +13 -16
- package/src/web-components/ui/BdsDescriptionList.stories.tsx +34 -49
- package/src/web-components/ui/BdsIconWrapper.mdx +20 -20
- package/src/web-components/ui/BdsIconWrapper.stories.tsx +46 -41
- package/src/web-components/ui/BdsLink.mdx +21 -17
- package/src/web-components/ui/BdsLink.stories.tsx +19 -20
- package/src/web-components/ui/BdsLoading.mdx +9 -13
- package/src/web-components/ui/BdsLoading.stories.tsx +8 -10
- package/src/web-components/ui/BdsNotificationBanner.mdx +32 -37
- package/src/web-components/ui/BdsNotificationBanner.stories.tsx +38 -47
- 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 +24 -17
- package/src/web-components/ui/BdsProgress.stories.tsx +55 -29
- package/src/web-components/ui/BdsProgressCircle.mdx +24 -18
- package/src/web-components/ui/BdsProgressCircle.stories.tsx +17 -29
- package/src/web-components/ui/BdsRating.mdx +22 -22
- package/src/web-components/ui/BdsRating.stories.tsx +19 -13
- package/src/web-components/ui/BdsSectionHeader.mdx +31 -28
- package/src/web-components/ui/BdsSectionHeader.stories.tsx +21 -23
- package/src/web-components/ui/BdsSeparator.mdx +22 -8
- package/src/web-components/ui/BdsSeparator.stories.tsx +27 -19
- package/src/web-components/ui/BdsSkeleton.mdx +20 -26
- package/src/web-components/ui/BdsSkeleton.stories.tsx +23 -23
- 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 +20 -29
- package/src/web-components/ui/BdsTypography.stories.tsx +19 -18
- 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-carousel.spec.ts +64 -0
- package/src/web-components/ui/bds-carousel.ts +296 -0
- 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
|
@@ -1,28 +1,56 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
3
|
import type { DrawerSide } from './bds-drawer';
|
|
4
|
-
import '../index';
|
|
4
|
+
import '../index'; // auto-registers all custom elements
|
|
5
|
+
|
|
6
|
+
// Thin React wrapper so Storybook controls work with the custom element's props
|
|
7
|
+
function BdsDrawer({
|
|
8
|
+
side = 'right',
|
|
9
|
+
triggerLabel = 'Open drawer',
|
|
10
|
+
title = 'Drawer',
|
|
11
|
+
children,
|
|
12
|
+
}: {
|
|
13
|
+
side?: DrawerSide;
|
|
14
|
+
triggerLabel?: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
children?: React.ReactNode;
|
|
17
|
+
}) {
|
|
18
|
+
const id = React.useId();
|
|
19
|
+
const [isOpen, setIsOpen] = React.useState(false);
|
|
20
|
+
const drawerRef = React.useRef<HTMLElement>(null);
|
|
21
|
+
|
|
22
|
+
React.useEffect(() => {
|
|
23
|
+
const el = drawerRef.current;
|
|
24
|
+
if (!el) return;
|
|
25
|
+
const handler = () => setIsOpen(false);
|
|
26
|
+
el.addEventListener('bds-close', handler);
|
|
27
|
+
return () => el.removeEventListener('bds-close', handler);
|
|
28
|
+
}, []);
|
|
5
29
|
|
|
6
|
-
function DrawerDemo({ side = 'right' }: { side?: DrawerSide }) {
|
|
7
|
-
const [open, setOpen] = React.useState(false);
|
|
8
30
|
return (
|
|
9
31
|
<div>
|
|
10
|
-
<button
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
onClick={() => setIsOpen(true)}
|
|
35
|
+
style={{ padding: '0.5rem 1rem', cursor: 'pointer' }}
|
|
36
|
+
>
|
|
37
|
+
{triggerLabel}
|
|
38
|
+
</button>
|
|
11
39
|
{React.createElement(
|
|
12
40
|
'bds-drawer',
|
|
13
|
-
{
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
41
|
+
{
|
|
42
|
+
id,
|
|
43
|
+
ref: drawerRef,
|
|
44
|
+
side,
|
|
45
|
+
open: isOpen || undefined,
|
|
46
|
+
},
|
|
47
|
+
React.createElement('span', { slot: 'title' }, title),
|
|
48
|
+
children ?? React.createElement('p', { style: { margin: 0 } }, 'Drawer body content goes here.'),
|
|
17
49
|
)}
|
|
18
50
|
</div>
|
|
19
51
|
);
|
|
20
52
|
}
|
|
21
53
|
|
|
22
|
-
function BdsDrawer({ side }: { side?: DrawerSide }) {
|
|
23
|
-
return React.createElement(DrawerDemo, { side });
|
|
24
|
-
}
|
|
25
|
-
|
|
26
54
|
const meta = {
|
|
27
55
|
title: 'Web Components/Interaction/Drawer',
|
|
28
56
|
component: BdsDrawer,
|
|
@@ -30,37 +58,139 @@ const meta = {
|
|
|
30
58
|
parameters: { layout: 'centered' },
|
|
31
59
|
argTypes: {
|
|
32
60
|
side: { control: 'select', options: ['left', 'right'] },
|
|
61
|
+
triggerLabel: { control: 'text' },
|
|
62
|
+
title: { control: 'text' },
|
|
33
63
|
},
|
|
34
64
|
} satisfies Meta<typeof BdsDrawer>;
|
|
35
65
|
|
|
36
66
|
export default meta;
|
|
37
67
|
type Story = StoryObj<typeof meta>;
|
|
38
68
|
|
|
39
|
-
export const
|
|
40
|
-
|
|
69
|
+
export const Default: Story = {
|
|
70
|
+
args: { side: 'right', title: 'Settings', triggerLabel: 'Open drawer' },
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const LeftSide: Story = {
|
|
74
|
+
args: { side: 'left', title: 'Navigation', triggerLabel: 'Open left drawer' },
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
function RichDrawer() {
|
|
78
|
+
const id = React.useId();
|
|
79
|
+
const [isOpen, setIsOpen] = React.useState(false);
|
|
80
|
+
const drawerRef = React.useRef<HTMLElement>(null);
|
|
81
|
+
|
|
82
|
+
React.useEffect(() => {
|
|
83
|
+
const el = drawerRef.current;
|
|
84
|
+
if (!el) return;
|
|
85
|
+
const handler = () => setIsOpen(false);
|
|
86
|
+
el.addEventListener('bds-close', handler);
|
|
87
|
+
return () => el.removeEventListener('bds-close', handler);
|
|
88
|
+
}, []);
|
|
41
89
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
90
|
+
return (
|
|
91
|
+
<div>
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
onClick={() => setIsOpen(true)}
|
|
95
|
+
style={{ padding: '0.5rem 1rem', cursor: 'pointer' }}
|
|
96
|
+
>
|
|
97
|
+
Open rich drawer
|
|
98
|
+
</button>
|
|
99
|
+
{React.createElement(
|
|
100
|
+
'bds-drawer',
|
|
101
|
+
{
|
|
102
|
+
id,
|
|
103
|
+
ref: drawerRef,
|
|
104
|
+
side: 'right',
|
|
105
|
+
open: isOpen || undefined,
|
|
106
|
+
},
|
|
107
|
+
React.createElement('span', { slot: 'title' }, 'Edit settings'),
|
|
108
|
+
React.createElement(
|
|
109
|
+
'form',
|
|
110
|
+
{
|
|
111
|
+
style: { display: 'flex', flexDirection: 'column', gap: '1rem' },
|
|
112
|
+
onSubmit: (e: React.FormEvent) => { e.preventDefault(); setIsOpen(false); },
|
|
113
|
+
},
|
|
114
|
+
React.createElement(
|
|
115
|
+
'label',
|
|
116
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: '0.25rem' } },
|
|
117
|
+
'Display name',
|
|
118
|
+
React.createElement('input', { type: 'text', defaultValue: 'Jane Doe', style: { padding: '0.5rem' } }),
|
|
119
|
+
),
|
|
120
|
+
React.createElement(
|
|
121
|
+
'label',
|
|
122
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: '0.25rem' } },
|
|
123
|
+
'Language',
|
|
124
|
+
React.createElement(
|
|
125
|
+
'select',
|
|
126
|
+
{ style: { padding: '0.5rem' } },
|
|
127
|
+
React.createElement('option', null, 'English'),
|
|
128
|
+
React.createElement('option', null, 'Dutch'),
|
|
129
|
+
React.createElement('option', null, 'French'),
|
|
130
|
+
),
|
|
131
|
+
),
|
|
132
|
+
React.createElement(
|
|
133
|
+
'button',
|
|
134
|
+
{ type: 'submit', style: { padding: '0.5rem 1rem', cursor: 'pointer' } },
|
|
135
|
+
'Save changes',
|
|
136
|
+
),
|
|
137
|
+
),
|
|
138
|
+
)}
|
|
47
139
|
</div>
|
|
48
|
-
)
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const WithRichContent: Story = {
|
|
144
|
+
render: () => React.createElement(RichDrawer),
|
|
49
145
|
};
|
|
50
146
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
147
|
+
function AllSides() {
|
|
148
|
+
const [openSide, setOpenSide] = React.useState<DrawerSide | null>(null);
|
|
149
|
+
const leftRef = React.useRef<HTMLElement>(null);
|
|
150
|
+
const rightRef = React.useRef<HTMLElement>(null);
|
|
151
|
+
|
|
152
|
+
React.useEffect(() => {
|
|
153
|
+
const close = () => setOpenSide(null);
|
|
154
|
+
const left = leftRef.current;
|
|
155
|
+
const right = rightRef.current;
|
|
156
|
+
left?.addEventListener('bds-close', close);
|
|
157
|
+
right?.addEventListener('bds-close', close);
|
|
158
|
+
return () => {
|
|
159
|
+
left?.removeEventListener('bds-close', close);
|
|
160
|
+
right?.removeEventListener('bds-close', close);
|
|
161
|
+
};
|
|
162
|
+
}, []);
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
|
|
166
|
+
{(['left', 'right'] as const).map(side =>
|
|
167
|
+
React.createElement(
|
|
168
|
+
'button',
|
|
169
|
+
{
|
|
170
|
+
key: side,
|
|
171
|
+
type: 'button',
|
|
172
|
+
onClick: () => setOpenSide(side),
|
|
173
|
+
style: { padding: '0.5rem 1rem', cursor: 'pointer' },
|
|
174
|
+
},
|
|
175
|
+
`Open ${side}`,
|
|
176
|
+
),
|
|
177
|
+
)}
|
|
58
178
|
{React.createElement(
|
|
59
179
|
'bds-drawer',
|
|
60
|
-
{
|
|
61
|
-
React.createElement('span', { slot: 'title' }, '
|
|
62
|
-
React.createElement('p', { style: { margin: 0 } }, 'This drawer
|
|
180
|
+
{ ref: leftRef, side: 'left', open: openSide === 'left' || undefined },
|
|
181
|
+
React.createElement('span', { slot: 'title' }, 'Left drawer'),
|
|
182
|
+
React.createElement('p', { style: { margin: 0 } }, 'This drawer slides in from the left.'),
|
|
183
|
+
)}
|
|
184
|
+
{React.createElement(
|
|
185
|
+
'bds-drawer',
|
|
186
|
+
{ ref: rightRef, side: 'right', open: openSide === 'right' || undefined },
|
|
187
|
+
React.createElement('span', { slot: 'title' }, 'Right drawer'),
|
|
188
|
+
React.createElement('p', { style: { margin: 0 } }, 'This drawer slides in from the right.'),
|
|
63
189
|
)}
|
|
64
190
|
</div>
|
|
65
|
-
)
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export const AllPlacements: Story = {
|
|
195
|
+
render: () => React.createElement(AllSides),
|
|
66
196
|
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
|
|
2
|
+
import * as Stories from './BdsDropdownMenu.stories';
|
|
3
|
+
|
|
4
|
+
<Meta of={Stories} />
|
|
5
|
+
|
|
6
|
+
# <bds-dropdown-menu>
|
|
7
|
+
|
|
8
|
+
Framework-agnostic Dropdown Menu custom element. A trigger-anchored menu panel with keyboard navigation, light-dismiss, and support for disabled and destructive items.
|
|
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
|
+
Or in HTML without a bundler:
|
|
19
|
+
|
|
20
|
+
```html
|
|
21
|
+
<script type="module">
|
|
22
|
+
import '@boostdev/components/web-components';
|
|
23
|
+
</script>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## When to use
|
|
27
|
+
|
|
28
|
+
- Contextual action menus (e.g. row actions in a table)
|
|
29
|
+
- Overflow menus triggered by a kebab (⋮) or ellipsis (…) button
|
|
30
|
+
- Any set of related actions that shouldn't always be visible
|
|
31
|
+
|
|
32
|
+
## When not to use
|
|
33
|
+
|
|
34
|
+
- For selecting a value from a list — use `<bds-select>` or `<bds-combobox>` instead
|
|
35
|
+
- For navigation links — use a nav element or `<bds-popover>`
|
|
36
|
+
|
|
37
|
+
## Examples
|
|
38
|
+
|
|
39
|
+
### Default
|
|
40
|
+
<Canvas of={Stories.Default} />
|
|
41
|
+
|
|
42
|
+
### With destructive item
|
|
43
|
+
<Canvas of={Stories.WithDestructiveItem} />
|
|
44
|
+
|
|
45
|
+
### With disabled item
|
|
46
|
+
<Canvas of={Stories.WithDisabledItem} />
|
|
47
|
+
|
|
48
|
+
### All placements
|
|
49
|
+
<Canvas of={Stories.AllPlacements} />
|
|
50
|
+
|
|
51
|
+
## Props
|
|
52
|
+
|
|
53
|
+
<ArgTypes of={Stories} />
|
|
54
|
+
|
|
55
|
+
## Attributes — `<bds-dropdown-menu>`
|
|
56
|
+
|
|
57
|
+
| Attribute | Type | Default | Description |
|
|
58
|
+
|-----------|------|---------|-------------|
|
|
59
|
+
| `open` | boolean | `false` | Controls open/closed state (reflected) |
|
|
60
|
+
| `placement` | `"bottom-start" \| "bottom-end" \| "top-start" \| "top-end"` | `"bottom-start"` | Menu position relative to the trigger |
|
|
61
|
+
|
|
62
|
+
## Attributes — `<bds-dropdown-menu-item>`
|
|
63
|
+
|
|
64
|
+
| Attribute | Type | Default | Description |
|
|
65
|
+
|-----------|------|---------|-------------|
|
|
66
|
+
| `value` | string | `""` | Value emitted in `bds-select` event detail |
|
|
67
|
+
| `disabled` | boolean | `false` | Prevents interaction; item is excluded from keyboard navigation |
|
|
68
|
+
| `destructive` | boolean | `false` | Renders the item in a destructive (error colour) style |
|
|
69
|
+
|
|
70
|
+
## Slots — `<bds-dropdown-menu>`
|
|
71
|
+
|
|
72
|
+
| Slot | Description |
|
|
73
|
+
|------|-------------|
|
|
74
|
+
| `trigger` | The activating element (typically a `<button>`) |
|
|
75
|
+
| `(default)` | `<bds-dropdown-menu-item>` elements |
|
|
76
|
+
|
|
77
|
+
## Slots — `<bds-dropdown-menu-item>`
|
|
78
|
+
|
|
79
|
+
| Slot | Description |
|
|
80
|
+
|------|-------------|
|
|
81
|
+
| `(default)` | Item label text |
|
|
82
|
+
|
|
83
|
+
## Events
|
|
84
|
+
|
|
85
|
+
| Event | Fired by | Bubbles | Detail | Description |
|
|
86
|
+
|-------|----------|---------|--------|-------------|
|
|
87
|
+
| `bds-open` | `<bds-dropdown-menu>` | Yes | — | Fired when the menu opens |
|
|
88
|
+
| `bds-close` | `<bds-dropdown-menu>` | Yes | — | Fired when the menu closes |
|
|
89
|
+
| `bds-select` | `<bds-dropdown-menu-item>` | Yes | `{ value: string }` | Fired when an item is clicked or activated via keyboard |
|
|
90
|
+
|
|
91
|
+
## Usage in plain HTML
|
|
92
|
+
|
|
93
|
+
```html
|
|
94
|
+
<bds-dropdown-menu placement="bottom-start">
|
|
95
|
+
<button slot="trigger" type="button">Options</button>
|
|
96
|
+
<bds-dropdown-menu-item value="edit">Edit</bds-dropdown-menu-item>
|
|
97
|
+
<bds-dropdown-menu-item value="duplicate">Duplicate</bds-dropdown-menu-item>
|
|
98
|
+
<bds-dropdown-menu-item value="delete" destructive>Delete</bds-dropdown-menu-item>
|
|
99
|
+
</bds-dropdown-menu>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Accessibility
|
|
103
|
+
|
|
104
|
+
- The menu panel has `role="menu"` and `aria-hidden` toggled on open/close
|
|
105
|
+
- Each item renders a `<button role="menuitem">`
|
|
106
|
+
- Keyboard navigation: ArrowDown / ArrowUp moves between enabled items; Escape closes and returns focus to the trigger; Tab closes the menu
|
|
107
|
+
- Disabled items are excluded from keyboard navigation
|
|
108
|
+
- Focus returns to the trigger element on close
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import type { DropdownMenuPlacement } from './bds-dropdown-menu';
|
|
4
|
+
import '../index'; // auto-registers all custom elements
|
|
5
|
+
|
|
6
|
+
// Thin React wrapper so Storybook controls work with the custom element's props
|
|
7
|
+
function BdsDropdownMenu({
|
|
8
|
+
placement = 'bottom-start',
|
|
9
|
+
triggerLabel = 'Options',
|
|
10
|
+
children,
|
|
11
|
+
}: {
|
|
12
|
+
placement?: DropdownMenuPlacement;
|
|
13
|
+
triggerLabel?: string;
|
|
14
|
+
children?: React.ReactNode;
|
|
15
|
+
}) {
|
|
16
|
+
return React.createElement(
|
|
17
|
+
'bds-dropdown-menu',
|
|
18
|
+
{ placement },
|
|
19
|
+
React.createElement('button', { slot: 'trigger', type: 'button' }, triggerLabel),
|
|
20
|
+
children ?? [
|
|
21
|
+
React.createElement('bds-dropdown-menu-item', { key: 'edit', value: 'edit' }, 'Edit'),
|
|
22
|
+
React.createElement('bds-dropdown-menu-item', { key: 'duplicate', value: 'duplicate' }, 'Duplicate'),
|
|
23
|
+
React.createElement('bds-dropdown-menu-item', { key: 'archive', value: 'archive' }, 'Archive'),
|
|
24
|
+
],
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const meta = {
|
|
29
|
+
title: 'Web Components/Interaction/DropdownMenu',
|
|
30
|
+
component: BdsDropdownMenu,
|
|
31
|
+
tags: ['!stable', 'experimental'],
|
|
32
|
+
parameters: { layout: 'centered' },
|
|
33
|
+
argTypes: {
|
|
34
|
+
placement: {
|
|
35
|
+
control: 'select',
|
|
36
|
+
options: ['bottom-start', 'bottom-end', 'top-start', 'top-end'],
|
|
37
|
+
},
|
|
38
|
+
triggerLabel: { control: 'text' },
|
|
39
|
+
},
|
|
40
|
+
} satisfies Meta<typeof BdsDropdownMenu>;
|
|
41
|
+
|
|
42
|
+
export default meta;
|
|
43
|
+
type Story = StoryObj<typeof meta>;
|
|
44
|
+
|
|
45
|
+
export const Default: Story = {
|
|
46
|
+
args: { placement: 'bottom-start', triggerLabel: 'Options' },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const WithDestructiveItem: Story = {
|
|
50
|
+
render: () =>
|
|
51
|
+
React.createElement(
|
|
52
|
+
'bds-dropdown-menu',
|
|
53
|
+
{ placement: 'bottom-start' },
|
|
54
|
+
React.createElement('button', { slot: 'trigger', type: 'button' }, 'Manage'),
|
|
55
|
+
React.createElement('bds-dropdown-menu-item', { value: 'edit' }, 'Edit'),
|
|
56
|
+
React.createElement('bds-dropdown-menu-item', { value: 'duplicate' }, 'Duplicate'),
|
|
57
|
+
React.createElement('bds-dropdown-menu-item', { value: 'delete', destructive: '' }, 'Delete'),
|
|
58
|
+
),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const WithDisabledItem: Story = {
|
|
62
|
+
render: () =>
|
|
63
|
+
React.createElement(
|
|
64
|
+
'bds-dropdown-menu',
|
|
65
|
+
{ placement: 'bottom-start' },
|
|
66
|
+
React.createElement('button', { slot: 'trigger', type: 'button' }, 'Actions'),
|
|
67
|
+
React.createElement('bds-dropdown-menu-item', { value: 'view' }, 'View'),
|
|
68
|
+
React.createElement('bds-dropdown-menu-item', { value: 'export', disabled: '' }, 'Export (unavailable)'),
|
|
69
|
+
React.createElement('bds-dropdown-menu-item', { value: 'share' }, 'Share'),
|
|
70
|
+
),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const AllPlacements: Story = {
|
|
74
|
+
render: () => (
|
|
75
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, auto)', gap: '6rem', padding: '5rem' }}>
|
|
76
|
+
{(['bottom-start', 'bottom-end', 'top-start', 'top-end'] as const).map(p =>
|
|
77
|
+
React.createElement(
|
|
78
|
+
'div',
|
|
79
|
+
{ key: p, style: { display: 'flex', justifyContent: 'center' } },
|
|
80
|
+
React.createElement(
|
|
81
|
+
'bds-dropdown-menu',
|
|
82
|
+
{ placement: p },
|
|
83
|
+
React.createElement('button', { slot: 'trigger', type: 'button' }, p),
|
|
84
|
+
React.createElement('bds-dropdown-menu-item', { value: 'a' }, 'Item A'),
|
|
85
|
+
React.createElement('bds-dropdown-menu-item', { value: 'b' }, 'Item B'),
|
|
86
|
+
),
|
|
87
|
+
),
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
),
|
|
91
|
+
};
|
|
@@ -3,32 +3,88 @@ import * as Stories from './BdsSkipLink.stories';
|
|
|
3
3
|
|
|
4
4
|
<Meta of={Stories} />
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# <bds-skip-link>
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Framework-agnostic Skip Navigation Link custom element. Visually hidden until focused, allowing keyboard users to jump directly to the main content area and bypass repeated navigation.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
> **Status: experimental** — API may change before stable release.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import '@boostdev/components/web-components';
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or in HTML without a bundler:
|
|
11
19
|
|
|
12
20
|
```html
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
21
|
+
<script type="module">
|
|
22
|
+
import '@boostdev/components/web-components';
|
|
23
|
+
</script>
|
|
16
24
|
```
|
|
17
25
|
|
|
26
|
+
## When to use
|
|
27
|
+
|
|
28
|
+
- Place as the very first focusable element on every page that has a navigation region before the main content
|
|
29
|
+
- Required on all pages to comply with WCAG 2.4.1 (Bypass Blocks, Level A)
|
|
30
|
+
|
|
31
|
+
## When not to use
|
|
32
|
+
|
|
33
|
+
- Do not place it anywhere other than the start of the document flow
|
|
34
|
+
- Do not use it as a general-purpose navigation shortcut — it is specifically for skipping repetitive blocks
|
|
35
|
+
|
|
36
|
+
## Examples
|
|
37
|
+
|
|
38
|
+
### Default
|
|
39
|
+
<Canvas of={Stories.Default} />
|
|
40
|
+
|
|
41
|
+
### With custom label
|
|
42
|
+
<Canvas of={Stories.WithCustomLabel} />
|
|
43
|
+
|
|
44
|
+
## Props
|
|
45
|
+
|
|
46
|
+
<ArgTypes of={Stories} />
|
|
47
|
+
|
|
18
48
|
## Attributes
|
|
19
49
|
|
|
20
|
-
| Attribute | Type
|
|
21
|
-
|
|
22
|
-
| `href`
|
|
50
|
+
| Attribute | Type | Default | Description |
|
|
51
|
+
|-----------|------|---------|-------------|
|
|
52
|
+
| `href` | string | `"#main"` | The target anchor ID to jump to (e.g. `"#main-content"`) |
|
|
23
53
|
|
|
24
54
|
## Slots
|
|
25
55
|
|
|
26
|
-
| Slot
|
|
27
|
-
|
|
28
|
-
| (default) |
|
|
56
|
+
| Slot | Description |
|
|
57
|
+
|------|-------------|
|
|
58
|
+
| `(default)` | Link text; defaults to `"Skip to main content"` when no content is provided |
|
|
59
|
+
|
|
60
|
+
## Usage in plain HTML
|
|
61
|
+
|
|
62
|
+
Place `<bds-skip-link>` as the very first element inside `<body>`:
|
|
63
|
+
|
|
64
|
+
```html
|
|
65
|
+
<body>
|
|
66
|
+
<bds-skip-link href="#main-content"></bds-skip-link>
|
|
67
|
+
|
|
68
|
+
<header>
|
|
69
|
+
<nav>...</nav>
|
|
70
|
+
</header>
|
|
71
|
+
|
|
72
|
+
<main id="main-content">
|
|
73
|
+
Page content
|
|
74
|
+
</main>
|
|
75
|
+
</body>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
With custom label text:
|
|
79
|
+
|
|
80
|
+
```html
|
|
81
|
+
<bds-skip-link href="#main-content">Skip to main content</bds-skip-link>
|
|
82
|
+
```
|
|
29
83
|
|
|
30
|
-
##
|
|
84
|
+
## Accessibility
|
|
31
85
|
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
86
|
+
- Visually hidden via `transform: translateY(...)` until focused — avoids `display: none` and `visibility: hidden` which remove the element from the accessibility tree
|
|
87
|
+
- Becomes visible on `:focus-visible`, ensuring it is discoverable by keyboard and switch-access users
|
|
88
|
+
- Uses an `<a>` element so it is announced as a link by screen readers
|
|
89
|
+
- `z-index: 9999` ensures it appears above all other content when focused
|
|
90
|
+
- Satisfies WCAG 2.4.1 Bypass Blocks (Level A)
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
-
import '../index';
|
|
3
|
+
import '../index'; // auto-registers all custom elements
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
// Thin React wrapper so Storybook controls work with the custom element's props
|
|
6
|
+
function BdsSkipLink({
|
|
7
|
+
href = '#main',
|
|
8
|
+
children,
|
|
9
|
+
}: {
|
|
10
|
+
href?: string;
|
|
11
|
+
children?: React.ReactNode;
|
|
12
|
+
}) {
|
|
13
|
+
return React.createElement(
|
|
14
|
+
'bds-skip-link',
|
|
15
|
+
{ href },
|
|
16
|
+
children,
|
|
17
|
+
);
|
|
7
18
|
}
|
|
8
19
|
|
|
9
20
|
const meta = {
|
|
@@ -11,45 +22,47 @@ const meta = {
|
|
|
11
22
|
component: BdsSkipLink,
|
|
12
23
|
tags: ['!stable', 'experimental'],
|
|
13
24
|
parameters: { layout: 'padded' },
|
|
25
|
+
argTypes: {
|
|
26
|
+
href: { control: 'text' },
|
|
27
|
+
},
|
|
14
28
|
} satisfies Meta<typeof BdsSkipLink>;
|
|
15
29
|
|
|
16
30
|
export default meta;
|
|
17
31
|
type Story = StoryObj<typeof meta>;
|
|
18
32
|
|
|
19
33
|
export const Default: Story = {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
</div>
|
|
36
|
-
),
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export const CustomLabel: Story = {
|
|
40
|
-
render: () => (
|
|
41
|
-
<div style={{ position: 'relative', minHeight: '80px' }}>
|
|
42
|
-
<BdsSkipLink href="#content">Jump to content</BdsSkipLink>
|
|
43
|
-
</div>
|
|
44
|
-
),
|
|
34
|
+
args: { href: '#main' },
|
|
35
|
+
// Hint to the viewer that the link is visually hidden until focused
|
|
36
|
+
decorators: [
|
|
37
|
+
(Story) => (
|
|
38
|
+
<div>
|
|
39
|
+
<p style={{ marginBottom: '0.5rem', fontSize: '0.875rem', color: 'var(--bds-color_on-bg, #333)' }}>
|
|
40
|
+
Tab into the preview to see the skip link appear.
|
|
41
|
+
</p>
|
|
42
|
+
<Story />
|
|
43
|
+
<main id="main" style={{ marginTop: '1rem', padding: '1rem', border: '1px dashed currentcolor' }}>
|
|
44
|
+
Main content area
|
|
45
|
+
</main>
|
|
46
|
+
</div>
|
|
47
|
+
),
|
|
48
|
+
],
|
|
45
49
|
};
|
|
46
50
|
|
|
47
|
-
export const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<
|
|
52
|
-
|
|
51
|
+
export const WithCustomLabel: Story = {
|
|
52
|
+
args: { href: '#content' },
|
|
53
|
+
render: (args) => (
|
|
54
|
+
<div>
|
|
55
|
+
<p style={{ marginBottom: '0.5rem', fontSize: '0.875rem', color: 'var(--bds-color_on-bg, #333)' }}>
|
|
56
|
+
Tab into the preview to see the skip link appear.
|
|
57
|
+
</p>
|
|
58
|
+
{React.createElement(
|
|
59
|
+
'bds-skip-link',
|
|
60
|
+
{ href: args.href },
|
|
61
|
+
'Skip to main content',
|
|
62
|
+
)}
|
|
63
|
+
<main id="content" style={{ marginTop: '1rem', padding: '1rem', border: '1px dashed currentcolor' }}>
|
|
64
|
+
Main content area
|
|
65
|
+
</main>
|
|
53
66
|
</div>
|
|
54
67
|
),
|
|
55
68
|
};
|