@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.
Files changed (168) hide show
  1. package/AGENTS.md +46 -4
  2. package/README.md +15 -15
  3. package/dist/client.cjs +149 -124
  4. package/dist/client.css +503 -525
  5. package/dist/client.d.cts +71 -96
  6. package/dist/client.d.ts +71 -96
  7. package/dist/client.js +149 -124
  8. package/dist/index.cjs +149 -124
  9. package/dist/index.css +503 -525
  10. package/dist/index.d.cts +71 -96
  11. package/dist/index.d.ts +71 -96
  12. package/dist/index.js +149 -124
  13. package/dist/web-components/{chunk-6MH5UWUD.js → chunk-2FGATTGT.js} +5 -0
  14. package/dist/web-components/{chunk-5IPHEONG.js → chunk-6I2DBFQ7.js} +1 -0
  15. package/dist/web-components/{chunk-DI46Q2EA.js → chunk-OCODKRVZ.js} +57 -82
  16. package/dist/web-components/index.d.ts +571 -1
  17. package/dist/web-components/index.js +1501 -4
  18. package/dist/web-components/interaction/bds-collapsible.js +1 -1
  19. package/dist/web-components/interaction/bds-drawer.js +1 -1
  20. package/dist/web-components/interaction/form/bds-segmented-control.js +1 -1
  21. package/package.json +1 -1
  22. package/src/components/interaction/Command/Command.tsx +4 -3
  23. package/src/components/interaction/Dialog/Dialog.tsx +4 -5
  24. package/src/components/interaction/Drawer/Drawer.module.css +37 -62
  25. package/src/components/interaction/Drawer/Drawer.spec.tsx +3 -3
  26. package/src/components/interaction/Drawer/Drawer.tsx +25 -24
  27. package/src/components/interaction/DropdownMenu/DropdownMenu.tsx +4 -2
  28. package/src/components/interaction/Popover/Popover.tsx +4 -3
  29. package/src/components/interaction/Rating/Rating.tsx +4 -2
  30. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.tsx +4 -6
  31. package/src/components/interaction/form/Combobox/Combobox.tsx +4 -2
  32. package/src/components/interaction/form/FileInput/FileInput.tsx +4 -3
  33. package/src/components/interaction/form/NumberInput/NumberInput.tsx +4 -3
  34. package/src/components/interaction/form/RadioGroup/RadioGroup.tsx +4 -6
  35. package/src/components/interaction/form/SegmentedControl/SegmentedControl.mdx +8 -4
  36. package/src/components/interaction/form/SegmentedControl/SegmentedControl.module.css +13 -6
  37. package/src/components/interaction/form/SegmentedControl/SegmentedControl.tsx +4 -6
  38. package/src/components/interaction/form/atoms/InputContainer.tsx +2 -2
  39. package/src/components/layout/ButtonGroup/ButtonGroup.tsx +4 -6
  40. package/src/components/layout/Card/Card.tsx +4 -10
  41. package/src/components/layout/IconWrapper/IconWrapper.tsx +4 -5
  42. package/src/components/layout/SectionHeader/SectionHeader.tsx +5 -4
  43. package/src/components/ui/Accordion/Accordion.tsx +4 -3
  44. package/src/components/ui/Alert/Alert.tsx +4 -3
  45. package/src/components/ui/Avatar/Avatar.tsx +5 -3
  46. package/src/components/ui/Badge/Badge.tsx +4 -5
  47. package/src/components/ui/Breadcrumb/Breadcrumb.tsx +4 -3
  48. package/src/components/ui/Calendar/Calendar.tsx +4 -4
  49. package/src/components/ui/Carousel/Carousel.tsx +4 -4
  50. package/src/components/ui/DescriptionList/DescriptionList.tsx +4 -4
  51. package/src/components/ui/Loading/Loading.tsx +4 -3
  52. package/src/components/ui/NotificationBanner/NotificationBanner.tsx +4 -2
  53. package/src/components/ui/Pagination/Pagination.tsx +4 -2
  54. package/src/components/ui/Progress/Progress.tsx +4 -2
  55. package/src/components/ui/ProgressCircle/ProgressCircle.tsx +4 -1
  56. package/src/components/ui/Separator/Separator.tsx +5 -3
  57. package/src/components/ui/Skeleton/Skeleton.tsx +4 -3
  58. package/src/components/ui/SkipLink/SkipLink.tsx +4 -5
  59. package/src/components/ui/Tabs/Tabs.tsx +4 -4
  60. package/src/components/ui/Tooltip/Tooltip.tsx +4 -2
  61. package/src/components/ui/Typography/Typography.tsx +4 -5
  62. package/src/stories/DesignSystem/Grid.mdx +2 -0
  63. package/src/stories/Introduction.mdx +2 -1
  64. package/src/web-components/index.ts +12 -0
  65. package/src/web-components/interaction/BdsAccordion.mdx +80 -28
  66. package/src/web-components/interaction/BdsAccordion.stories.tsx +67 -58
  67. package/src/web-components/interaction/BdsCollapsible.mdx +93 -23
  68. package/src/web-components/interaction/BdsCollapsible.stories.tsx +29 -48
  69. package/src/web-components/interaction/BdsDialog.mdx +88 -27
  70. package/src/web-components/interaction/BdsDialog.stories.tsx +129 -47
  71. package/src/web-components/interaction/BdsDrawer.mdx +85 -27
  72. package/src/web-components/interaction/BdsDrawer.stories.tsx +161 -31
  73. package/src/web-components/interaction/BdsDropdownMenu.mdx +108 -0
  74. package/src/web-components/interaction/BdsDropdownMenu.stories.tsx +91 -0
  75. package/src/web-components/interaction/BdsSkipLink.mdx +72 -16
  76. package/src/web-components/interaction/BdsSkipLink.stories.tsx +47 -34
  77. package/src/web-components/interaction/BdsTabs.mdx +97 -27
  78. package/src/web-components/interaction/BdsTabs.stories.tsx +69 -55
  79. package/src/web-components/interaction/BdsTooltip.mdx +84 -18
  80. package/src/web-components/interaction/BdsTooltip.stories.tsx +49 -30
  81. package/src/web-components/interaction/bds-collapsible.ts +1 -0
  82. package/src/web-components/interaction/bds-drawer.ts +59 -82
  83. package/src/web-components/interaction/bds-dropdown-menu-item.ts +124 -0
  84. package/src/web-components/interaction/bds-dropdown-menu.spec.ts +102 -0
  85. package/src/web-components/interaction/bds-dropdown-menu.ts +200 -0
  86. package/src/web-components/interaction/form/BdsCheckbox.mdx +57 -20
  87. package/src/web-components/interaction/form/BdsCheckbox.stories.tsx +35 -36
  88. package/src/web-components/interaction/form/BdsCheckboxGroup.mdx +70 -0
  89. package/src/web-components/interaction/form/BdsCheckboxGroup.stories.tsx +81 -0
  90. package/src/web-components/interaction/form/BdsCombobox.mdx +52 -33
  91. package/src/web-components/interaction/form/BdsCombobox.stories.tsx +55 -53
  92. package/src/web-components/interaction/form/BdsFileInput.mdx +59 -24
  93. package/src/web-components/interaction/form/BdsFileInput.stories.tsx +38 -53
  94. package/src/web-components/interaction/form/BdsFormInput.mdx +98 -0
  95. package/src/web-components/interaction/form/BdsFormInput.stories.tsx +139 -0
  96. package/src/web-components/interaction/form/BdsNumberInput.mdx +51 -22
  97. package/src/web-components/interaction/form/BdsNumberInput.stories.tsx +24 -46
  98. package/src/web-components/interaction/form/BdsRadio.mdx +53 -22
  99. package/src/web-components/interaction/form/BdsRadio.stories.tsx +19 -51
  100. package/src/web-components/interaction/form/BdsRadioGroup.mdx +73 -0
  101. package/src/web-components/interaction/form/BdsRadioGroup.stories.tsx +88 -0
  102. package/src/web-components/interaction/form/BdsSegmentedControl.mdx +76 -23
  103. package/src/web-components/interaction/form/BdsSegmentedControl.stories.tsx +64 -54
  104. package/src/web-components/interaction/form/BdsSelect.mdx +59 -21
  105. package/src/web-components/interaction/form/BdsSelect.stories.tsx +47 -56
  106. package/src/web-components/interaction/form/BdsSlider.mdx +55 -27
  107. package/src/web-components/interaction/form/BdsSlider.stories.tsx +23 -44
  108. package/src/web-components/interaction/form/BdsSwitch.mdx +53 -20
  109. package/src/web-components/interaction/form/BdsSwitch.stories.tsx +22 -37
  110. package/src/web-components/interaction/form/BdsTextarea.mdx +54 -22
  111. package/src/web-components/interaction/form/BdsTextarea.stories.tsx +35 -44
  112. package/src/web-components/interaction/form/bds-checkbox-group.spec.ts +55 -0
  113. package/src/web-components/interaction/form/bds-checkbox-group.ts +117 -0
  114. package/src/web-components/interaction/form/bds-form-input.spec.ts +83 -0
  115. package/src/web-components/interaction/form/bds-form-input.ts +268 -0
  116. package/src/web-components/interaction/form/bds-radio-group.spec.ts +62 -0
  117. package/src/web-components/interaction/form/bds-radio-group.ts +142 -0
  118. package/src/web-components/interaction/form/bds-segmented-control.ts +5 -0
  119. package/src/web-components/ui/BdsAvatar.mdx +29 -20
  120. package/src/web-components/ui/BdsAvatar.stories.tsx +14 -28
  121. package/src/web-components/ui/BdsBreadcrumb.mdx +22 -19
  122. package/src/web-components/ui/BdsBreadcrumb.stories.tsx +11 -21
  123. package/src/web-components/ui/BdsButtonGroup.mdx +69 -0
  124. package/src/web-components/ui/BdsButtonGroup.stories.tsx +66 -0
  125. package/src/web-components/ui/BdsCalendar.mdx +93 -0
  126. package/src/web-components/ui/BdsCalendar.stories.tsx +63 -0
  127. package/src/web-components/ui/BdsCard.mdx +30 -25
  128. package/src/web-components/ui/BdsCard.stories.tsx +19 -42
  129. package/src/web-components/ui/BdsCarousel.mdx +83 -0
  130. package/src/web-components/ui/BdsCarousel.stories.tsx +96 -0
  131. package/src/web-components/ui/BdsDescriptionList.mdx +13 -16
  132. package/src/web-components/ui/BdsDescriptionList.stories.tsx +34 -49
  133. package/src/web-components/ui/BdsIconWrapper.mdx +20 -20
  134. package/src/web-components/ui/BdsIconWrapper.stories.tsx +46 -41
  135. package/src/web-components/ui/BdsLink.mdx +21 -17
  136. package/src/web-components/ui/BdsLink.stories.tsx +19 -20
  137. package/src/web-components/ui/BdsLoading.mdx +9 -13
  138. package/src/web-components/ui/BdsLoading.stories.tsx +8 -10
  139. package/src/web-components/ui/BdsNotificationBanner.mdx +32 -37
  140. package/src/web-components/ui/BdsNotificationBanner.stories.tsx +38 -47
  141. package/src/web-components/ui/BdsPagination.mdx +74 -0
  142. package/src/web-components/ui/BdsPagination.stories.tsx +36 -0
  143. package/src/web-components/ui/BdsProgress.mdx +24 -17
  144. package/src/web-components/ui/BdsProgress.stories.tsx +55 -29
  145. package/src/web-components/ui/BdsProgressCircle.mdx +24 -18
  146. package/src/web-components/ui/BdsProgressCircle.stories.tsx +17 -29
  147. package/src/web-components/ui/BdsRating.mdx +22 -22
  148. package/src/web-components/ui/BdsRating.stories.tsx +19 -13
  149. package/src/web-components/ui/BdsSectionHeader.mdx +31 -28
  150. package/src/web-components/ui/BdsSectionHeader.stories.tsx +21 -23
  151. package/src/web-components/ui/BdsSeparator.mdx +22 -8
  152. package/src/web-components/ui/BdsSeparator.stories.tsx +27 -19
  153. package/src/web-components/ui/BdsSkeleton.mdx +20 -26
  154. package/src/web-components/ui/BdsSkeleton.stories.tsx +23 -23
  155. package/src/web-components/ui/BdsTable.mdx +81 -0
  156. package/src/web-components/ui/BdsTable.stories.tsx +83 -0
  157. package/src/web-components/ui/BdsTypography.mdx +20 -29
  158. package/src/web-components/ui/BdsTypography.stories.tsx +19 -18
  159. package/src/web-components/ui/bds-button-group.spec.ts +40 -0
  160. package/src/web-components/ui/bds-button-group.ts +78 -0
  161. package/src/web-components/ui/bds-calendar.spec.ts +91 -0
  162. package/src/web-components/ui/bds-calendar.ts +427 -0
  163. package/src/web-components/ui/bds-carousel.spec.ts +64 -0
  164. package/src/web-components/ui/bds-carousel.ts +296 -0
  165. package/src/web-components/ui/bds-pagination.spec.ts +67 -0
  166. package/src/web-components/ui/bds-pagination.ts +197 -0
  167. package/src/web-components/ui/bds-table.spec.ts +45 -0
  168. 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 type="button" onClick={() => setOpen(true)}>Open drawer</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
- { open: open || undefined, side, onbdsclose: () => setOpen(false) },
14
- React.createElement('span', { slot: 'title' }, side === 'left' ? 'Left Drawer' : 'Settings'),
15
- React.createElement('p', { style: { margin: '0 0 12px' } }, 'Drawer content goes here. Press Escape or click × to close.'),
16
- React.createElement('button', { type: 'button', onClick: () => setOpen(false) }, 'Close'),
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 Right: Story = { args: { side: 'right' } };
40
- export const Left: Story = { args: { side: 'left' } };
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
- export const AllVariants: Story = {
43
- render: () => (
44
- <div style={{ display: 'flex', gap: '12px' }}>
45
- <DrawerDemo side="right" />
46
- <DrawerDemo side="left" />
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
- export const InvokerAPI: Story = {
52
- render: () => (
53
- <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '16px' }}>
54
- <p style={{ margin: 0, fontSize: '0.875rem', color: '#666' }}>
55
- Uses native Invoker Commands API — no JavaScript needed.
56
- </p>
57
- {React.createElement('button', { type: 'button', commandfor: 'invoker-drawer', command: 'show-modal' }, 'Open via commandfor')}
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
- { id: 'invoker-drawer', side: 'right' },
61
- React.createElement('span', { slot: 'title' }, 'Opened via Invoker API'),
62
- React.createElement('p', { style: { margin: 0 } }, 'This drawer was opened declaratively using commandfor and command attributes.'),
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
+ # &lt;bds-dropdown-menu&gt;
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
- # Skip Link
6
+ # &lt;bds-skip-link&gt;
7
7
 
8
- `<bds-skip-link>` renders a visually hidden anchor that becomes visible on focus, allowing keyboard users to skip directly to the main content. This is a critical accessibility component that should be the first focusable element on every page.
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
- ## Usage
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
- <bds-skip-link href="#main-content">Skip to main content</bds-skip-link>
14
- <!-- ... navigation ... -->
15
- <main id="main-content">...</main>
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 | Default | Description |
21
- |-----------|----------|---------|----------------------------------------|
22
- | `href` | `string` | `""` | Target anchor ID (e.g. `#main-content`) |
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 | Description |
27
- |-----------|--------------------------|
28
- | (default) | The visible link text |
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
- ## Notes
84
+ ## Accessibility
31
85
 
32
- - The link is visually hidden until it receives keyboard focus (Tab key)
33
- - Place it as the very first element inside `<body>` before any navigation
34
- - The `href` must point to an `id` on the main content container
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
- function BdsSkipLink({ href, children }: { href?: string; children?: React.ReactNode }) {
6
- return React.createElement('bds-skip-link', { href }, children);
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
- render: () => (
21
- <div style={{ position: 'relative', minHeight: '80px', border: '1px dashed #ccc', padding: '8px' }}>
22
- <p style={{ margin: '0 0 8px', fontSize: '0.75rem', color: '#888' }}>
23
- Tab into this area to reveal the skip link
24
- </p>
25
- <BdsSkipLink href="#main">Skip to main content</BdsSkipLink>
26
- <p style={{ margin: '8px 0 0', fontSize: '0.875rem' }}>Press Tab to focus the skip link above</p>
27
- </div>
28
- ),
29
- };
30
-
31
- export const CustomTarget: Story = {
32
- render: () => (
33
- <div style={{ position: 'relative', minHeight: '80px' }}>
34
- <BdsSkipLink href="#main-content">Skip to main content</BdsSkipLink>
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 AllVariants: Story = {
48
- render: () => (
49
- <div style={{ position: 'relative', minHeight: '120px', display: 'flex', flexDirection: 'column', gap: '8px' }}>
50
- <p style={{ margin: 0, fontSize: '0.75rem', color: '#888' }}>Tab to reveal — visually hidden until focused</p>
51
- <BdsSkipLink href="#main">Skip to main content</BdsSkipLink>
52
- <BdsSkipLink href="#nav">Skip to navigation</BdsSkipLink>
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
  };