@boostdev/design-system-components 1.1.0 → 1.1.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 (269) hide show
  1. package/AGENTS.md +46 -5
  2. package/dist/client.cjs +206 -113
  3. package/dist/client.css +664 -624
  4. package/dist/client.d.cts +9 -3
  5. package/dist/client.d.ts +9 -3
  6. package/dist/client.js +241 -149
  7. package/dist/index.cjs +206 -113
  8. package/dist/index.css +664 -624
  9. package/dist/index.d.cts +9 -3
  10. package/dist/index.d.ts +9 -3
  11. package/dist/index.js +241 -149
  12. package/dist/web-components/chunk-2QA62WCQ.js +48 -0
  13. package/dist/web-components/chunk-4BX6AWXX.js +146 -0
  14. package/dist/web-components/chunk-57TRNCWQ.js +93 -0
  15. package/dist/web-components/chunk-5IPHEONG.js +139 -0
  16. package/dist/web-components/chunk-5R6IFELV.js +63 -0
  17. package/dist/web-components/chunk-5TFWOPLH.js +49 -0
  18. package/dist/web-components/chunk-6I3CFC26.js +89 -0
  19. package/dist/web-components/chunk-6T3RKUKG.js +65 -0
  20. package/dist/web-components/chunk-7FMR53BW.js +127 -0
  21. package/dist/web-components/chunk-A67LCT7C.js +134 -0
  22. package/dist/web-components/chunk-AGKJTMJI.js +56 -0
  23. package/dist/web-components/chunk-BQLZ7I4G.js +56 -0
  24. package/dist/web-components/chunk-CAPI25CU.js +53 -0
  25. package/dist/web-components/chunk-CJ5ZOO42.js +229 -0
  26. package/dist/web-components/chunk-CR5PGUHV.js +109 -0
  27. package/dist/web-components/chunk-ECW3HHWA.js +185 -0
  28. package/dist/web-components/chunk-HGBNDR22.js +98 -0
  29. package/dist/web-components/chunk-IK3PTFZ4.js +133 -0
  30. package/dist/web-components/chunk-IQGLIQSO.js +188 -0
  31. package/dist/web-components/chunk-ITHLKR7Q.js +105 -0
  32. package/dist/web-components/chunk-K5CE6HSB.js +169 -0
  33. package/dist/web-components/chunk-L3XHOMN6.js +111 -0
  34. package/dist/{web-components.js → web-components/chunk-LGEZYFUU.js} +62 -834
  35. package/dist/web-components/chunk-LSC4N56Q.js +168 -0
  36. package/dist/web-components/chunk-LY62SAVN.js +64 -0
  37. package/dist/web-components/chunk-O4UFI2RX.js +244 -0
  38. package/dist/web-components/chunk-O7I63SQX.js +124 -0
  39. package/dist/web-components/chunk-OHUTLSIQ.js +69 -0
  40. package/dist/web-components/chunk-OQTRTMAB.js +27 -0
  41. package/dist/web-components/chunk-OTX4COSV.js +187 -0
  42. package/dist/web-components/chunk-PY62FTRW.js +179 -0
  43. package/dist/web-components/chunk-Q4T5BJT6.js +94 -0
  44. package/dist/web-components/chunk-QDRH46TF.js +281 -0
  45. package/dist/web-components/chunk-QDST57JC.js +119 -0
  46. package/dist/web-components/chunk-QPOKHLMQ.js +45 -0
  47. package/dist/web-components/chunk-SOQQ7RVJ.js +88 -0
  48. package/dist/web-components/chunk-UFFCKVZE.js +149 -0
  49. package/dist/web-components/chunk-VM3YHGQ2.js +152 -0
  50. package/dist/web-components/chunk-VMMVWLXL.js +53 -0
  51. package/dist/web-components/chunk-VSS5ITNE.js +109 -0
  52. package/dist/web-components/chunk-XIENXR7H.js +118 -0
  53. package/dist/web-components/chunk-Y4MAXTMA.js +80 -0
  54. package/dist/web-components/chunk-YLVTEY23.js +165 -0
  55. package/dist/web-components/globals.d.ts +2 -0
  56. package/dist/web-components/globals.js +1 -0
  57. package/dist/web-components/index.d.ts +41 -0
  58. package/dist/web-components/index.js +165 -0
  59. package/dist/web-components/interaction/bds-accordion-item.d.ts +54 -0
  60. package/dist/web-components/interaction/bds-accordion-item.js +7 -0
  61. package/dist/web-components/interaction/bds-accordion.d.ts +50 -0
  62. package/dist/web-components/interaction/bds-accordion.js +7 -0
  63. package/dist/web-components/interaction/bds-button.d.ts +100 -0
  64. package/dist/web-components/interaction/bds-button.js +7 -0
  65. package/dist/web-components/interaction/bds-collapsible.d.ts +52 -0
  66. package/dist/web-components/interaction/bds-collapsible.js +7 -0
  67. package/dist/web-components/interaction/bds-dialog.d.ts +56 -0
  68. package/dist/web-components/interaction/bds-dialog.js +7 -0
  69. package/dist/web-components/interaction/bds-drawer.d.ts +64 -0
  70. package/dist/web-components/interaction/bds-drawer.js +7 -0
  71. package/dist/web-components/interaction/bds-popover.d.ts +62 -0
  72. package/dist/web-components/interaction/bds-popover.js +7 -0
  73. package/dist/web-components/interaction/bds-skip-link.d.ts +36 -0
  74. package/dist/web-components/interaction/bds-skip-link.js +7 -0
  75. package/dist/web-components/interaction/bds-tab-panel.d.ts +26 -0
  76. package/dist/web-components/interaction/bds-tab-panel.js +7 -0
  77. package/dist/web-components/interaction/bds-tab.d.ts +45 -0
  78. package/dist/web-components/interaction/bds-tab.js +7 -0
  79. package/dist/web-components/interaction/bds-tabs.d.ts +46 -0
  80. package/dist/web-components/interaction/bds-tabs.js +7 -0
  81. package/dist/web-components/interaction/bds-toast-provider.d.ts +57 -0
  82. package/dist/web-components/interaction/bds-toast-provider.js +7 -0
  83. package/dist/web-components/interaction/bds-tooltip.d.ts +50 -0
  84. package/dist/web-components/interaction/bds-tooltip.js +7 -0
  85. package/dist/web-components/interaction/form/bds-checkbox.d.ts +70 -0
  86. package/dist/web-components/interaction/form/bds-checkbox.js +7 -0
  87. package/dist/web-components/interaction/form/bds-combobox.d.ts +84 -0
  88. package/dist/web-components/interaction/form/bds-combobox.js +7 -0
  89. package/dist/web-components/interaction/form/bds-file-input.d.ts +62 -0
  90. package/dist/web-components/interaction/form/bds-file-input.js +7 -0
  91. package/dist/web-components/interaction/form/bds-number-input.d.ts +68 -0
  92. package/dist/web-components/interaction/form/bds-number-input.js +7 -0
  93. package/dist/web-components/interaction/form/bds-radio.d.ts +68 -0
  94. package/dist/web-components/interaction/form/bds-radio.js +7 -0
  95. package/dist/web-components/interaction/form/bds-segmented-control.d.ts +71 -0
  96. package/dist/web-components/interaction/form/bds-segmented-control.js +7 -0
  97. package/dist/web-components/interaction/form/bds-select.d.ts +79 -0
  98. package/dist/web-components/interaction/form/bds-select.js +7 -0
  99. package/dist/web-components/interaction/form/bds-slider.d.ts +70 -0
  100. package/dist/web-components/interaction/form/bds-slider.js +7 -0
  101. package/dist/web-components/interaction/form/bds-switch.d.ts +67 -0
  102. package/dist/web-components/interaction/form/bds-switch.js +7 -0
  103. package/dist/web-components/interaction/form/bds-textarea.d.ts +70 -0
  104. package/dist/web-components/interaction/form/bds-textarea.js +7 -0
  105. package/dist/web-components/ui/bds-alert.d.ts +66 -0
  106. package/dist/web-components/ui/bds-alert.js +7 -0
  107. package/dist/web-components/ui/bds-avatar.d.ts +50 -0
  108. package/dist/web-components/ui/bds-avatar.js +7 -0
  109. package/dist/web-components/ui/bds-badge.d.ts +35 -0
  110. package/dist/web-components/ui/bds-badge.js +7 -0
  111. package/dist/web-components/ui/bds-breadcrumb.d.ts +35 -0
  112. package/dist/web-components/ui/bds-breadcrumb.js +7 -0
  113. package/dist/web-components/ui/bds-card.d.ts +72 -0
  114. package/dist/web-components/ui/bds-card.js +7 -0
  115. package/dist/web-components/ui/bds-description-list.d.ts +42 -0
  116. package/dist/web-components/ui/bds-description-list.js +7 -0
  117. package/dist/web-components/ui/bds-icon-wrapper.d.ts +39 -0
  118. package/dist/web-components/ui/bds-icon-wrapper.js +7 -0
  119. package/dist/web-components/ui/bds-link.d.ts +59 -0
  120. package/dist/web-components/ui/bds-link.js +7 -0
  121. package/dist/web-components/ui/bds-loading.d.ts +32 -0
  122. package/dist/web-components/ui/bds-loading.js +7 -0
  123. package/dist/web-components/ui/bds-notification-banner.d.ts +61 -0
  124. package/dist/web-components/ui/bds-notification-banner.js +7 -0
  125. package/dist/web-components/ui/bds-progress-circle.d.ts +53 -0
  126. package/dist/web-components/ui/bds-progress-circle.js +7 -0
  127. package/dist/web-components/ui/bds-progress.d.ts +54 -0
  128. package/dist/web-components/ui/bds-progress.js +7 -0
  129. package/dist/web-components/ui/bds-rating.d.ts +36 -0
  130. package/dist/web-components/ui/bds-rating.js +7 -0
  131. package/dist/web-components/ui/bds-section-header.d.ts +53 -0
  132. package/dist/web-components/ui/bds-section-header.js +8 -0
  133. package/dist/web-components/ui/bds-separator.d.ts +32 -0
  134. package/dist/web-components/ui/bds-separator.js +7 -0
  135. package/dist/web-components/ui/bds-skeleton.d.ts +22 -0
  136. package/dist/web-components/ui/bds-skeleton.js +7 -0
  137. package/dist/web-components/ui/bds-typography.d.ts +44 -0
  138. package/dist/web-components/ui/bds-typography.js +8 -0
  139. package/package.json +169 -4
  140. package/src/components/interaction/Button/Button.module.css +1 -1
  141. package/src/components/interaction/Button/Button.native.mdx +72 -0
  142. package/src/components/interaction/Command/Command.module.css +7 -7
  143. package/src/components/interaction/Command/Command.spec.tsx +37 -0
  144. package/src/components/interaction/Command/Command.tsx +37 -2
  145. package/src/components/interaction/Dialog/Dialog.module.css +10 -13
  146. package/src/components/interaction/Dialog/Dialog.spec.tsx +53 -1
  147. package/src/components/interaction/Dialog/Dialog.stories.tsx +17 -0
  148. package/src/components/interaction/Dialog/Dialog.tsx +50 -16
  149. package/src/components/interaction/Drawer/Drawer.module.css +11 -11
  150. package/src/components/interaction/Drawer/Drawer.spec.tsx +48 -1
  151. package/src/components/interaction/Drawer/Drawer.tsx +38 -4
  152. package/src/components/interaction/DropdownMenu/DropdownMenu.module.css +6 -6
  153. package/src/components/interaction/Popover/Popover.module.css +74 -15
  154. package/src/components/interaction/Popover/Popover.spec.tsx +94 -59
  155. package/src/components/interaction/Popover/Popover.tsx +44 -60
  156. package/src/components/interaction/Toast/Toast.module.css +7 -7
  157. package/src/components/interaction/form/Checkbox/Checkbox.module.css +6 -6
  158. package/src/components/interaction/form/Checkbox/Checkbox.native.mdx +74 -0
  159. package/src/components/interaction/form/Combobox/Combobox.module.css +7 -8
  160. package/src/components/interaction/form/FileInput/FileInput.module.css +4 -4
  161. package/src/components/interaction/form/NumberInput/NumberInput.module.css +1 -1
  162. package/src/components/interaction/form/Radio/Radio.module.css +6 -6
  163. package/src/components/interaction/form/SegmentedControl/SegmentedControl.module.css +5 -6
  164. package/src/components/interaction/form/Select/Select.module.css +3 -3
  165. package/src/components/interaction/form/Slider/Slider.module.css +6 -6
  166. package/src/components/interaction/form/Switch/Switch.module.css +8 -8
  167. package/src/components/interaction/form/Textarea/Textarea.module.css +1 -1
  168. package/src/components/layout/IconWrapper/IconWrapper.module.css +2 -2
  169. package/src/components/ui/Accordion/Accordion.module.css +5 -5
  170. package/src/components/ui/Alert/Alert.native.mdx +62 -0
  171. package/src/components/ui/Avatar/Avatar.native.mdx +50 -0
  172. package/src/components/ui/Badge/Badge.native.mdx +54 -0
  173. package/src/components/ui/Calendar/Calendar.module.css +7 -7
  174. package/src/components/ui/Carousel/Carousel.module.css +5 -5
  175. package/src/components/ui/Collapsible/Collapsible.module.css +4 -4
  176. package/src/components/ui/Link/Link.module.css +2 -2
  177. package/src/components/ui/Loading/Loading.module.css +7 -7
  178. package/src/components/ui/NotificationBanner/NotificationBanner.module.css +3 -3
  179. package/src/components/ui/Pagination/Pagination.module.css +6 -6
  180. package/src/components/ui/Progress/Progress.module.css +5 -5
  181. package/src/components/ui/Separator/Separator.module.css +3 -3
  182. package/src/components/ui/Skeleton/Skeleton.module.css +1 -1
  183. package/src/components/ui/Table/Table.module.css +9 -9
  184. package/src/components/ui/Tabs/Tabs.module.css +3 -3
  185. package/src/components/ui/Tooltip/Tooltip.module.css +8 -8
  186. package/src/components/ui/Typography/Typography.native.mdx +56 -0
  187. package/src/polyfill-invoker-commands.ts +68 -0
  188. package/src/react-augment.d.ts +19 -0
  189. package/src/stories/ReactNative.mdx +121 -0
  190. package/src/web-components/index.ts +47 -0
  191. package/src/web-components/interaction/BdsButton.mdx +106 -0
  192. package/src/web-components/interaction/BdsPopover.mdx +120 -0
  193. package/src/web-components/interaction/BdsToastProvider.mdx +94 -0
  194. package/src/web-components/interaction/bds-accordion-item.spec.ts +75 -0
  195. package/src/web-components/interaction/bds-accordion-item.ts +181 -0
  196. package/src/web-components/interaction/bds-accordion.spec.ts +73 -0
  197. package/src/web-components/interaction/bds-accordion.ts +99 -0
  198. package/src/web-components/interaction/bds-collapsible.spec.ts +56 -0
  199. package/src/web-components/interaction/bds-collapsible.ts +170 -0
  200. package/src/web-components/interaction/bds-dialog.spec.ts +119 -0
  201. package/src/web-components/interaction/bds-dialog.ts +224 -0
  202. package/src/web-components/interaction/bds-drawer.spec.ts +113 -0
  203. package/src/web-components/interaction/bds-drawer.ts +266 -0
  204. package/src/web-components/interaction/bds-popover.spec.ts +49 -1
  205. package/src/web-components/interaction/bds-popover.ts +12 -0
  206. package/src/web-components/interaction/bds-skip-link.spec.ts +42 -0
  207. package/src/web-components/interaction/bds-skip-link.ts +74 -0
  208. package/src/web-components/interaction/bds-tab-panel.spec.ts +32 -0
  209. package/src/web-components/interaction/bds-tab-panel.ts +38 -0
  210. package/src/web-components/interaction/bds-tab.spec.ts +71 -0
  211. package/src/web-components/interaction/bds-tab.ts +110 -0
  212. package/src/web-components/interaction/bds-tabs.spec.ts +120 -0
  213. package/src/web-components/interaction/bds-tabs.ts +152 -0
  214. package/src/web-components/interaction/bds-tooltip.spec.ts +49 -0
  215. package/src/web-components/interaction/bds-tooltip.ts +127 -0
  216. package/src/web-components/interaction/form/bds-checkbox.spec.ts +63 -0
  217. package/src/web-components/interaction/form/bds-checkbox.ts +168 -0
  218. package/src/web-components/interaction/form/bds-combobox.spec.ts +117 -0
  219. package/src/web-components/interaction/form/bds-combobox.ts +336 -0
  220. package/src/web-components/interaction/form/bds-file-input.spec.ts +58 -0
  221. package/src/web-components/interaction/form/bds-file-input.ts +209 -0
  222. package/src/web-components/interaction/form/bds-number-input.spec.ts +87 -0
  223. package/src/web-components/interaction/form/bds-number-input.ts +221 -0
  224. package/src/web-components/interaction/form/bds-radio.spec.ts +70 -0
  225. package/src/web-components/interaction/form/bds-radio.ts +157 -0
  226. package/src/web-components/interaction/form/bds-segmented-control.spec.ts +74 -0
  227. package/src/web-components/interaction/form/bds-segmented-control.ts +211 -0
  228. package/src/web-components/interaction/form/bds-select.spec.ts +74 -0
  229. package/src/web-components/interaction/form/bds-select.ts +190 -0
  230. package/src/web-components/interaction/form/bds-slider.spec.ts +41 -0
  231. package/src/web-components/interaction/form/bds-slider.ts +162 -0
  232. package/src/web-components/interaction/form/bds-switch.spec.ts +62 -0
  233. package/src/web-components/interaction/form/bds-switch.ts +166 -0
  234. package/src/web-components/interaction/form/bds-textarea.spec.ts +55 -0
  235. package/src/web-components/interaction/form/bds-textarea.ts +143 -0
  236. package/src/web-components/test/helpers.ts +8 -0
  237. package/src/web-components/ui/BdsAlert.mdx +90 -0
  238. package/src/web-components/ui/BdsBadge.mdx +74 -0
  239. package/src/web-components/ui/bds-avatar.spec.ts +75 -0
  240. package/src/web-components/ui/bds-avatar.ts +122 -0
  241. package/src/web-components/ui/bds-breadcrumb.spec.ts +55 -0
  242. package/src/web-components/ui/bds-breadcrumb.ts +114 -0
  243. package/src/web-components/ui/bds-card.spec.ts +70 -0
  244. package/src/web-components/ui/bds-card.ts +156 -0
  245. package/src/web-components/ui/bds-description-list.spec.ts +36 -0
  246. package/src/web-components/ui/bds-description-list.ts +79 -0
  247. package/src/web-components/ui/bds-icon-wrapper.spec.ts +21 -0
  248. package/src/web-components/ui/bds-icon-wrapper.ts +68 -0
  249. package/src/web-components/ui/bds-link.spec.ts +72 -0
  250. package/src/web-components/ui/bds-link.ts +138 -0
  251. package/src/web-components/ui/bds-loading.spec.ts +38 -0
  252. package/src/web-components/ui/bds-loading.ts +76 -0
  253. package/src/web-components/ui/bds-notification-banner.spec.ts +82 -0
  254. package/src/web-components/ui/bds-notification-banner.ts +185 -0
  255. package/src/web-components/ui/bds-progress-circle.spec.ts +58 -0
  256. package/src/web-components/ui/bds-progress-circle.ts +142 -0
  257. package/src/web-components/ui/bds-progress.spec.ts +66 -0
  258. package/src/web-components/ui/bds-progress.ts +127 -0
  259. package/src/web-components/ui/bds-rating.spec.ts +70 -0
  260. package/src/web-components/ui/bds-rating.ts +80 -0
  261. package/src/web-components/ui/bds-section-header.spec.ts +57 -0
  262. package/src/web-components/ui/bds-section-header.ts +96 -0
  263. package/src/web-components/ui/bds-separator.spec.ts +50 -0
  264. package/src/web-components/ui/bds-separator.ts +66 -0
  265. package/src/web-components/ui/bds-skeleton.spec.ts +22 -0
  266. package/src/web-components/ui/bds-skeleton.ts +55 -0
  267. package/src/web-components/ui/bds-typography.spec.ts +76 -0
  268. package/src/web-components/ui/bds-typography.ts +112 -0
  269. package/dist/web-components.d.ts +0 -303
@@ -0,0 +1,75 @@
1
+ import { fixture, cleanup } from '../test/helpers';
2
+ import { BdsAccordionItem } from './bds-accordion-item';
3
+
4
+ describe('bds-accordion-item', () => {
5
+ it('is registered as a custom element', () => {
6
+ expect(customElements.get('bds-accordion-item')).toBe(BdsAccordionItem);
7
+ });
8
+
9
+ it('is closed by default', async () => {
10
+ const el = await fixture('<bds-accordion-item><span slot="header">Q</span><p>A</p></bds-accordion-item>') as BdsAccordionItem;
11
+ expect(el.open).toBe(false);
12
+ cleanup(el);
13
+ });
14
+
15
+ it('renders a trigger button', async () => {
16
+ const el = await fixture('<bds-accordion-item><span slot="header">Q</span><p>A</p></bds-accordion-item>');
17
+ expect(el.shadowRoot!.querySelector('button.trigger')).not.toBeNull();
18
+ cleanup(el);
19
+ });
20
+
21
+ it('trigger has aria-expanded="false" when closed', async () => {
22
+ const el = await fixture('<bds-accordion-item><span slot="header">Q</span><p>A</p></bds-accordion-item>');
23
+ const btn = el.shadowRoot!.querySelector('button')!;
24
+ expect(btn.getAttribute('aria-expanded')).toBe('false');
25
+ cleanup(el);
26
+ });
27
+
28
+ it('trigger has aria-expanded="true" when open', async () => {
29
+ const el = await fixture('<bds-accordion-item open><span slot="header">Q</span><p>A</p></bds-accordion-item>');
30
+ const btn = el.shadowRoot!.querySelector('button')!;
31
+ expect(btn.getAttribute('aria-expanded')).toBe('true');
32
+ cleanup(el);
33
+ });
34
+
35
+ it('panel is hidden when closed', async () => {
36
+ const el = await fixture('<bds-accordion-item><span slot="header">Q</span><p>A</p></bds-accordion-item>');
37
+ const panel = el.shadowRoot!.querySelector('[role="region"]')!;
38
+ expect(panel.hasAttribute('hidden')).toBe(true);
39
+ cleanup(el);
40
+ });
41
+
42
+ it('panel is visible when open', async () => {
43
+ const el = await fixture('<bds-accordion-item open><span slot="header">Q</span><p>A</p></bds-accordion-item>');
44
+ const panel = el.shadowRoot!.querySelector('[role="region"]')!;
45
+ expect(panel.hasAttribute('hidden')).toBe(false);
46
+ cleanup(el);
47
+ });
48
+
49
+ it('fires bds-accordion-item-toggle when trigger is clicked', async () => {
50
+ const el = await fixture('<bds-accordion-item><span slot="header">Q</span><p>A</p></bds-accordion-item>');
51
+ const btn = el.shadowRoot!.querySelector('button')!;
52
+ const handler = vi.fn();
53
+ el.addEventListener('bds-accordion-item-toggle', handler);
54
+ btn.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
55
+ expect(handler).toHaveBeenCalledOnce();
56
+ cleanup(el);
57
+ });
58
+
59
+ it('does not fire toggle event when disabled', async () => {
60
+ const el = await fixture('<bds-accordion-item disabled><span slot="header">Q</span><p>A</p></bds-accordion-item>');
61
+ const btn = el.shadowRoot!.querySelector('button')!;
62
+ const handler = vi.fn();
63
+ el.addEventListener('bds-accordion-item-toggle', handler);
64
+ btn.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
65
+ expect(handler).not.toHaveBeenCalled();
66
+ cleanup(el);
67
+ });
68
+
69
+ it('button is disabled when disabled attribute is set', async () => {
70
+ const el = await fixture('<bds-accordion-item disabled><span slot="header">Q</span><p>A</p></bds-accordion-item>');
71
+ const btn = el.shadowRoot!.querySelector('button')!;
72
+ expect(btn.hasAttribute('disabled')).toBe(true);
73
+ cleanup(el);
74
+ });
75
+ });
@@ -0,0 +1,181 @@
1
+ import { LitElement, css, html } from 'lit';
2
+
3
+ /**
4
+ * `<bds-accordion-item>` — child element of `<bds-accordion>`.
5
+ *
6
+ * Manages a single collapsible panel. The `open` and `disabled` state is
7
+ * controlled by the parent `<bds-accordion>` element.
8
+ *
9
+ * Attributes:
10
+ * open — boolean; whether the panel is expanded
11
+ * disabled — boolean; prevents the user from toggling this item
12
+ *
13
+ * Slots:
14
+ * header — the always-visible trigger label
15
+ * (default) — the content revealed when open
16
+ *
17
+ * Events:
18
+ * bds-accordion-item-toggle — fired (bubbles, composed) when the trigger
19
+ * button is clicked; the parent accordion handles
20
+ * the state change.
21
+ *
22
+ * @example
23
+ * <bds-accordion-item>
24
+ * <span slot="header">Question</span>
25
+ * <p>Answer content</p>
26
+ * </bds-accordion-item>
27
+ */
28
+ export class BdsAccordionItem extends LitElement {
29
+ static styles = css`
30
+ :host {
31
+ display: block;
32
+ border-block-end: 1px solid var(--bds-color_bg--subtle);
33
+ }
34
+
35
+ :host(:last-child) {
36
+ border-block-end: none;
37
+ }
38
+
39
+ .heading {
40
+ margin: 0;
41
+ }
42
+
43
+ .trigger {
44
+ all: unset;
45
+ display: flex;
46
+ align-items: center;
47
+ justify-content: space-between;
48
+ inline-size: 100%;
49
+ padding: var(--bds-space_m);
50
+ font-family: var(--bds-font_family--body);
51
+ font-size: var(--bds-font_size--body);
52
+ font-weight: var(--bds-font_weight--semibold);
53
+ color: var(--bds-color_on-bg);
54
+ cursor: pointer;
55
+ transition: var(--bds-animation_transition);
56
+ box-sizing: border-box;
57
+ }
58
+
59
+ .trigger:disabled {
60
+ opacity: 0.4;
61
+ cursor: not-allowed;
62
+ }
63
+
64
+ .trigger:focus-visible {
65
+ outline: var(--bds-outline_default);
66
+ outline-offset: calc(var(--bds-outline_offset) * -1);
67
+ border-radius: var(--bds-border_radius--xs);
68
+ }
69
+
70
+ @media (hover: hover) and (pointer: fine) {
71
+ .trigger:not(:disabled):hover {
72
+ background-color: var(--bds-color_bg--subtle);
73
+ }
74
+ }
75
+
76
+ .trigger-label {
77
+ flex: 1;
78
+ text-align: start;
79
+ }
80
+
81
+ .chevron {
82
+ inline-size: 1.25rem;
83
+ block-size: 1.25rem;
84
+ flex-shrink: 0;
85
+ transition: transform var(--bds-animation_transition-duration) var(--bds-animation_easing);
86
+ }
87
+
88
+ :host([open]) .chevron {
89
+ transform: rotate(180deg);
90
+ }
91
+
92
+ .panel {
93
+ overflow: hidden;
94
+ }
95
+
96
+ .panel-content {
97
+ padding: 0 var(--bds-space_m) var(--bds-space_m);
98
+ color: var(--bds-color_on-bg);
99
+ font-size: var(--bds-font_size--body);
100
+ line-height: var(--bds-font_line-height--body);
101
+ }
102
+
103
+ @media (prefers-reduced-motion: reduce) {
104
+ .chevron {
105
+ transition: none;
106
+ }
107
+ }
108
+ `;
109
+
110
+ static properties = {
111
+ open: { type: Boolean, reflect: true },
112
+ disabled: { type: Boolean, reflect: true },
113
+ };
114
+
115
+ declare open: boolean;
116
+ declare disabled: boolean;
117
+
118
+ // Stable IDs for aria-controls / aria-labelledby
119
+ private readonly _uid = Math.random().toString(36).slice(2, 8);
120
+
121
+ constructor() {
122
+ super();
123
+ this.open = false;
124
+ this.disabled = false;
125
+ }
126
+
127
+ private _handleTriggerClick() {
128
+ if (this.disabled) return;
129
+ this.dispatchEvent(
130
+ new CustomEvent('bds-accordion-item-toggle', { bubbles: true, composed: true }),
131
+ );
132
+ }
133
+
134
+ render() {
135
+ const triggerId = `bds-acc-trigger-${this._uid}`;
136
+ const panelId = `bds-acc-panel-${this._uid}`;
137
+
138
+ return html`
139
+ <h3 class="heading">
140
+ <button
141
+ type="button"
142
+ id=${triggerId}
143
+ class="trigger"
144
+ aria-expanded=${this.open}
145
+ aria-controls=${panelId}
146
+ ?disabled=${this.disabled}
147
+ @click=${this._handleTriggerClick}
148
+ >
149
+ <span class="trigger-label"><slot name="header"></slot></span>
150
+ <svg
151
+ class="chevron"
152
+ aria-hidden="true"
153
+ viewBox="0 0 24 24"
154
+ fill="none"
155
+ stroke="currentColor"
156
+ stroke-width="2"
157
+ >
158
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 9l6 6 6-6" />
159
+ </svg>
160
+ </button>
161
+ </h3>
162
+ <div
163
+ id=${panelId}
164
+ class="panel"
165
+ role="region"
166
+ aria-labelledby=${triggerId}
167
+ ?hidden=${!this.open}
168
+ >
169
+ <div class="panel-content"><slot></slot></div>
170
+ </div>
171
+ `;
172
+ }
173
+ }
174
+
175
+ customElements.define('bds-accordion-item', BdsAccordionItem);
176
+
177
+ declare global {
178
+ interface HTMLElementTagNameMap {
179
+ 'bds-accordion-item': BdsAccordionItem;
180
+ }
181
+ }
@@ -0,0 +1,73 @@
1
+ import { fixture, cleanup } from '../test/helpers';
2
+ import { BdsAccordion } from './bds-accordion';
3
+ import { BdsAccordionItem } from './bds-accordion-item';
4
+
5
+ const html = `
6
+ <bds-accordion>
7
+ <bds-accordion-item><span slot="header">Q1</span><p>A1</p></bds-accordion-item>
8
+ <bds-accordion-item><span slot="header">Q2</span><p>A2</p></bds-accordion-item>
9
+ <bds-accordion-item><span slot="header">Q3</span><p>A3</p></bds-accordion-item>
10
+ </bds-accordion>
11
+ `;
12
+
13
+ describe('bds-accordion', () => {
14
+ it('is registered as a custom element', () => {
15
+ expect(customElements.get('bds-accordion')).toBe(BdsAccordion);
16
+ });
17
+
18
+ it('all items are closed by default', async () => {
19
+ const el = await fixture(html);
20
+ const items = [...el.querySelectorAll<BdsAccordionItem>('bds-accordion-item')];
21
+ // Reflect: true means open=false removes the attribute; check attribute absence.
22
+ expect(items.every(i => !i.hasAttribute('open'))).toBe(true);
23
+ cleanup(el);
24
+ });
25
+
26
+ it('opens the toggled item when a bds-accordion-item-toggle fires', async () => {
27
+ const el = await fixture(html);
28
+ const items = [...el.querySelectorAll<BdsAccordionItem>('bds-accordion-item')];
29
+ items[0].dispatchEvent(new CustomEvent('bds-accordion-item-toggle', { bubbles: true, composed: true }));
30
+ expect(items[0].open).toBe(true);
31
+ cleanup(el);
32
+ });
33
+
34
+ it('closes other items when a new one is toggled (exclusive by default)', async () => {
35
+ const el = await fixture(html);
36
+ const items = [...el.querySelectorAll<BdsAccordionItem>('bds-accordion-item')];
37
+ // Open first
38
+ items[0].dispatchEvent(new CustomEvent('bds-accordion-item-toggle', { bubbles: true, composed: true }));
39
+ expect(items[0].open).toBe(true);
40
+ // Toggle second
41
+ items[1].dispatchEvent(new CustomEvent('bds-accordion-item-toggle', { bubbles: true, composed: true }));
42
+ expect(items[1].open).toBe(true);
43
+ expect(items[0].open).toBe(false);
44
+ cleanup(el);
45
+ });
46
+
47
+ it('allows multiple open items when allow-multiple is set', async () => {
48
+ const el = await fixture(`
49
+ <bds-accordion allow-multiple>
50
+ <bds-accordion-item><span slot="header">Q1</span><p>A1</p></bds-accordion-item>
51
+ <bds-accordion-item><span slot="header">Q2</span><p>A2</p></bds-accordion-item>
52
+ </bds-accordion>
53
+ `);
54
+ const items = [...el.querySelectorAll<BdsAccordionItem>('bds-accordion-item')];
55
+ items[0].dispatchEvent(new CustomEvent('bds-accordion-item-toggle', { bubbles: true, composed: true }));
56
+ items[1].dispatchEvent(new CustomEvent('bds-accordion-item-toggle', { bubbles: true, composed: true }));
57
+ expect(items[0].open).toBe(true);
58
+ expect(items[1].open).toBe(true);
59
+ cleanup(el);
60
+ });
61
+
62
+ it('closes an already-open item when toggled again', async () => {
63
+ const el = await fixture(html);
64
+ const items = [...el.querySelectorAll<BdsAccordionItem>('bds-accordion-item')];
65
+ // Open first
66
+ items[0].dispatchEvent(new CustomEvent('bds-accordion-item-toggle', { bubbles: true, composed: true }));
67
+ expect(items[0].open).toBe(true);
68
+ // Toggle again to close
69
+ items[0].dispatchEvent(new CustomEvent('bds-accordion-item-toggle', { bubbles: true, composed: true }));
70
+ expect(items[0].open).toBe(false);
71
+ cleanup(el);
72
+ });
73
+ });
@@ -0,0 +1,99 @@
1
+ import { LitElement, css, html } from 'lit';
2
+ import type { BdsAccordionItem } from './bds-accordion-item';
3
+
4
+ /**
5
+ * `<bds-accordion>` — framework-agnostic Accordion custom element.
6
+ *
7
+ * Contains `<bds-accordion-item>` children. Coordinates open/closed state
8
+ * across items; by default only one item is open at a time.
9
+ *
10
+ * Attributes:
11
+ * allow-multiple — boolean; when set, multiple items can be open simultaneously
12
+ *
13
+ * @example
14
+ * <bds-accordion>
15
+ * <bds-accordion-item>
16
+ * <span slot="header">Question 1</span>
17
+ * <p>Answer 1</p>
18
+ * </bds-accordion-item>
19
+ * <bds-accordion-item>
20
+ * <span slot="header">Question 2</span>
21
+ * <p>Answer 2</p>
22
+ * </bds-accordion-item>
23
+ * </bds-accordion>
24
+ */
25
+ export class BdsAccordion extends LitElement {
26
+ static styles = css`
27
+ :host {
28
+ display: block;
29
+ --bdc_color: var(--bds-color_bg--subtle);
30
+ --bdc-outline_radius: var(--bds-border_radius--s);
31
+ outline: var(--bdc-outline_width) solid var(--bdc_color);
32
+ outline-offset: var(--bdc-outline_offset);
33
+ border-radius: var(--accordion_radius, var(--bdc-outline_radius));
34
+ overflow: hidden;
35
+ }
36
+ `;
37
+
38
+ static properties = {
39
+ allowMultiple: { type: Boolean, attribute: 'allow-multiple', reflect: true },
40
+ };
41
+
42
+ declare allowMultiple: boolean;
43
+
44
+ private _mo: MutationObserver | null = null;
45
+
46
+ constructor() {
47
+ super();
48
+ this.allowMultiple = false;
49
+ }
50
+
51
+ connectedCallback() {
52
+ super.connectedCallback();
53
+ this.addEventListener('bds-accordion-item-toggle', this._handleItemToggle);
54
+ this._mo = new MutationObserver(() => this._discoverItems());
55
+ this._mo.observe(this, { childList: true, subtree: false });
56
+ }
57
+
58
+ disconnectedCallback() {
59
+ super.disconnectedCallback();
60
+ this.removeEventListener('bds-accordion-item-toggle', this._handleItemToggle);
61
+ this._mo?.disconnect();
62
+ this._mo = null;
63
+ }
64
+
65
+ private get _items(): BdsAccordionItem[] {
66
+ return [...this.querySelectorAll<BdsAccordionItem>('bds-accordion-item')];
67
+ }
68
+
69
+ private _discoverItems() {
70
+ // No-op: items are queried on demand via _items getter.
71
+ // The MutationObserver ensures we react to added/removed items.
72
+ this.requestUpdate();
73
+ }
74
+
75
+ private _handleItemToggle = (e: Event) => {
76
+ const toggled = e.target as BdsAccordionItem;
77
+ const items = this._items;
78
+ const wasOpen = toggled.open;
79
+
80
+ if (!this.allowMultiple) {
81
+ // Close all others
82
+ items.forEach(item => { item.open = false; });
83
+ }
84
+ // Toggle the clicked item
85
+ toggled.open = !wasOpen;
86
+ };
87
+
88
+ render() {
89
+ return html`<slot></slot>`;
90
+ }
91
+ }
92
+
93
+ customElements.define('bds-accordion', BdsAccordion);
94
+
95
+ declare global {
96
+ interface HTMLElementTagNameMap {
97
+ 'bds-accordion': BdsAccordion;
98
+ }
99
+ }
@@ -0,0 +1,56 @@
1
+ import { fixture, cleanup } from '../test/helpers';
2
+ import { BdsCollapsible } from './bds-collapsible';
3
+
4
+ describe('bds-collapsible', () => {
5
+ it('is registered as a custom element', () => {
6
+ expect(customElements.get('bds-collapsible')).toBe(BdsCollapsible);
7
+ });
8
+
9
+ it('is closed by default', async () => {
10
+ const el = await fixture('<bds-collapsible><span slot="summary">Title</span><p>Content</p></bds-collapsible>') as BdsCollapsible;
11
+ expect(el.open).toBe(false);
12
+ expect(el.hasAttribute('open')).toBe(false);
13
+ cleanup(el);
14
+ });
15
+
16
+ it('renders a native <details> element in shadow DOM', async () => {
17
+ const el = await fixture('<bds-collapsible><span slot="summary">Title</span><p>Content</p></bds-collapsible>');
18
+ expect(el.shadowRoot!.querySelector('details')).not.toBeNull();
19
+ cleanup(el);
20
+ });
21
+
22
+ it('renders a <summary> element', async () => {
23
+ const el = await fixture('<bds-collapsible><span slot="summary">Title</span><p>Content</p></bds-collapsible>');
24
+ expect(el.shadowRoot!.querySelector('summary')).not.toBeNull();
25
+ cleanup(el);
26
+ });
27
+
28
+ it('is open when open attribute is set', async () => {
29
+ const el = await fixture('<bds-collapsible open><span slot="summary">Title</span><p>Content</p></bds-collapsible>') as BdsCollapsible;
30
+ expect(el.open).toBe(true);
31
+ const details = el.shadowRoot!.querySelector('details');
32
+ expect(details!.hasAttribute('open')).toBe(true);
33
+ cleanup(el);
34
+ });
35
+
36
+ it('fires bds-toggle event when toggled', async () => {
37
+ const el = await fixture('<bds-collapsible><span slot="summary">Title</span><p>Content</p></bds-collapsible>') as BdsCollapsible;
38
+ const handler = vi.fn();
39
+ el.addEventListener('bds-toggle', handler);
40
+ // jsdom does not fire the native toggle event on summary click;
41
+ // dispatch it directly on the <details> element instead.
42
+ const details = el.shadowRoot!.querySelector('details')!;
43
+ details.dispatchEvent(new Event('toggle', { bubbles: false }));
44
+ await el.updateComplete;
45
+ expect(handler).toHaveBeenCalledOnce();
46
+ cleanup(el);
47
+ });
48
+
49
+ it('reflects open state change from outside', async () => {
50
+ const el = await fixture('<bds-collapsible><span slot="summary">Title</span><p>Content</p></bds-collapsible>') as BdsCollapsible;
51
+ el.open = true;
52
+ await el.updateComplete;
53
+ expect(el.getAttribute('open')).not.toBeNull();
54
+ cleanup(el);
55
+ });
56
+ });
@@ -0,0 +1,170 @@
1
+ import { LitElement, css, html } from 'lit';
2
+
3
+ /**
4
+ * `<bds-collapsible>` — framework-agnostic Collapsible custom element.
5
+ *
6
+ * Wraps a native `<details>/<summary>` element for built-in toggle behaviour.
7
+ *
8
+ * Attributes:
9
+ * open — boolean; controlled open state
10
+ * name — groups multiple collapsibles so only one is open at a time
11
+ * (native <details name="…"> behaviour)
12
+ *
13
+ * Slots:
14
+ * summary — the always-visible trigger text
15
+ * (default) — the content revealed when expanded
16
+ *
17
+ * Events:
18
+ * bds-toggle — fired (bubbles, composed) when the collapsible opens or closes;
19
+ * event.detail.open reflects the new state
20
+ *
21
+ * @example
22
+ * <bds-collapsible>
23
+ * <span slot="summary">What is this?</span>
24
+ * <p>It's a collapsible panel.</p>
25
+ * </bds-collapsible>
26
+ */
27
+ export class BdsCollapsible extends LitElement {
28
+ static styles = css`
29
+ :host {
30
+ display: block;
31
+ }
32
+
33
+ .collapsible {
34
+ border: var(--collapsible_border-width, 1px) solid var(--collapsible_border-color, var(--bds-color_bg--subtle));
35
+ border-radius: var(--collapsible_border-radius, var(--bds-border_radius--m));
36
+ overflow: hidden;
37
+ }
38
+
39
+ .summary {
40
+ list-style: none;
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: space-between;
44
+ gap: var(--bds-space_m);
45
+ padding: var(--bds-space_m);
46
+ cursor: pointer;
47
+ font-weight: var(--bds-font_weight--semibold);
48
+ font-size: var(--bds-font_size--body);
49
+ color: var(--collapsible_color, var(--bds-color_on-bg));
50
+ background-color: var(--collapsible_bg, var(--bds-color_bg));
51
+ user-select: none;
52
+ transition: var(--bds-animation_transition);
53
+ }
54
+
55
+ .summary::-webkit-details-marker {
56
+ display: none;
57
+ }
58
+
59
+ .summary:focus-visible {
60
+ outline: var(--bds-outline_default);
61
+ outline-offset: calc(var(--bds-outline_offset) * -1);
62
+ }
63
+
64
+ @media (hover: hover) and (pointer: fine) {
65
+ .summary:hover {
66
+ background-color: var(--collapsible_bg--hover, var(--bds-color_bg--subtle));
67
+ }
68
+ }
69
+
70
+ .collapsible[open] > .summary {
71
+ border-block-end: var(--collapsible_border-width, 1px) solid var(--collapsible_border-color, var(--bds-color_bg--subtle));
72
+ }
73
+
74
+ .summary-content {
75
+ flex: 1;
76
+ }
77
+
78
+ .icon {
79
+ inline-size: 0.5rem;
80
+ block-size: 0.5rem;
81
+ border-inline-end: 2px solid currentcolor;
82
+ border-block-end: 2px solid currentcolor;
83
+ transform: rotate(45deg);
84
+ flex-shrink: 0;
85
+ transition: transform var(--bds-animation_transition-duration) var(--bds-animation_easing);
86
+ }
87
+
88
+ .collapsible[open] > .summary .icon {
89
+ transform: rotate(-135deg);
90
+ }
91
+
92
+ .content {
93
+ padding: var(--bds-space_m);
94
+ color: var(--collapsible_on-color, var(--bds-color_on-bg));
95
+ font-size: var(--bds-font_size--body);
96
+ line-height: var(--bds-font_line-height--body);
97
+ }
98
+
99
+ @media (prefers-reduced-motion: reduce) {
100
+ .icon {
101
+ transition: none;
102
+ }
103
+ }
104
+ `;
105
+
106
+ static properties = {
107
+ open: { type: Boolean, reflect: true },
108
+ name: { type: String },
109
+ };
110
+
111
+ declare open: boolean;
112
+ declare name: string;
113
+
114
+ constructor() {
115
+ super();
116
+ this.open = false;
117
+ this.name = '';
118
+ }
119
+
120
+ updated(changed: Map<string, unknown>) {
121
+ super.updated(changed);
122
+ // Keep the inner <details> name attribute in sync since it can't be bound directly
123
+ if (changed.has('name')) {
124
+ const details = this.shadowRoot?.querySelector('details');
125
+ if (details) {
126
+ if (this.name) {
127
+ details.setAttribute('name', this.name);
128
+ } else {
129
+ details.removeAttribute('name');
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ private _onToggle(e: Event) {
136
+ const details = e.currentTarget as HTMLDetailsElement;
137
+ this.open = details.open;
138
+ this.dispatchEvent(
139
+ new CustomEvent('bds-toggle', {
140
+ bubbles: true,
141
+ composed: true,
142
+ detail: { open: this.open },
143
+ }),
144
+ );
145
+ }
146
+
147
+ render() {
148
+ return html`
149
+ <details
150
+ class="collapsible"
151
+ ?open=${this.open}
152
+ @toggle=${this._onToggle}
153
+ >
154
+ <summary class="summary">
155
+ <span class="summary-content"><slot name="summary"></slot></span>
156
+ <span class="icon" aria-hidden="true"></span>
157
+ </summary>
158
+ <div class="content"><slot></slot></div>
159
+ </details>
160
+ `;
161
+ }
162
+ }
163
+
164
+ customElements.define('bds-collapsible', BdsCollapsible);
165
+
166
+ declare global {
167
+ interface HTMLElementTagNameMap {
168
+ 'bds-collapsible': BdsCollapsible;
169
+ }
170
+ }