@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,336 @@
1
+ import { LitElement, css, html } from 'lit';
2
+
3
+ export interface ComboboxOption {
4
+ value: string;
5
+ label: string;
6
+ disabled?: boolean;
7
+ }
8
+
9
+ /**
10
+ * `<bds-combobox>` — form-associated Combobox (autocomplete) custom element.
11
+ *
12
+ * Renders a text input with a filterable dropdown. Keyboard navigation:
13
+ * ArrowDown/ArrowUp to move focus, Enter to select, Escape to close.
14
+ *
15
+ * Attributes:
16
+ * name — form field name
17
+ * value — currently selected value
18
+ * options — JSON array of `{ value, label, disabled? }` option objects
19
+ * placeholder — input placeholder text
20
+ * disabled — boolean
21
+ *
22
+ * Events:
23
+ * bds-change — fired with `{ detail: { value, label } }` when an option is selected
24
+ *
25
+ * @example
26
+ * <bds-combobox
27
+ * name="country"
28
+ * placeholder="Search…"
29
+ * options='[{"value":"nl","label":"Netherlands"},{"value":"be","label":"Belgium"}]'
30
+ * ></bds-combobox>
31
+ */
32
+ export class BdsCombobox extends LitElement {
33
+ static formAssociated = true;
34
+
35
+ static styles = css`
36
+ :host {
37
+ display: block;
38
+ position: relative;
39
+ }
40
+
41
+ :host([disabled]) {
42
+ opacity: 0.5;
43
+ pointer-events: none;
44
+ }
45
+
46
+ .wrapper {
47
+ position: relative;
48
+ }
49
+
50
+ .input {
51
+ inline-size: 100%;
52
+ font-family: var(--bds-font_family--body);
53
+ font-size: var(--bds-font_size--body);
54
+ padding: var(--bds-space_s);
55
+ padding-inline-end: var(--bds-space_xl);
56
+ border: none;
57
+ --bdc_color: currentcolor;
58
+ --bdc-outline_radius: var(--bds-border_radius--xs);
59
+
60
+ outline: var(--bdc-outline_width) solid var(--bdc_color);
61
+ outline-offset: var(--bdc-outline_offset);
62
+ border-radius: var(--combobox_radius, var(--bdc-outline_radius));
63
+ box-shadow: var(--combobox_shadow, var(--bdc-outline_shadow));
64
+ background-color: var(--combobox_color_bg, var(--bds-color_bg));
65
+ color: var(--combobox_color, var(--bds-color_on-bg));
66
+ box-sizing: border-box;
67
+ transition: --bdc_color var(--bds-animation_transition-duration) var(--bds-animation_easing);
68
+ }
69
+
70
+ .input:focus {
71
+ --bdc_color: var(--bdc_color--focus);
72
+ }
73
+
74
+ .input:focus-visible {
75
+ outline: var(--bds-outline_default);
76
+ outline-offset: var(--bds-outline_offset);
77
+ }
78
+
79
+ .chevron {
80
+ position: absolute;
81
+ inset-inline-end: var(--bds-space_s);
82
+ inset-block-start: 50%;
83
+ transform: translateY(-50%);
84
+ pointer-events: none;
85
+ inline-size: 1rem;
86
+ block-size: 1rem;
87
+ color: currentcolor;
88
+ }
89
+
90
+ .chevron svg {
91
+ inline-size: 100%;
92
+ block-size: 100%;
93
+ }
94
+
95
+ .listbox {
96
+ position: absolute;
97
+ inset-block-start: calc(100% + var(--bds-space_xs));
98
+ inset-inline-start: 0;
99
+ inline-size: 100%;
100
+ max-block-size: 15rem;
101
+ overflow-y: auto;
102
+ background-color: var(--combobox_list-bg, var(--bds-color_bg));
103
+ border-radius: var(--bds-border_radius--s);
104
+ box-shadow: var(--bds-shadow_m);
105
+ z-index: var(--combobox_z-index, 100);
106
+ padding-block: var(--bds-space_xs);
107
+ }
108
+
109
+ .option {
110
+ display: block;
111
+ padding: var(--bds-space_s) var(--bds-space_m);
112
+ cursor: pointer;
113
+ font-family: var(--bds-font_family--body);
114
+ font-size: var(--bds-font_size--body);
115
+ color: var(--bds-color_on-bg);
116
+ transition: background-color var(--bds-animation_transition-duration) var(--bds-animation_easing);
117
+ }
118
+
119
+ .option:hover,
120
+ .option[aria-selected='true'] {
121
+ background-color: var(--bds-color_bg--subtle);
122
+ }
123
+
124
+ .option[aria-disabled='true'] {
125
+ opacity: 0.4;
126
+ cursor: not-allowed;
127
+ }
128
+
129
+ .option.--focused {
130
+ background-color: var(--bds-color_bg--subtle);
131
+ outline: none;
132
+ }
133
+
134
+ .no-results {
135
+ padding: var(--bds-space_s) var(--bds-space_m);
136
+ font-family: var(--bds-font_family--body);
137
+ font-size: var(--bds-font_size--body--s);
138
+ color: var(--bds-color_on-bg--subtle);
139
+ }
140
+ `;
141
+
142
+ static properties = {
143
+ name: { type: String, reflect: true },
144
+ value: { type: String },
145
+ options: { type: String },
146
+ placeholder: { type: String },
147
+ disabled: { type: Boolean, reflect: true },
148
+ };
149
+
150
+ declare name: string;
151
+ declare value: string;
152
+ declare options: string;
153
+ declare placeholder: string;
154
+ declare disabled: boolean;
155
+
156
+ private _internals!: ElementInternals;
157
+ private _open = false;
158
+ private _query = '';
159
+ private _focusedIndex = -1;
160
+
161
+ constructor() {
162
+ super();
163
+ this._internals = this.attachInternals();
164
+ this.name = '';
165
+ this.value = '';
166
+ this.options = '[]';
167
+ this.placeholder = '';
168
+ this.disabled = false;
169
+ }
170
+
171
+ private get _options(): ComboboxOption[] {
172
+ try {
173
+ return JSON.parse(this.options) as ComboboxOption[];
174
+ } catch {
175
+ return [];
176
+ }
177
+ }
178
+
179
+ private get _filtered(): ComboboxOption[] {
180
+ const q = this._query.toLowerCase();
181
+ return q
182
+ ? this._options.filter(o => o.label.toLowerCase().includes(q))
183
+ : this._options;
184
+ }
185
+
186
+ private get _displayLabel(): string {
187
+ if (this.value) {
188
+ return this._options.find(o => o.value === this.value)?.label ?? '';
189
+ }
190
+ return '';
191
+ }
192
+
193
+ updated(changed: Map<string, unknown>) {
194
+ super.updated(changed);
195
+ if (changed.has('value')) {
196
+ this._internals.setFormValue?.(this.value || null);
197
+ }
198
+ }
199
+
200
+ formResetCallback() {
201
+ this.value = '';
202
+ this._query = '';
203
+ this._open = false;
204
+ this._internals.setFormValue?.(null);
205
+ this.requestUpdate();
206
+ }
207
+
208
+ private _openList() {
209
+ this._open = true;
210
+ this._focusedIndex = this._filtered.findIndex(o => o.value === this.value);
211
+ this.requestUpdate();
212
+ }
213
+
214
+ private _closeList() {
215
+ this._open = false;
216
+ this._query = '';
217
+ this._focusedIndex = -1;
218
+ this.requestUpdate();
219
+ }
220
+
221
+ private _selectOption(opt: ComboboxOption) {
222
+ if (opt.disabled) return;
223
+ this.value = opt.value;
224
+ this._closeList();
225
+ this.dispatchEvent(new CustomEvent('bds-change', {
226
+ bubbles: true,
227
+ composed: true,
228
+ detail: { value: opt.value, label: opt.label },
229
+ }));
230
+ }
231
+
232
+ private _handleInput(e: Event) {
233
+ this._query = (e.target as HTMLInputElement).value;
234
+ this._open = true;
235
+ this._focusedIndex = -1;
236
+ this.requestUpdate();
237
+ }
238
+
239
+ private _handleFocus() {
240
+ this._openList();
241
+ }
242
+
243
+ private _handleBlur() {
244
+ // Delay close so option clicks register first
245
+ setTimeout(() => {
246
+ this._closeList();
247
+ }, 150);
248
+ }
249
+
250
+ private _handleKeydown(e: KeyboardEvent) {
251
+ const filtered = this._filtered;
252
+ if (!this._open) {
253
+ if (e.key === 'ArrowDown' || e.key === 'Enter') {
254
+ e.preventDefault();
255
+ this._openList();
256
+ }
257
+ return;
258
+ }
259
+
260
+ if (e.key === 'ArrowDown') {
261
+ e.preventDefault();
262
+ let next = this._focusedIndex + 1;
263
+ while (next < filtered.length && filtered[next].disabled) next++;
264
+ if (next < filtered.length) this._focusedIndex = next;
265
+ } else if (e.key === 'ArrowUp') {
266
+ e.preventDefault();
267
+ let prev = this._focusedIndex - 1;
268
+ while (prev >= 0 && filtered[prev].disabled) prev--;
269
+ if (prev >= 0) this._focusedIndex = prev;
270
+ } else if (e.key === 'Enter') {
271
+ e.preventDefault();
272
+ if (this._focusedIndex >= 0 && this._focusedIndex < filtered.length) {
273
+ this._selectOption(filtered[this._focusedIndex]);
274
+ }
275
+ } else if (e.key === 'Escape') {
276
+ e.preventDefault();
277
+ this._closeList();
278
+ }
279
+ this.requestUpdate();
280
+ }
281
+
282
+ render() {
283
+ const filtered = this._filtered;
284
+ const inputValue = this._open ? this._query : this._displayLabel;
285
+
286
+ return html`
287
+ <div class="wrapper">
288
+ <input
289
+ type="text"
290
+ class="input"
291
+ role="combobox"
292
+ aria-expanded=${this._open}
293
+ aria-autocomplete="list"
294
+ aria-haspopup="listbox"
295
+ name=${this.name}
296
+ placeholder=${this.placeholder}
297
+ .value=${inputValue}
298
+ ?disabled=${this.disabled}
299
+ @input=${this._handleInput}
300
+ @focus=${this._handleFocus}
301
+ @blur=${this._handleBlur}
302
+ @keydown=${this._handleKeydown}
303
+ />
304
+ <span class="chevron" aria-hidden="true">
305
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
306
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 9l6 6 6-6" />
307
+ </svg>
308
+ </span>
309
+ ${this._open ? html`
310
+ <ul class="listbox" role="listbox" aria-label="Options">
311
+ ${filtered.length === 0
312
+ ? html`<li class="no-results">No results</li>`
313
+ : filtered.map((opt, i) => html`
314
+ <li
315
+ class="option ${i === this._focusedIndex ? '--focused' : ''}"
316
+ role="option"
317
+ aria-selected=${opt.value === this.value}
318
+ aria-disabled=${opt.disabled ?? false}
319
+ @mousedown=${(e: Event) => { e.preventDefault(); this._selectOption(opt); }}
320
+ >${opt.label}</li>
321
+ `)
322
+ }
323
+ </ul>
324
+ ` : ''}
325
+ </div>
326
+ `;
327
+ }
328
+ }
329
+
330
+ customElements.define('bds-combobox', BdsCombobox);
331
+
332
+ declare global {
333
+ interface HTMLElementTagNameMap {
334
+ 'bds-combobox': BdsCombobox;
335
+ }
336
+ }
@@ -0,0 +1,58 @@
1
+ import { fixture, cleanup } from '../../test/helpers';
2
+ import { BdsFileInput } from './bds-file-input';
3
+
4
+ describe('bds-file-input', () => {
5
+ it('is registered as a custom element', () => {
6
+ expect(customElements.get('bds-file-input')).toBe(BdsFileInput);
7
+ });
8
+
9
+ it('renders a file input in shadow DOM', async () => {
10
+ const el = await fixture('<bds-file-input name="avatar"></bds-file-input>');
11
+ expect(el.shadowRoot!.querySelector('input[type="file"]')).not.toBeNull();
12
+ cleanup(el);
13
+ });
14
+
15
+ it('renders a drop zone with role="button"', async () => {
16
+ const el = await fixture('<bds-file-input name="avatar"></bds-file-input>');
17
+ expect(el.shadowRoot!.querySelector('[role="button"]')).not.toBeNull();
18
+ cleanup(el);
19
+ });
20
+
21
+ it('drop zone has aria-label="Upload file"', async () => {
22
+ const el = await fixture('<bds-file-input name="avatar"></bds-file-input>');
23
+ const zone = el.shadowRoot!.querySelector('[role="button"]')!;
24
+ expect(zone.getAttribute('aria-label')).toBe('Upload file');
25
+ cleanup(el);
26
+ });
27
+
28
+ it('input is disabled when disabled attribute is set', async () => {
29
+ const el = await fixture('<bds-file-input name="avatar" disabled></bds-file-input>');
30
+ const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
31
+ expect(input.disabled).toBe(true);
32
+ cleanup(el);
33
+ });
34
+
35
+ it('forwards accept attribute to input', async () => {
36
+ const el = await fixture('<bds-file-input name="avatar" accept="image/*"></bds-file-input>');
37
+ const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
38
+ expect(input.accept).toBe('image/*');
39
+ cleanup(el);
40
+ });
41
+
42
+ it('forwards multiple attribute to input', async () => {
43
+ const el = await fixture('<bds-file-input name="files" multiple></bds-file-input>');
44
+ const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
45
+ expect(input.multiple).toBe(true);
46
+ cleanup(el);
47
+ });
48
+
49
+ it('renders slotted label text', async () => {
50
+ const el = await fixture('<bds-file-input name="avatar">Upload photo</bds-file-input>');
51
+ expect(el.textContent?.trim()).toBe('Upload photo');
52
+ cleanup(el);
53
+ });
54
+
55
+ it('is form-associated', () => {
56
+ expect(BdsFileInput.formAssociated).toBe(true);
57
+ });
58
+ });
@@ -0,0 +1,209 @@
1
+ import { LitElement, css, html } from 'lit';
2
+
3
+ /**
4
+ * `<bds-file-input>` — form-associated File Input with drag-and-drop zone.
5
+ *
6
+ * Attributes:
7
+ * name — form field name
8
+ * accept — file types to accept (e.g. "image/*,.pdf")
9
+ * multiple — boolean; allow selecting multiple files
10
+ * disabled — boolean
11
+ *
12
+ * Events:
13
+ * bds-change — fired with `{ detail: FileList }` when files are selected
14
+ *
15
+ * @example
16
+ * <bds-file-input name="avatar" accept="image/*">Upload photo</bds-file-input>
17
+ */
18
+ export class BdsFileInput extends LitElement {
19
+ static formAssociated = true;
20
+
21
+ static styles = css`
22
+ :host {
23
+ display: block;
24
+ }
25
+
26
+ :host([disabled]) {
27
+ opacity: 0.5;
28
+ pointer-events: none;
29
+ }
30
+
31
+ .zone {
32
+ display: flex;
33
+ flex-direction: column;
34
+ align-items: center;
35
+ justify-content: center;
36
+ gap: var(--bds-space_s);
37
+ padding: var(--bds-space_xl);
38
+ border-radius: var(--bds-border_radius--m);
39
+ border: 2px dashed var(--file-input_border-color, var(--bds-color_bg--subtle));
40
+ background-color: var(--file-input_color_bg, var(--bds-color_bg));
41
+ color: var(--bds-color_on-bg);
42
+ font-family: var(--bds-font_family--body);
43
+ font-size: var(--bds-font_size--body);
44
+ cursor: pointer;
45
+ text-align: center;
46
+ transition: var(--bds-animation_transition);
47
+ }
48
+
49
+ .zone.--dragging {
50
+ border-color: var(--bds-color_interactive);
51
+ background-color: var(--bds-color_bg--subtle);
52
+ }
53
+
54
+ .zone-input {
55
+ position: absolute;
56
+ opacity: 0;
57
+ inline-size: 0;
58
+ block-size: 0;
59
+ overflow: hidden;
60
+ }
61
+
62
+ .zone-label {
63
+ display: flex;
64
+ flex-direction: column;
65
+ align-items: center;
66
+ gap: var(--bds-space_xs);
67
+ cursor: pointer;
68
+ }
69
+
70
+ .icon {
71
+ inline-size: 2.5rem;
72
+ block-size: 2.5rem;
73
+ color: var(--bds-color_on-bg--subtle);
74
+ }
75
+
76
+ .icon svg {
77
+ inline-size: 100%;
78
+ block-size: 100%;
79
+ }
80
+
81
+ .hint {
82
+ font-size: var(--bds-font_size--body--s);
83
+ color: var(--bds-color_on-bg--subtle);
84
+ }
85
+
86
+ @media (hover: hover) and (pointer: fine) {
87
+ .zone:hover {
88
+ border-color: var(--bds-color_interactive);
89
+ background-color: var(--bds-color_bg--subtle);
90
+ }
91
+ }
92
+ `;
93
+
94
+ static properties = {
95
+ name: { type: String, reflect: true },
96
+ accept: { type: String },
97
+ multiple: { type: Boolean, reflect: true },
98
+ disabled: { type: Boolean, reflect: true },
99
+ };
100
+
101
+ declare name: string;
102
+ declare accept: string;
103
+ declare multiple: boolean;
104
+ declare disabled: boolean;
105
+
106
+ private _internals!: ElementInternals;
107
+ private _isDragging = false;
108
+ private readonly _uid = Math.random().toString(36).slice(2, 8);
109
+
110
+ constructor() {
111
+ super();
112
+ this._internals = this.attachInternals();
113
+ this.name = '';
114
+ this.accept = '';
115
+ this.multiple = false;
116
+ this.disabled = false;
117
+ }
118
+
119
+ formResetCallback() {
120
+ this._internals.setFormValue?.(null);
121
+ const input = this.shadowRoot?.querySelector<HTMLInputElement>('input');
122
+ if (input) input.value = '';
123
+ }
124
+
125
+ private _handleFiles(files: FileList | null) {
126
+ if (!files || files.length === 0) return;
127
+ const formData = new FormData();
128
+ Array.from(files).forEach(file => formData.append(this.name, file));
129
+ this._internals.setFormValue?.(formData);
130
+ this.dispatchEvent(new CustomEvent('bds-change', {
131
+ bubbles: true,
132
+ composed: true,
133
+ detail: files,
134
+ }));
135
+ }
136
+
137
+ private _handleInputChange(e: Event) {
138
+ this._handleFiles((e.target as HTMLInputElement).files);
139
+ }
140
+
141
+ private _handleClick() {
142
+ this.shadowRoot?.querySelector<HTMLInputElement>('input')?.click();
143
+ }
144
+
145
+ private _handleDragover(e: DragEvent) {
146
+ e.preventDefault();
147
+ if (!this._isDragging) {
148
+ this._isDragging = true;
149
+ this.requestUpdate();
150
+ }
151
+ }
152
+
153
+ private _handleDragleave() {
154
+ this._isDragging = false;
155
+ this.requestUpdate();
156
+ }
157
+
158
+ private _handleDrop(e: DragEvent) {
159
+ e.preventDefault();
160
+ this._isDragging = false;
161
+ this.requestUpdate();
162
+ this._handleFiles(e.dataTransfer?.files ?? null);
163
+ }
164
+
165
+ render() {
166
+ const id = `bds-file-${this._uid}`;
167
+ return html`
168
+ <input
169
+ id=${id}
170
+ type="file"
171
+ class="zone-input"
172
+ name=${this.name}
173
+ accept=${this.accept}
174
+ ?multiple=${this.multiple}
175
+ ?disabled=${this.disabled}
176
+ @change=${this._handleInputChange}
177
+ />
178
+ <div
179
+ class="zone ${this._isDragging ? '--dragging' : ''}"
180
+ role="button"
181
+ tabindex=${this.disabled ? '-1' : '0'}
182
+ aria-label="Upload file"
183
+ @click=${this._handleClick}
184
+ @keydown=${(e: KeyboardEvent) => (e.key === 'Enter' || e.key === ' ') && this._handleClick()}
185
+ @dragover=${this._handleDragover}
186
+ @dragleave=${this._handleDragleave}
187
+ @drop=${this._handleDrop}
188
+ >
189
+ <span class="icon" aria-hidden="true">
190
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
191
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 16.5V9.75m0 0-3 3m3-3 3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.338-2.329 3 3 0 013.83 3.849A3.75 3.75 0 0118 19.5H6.75z" />
192
+ </svg>
193
+ </span>
194
+ <span class="zone-label">
195
+ <slot>Drag &amp; drop or click to browse</slot>
196
+ </span>
197
+ <span class="hint">${this.accept ? `Accepted: ${this.accept}` : 'Any file type'}</span>
198
+ </div>
199
+ `;
200
+ }
201
+ }
202
+
203
+ customElements.define('bds-file-input', BdsFileInput);
204
+
205
+ declare global {
206
+ interface HTMLElementTagNameMap {
207
+ 'bds-file-input': BdsFileInput;
208
+ }
209
+ }
@@ -0,0 +1,87 @@
1
+ import { fixture, cleanup } from '../../test/helpers';
2
+ import { BdsNumberInput } from './bds-number-input';
3
+
4
+ describe('bds-number-input', () => {
5
+ it('is registered as a custom element', () => {
6
+ expect(customElements.get('bds-number-input')).toBe(BdsNumberInput);
7
+ });
8
+
9
+ it('renders a number input in shadow DOM', async () => {
10
+ const el = await fixture('<bds-number-input name="qty"></bds-number-input>');
11
+ expect(el.shadowRoot!.querySelector('input[type="number"]')).not.toBeNull();
12
+ cleanup(el);
13
+ });
14
+
15
+ it('renders a decrease button with aria-label="Decrease"', async () => {
16
+ const el = await fixture('<bds-number-input name="qty"></bds-number-input>');
17
+ const btn = el.shadowRoot!.querySelector<HTMLButtonElement>('button[aria-label="Decrease"]');
18
+ expect(btn).not.toBeNull();
19
+ cleanup(el);
20
+ });
21
+
22
+ it('renders an increase button with aria-label="Increase"', async () => {
23
+ const el = await fixture('<bds-number-input name="qty"></bds-number-input>');
24
+ const btn = el.shadowRoot!.querySelector<HTMLButtonElement>('button[aria-label="Increase"]');
25
+ expect(btn).not.toBeNull();
26
+ cleanup(el);
27
+ });
28
+
29
+ it('increase button increments value by step', async () => {
30
+ const el = await fixture('<bds-number-input name="qty" step="2"></bds-number-input>') as BdsNumberInput;
31
+ const incBtn = el.shadowRoot!.querySelector<HTMLButtonElement>('button[aria-label="Increase"]')!;
32
+ incBtn.click();
33
+ expect(el.value).toBe(2);
34
+ cleanup(el);
35
+ });
36
+
37
+ it('decrease button decrements value by step', async () => {
38
+ const el = await fixture('<bds-number-input name="qty" step="1"></bds-number-input>') as BdsNumberInput;
39
+ el.value = 5;
40
+ await el.updateComplete;
41
+ const decBtn = el.shadowRoot!.querySelector<HTMLButtonElement>('button[aria-label="Decrease"]')!;
42
+ decBtn.click();
43
+ expect(el.value).toBe(4);
44
+ cleanup(el);
45
+ });
46
+
47
+ it('clamps value to max when increasing beyond max', async () => {
48
+ const el = await fixture('<bds-number-input name="qty" max="10"></bds-number-input>') as BdsNumberInput;
49
+ el.value = 10;
50
+ await el.updateComplete;
51
+ const incBtn = el.shadowRoot!.querySelector<HTMLButtonElement>('button[aria-label="Increase"]')!;
52
+ incBtn.click();
53
+ expect(el.value).toBe(10);
54
+ cleanup(el);
55
+ });
56
+
57
+ it('clamps value to min when decreasing below min', async () => {
58
+ const el = await fixture('<bds-number-input name="qty" min="0"></bds-number-input>') as BdsNumberInput;
59
+ el.value = 0;
60
+ await el.updateComplete;
61
+ const decBtn = el.shadowRoot!.querySelector<HTMLButtonElement>('button[aria-label="Decrease"]')!;
62
+ decBtn.click();
63
+ expect(el.value).toBe(0);
64
+ cleanup(el);
65
+ });
66
+
67
+ it('all inputs are disabled when disabled attribute is set', async () => {
68
+ const el = await fixture('<bds-number-input name="qty" disabled></bds-number-input>');
69
+ const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
70
+ expect(input.disabled).toBe(true);
71
+ cleanup(el);
72
+ });
73
+
74
+ it('fires an input event on stepper click', async () => {
75
+ const el = await fixture('<bds-number-input name="qty"></bds-number-input>');
76
+ const handler = vi.fn();
77
+ el.addEventListener('input', handler);
78
+ const incBtn = el.shadowRoot!.querySelector<HTMLButtonElement>('button[aria-label="Increase"]')!;
79
+ incBtn.click();
80
+ expect(handler).toHaveBeenCalledOnce();
81
+ cleanup(el);
82
+ });
83
+
84
+ it('is form-associated', () => {
85
+ expect(BdsNumberInput.formAssociated).toBe(true);
86
+ });
87
+ });