@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,224 @@
1
+ import { LitElement, css, html } from 'lit';
2
+
3
+ const FOCUSABLE = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
4
+
5
+ /**
6
+ * `<bds-dialog>` — framework-agnostic Dialog custom element.
7
+ *
8
+ * Wraps a native `<dialog>` element with `showModal()` / `close()` lifecycle,
9
+ * backdrop-click to dismiss, Escape key handling, Tab focus trapping, and
10
+ * the Invoker Commands API (`command="show-modal"` / `command="close"`).
11
+ *
12
+ * Attributes:
13
+ * open — boolean; controlled open state (syncs to native showModal/close)
14
+ *
15
+ * Slots:
16
+ * (default) — dialog content
17
+ *
18
+ * Events:
19
+ * bds-open — fired (bubbles, composed) when the dialog opens
20
+ * bds-close — fired (bubbles, composed) when the dialog closes
21
+ *
22
+ * @example
23
+ * <bds-dialog id="my-dialog">
24
+ * <p>Dialog content</p>
25
+ * </bds-dialog>
26
+ * <button commandfor="my-dialog" command="show-modal">Open</button>
27
+ */
28
+ export class BdsDialog extends LitElement {
29
+ static styles = css`
30
+ :host {
31
+ display: contents;
32
+ }
33
+
34
+ .dialog {
35
+ font-size: var(--bds-font_size--body--s);
36
+ border: var(--dialog_border, none);
37
+ --bdc_color: currentcolor;
38
+ --bdc-outline_radius: var(--bds-border_radius--m);
39
+
40
+ outline: var(--bdc-outline_width) solid var(--bdc_color);
41
+ outline-offset: var(--bdc-outline_offset);
42
+ border-radius: var(--dialog_radius, var(--bdc-outline_radius));
43
+ box-shadow: var(--dialog_shadow, var(--bdc-outline_shadow));
44
+ background-color: var(--dialog_color, var(--bds-color_grey--subtle));
45
+ color: var(--dialog_on-color, var(--bds-color_on-grey--subtle));
46
+ padding: var(--bds-space_l);
47
+ max-inline-size: 90svw;
48
+ max-block-size: 90svh;
49
+ overflow: visible;
50
+ position: relative;
51
+ }
52
+
53
+ .dialog-content {
54
+ display: grid;
55
+ gap: var(--bds-grid_gap);
56
+ overflow-block: auto;
57
+ min-block-size: 0;
58
+ flex: 1;
59
+ }
60
+
61
+ .close-button {
62
+ all: unset;
63
+ position: absolute;
64
+ inset-block-start: -1em;
65
+ inset-inline-end: -1em;
66
+ inline-size: 2em;
67
+ block-size: 2em;
68
+ display: flex;
69
+ align-items: center;
70
+ justify-content: center;
71
+ border-radius: 50%;
72
+ background-color: var(--bds-color_bg);
73
+ color: currentcolor;
74
+ border: var(--dialog_close-border, none);
75
+ --bdc_color: currentcolor;
76
+
77
+ outline: var(--bdc-outline_width) solid var(--bdc_color);
78
+ outline-offset: var(--bdc-outline_offset);
79
+ cursor: pointer;
80
+ }
81
+
82
+ .close-button svg {
83
+ inline-size: 1.25em;
84
+ block-size: 1.25em;
85
+ }
86
+
87
+ .dialog::backdrop {
88
+ background-color: var(--color_backdrop, rgb(0 0 0 / 50%));
89
+ backdrop-filter: blur(3px);
90
+ }
91
+ `;
92
+
93
+ static properties = {
94
+ open: { type: Boolean, reflect: true },
95
+ };
96
+
97
+ declare open: boolean;
98
+
99
+ private _triggerEl: Element | null = null;
100
+
101
+ constructor() {
102
+ super();
103
+ this.open = false;
104
+ }
105
+
106
+ connectedCallback() {
107
+ super.connectedCallback();
108
+ this.addEventListener('command', this._onCommand);
109
+ }
110
+
111
+ disconnectedCallback() {
112
+ super.disconnectedCallback();
113
+ this.removeEventListener('command', this._onCommand);
114
+ }
115
+
116
+ updated(changed: Map<string, unknown>) {
117
+ super.updated(changed);
118
+ if (changed.has('open')) {
119
+ const dialog = this._dialog;
120
+ if (!dialog) return;
121
+ if (this.open && !dialog.open) {
122
+ this._triggerEl = document.activeElement;
123
+ dialog.showModal();
124
+ document.body.style.overflow = 'hidden';
125
+ } else if (!this.open && dialog.open) {
126
+ dialog.close();
127
+ document.body.style.overflow = '';
128
+ (this._triggerEl as HTMLElement | null)?.focus();
129
+ this._triggerEl = null;
130
+ }
131
+ }
132
+ }
133
+
134
+ private get _dialog(): HTMLDialogElement | null {
135
+ return this.shadowRoot?.querySelector('dialog') ?? null;
136
+ }
137
+
138
+ private _onCommand = (e: Event) => {
139
+ const command = (e as CommandEvent).command;
140
+ if (command === 'show-modal') this._openDialog();
141
+ else if (command === 'close') this._closeDialog();
142
+ };
143
+
144
+ private _openDialog() {
145
+ if (this.open) return;
146
+ this._triggerEl = document.activeElement;
147
+ this.open = true;
148
+ this.dispatchEvent(new CustomEvent('bds-open', { bubbles: true, composed: true }));
149
+ }
150
+
151
+ private _closeDialog() {
152
+ if (!this.open) return;
153
+ this.open = false;
154
+ this.dispatchEvent(new CustomEvent('bds-close', { bubbles: true, composed: true }));
155
+ }
156
+
157
+ private _handleBackdropClick(e: MouseEvent) {
158
+ if (e.target === this._dialog) this._closeDialog();
159
+ }
160
+
161
+ private _handleCancel(e: Event) {
162
+ e.preventDefault();
163
+ this._closeDialog();
164
+ }
165
+
166
+ private _handleKeydown(e: KeyboardEvent) {
167
+ if (e.key !== 'Tab') return;
168
+ const dialog = this._dialog;
169
+ if (!dialog) return;
170
+ const focusable = Array.from(dialog.querySelectorAll<HTMLElement>(FOCUSABLE));
171
+ // Also include slotted elements from light DOM via the slot
172
+ const slot = dialog.querySelector('slot');
173
+ const slottedFocusable = slot
174
+ ? (slot as HTMLSlotElement).assignedElements({ flatten: true })
175
+ .flatMap(el => [
176
+ ...(el.matches(FOCUSABLE) ? [el as HTMLElement] : []),
177
+ ...Array.from(el.querySelectorAll<HTMLElement>(FOCUSABLE)),
178
+ ])
179
+ : [];
180
+ const all = [...focusable, ...slottedFocusable];
181
+ if (all.length === 0) return;
182
+ const first = all[0];
183
+ const last = all[all.length - 1];
184
+ if (e.shiftKey) {
185
+ if (document.activeElement === first) { e.preventDefault(); last.focus(); }
186
+ } else {
187
+ if (document.activeElement === last) { e.preventDefault(); first.focus(); }
188
+ }
189
+ }
190
+
191
+ render() {
192
+ return html`
193
+ <dialog
194
+ class="dialog"
195
+ aria-modal="true"
196
+ @click=${this._handleBackdropClick}
197
+ @cancel=${this._handleCancel}
198
+ @keydown=${this._handleKeydown}
199
+ >
200
+ <button
201
+ type="button"
202
+ class="close-button"
203
+ aria-label="Close dialog"
204
+ @click=${this._closeDialog}
205
+ >
206
+ <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
207
+ <path stroke-linecap="round" stroke-linejoin="round" d="M18 6L6 18M6 6l12 12" />
208
+ </svg>
209
+ </button>
210
+ <div class="dialog-content">
211
+ <slot></slot>
212
+ </div>
213
+ </dialog>
214
+ `;
215
+ }
216
+ }
217
+
218
+ customElements.define('bds-dialog', BdsDialog);
219
+
220
+ declare global {
221
+ interface HTMLElementTagNameMap {
222
+ 'bds-dialog': BdsDialog;
223
+ }
224
+ }
@@ -0,0 +1,113 @@
1
+ import { fixture, cleanup, makeCommandEvent } from '../test/helpers';
2
+ import { BdsDrawer } from './bds-drawer';
3
+
4
+ beforeEach(() => {
5
+ HTMLDialogElement.prototype.showModal = vi.fn(function (this: HTMLDialogElement) {
6
+ this.setAttribute('open', '');
7
+ });
8
+ HTMLDialogElement.prototype.close = vi.fn(function (this: HTMLDialogElement) {
9
+ this.removeAttribute('open');
10
+ });
11
+ document.body.style.overflow = '';
12
+ });
13
+
14
+ describe('bds-drawer', () => {
15
+ it('is registered as a custom element', () => {
16
+ expect(customElements.get('bds-drawer')).toBe(BdsDrawer);
17
+ });
18
+
19
+ it('renders a <dialog> element in shadow DOM', async () => {
20
+ const el = await fixture('<bds-drawer><p>Content</p></bds-drawer>');
21
+ expect(el.shadowRoot!.querySelector('dialog')).not.toBeNull();
22
+ cleanup(el);
23
+ });
24
+
25
+ it('dialog has aria-modal="true"', async () => {
26
+ const el = await fixture('<bds-drawer><p>Content</p></bds-drawer>');
27
+ const dialog = el.shadowRoot!.querySelector('dialog')!;
28
+ expect(dialog.getAttribute('aria-modal')).toBe('true');
29
+ cleanup(el);
30
+ });
31
+
32
+ it('defaults to side="right"', async () => {
33
+ const el = await fixture('<bds-drawer><p>Content</p></bds-drawer>') as BdsDrawer;
34
+ expect(el.side).toBe('right');
35
+ cleanup(el);
36
+ });
37
+
38
+ it('reflects side attribute', async () => {
39
+ const el = await fixture('<bds-drawer side="left"><p>Content</p></bds-drawer>') as BdsDrawer;
40
+ await el.updateComplete;
41
+ expect(el.getAttribute('side')).toBe('left');
42
+ cleanup(el);
43
+ });
44
+
45
+ it('renders a close button with aria-label="Close drawer"', async () => {
46
+ const el = await fixture('<bds-drawer><p>Content</p></bds-drawer>');
47
+ const btn = el.shadowRoot!.querySelector('button.close-button');
48
+ expect(btn).not.toBeNull();
49
+ expect(btn!.getAttribute('aria-label')).toBe('Close drawer');
50
+ cleanup(el);
51
+ });
52
+
53
+ it('calls showModal when open attribute is set', async () => {
54
+ const el = await fixture('<bds-drawer open><p>Content</p></bds-drawer>');
55
+ expect(HTMLDialogElement.prototype.showModal).toHaveBeenCalled();
56
+ cleanup(el);
57
+ });
58
+
59
+ it('sets body overflow to hidden when open', async () => {
60
+ const el = await fixture('<bds-drawer open><p>Content</p></bds-drawer>');
61
+ expect(document.body.style.overflow).toBe('hidden');
62
+ cleanup(el);
63
+ });
64
+
65
+ it('calls close() and restores body scroll when open is set to false', async () => {
66
+ const el = await fixture('<bds-drawer open><p>Content</p></bds-drawer>') as BdsDrawer;
67
+ el.open = false;
68
+ await el.updateComplete;
69
+ expect(HTMLDialogElement.prototype.close).toHaveBeenCalled();
70
+ expect(document.body.style.overflow).toBe('');
71
+ cleanup(el);
72
+ });
73
+
74
+ it('fires bds-open on show-modal command', async () => {
75
+ const el = await fixture('<bds-drawer><p>Content</p></bds-drawer>');
76
+ const handler = vi.fn();
77
+ el.addEventListener('bds-open', handler);
78
+ el.dispatchEvent(makeCommandEvent('show-modal'));
79
+ expect(handler).toHaveBeenCalledOnce();
80
+ cleanup(el);
81
+ });
82
+
83
+ it('fires bds-close on close command', async () => {
84
+ const el = await fixture('<bds-drawer open><p>Content</p></bds-drawer>');
85
+ const handler = vi.fn();
86
+ el.addEventListener('bds-close', handler);
87
+ el.dispatchEvent(makeCommandEvent('close'));
88
+ expect(handler).toHaveBeenCalledOnce();
89
+ cleanup(el);
90
+ });
91
+
92
+ it('close button click fires bds-close', async () => {
93
+ const el = await fixture('<bds-drawer open><p>Content</p></bds-drawer>');
94
+ const handler = vi.fn();
95
+ el.addEventListener('bds-close', handler);
96
+ const btn = el.shadowRoot!.querySelector<HTMLButtonElement>('button.close-button')!;
97
+ btn.click();
98
+ expect(handler).toHaveBeenCalledOnce();
99
+ cleanup(el);
100
+ });
101
+
102
+ it('renders slotted title content', async () => {
103
+ const el = await fixture('<bds-drawer><span slot="title">Settings</span></bds-drawer>');
104
+ expect(el.querySelector('[slot="title"]')!.textContent).toBe('Settings');
105
+ cleanup(el);
106
+ });
107
+
108
+ it('renders slotted body content', async () => {
109
+ const el = await fixture('<bds-drawer><p>Body content</p></bds-drawer>');
110
+ expect(el.querySelector('p')!.textContent).toBe('Body content');
111
+ cleanup(el);
112
+ });
113
+ });
@@ -0,0 +1,266 @@
1
+ import { LitElement, css, html } from 'lit';
2
+
3
+ const FOCUSABLE = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
4
+
5
+ export type DrawerSide = 'left' | 'right';
6
+
7
+ /**
8
+ * `<bds-drawer>` — framework-agnostic Drawer (slide-in panel) custom element.
9
+ *
10
+ * Same modal infrastructure as `<bds-dialog>` but positioned as a side panel.
11
+ * Uses a native `<dialog>` element with a slide-in inner panel.
12
+ *
13
+ * Attributes:
14
+ * open — boolean; controlled open state
15
+ * side — "left" | "right" (default: "right")
16
+ *
17
+ * Slots:
18
+ * title — drawer title text
19
+ * (default) — drawer body content
20
+ *
21
+ * Events:
22
+ * bds-open — fired (bubbles, composed) when the drawer opens
23
+ * bds-close — fired (bubbles, composed) when the drawer closes
24
+ *
25
+ * @example
26
+ * <bds-drawer id="my-drawer" side="right">
27
+ * <span slot="title">Settings</span>
28
+ * <p>Drawer content</p>
29
+ * </bds-drawer>
30
+ * <button commandfor="my-drawer" command="show-modal">Open</button>
31
+ */
32
+ export class BdsDrawer extends LitElement {
33
+ static styles = css`
34
+ :host {
35
+ display: contents;
36
+ }
37
+
38
+ .drawer {
39
+ position: fixed;
40
+ inset: 0;
41
+ margin: 0;
42
+ padding: 0;
43
+ inline-size: 100%;
44
+ max-inline-size: 100%;
45
+ block-size: 100%;
46
+ max-block-size: 100%;
47
+ border: none;
48
+ background: transparent;
49
+ overflow: hidden;
50
+ }
51
+
52
+ .drawer::backdrop {
53
+ background-color: var(--color_backdrop, rgb(0 0 0 / 50%));
54
+ backdrop-filter: blur(3px);
55
+ }
56
+
57
+ .panel {
58
+ position: absolute;
59
+ inset-block: 0;
60
+ display: flex;
61
+ flex-direction: column;
62
+ inline-size: min(28rem, 90vw);
63
+ block-size: 100%;
64
+ background-color: var(--drawer_color, var(--bds-color_bg));
65
+ color: var(--drawer_on-color, var(--bds-color_on-bg));
66
+ box-shadow: var(--bds-shadow_xl);
67
+ overflow: hidden;
68
+ transition: var(--bds-animation_transition);
69
+ }
70
+
71
+ :host([side='right']) .panel {
72
+ inset-inline-end: 0;
73
+ }
74
+
75
+ :host([side='left']) .panel,
76
+ :host(:not([side])) .panel {
77
+ inset-inline-start: 0;
78
+ }
79
+
80
+ .header {
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: space-between;
84
+ padding: var(--bds-space_m) var(--bds-space_l);
85
+ border-block-end: 1px solid currentcolor;
86
+ flex-shrink: 0;
87
+ }
88
+
89
+ .title {
90
+ font-weight: var(--bds-font_weight--semibold);
91
+ font-size: var(--bds-font_size--body);
92
+ }
93
+
94
+ .close-button {
95
+ all: unset;
96
+ display: flex;
97
+ align-items: center;
98
+ justify-content: center;
99
+ inline-size: 2rem;
100
+ block-size: 2rem;
101
+ border-radius: 50%;
102
+ cursor: pointer;
103
+ transition: var(--bds-animation_transition);
104
+ }
105
+
106
+ .close-button svg {
107
+ inline-size: 1.25rem;
108
+ block-size: 1.25rem;
109
+ }
110
+
111
+ .close-button:focus-visible {
112
+ outline: var(--bds-outline_default);
113
+ outline-offset: var(--bds-outline_offset);
114
+ }
115
+
116
+ @media (hover: hover) and (pointer: fine) {
117
+ .close-button:hover {
118
+ background-color: var(--bds-color_bg--subtle);
119
+ }
120
+ }
121
+
122
+ .body {
123
+ flex: 1;
124
+ overflow-block: auto;
125
+ padding: var(--bds-space_l);
126
+ display: flex;
127
+ flex-direction: column;
128
+ }
129
+
130
+ @media (prefers-reduced-motion: reduce) {
131
+ .panel {
132
+ transition: none;
133
+ }
134
+ }
135
+ `;
136
+
137
+ static properties = {
138
+ open: { type: Boolean, reflect: true },
139
+ side: { type: String, reflect: true },
140
+ };
141
+
142
+ declare open: boolean;
143
+ declare side: DrawerSide;
144
+
145
+ private _triggerEl: Element | null = null;
146
+
147
+ constructor() {
148
+ super();
149
+ this.open = false;
150
+ this.side = 'right';
151
+ }
152
+
153
+ connectedCallback() {
154
+ super.connectedCallback();
155
+ this.addEventListener('command', this._onCommand);
156
+ }
157
+
158
+ disconnectedCallback() {
159
+ super.disconnectedCallback();
160
+ this.removeEventListener('command', this._onCommand);
161
+ }
162
+
163
+ updated(changed: Map<string, unknown>) {
164
+ super.updated(changed);
165
+ if (changed.has('open')) {
166
+ const dialog = this._dialog;
167
+ if (!dialog) return;
168
+ if (this.open && !dialog.open) {
169
+ this._triggerEl = document.activeElement;
170
+ dialog.showModal();
171
+ document.body.style.overflow = 'hidden';
172
+ } else if (!this.open && dialog.open) {
173
+ dialog.close();
174
+ document.body.style.overflow = '';
175
+ (this._triggerEl as HTMLElement | null)?.focus();
176
+ this._triggerEl = null;
177
+ }
178
+ }
179
+ }
180
+
181
+ private get _dialog(): HTMLDialogElement | null {
182
+ return this.shadowRoot?.querySelector('dialog') ?? null;
183
+ }
184
+
185
+ private _onCommand = (e: Event) => {
186
+ const command = (e as CommandEvent).command;
187
+ if (command === 'show-modal') this._openDrawer();
188
+ else if (command === 'close') this._closeDrawer();
189
+ };
190
+
191
+ private _openDrawer() {
192
+ if (this.open) return;
193
+ this._triggerEl = document.activeElement;
194
+ this.open = true;
195
+ this.dispatchEvent(new CustomEvent('bds-open', { bubbles: true, composed: true }));
196
+ }
197
+
198
+ private _closeDrawer() {
199
+ if (!this.open) return;
200
+ this.open = false;
201
+ this.dispatchEvent(new CustomEvent('bds-close', { bubbles: true, composed: true }));
202
+ }
203
+
204
+ private _handleBackdropClick(e: MouseEvent) {
205
+ if (e.target === this._dialog) this._closeDrawer();
206
+ }
207
+
208
+ private _handleCancel(e: Event) {
209
+ e.preventDefault();
210
+ this._closeDrawer();
211
+ }
212
+
213
+ private _handleKeydown(e: KeyboardEvent) {
214
+ if (e.key !== 'Tab') return;
215
+ const dialog = this._dialog;
216
+ if (!dialog) return;
217
+ const focusable = Array.from(dialog.querySelectorAll<HTMLElement>(FOCUSABLE));
218
+ if (focusable.length === 0) return;
219
+ const first = focusable[0];
220
+ const last = focusable[focusable.length - 1];
221
+ if (e.shiftKey) {
222
+ if (document.activeElement === first) { e.preventDefault(); last.focus(); }
223
+ } else {
224
+ if (document.activeElement === last) { e.preventDefault(); first.focus(); }
225
+ }
226
+ }
227
+
228
+ render() {
229
+ return html`
230
+ <dialog
231
+ class="drawer"
232
+ aria-modal="true"
233
+ @click=${this._handleBackdropClick}
234
+ @cancel=${this._handleCancel}
235
+ @keydown=${this._handleKeydown}
236
+ >
237
+ <div class="panel">
238
+ <div class="header">
239
+ <span class="title"><slot name="title"></slot></span>
240
+ <button
241
+ type="button"
242
+ class="close-button"
243
+ aria-label="Close drawer"
244
+ @click=${this._closeDrawer}
245
+ >
246
+ <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
247
+ <path stroke-linecap="round" stroke-linejoin="round" d="M18 6L6 18M6 6l12 12" />
248
+ </svg>
249
+ </button>
250
+ </div>
251
+ <div class="body">
252
+ <slot></slot>
253
+ </div>
254
+ </div>
255
+ </dialog>
256
+ `;
257
+ }
258
+ }
259
+
260
+ customElements.define('bds-drawer', BdsDrawer);
261
+
262
+ declare global {
263
+ interface HTMLElementTagNameMap {
264
+ 'bds-drawer': BdsDrawer;
265
+ }
266
+ }
@@ -0,0 +1,42 @@
1
+ import { fixture, cleanup } from '../test/helpers';
2
+ import { BdsSkipLink } from './bds-skip-link';
3
+
4
+ describe('bds-skip-link', () => {
5
+ it('is registered as a custom element', () => {
6
+ expect(customElements.get('bds-skip-link')).toBe(BdsSkipLink);
7
+ });
8
+
9
+ it('defaults to href="#main"', async () => {
10
+ const el = await fixture('<bds-skip-link></bds-skip-link>') as BdsSkipLink;
11
+ expect(el.href).toBe('#main');
12
+ cleanup(el);
13
+ });
14
+
15
+ it('renders an <a> element in shadow DOM', async () => {
16
+ const el = await fixture('<bds-skip-link></bds-skip-link>');
17
+ const a = el.shadowRoot!.querySelector('a.skip-link');
18
+ expect(a).not.toBeNull();
19
+ cleanup(el);
20
+ });
21
+
22
+ it('uses the href attribute on the anchor', async () => {
23
+ const el = await fixture('<bds-skip-link href="#content"></bds-skip-link>');
24
+ const a = el.shadowRoot!.querySelector('a');
25
+ expect(a!.getAttribute('href')).toBe('#content');
26
+ cleanup(el);
27
+ });
28
+
29
+ it('renders default slot text "Skip to main content"', async () => {
30
+ const el = await fixture('<bds-skip-link></bds-skip-link>');
31
+ // The slot default content is not visible in textContent from light DOM.
32
+ // Verify the anchor exists and the slot is present.
33
+ expect(el.shadowRoot!.querySelector('slot')).not.toBeNull();
34
+ cleanup(el);
35
+ });
36
+
37
+ it('renders slotted link text', async () => {
38
+ const el = await fixture('<bds-skip-link href="#main">Go to main</bds-skip-link>');
39
+ expect(el.textContent?.trim()).toBe('Go to main');
40
+ cleanup(el);
41
+ });
42
+ });