@boostdev/design-system-components 1.1.1 → 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 (206) hide show
  1. package/dist/client.cjs +50 -50
  2. package/dist/client.css +456 -456
  3. package/dist/client.js +50 -50
  4. package/dist/index.cjs +50 -50
  5. package/dist/index.css +456 -456
  6. package/dist/index.js +50 -50
  7. package/dist/web-components/chunk-2QA62WCQ.js +48 -0
  8. package/dist/web-components/chunk-4BX6AWXX.js +146 -0
  9. package/dist/web-components/chunk-57TRNCWQ.js +93 -0
  10. package/dist/web-components/chunk-5IPHEONG.js +139 -0
  11. package/dist/web-components/chunk-5R6IFELV.js +63 -0
  12. package/dist/web-components/chunk-5TFWOPLH.js +49 -0
  13. package/dist/web-components/chunk-6I3CFC26.js +89 -0
  14. package/dist/web-components/chunk-6T3RKUKG.js +65 -0
  15. package/dist/web-components/chunk-7FMR53BW.js +127 -0
  16. package/dist/web-components/chunk-A67LCT7C.js +134 -0
  17. package/dist/web-components/chunk-AGKJTMJI.js +56 -0
  18. package/dist/web-components/chunk-BQLZ7I4G.js +56 -0
  19. package/dist/web-components/chunk-CAPI25CU.js +53 -0
  20. package/dist/web-components/chunk-CJ5ZOO42.js +229 -0
  21. package/dist/web-components/chunk-CR5PGUHV.js +109 -0
  22. package/dist/web-components/chunk-ECW3HHWA.js +185 -0
  23. package/dist/web-components/chunk-HGBNDR22.js +98 -0
  24. package/dist/web-components/chunk-IK3PTFZ4.js +133 -0
  25. package/dist/web-components/chunk-IQGLIQSO.js +188 -0
  26. package/dist/web-components/chunk-ITHLKR7Q.js +105 -0
  27. package/dist/web-components/chunk-K5CE6HSB.js +169 -0
  28. package/dist/web-components/chunk-L3XHOMN6.js +111 -0
  29. package/dist/{web-components.js → web-components/chunk-LGEZYFUU.js} +62 -844
  30. package/dist/web-components/chunk-LSC4N56Q.js +168 -0
  31. package/dist/web-components/chunk-LY62SAVN.js +64 -0
  32. package/dist/web-components/chunk-O4UFI2RX.js +244 -0
  33. package/dist/web-components/chunk-O7I63SQX.js +124 -0
  34. package/dist/web-components/chunk-OHUTLSIQ.js +69 -0
  35. package/dist/web-components/chunk-OQTRTMAB.js +27 -0
  36. package/dist/web-components/chunk-OTX4COSV.js +187 -0
  37. package/dist/web-components/chunk-PY62FTRW.js +179 -0
  38. package/dist/web-components/chunk-Q4T5BJT6.js +94 -0
  39. package/dist/web-components/chunk-QDRH46TF.js +281 -0
  40. package/dist/web-components/chunk-QDST57JC.js +119 -0
  41. package/dist/web-components/chunk-QPOKHLMQ.js +45 -0
  42. package/dist/web-components/chunk-SOQQ7RVJ.js +88 -0
  43. package/dist/web-components/chunk-UFFCKVZE.js +149 -0
  44. package/dist/web-components/chunk-VM3YHGQ2.js +152 -0
  45. package/dist/web-components/chunk-VMMVWLXL.js +53 -0
  46. package/dist/web-components/chunk-VSS5ITNE.js +109 -0
  47. package/dist/web-components/chunk-XIENXR7H.js +118 -0
  48. package/dist/web-components/chunk-Y4MAXTMA.js +80 -0
  49. package/dist/web-components/chunk-YLVTEY23.js +165 -0
  50. package/dist/web-components/globals.d.ts +2 -0
  51. package/dist/web-components/globals.js +1 -0
  52. package/dist/web-components/index.d.ts +41 -0
  53. package/dist/web-components/index.js +165 -0
  54. package/dist/web-components/interaction/bds-accordion-item.d.ts +54 -0
  55. package/dist/web-components/interaction/bds-accordion-item.js +7 -0
  56. package/dist/web-components/interaction/bds-accordion.d.ts +50 -0
  57. package/dist/web-components/interaction/bds-accordion.js +7 -0
  58. package/dist/web-components/interaction/bds-button.d.ts +100 -0
  59. package/dist/web-components/interaction/bds-button.js +7 -0
  60. package/dist/web-components/interaction/bds-collapsible.d.ts +52 -0
  61. package/dist/web-components/interaction/bds-collapsible.js +7 -0
  62. package/dist/web-components/interaction/bds-dialog.d.ts +56 -0
  63. package/dist/web-components/interaction/bds-dialog.js +7 -0
  64. package/dist/web-components/interaction/bds-drawer.d.ts +64 -0
  65. package/dist/web-components/interaction/bds-drawer.js +7 -0
  66. package/dist/web-components/interaction/bds-popover.d.ts +62 -0
  67. package/dist/web-components/interaction/bds-popover.js +7 -0
  68. package/dist/web-components/interaction/bds-skip-link.d.ts +36 -0
  69. package/dist/web-components/interaction/bds-skip-link.js +7 -0
  70. package/dist/web-components/interaction/bds-tab-panel.d.ts +26 -0
  71. package/dist/web-components/interaction/bds-tab-panel.js +7 -0
  72. package/dist/web-components/interaction/bds-tab.d.ts +45 -0
  73. package/dist/web-components/interaction/bds-tab.js +7 -0
  74. package/dist/web-components/interaction/bds-tabs.d.ts +46 -0
  75. package/dist/web-components/interaction/bds-tabs.js +7 -0
  76. package/dist/web-components/interaction/bds-toast-provider.d.ts +57 -0
  77. package/dist/web-components/interaction/bds-toast-provider.js +7 -0
  78. package/dist/web-components/interaction/bds-tooltip.d.ts +50 -0
  79. package/dist/web-components/interaction/bds-tooltip.js +7 -0
  80. package/dist/web-components/interaction/form/bds-checkbox.d.ts +70 -0
  81. package/dist/web-components/interaction/form/bds-checkbox.js +7 -0
  82. package/dist/web-components/interaction/form/bds-combobox.d.ts +84 -0
  83. package/dist/web-components/interaction/form/bds-combobox.js +7 -0
  84. package/dist/web-components/interaction/form/bds-file-input.d.ts +62 -0
  85. package/dist/web-components/interaction/form/bds-file-input.js +7 -0
  86. package/dist/web-components/interaction/form/bds-number-input.d.ts +68 -0
  87. package/dist/web-components/interaction/form/bds-number-input.js +7 -0
  88. package/dist/web-components/interaction/form/bds-radio.d.ts +68 -0
  89. package/dist/web-components/interaction/form/bds-radio.js +7 -0
  90. package/dist/web-components/interaction/form/bds-segmented-control.d.ts +71 -0
  91. package/dist/web-components/interaction/form/bds-segmented-control.js +7 -0
  92. package/dist/web-components/interaction/form/bds-select.d.ts +79 -0
  93. package/dist/web-components/interaction/form/bds-select.js +7 -0
  94. package/dist/web-components/interaction/form/bds-slider.d.ts +70 -0
  95. package/dist/web-components/interaction/form/bds-slider.js +7 -0
  96. package/dist/web-components/interaction/form/bds-switch.d.ts +67 -0
  97. package/dist/web-components/interaction/form/bds-switch.js +7 -0
  98. package/dist/web-components/interaction/form/bds-textarea.d.ts +70 -0
  99. package/dist/web-components/interaction/form/bds-textarea.js +7 -0
  100. package/dist/web-components/ui/bds-alert.d.ts +66 -0
  101. package/dist/web-components/ui/bds-alert.js +7 -0
  102. package/dist/web-components/ui/bds-avatar.d.ts +50 -0
  103. package/dist/web-components/ui/bds-avatar.js +7 -0
  104. package/dist/web-components/ui/bds-badge.d.ts +35 -0
  105. package/dist/web-components/ui/bds-badge.js +7 -0
  106. package/dist/web-components/ui/bds-breadcrumb.d.ts +35 -0
  107. package/dist/web-components/ui/bds-breadcrumb.js +7 -0
  108. package/dist/web-components/ui/bds-card.d.ts +72 -0
  109. package/dist/web-components/ui/bds-card.js +7 -0
  110. package/dist/web-components/ui/bds-description-list.d.ts +42 -0
  111. package/dist/web-components/ui/bds-description-list.js +7 -0
  112. package/dist/web-components/ui/bds-icon-wrapper.d.ts +39 -0
  113. package/dist/web-components/ui/bds-icon-wrapper.js +7 -0
  114. package/dist/web-components/ui/bds-link.d.ts +59 -0
  115. package/dist/web-components/ui/bds-link.js +7 -0
  116. package/dist/web-components/ui/bds-loading.d.ts +32 -0
  117. package/dist/web-components/ui/bds-loading.js +7 -0
  118. package/dist/web-components/ui/bds-notification-banner.d.ts +61 -0
  119. package/dist/web-components/ui/bds-notification-banner.js +7 -0
  120. package/dist/web-components/ui/bds-progress-circle.d.ts +53 -0
  121. package/dist/web-components/ui/bds-progress-circle.js +7 -0
  122. package/dist/web-components/ui/bds-progress.d.ts +54 -0
  123. package/dist/web-components/ui/bds-progress.js +7 -0
  124. package/dist/web-components/ui/bds-rating.d.ts +36 -0
  125. package/dist/web-components/ui/bds-rating.js +7 -0
  126. package/dist/web-components/ui/bds-section-header.d.ts +53 -0
  127. package/dist/web-components/ui/bds-section-header.js +8 -0
  128. package/dist/web-components/ui/bds-separator.d.ts +32 -0
  129. package/dist/web-components/ui/bds-separator.js +7 -0
  130. package/dist/web-components/ui/bds-skeleton.d.ts +22 -0
  131. package/dist/web-components/ui/bds-skeleton.js +7 -0
  132. package/dist/web-components/ui/bds-typography.d.ts +44 -0
  133. package/dist/web-components/ui/bds-typography.js +8 -0
  134. package/package.json +169 -4
  135. package/src/web-components/index.ts +47 -0
  136. package/src/web-components/interaction/bds-accordion-item.spec.ts +75 -0
  137. package/src/web-components/interaction/bds-accordion-item.ts +181 -0
  138. package/src/web-components/interaction/bds-accordion.spec.ts +73 -0
  139. package/src/web-components/interaction/bds-accordion.ts +99 -0
  140. package/src/web-components/interaction/bds-collapsible.spec.ts +56 -0
  141. package/src/web-components/interaction/bds-collapsible.ts +170 -0
  142. package/src/web-components/interaction/bds-dialog.spec.ts +119 -0
  143. package/src/web-components/interaction/bds-dialog.ts +224 -0
  144. package/src/web-components/interaction/bds-drawer.spec.ts +113 -0
  145. package/src/web-components/interaction/bds-drawer.ts +266 -0
  146. package/src/web-components/interaction/bds-skip-link.spec.ts +42 -0
  147. package/src/web-components/interaction/bds-skip-link.ts +74 -0
  148. package/src/web-components/interaction/bds-tab-panel.spec.ts +32 -0
  149. package/src/web-components/interaction/bds-tab-panel.ts +38 -0
  150. package/src/web-components/interaction/bds-tab.spec.ts +71 -0
  151. package/src/web-components/interaction/bds-tab.ts +110 -0
  152. package/src/web-components/interaction/bds-tabs.spec.ts +120 -0
  153. package/src/web-components/interaction/bds-tabs.ts +152 -0
  154. package/src/web-components/interaction/bds-tooltip.spec.ts +49 -0
  155. package/src/web-components/interaction/bds-tooltip.ts +127 -0
  156. package/src/web-components/interaction/form/bds-checkbox.spec.ts +63 -0
  157. package/src/web-components/interaction/form/bds-checkbox.ts +168 -0
  158. package/src/web-components/interaction/form/bds-combobox.spec.ts +117 -0
  159. package/src/web-components/interaction/form/bds-combobox.ts +336 -0
  160. package/src/web-components/interaction/form/bds-file-input.spec.ts +58 -0
  161. package/src/web-components/interaction/form/bds-file-input.ts +209 -0
  162. package/src/web-components/interaction/form/bds-number-input.spec.ts +87 -0
  163. package/src/web-components/interaction/form/bds-number-input.ts +221 -0
  164. package/src/web-components/interaction/form/bds-radio.spec.ts +70 -0
  165. package/src/web-components/interaction/form/bds-radio.ts +157 -0
  166. package/src/web-components/interaction/form/bds-segmented-control.spec.ts +74 -0
  167. package/src/web-components/interaction/form/bds-segmented-control.ts +211 -0
  168. package/src/web-components/interaction/form/bds-select.spec.ts +74 -0
  169. package/src/web-components/interaction/form/bds-select.ts +190 -0
  170. package/src/web-components/interaction/form/bds-slider.spec.ts +41 -0
  171. package/src/web-components/interaction/form/bds-slider.ts +162 -0
  172. package/src/web-components/interaction/form/bds-switch.spec.ts +62 -0
  173. package/src/web-components/interaction/form/bds-switch.ts +166 -0
  174. package/src/web-components/interaction/form/bds-textarea.spec.ts +55 -0
  175. package/src/web-components/interaction/form/bds-textarea.ts +143 -0
  176. package/src/web-components/ui/bds-avatar.spec.ts +75 -0
  177. package/src/web-components/ui/bds-avatar.ts +122 -0
  178. package/src/web-components/ui/bds-breadcrumb.spec.ts +55 -0
  179. package/src/web-components/ui/bds-breadcrumb.ts +114 -0
  180. package/src/web-components/ui/bds-card.spec.ts +70 -0
  181. package/src/web-components/ui/bds-card.ts +156 -0
  182. package/src/web-components/ui/bds-description-list.spec.ts +36 -0
  183. package/src/web-components/ui/bds-description-list.ts +79 -0
  184. package/src/web-components/ui/bds-icon-wrapper.spec.ts +21 -0
  185. package/src/web-components/ui/bds-icon-wrapper.ts +68 -0
  186. package/src/web-components/ui/bds-link.spec.ts +72 -0
  187. package/src/web-components/ui/bds-link.ts +138 -0
  188. package/src/web-components/ui/bds-loading.spec.ts +38 -0
  189. package/src/web-components/ui/bds-loading.ts +76 -0
  190. package/src/web-components/ui/bds-notification-banner.spec.ts +82 -0
  191. package/src/web-components/ui/bds-notification-banner.ts +185 -0
  192. package/src/web-components/ui/bds-progress-circle.spec.ts +58 -0
  193. package/src/web-components/ui/bds-progress-circle.ts +142 -0
  194. package/src/web-components/ui/bds-progress.spec.ts +66 -0
  195. package/src/web-components/ui/bds-progress.ts +127 -0
  196. package/src/web-components/ui/bds-rating.spec.ts +70 -0
  197. package/src/web-components/ui/bds-rating.ts +80 -0
  198. package/src/web-components/ui/bds-section-header.spec.ts +57 -0
  199. package/src/web-components/ui/bds-section-header.ts +96 -0
  200. package/src/web-components/ui/bds-separator.spec.ts +50 -0
  201. package/src/web-components/ui/bds-separator.ts +66 -0
  202. package/src/web-components/ui/bds-skeleton.spec.ts +22 -0
  203. package/src/web-components/ui/bds-skeleton.ts +55 -0
  204. package/src/web-components/ui/bds-typography.spec.ts +76 -0
  205. package/src/web-components/ui/bds-typography.ts +112 -0
  206. package/dist/web-components.d.ts +0 -304
@@ -0,0 +1,74 @@
1
+ import { LitElement, css, html } from 'lit';
2
+
3
+ /**
4
+ * `<bds-skip-link>` — framework-agnostic Skip Navigation Link custom element.
5
+ *
6
+ * Visually hidden until focused. Allows keyboard users to skip repetitive
7
+ * navigation and jump directly to main content.
8
+ *
9
+ * Attributes:
10
+ * href — the target anchor (default: "#main")
11
+ *
12
+ * Slots:
13
+ * (default) — link text (default: "Skip to main content")
14
+ *
15
+ * @example
16
+ * <bds-skip-link href="#main-content">Skip to main content</bds-skip-link>
17
+ */
18
+ export class BdsSkipLink extends LitElement {
19
+ static styles = css`
20
+ :host {
21
+ display: block;
22
+ }
23
+
24
+ .skip-link {
25
+ position: absolute;
26
+ inset-block-start: var(--bds-space_s);
27
+ inset-inline-start: var(--bds-space_s);
28
+ z-index: 9999;
29
+ padding: var(--bds-space_xs) var(--bds-space_m);
30
+ background-color: var(--bds-color_bg);
31
+ color: var(--bds-color_on-bg);
32
+ border-radius: var(--bds-border_radius--s);
33
+ font-weight: var(--bds-font_weight--semibold);
34
+ font-size: var(--bds-font_size--body);
35
+ text-decoration: none;
36
+ box-shadow: var(--bds-shadow_m);
37
+ transform: translateY(calc(-100% - var(--bds-space_s)));
38
+ transition: transform var(--bds-animation_transition-duration) var(--bds-animation_easing);
39
+ }
40
+
41
+ .skip-link:focus-visible {
42
+ transform: translateY(0);
43
+ outline: var(--bds-outline_default);
44
+ outline-offset: var(--bds-outline_offset);
45
+ }
46
+ `;
47
+
48
+ static properties = {
49
+ href: { type: String },
50
+ };
51
+
52
+ declare href: string;
53
+
54
+ constructor() {
55
+ super();
56
+ this.href = '#main';
57
+ }
58
+
59
+ render() {
60
+ return html`
61
+ <a class="skip-link" href=${this.href}>
62
+ <slot>Skip to main content</slot>
63
+ </a>
64
+ `;
65
+ }
66
+ }
67
+
68
+ customElements.define('bds-skip-link', BdsSkipLink);
69
+
70
+ declare global {
71
+ interface HTMLElementTagNameMap {
72
+ 'bds-skip-link': BdsSkipLink;
73
+ }
74
+ }
@@ -0,0 +1,32 @@
1
+ import { fixture, cleanup } from '../test/helpers';
2
+ import { BdsTabPanel } from './bds-tab-panel';
3
+
4
+ describe('bds-tab-panel', () => {
5
+ it('is registered as a custom element', () => {
6
+ expect(customElements.get('bds-tab-panel')).toBe(BdsTabPanel);
7
+ });
8
+
9
+ it('renders slotted content', async () => {
10
+ const el = await fixture('<bds-tab-panel>Panel content</bds-tab-panel>');
11
+ expect(el.textContent?.trim()).toBe('Panel content');
12
+ cleanup(el);
13
+ });
14
+
15
+ it('renders a slot in shadow DOM', async () => {
16
+ const el = await fixture('<bds-tab-panel>Content</bds-tab-panel>');
17
+ expect(el.shadowRoot!.querySelector('slot')).not.toBeNull();
18
+ cleanup(el);
19
+ });
20
+
21
+ it('is visible by default (no hidden attribute)', async () => {
22
+ const el = await fixture('<bds-tab-panel>Content</bds-tab-panel>');
23
+ expect(el.hasAttribute('hidden')).toBe(false);
24
+ cleanup(el);
25
+ });
26
+
27
+ it('applies hidden attribute when set externally', async () => {
28
+ const el = await fixture('<bds-tab-panel hidden>Content</bds-tab-panel>');
29
+ expect(el.hasAttribute('hidden')).toBe(true);
30
+ cleanup(el);
31
+ });
32
+ });
@@ -0,0 +1,38 @@
1
+ import { LitElement, css, html } from 'lit';
2
+
3
+ /**
4
+ * `<bds-tab-panel>` — child element of `<bds-tabs>`.
5
+ *
6
+ * Contains the content displayed for the associated tab. The `hidden`
7
+ * state is managed by the parent `<bds-tabs>` element.
8
+ *
9
+ * Slots:
10
+ * (default) — panel content
11
+ *
12
+ * @example
13
+ * <bds-tab-panel>Panel content here</bds-tab-panel>
14
+ */
15
+ export class BdsTabPanel extends LitElement {
16
+ static styles = css`
17
+ :host {
18
+ display: block;
19
+ padding-block-start: var(--bds-space_m);
20
+ }
21
+
22
+ :host([hidden]) {
23
+ display: none;
24
+ }
25
+ `;
26
+
27
+ render() {
28
+ return html`<slot></slot>`;
29
+ }
30
+ }
31
+
32
+ customElements.define('bds-tab-panel', BdsTabPanel);
33
+
34
+ declare global {
35
+ interface HTMLElementTagNameMap {
36
+ 'bds-tab-panel': BdsTabPanel;
37
+ }
38
+ }
@@ -0,0 +1,71 @@
1
+ import { fixture, cleanup } from '../test/helpers';
2
+ import { BdsTab } from './bds-tab';
3
+
4
+ describe('bds-tab', () => {
5
+ it('is registered as a custom element', () => {
6
+ expect(customElements.get('bds-tab')).toBe(BdsTab);
7
+ });
8
+
9
+ it('renders a button with role="tab"', async () => {
10
+ const el = await fixture('<bds-tab>Label</bds-tab>');
11
+ const btn = el.shadowRoot!.querySelector('button');
12
+ expect(btn).not.toBeNull();
13
+ expect(btn!.getAttribute('role')).toBe('tab');
14
+ cleanup(el);
15
+ });
16
+
17
+ it('is not selected by default', async () => {
18
+ const el = await fixture('<bds-tab>Label</bds-tab>') as BdsTab;
19
+ expect(el.selected).toBe(false);
20
+ expect(el.shadowRoot!.querySelector('button')!.getAttribute('aria-selected')).toBe('false');
21
+ cleanup(el);
22
+ });
23
+
24
+ it('has tabindex="-1" when not selected', async () => {
25
+ const el = await fixture('<bds-tab>Label</bds-tab>');
26
+ const btn = el.shadowRoot!.querySelector('button')!;
27
+ expect(btn.getAttribute('tabindex')).toBe('-1');
28
+ cleanup(el);
29
+ });
30
+
31
+ it('has tabindex="0" when selected', async () => {
32
+ const el = await fixture('<bds-tab selected>Label</bds-tab>');
33
+ const btn = el.shadowRoot!.querySelector('button')!;
34
+ expect(btn.getAttribute('tabindex')).toBe('0');
35
+ cleanup(el);
36
+ });
37
+
38
+ it('sets aria-selected="true" when selected', async () => {
39
+ const el = await fixture('<bds-tab selected>Label</bds-tab>');
40
+ const btn = el.shadowRoot!.querySelector('button')!;
41
+ expect(btn.getAttribute('aria-selected')).toBe('true');
42
+ cleanup(el);
43
+ });
44
+
45
+ it('fires bds-tab-select when clicked', async () => {
46
+ const el = await fixture('<bds-tab>Label</bds-tab>');
47
+ const btn = el.shadowRoot!.querySelector('button')!;
48
+ const handler = vi.fn();
49
+ el.addEventListener('bds-tab-select', handler);
50
+ btn.click();
51
+ expect(handler).toHaveBeenCalledOnce();
52
+ cleanup(el);
53
+ });
54
+
55
+ it('does not fire bds-tab-select when disabled', async () => {
56
+ const el = await fixture('<bds-tab disabled>Label</bds-tab>');
57
+ const btn = el.shadowRoot!.querySelector('button')!;
58
+ const handler = vi.fn();
59
+ el.addEventListener('bds-tab-select', handler);
60
+ btn.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
61
+ expect(handler).not.toHaveBeenCalled();
62
+ cleanup(el);
63
+ });
64
+
65
+ it('button is disabled when disabled attribute is set', async () => {
66
+ const el = await fixture('<bds-tab disabled>Label</bds-tab>');
67
+ const btn = el.shadowRoot!.querySelector('button')!;
68
+ expect(btn.hasAttribute('disabled')).toBe(true);
69
+ cleanup(el);
70
+ });
71
+ });
@@ -0,0 +1,110 @@
1
+ import { LitElement, css, html } from 'lit';
2
+
3
+ /**
4
+ * `<bds-tab>` — child element of `<bds-tabs>`.
5
+ *
6
+ * Represents a single tab button. State (`selected`, `tabIndex`, `disabled`)
7
+ * is managed by the parent `<bds-tabs>` element.
8
+ *
9
+ * Slots:
10
+ * (default) — tab label
11
+ *
12
+ * Events:
13
+ * bds-tab-select — fired (bubbles, composed) when the tab is clicked
14
+ *
15
+ * @example
16
+ * <bds-tab>Overview</bds-tab>
17
+ */
18
+ export class BdsTab extends LitElement {
19
+ static styles = css`
20
+ :host {
21
+ display: inline-flex;
22
+ }
23
+
24
+ .tab {
25
+ all: unset;
26
+ display: inline-flex;
27
+ align-items: center;
28
+ padding: var(--bds-space_s) var(--bds-space_m);
29
+ font-family: var(--bds-font_family--body);
30
+ font-size: var(--bds-font_size--body);
31
+ font-weight: var(--bds-font_weight--semibold);
32
+ color: var(--tab_color, var(--bds-color_on-bg));
33
+ border-block-end: 2px solid var(--tab_border-color, transparent);
34
+ margin-block-end: -1px;
35
+ cursor: pointer;
36
+ transition: var(--bds-animation_transition);
37
+ white-space: nowrap;
38
+ }
39
+
40
+ :host([selected]) .tab {
41
+ --tab_color: var(--bds-color_interactive_on-bg);
42
+ --tab_border-color: var(--bds-color_interactive);
43
+ }
44
+
45
+ .tab:disabled {
46
+ opacity: 0.4;
47
+ cursor: not-allowed;
48
+ }
49
+
50
+ .tab:focus-visible {
51
+ outline: var(--bds-outline_default);
52
+ outline-offset: var(--bds-outline_offset);
53
+ border-radius: var(--bds-border_radius--xs);
54
+ }
55
+
56
+ @media (hover: hover) and (pointer: fine) {
57
+ .tab:not(:disabled, [aria-selected='true']):hover {
58
+ --tab_color: var(--bds-color_interactive_on-bg);
59
+ }
60
+ }
61
+ `;
62
+
63
+ static properties = {
64
+ selected: { type: Boolean, reflect: true },
65
+ disabled: { type: Boolean, reflect: true },
66
+ };
67
+
68
+ declare selected: boolean;
69
+ declare disabled: boolean;
70
+
71
+ constructor() {
72
+ super();
73
+ this.selected = false;
74
+ this.disabled = false;
75
+ }
76
+
77
+ /** Called by `bds-tabs` to focus the inner button for keyboard navigation. */
78
+ focusButton() {
79
+ this.shadowRoot?.querySelector<HTMLButtonElement>('button')?.focus();
80
+ }
81
+
82
+ private _handleClick() {
83
+ if (this.disabled) return;
84
+ this.dispatchEvent(new CustomEvent('bds-tab-select', { bubbles: true, composed: true }));
85
+ }
86
+
87
+ render() {
88
+ return html`
89
+ <button
90
+ type="button"
91
+ class="tab"
92
+ role="tab"
93
+ aria-selected=${this.selected}
94
+ tabindex=${this.selected ? 0 : -1}
95
+ ?disabled=${this.disabled}
96
+ @click=${this._handleClick}
97
+ >
98
+ <slot></slot>
99
+ </button>
100
+ `;
101
+ }
102
+ }
103
+
104
+ customElements.define('bds-tab', BdsTab);
105
+
106
+ declare global {
107
+ interface HTMLElementTagNameMap {
108
+ 'bds-tab': BdsTab;
109
+ }
110
+ }
@@ -0,0 +1,120 @@
1
+ import { fixture, cleanup } from '../test/helpers';
2
+ import { BdsTabs } from './bds-tabs';
3
+ import { BdsTab } from './bds-tab';
4
+ import { BdsTabPanel } from './bds-tab-panel';
5
+
6
+ const html = `
7
+ <bds-tabs>
8
+ <bds-tab slot="tab">Tab 1</bds-tab>
9
+ <bds-tab slot="tab">Tab 2</bds-tab>
10
+ <bds-tab slot="tab" disabled>Tab 3</bds-tab>
11
+ <bds-tab-panel slot="panel">Panel 1</bds-tab-panel>
12
+ <bds-tab-panel slot="panel">Panel 2</bds-tab-panel>
13
+ <bds-tab-panel slot="panel">Panel 3</bds-tab-panel>
14
+ </bds-tabs>
15
+ `;
16
+
17
+ describe('bds-tabs', () => {
18
+ it('is registered as a custom element', () => {
19
+ expect(customElements.get('bds-tabs')).toBe(BdsTabs);
20
+ });
21
+
22
+ it('renders a tablist div with role="tablist"', async () => {
23
+ const el = await fixture(html);
24
+ const tablist = el.shadowRoot!.querySelector('[role="tablist"]');
25
+ expect(tablist).not.toBeNull();
26
+ cleanup(el);
27
+ });
28
+
29
+ it('selects the first tab by default', async () => {
30
+ const el = await fixture(html);
31
+ // Wait for deferred _applyState
32
+ await Promise.resolve();
33
+ const tabs = [...el.querySelectorAll<BdsTab>('bds-tab')];
34
+ expect(tabs[0].selected).toBe(true);
35
+ expect(tabs[1].selected).toBe(false);
36
+ cleanup(el);
37
+ });
38
+
39
+ it('first panel is visible, others are hidden by default', async () => {
40
+ const el = await fixture(html);
41
+ await Promise.resolve();
42
+ const panels = [...el.querySelectorAll<BdsTabPanel>('bds-tab-panel')];
43
+ expect(panels[0].hasAttribute('hidden')).toBe(false);
44
+ expect(panels[1].hasAttribute('hidden')).toBe(true);
45
+ cleanup(el);
46
+ });
47
+
48
+ it('switches to tab on bds-tab-select event', async () => {
49
+ const el = await fixture(html);
50
+ await Promise.resolve();
51
+ const tabs = [...el.querySelectorAll<BdsTab>('bds-tab')];
52
+ const panels = [...el.querySelectorAll<BdsTabPanel>('bds-tab-panel')];
53
+
54
+ tabs[1].dispatchEvent(new CustomEvent('bds-tab-select', { bubbles: true, composed: true }));
55
+
56
+ expect(tabs[0].selected).toBe(false);
57
+ expect(tabs[1].selected).toBe(true);
58
+ expect(panels[0].hasAttribute('hidden')).toBe(true);
59
+ expect(panels[1].hasAttribute('hidden')).toBe(false);
60
+ cleanup(el);
61
+ });
62
+
63
+ it('does not switch to a disabled tab on bds-tab-select', async () => {
64
+ const el = await fixture(html);
65
+ await Promise.resolve();
66
+ const tabs = [...el.querySelectorAll<BdsTab>('bds-tab')];
67
+ // Ensure the tab is marked disabled (Lit property may not reflect from HTML attribute in jsdom)
68
+ tabs[2].disabled = true;
69
+
70
+ tabs[2].dispatchEvent(new CustomEvent('bds-tab-select', { bubbles: true, composed: true }));
71
+
72
+ expect(tabs[0].selected).toBe(true);
73
+ expect(tabs[2].selected).toBe(false);
74
+ cleanup(el);
75
+ });
76
+
77
+ it('ArrowRight moves to next enabled tab', async () => {
78
+ const el = await fixture(html);
79
+ await Promise.resolve();
80
+ el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
81
+ await Promise.resolve();
82
+ const tabs = [...el.querySelectorAll<BdsTab>('bds-tab')];
83
+ expect(tabs[1].selected).toBe(true);
84
+ cleanup(el);
85
+ });
86
+
87
+ it('ArrowLeft wraps around to last enabled tab', async () => {
88
+ const el = await fixture(html);
89
+ await Promise.resolve();
90
+ const tabs = [...el.querySelectorAll<BdsTab>('bds-tab')];
91
+ tabs[2].disabled = true; // ensure disabled is set (Lit property may not reflect from HTML attribute in jsdom)
92
+ el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }));
93
+ // disabled tab is skipped; enabled tabs are 0 and 1 → wraps to last enabled (index 1)
94
+ expect(tabs[1].selected).toBe(true);
95
+ cleanup(el);
96
+ });
97
+
98
+ it('Home moves to the first enabled tab', async () => {
99
+ const el = await fixture(html);
100
+ await Promise.resolve();
101
+ const tabs = [...el.querySelectorAll<BdsTab>('bds-tab')];
102
+ await Promise.all(tabs.map(t => t.updateComplete));
103
+ // Navigate to tab 1 first
104
+ tabs[1].dispatchEvent(new CustomEvent('bds-tab-select', { bubbles: true, composed: true }));
105
+ el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Home', bubbles: true }));
106
+ expect(tabs[0].selected).toBe(true);
107
+ cleanup(el);
108
+ });
109
+
110
+ it('End moves to the last enabled tab', async () => {
111
+ const el = await fixture(html);
112
+ await Promise.resolve();
113
+ const tabs = [...el.querySelectorAll<BdsTab>('bds-tab')];
114
+ tabs[2].disabled = true; // ensure disabled is set
115
+ el.dispatchEvent(new KeyboardEvent('keydown', { key: 'End', bubbles: true }));
116
+ // Last enabled tab is index 1 (tab 3 is disabled)
117
+ expect(tabs[1].selected).toBe(true);
118
+ cleanup(el);
119
+ });
120
+ });
@@ -0,0 +1,152 @@
1
+ import { LitElement, css, html } from 'lit';
2
+ import type { BdsTab } from './bds-tab';
3
+ import type { BdsTabPanel } from './bds-tab-panel';
4
+
5
+ /**
6
+ * `<bds-tabs>` — framework-agnostic Tabs custom element.
7
+ *
8
+ * Contains `<bds-tab>` and `<bds-tab-panel>` children. Tabs and panels are
9
+ * matched by DOM order: the first tab corresponds to the first panel, etc.
10
+ *
11
+ * Use `slot="tab"` on `<bds-tab>` elements and `slot="panel"` on
12
+ * `<bds-tab-panel>` elements.
13
+ *
14
+ * Keyboard navigation: ArrowLeft/Right moves between tabs, Home/End jumps to
15
+ * first/last enabled tab.
16
+ *
17
+ * @example
18
+ * <bds-tabs>
19
+ * <bds-tab slot="tab">Overview</bds-tab>
20
+ * <bds-tab slot="tab">Details</bds-tab>
21
+ * <bds-tab slot="tab" disabled>Disabled</bds-tab>
22
+ * <bds-tab-panel slot="panel">Overview content</bds-tab-panel>
23
+ * <bds-tab-panel slot="panel">Details content</bds-tab-panel>
24
+ * <bds-tab-panel slot="panel">Disabled content</bds-tab-panel>
25
+ * </bds-tabs>
26
+ */
27
+ export class BdsTabs extends LitElement {
28
+ static styles = css`
29
+ :host {
30
+ display: flex;
31
+ flex-direction: column;
32
+ }
33
+
34
+ .tablist {
35
+ display: flex;
36
+ border-block-end: 1px solid var(--bds-color_bg--subtle);
37
+ gap: var(--bds-space_xxs);
38
+ }
39
+ `;
40
+
41
+ private _activeIndex = 0;
42
+ private _mo: MutationObserver | null = null;
43
+
44
+ connectedCallback() {
45
+ super.connectedCallback();
46
+ this.addEventListener('bds-tab-select', this._handleTabSelect);
47
+ this.addEventListener('keydown', this._handleKeydown);
48
+ this._mo = new MutationObserver(() => { this._applyState(); });
49
+ this._mo.observe(this, { childList: true, subtree: false });
50
+ // Defer initial state so children have been upgraded
51
+ Promise.resolve().then(() => this._applyState());
52
+ }
53
+
54
+ disconnectedCallback() {
55
+ super.disconnectedCallback();
56
+ this.removeEventListener('bds-tab-select', this._handleTabSelect);
57
+ this.removeEventListener('keydown', this._handleKeydown);
58
+ this._mo?.disconnect();
59
+ this._mo = null;
60
+ }
61
+
62
+ private get _tabs(): BdsTab[] {
63
+ return [...this.querySelectorAll<BdsTab>('bds-tab')];
64
+ }
65
+
66
+ private get _panels(): BdsTabPanel[] {
67
+ return [...this.querySelectorAll<BdsTabPanel>('bds-tab-panel')];
68
+ }
69
+
70
+ private get _enabledIndexes(): number[] {
71
+ return this._tabs
72
+ .map((tab, i) => ({ tab, i }))
73
+ .filter(({ tab }) => !tab.disabled)
74
+ .map(({ i }) => i);
75
+ }
76
+
77
+ private _applyState() {
78
+ const tabs = this._tabs;
79
+ const panels = this._panels;
80
+
81
+ // Clamp active index if tabs were removed
82
+ if (this._activeIndex >= tabs.length) {
83
+ this._activeIndex = Math.max(0, tabs.length - 1);
84
+ }
85
+
86
+ tabs.forEach((tab, i) => {
87
+ tab.selected = (i === this._activeIndex);
88
+ });
89
+
90
+ panels.forEach((panel, i) => {
91
+ if (i === this._activeIndex) {
92
+ panel.removeAttribute('hidden');
93
+ } else {
94
+ panel.setAttribute('hidden', '');
95
+ }
96
+ });
97
+ }
98
+
99
+ private _handleTabSelect = (e: Event) => {
100
+ const tab = e.target as BdsTab;
101
+ const index = this._tabs.indexOf(tab);
102
+ if (index !== -1 && !tab.disabled) {
103
+ this._activeIndex = index;
104
+ this._applyState();
105
+ }
106
+ };
107
+
108
+ private _handleKeydown = (e: KeyboardEvent) => {
109
+ const enabled = this._enabledIndexes;
110
+ if (!enabled.length) return;
111
+
112
+ const pos = enabled.indexOf(this._activeIndex);
113
+
114
+ let next = -1;
115
+ if (e.key === 'ArrowRight') {
116
+ e.preventDefault();
117
+ next = enabled[(pos + 1) % enabled.length];
118
+ } else if (e.key === 'ArrowLeft') {
119
+ e.preventDefault();
120
+ next = enabled[(pos - 1 + enabled.length) % enabled.length];
121
+ } else if (e.key === 'Home') {
122
+ e.preventDefault();
123
+ next = enabled[0];
124
+ } else if (e.key === 'End') {
125
+ e.preventDefault();
126
+ next = enabled[enabled.length - 1];
127
+ }
128
+
129
+ if (next !== -1) {
130
+ this._activeIndex = next;
131
+ this._applyState();
132
+ this._tabs[next]?.focusButton?.();
133
+ }
134
+ };
135
+
136
+ render() {
137
+ return html`
138
+ <div class="tablist" role="tablist">
139
+ <slot name="tab"></slot>
140
+ </div>
141
+ <slot name="panel"></slot>
142
+ `;
143
+ }
144
+ }
145
+
146
+ customElements.define('bds-tabs', BdsTabs);
147
+
148
+ declare global {
149
+ interface HTMLElementTagNameMap {
150
+ 'bds-tabs': BdsTabs;
151
+ }
152
+ }
@@ -0,0 +1,49 @@
1
+ import { fixture, cleanup } from '../test/helpers';
2
+ import { BdsTooltip } from './bds-tooltip';
3
+
4
+ describe('bds-tooltip', () => {
5
+ it('is registered as a custom element', () => {
6
+ expect(customElements.get('bds-tooltip')).toBe(BdsTooltip);
7
+ });
8
+
9
+ it('defaults to placement="top"', async () => {
10
+ const el = await fixture('<bds-tooltip content="Tip"><button>?</button></bds-tooltip>');
11
+ expect(el.getAttribute('placement')).toBe('top');
12
+ cleanup(el);
13
+ });
14
+
15
+ it('renders a tooltip span with role="tooltip"', async () => {
16
+ const el = await fixture('<bds-tooltip content="Helpful text"><button>?</button></bds-tooltip>');
17
+ const tooltip = el.shadowRoot!.querySelector('[role="tooltip"]');
18
+ expect(tooltip).not.toBeNull();
19
+ cleanup(el);
20
+ });
21
+
22
+ it('renders the content text in the tooltip', async () => {
23
+ const el = await fixture('<bds-tooltip content="Helpful text"><button>?</button></bds-tooltip>');
24
+ const tooltip = el.shadowRoot!.querySelector('[role="tooltip"]');
25
+ expect(tooltip!.textContent?.trim()).toBe('Helpful text');
26
+ cleanup(el);
27
+ });
28
+
29
+ it('applies placement class to the tooltip', async () => {
30
+ const el = await fixture('<bds-tooltip content="Tip" placement="bottom"><button>?</button></bds-tooltip>');
31
+ const tooltip = el.shadowRoot!.querySelector('.tooltip');
32
+ expect(tooltip!.classList.contains('--placement_bottom')).toBe(true);
33
+ cleanup(el);
34
+ });
35
+
36
+ it('reflects placement property updates', async () => {
37
+ const el = await fixture('<bds-tooltip content="Tip"><button>?</button></bds-tooltip>') as BdsTooltip;
38
+ el.placement = 'right';
39
+ await el.updateComplete;
40
+ expect(el.getAttribute('placement')).toBe('right');
41
+ cleanup(el);
42
+ });
43
+
44
+ it('renders slotted trigger content', async () => {
45
+ const el = await fixture('<bds-tooltip content="Tip"><button type="button">Help</button></bds-tooltip>');
46
+ expect(el.querySelector('button')!.textContent).toBe('Help');
47
+ cleanup(el);
48
+ });
49
+ });