@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,221 @@
1
+ import { LitElement, css, html } from 'lit';
2
+
3
+ /**
4
+ * `<bds-number-input>` — form-associated Number Input with stepper buttons.
5
+ *
6
+ * Attributes:
7
+ * name — form field name
8
+ * value — current numeric value (default: 0)
9
+ * min — minimum allowed value
10
+ * max — maximum allowed value
11
+ * step — increment/decrement step (default: 1)
12
+ * disabled — boolean
13
+ *
14
+ * Events:
15
+ * input — fired on stepper click and native input
16
+ * change — native change event
17
+ *
18
+ * @example
19
+ * <bds-number-input name="qty" value="1" min="0" max="99" step="1"></bds-number-input>
20
+ */
21
+ export class BdsNumberInput extends LitElement {
22
+ static formAssociated = true;
23
+
24
+ static styles = css`
25
+ :host {
26
+ display: block;
27
+ }
28
+
29
+ :host([disabled]) {
30
+ opacity: 0.5;
31
+ pointer-events: none;
32
+ }
33
+
34
+ .row {
35
+ display: flex;
36
+ align-items: center;
37
+ }
38
+
39
+ .input {
40
+ flex: 1;
41
+ font-family: var(--bds-font_family--body);
42
+ font-size: var(--bds-font_size--body);
43
+ padding: var(--bds-space_s);
44
+ border: none;
45
+ --bdc_color: currentcolor;
46
+ --bdc-outline_radius: var(--bds-border_radius--xs);
47
+
48
+ outline: var(--bdc-outline_width) solid var(--bdc_color);
49
+ outline-offset: var(--bdc-outline_offset);
50
+ border-radius: var(--number-input_radius, var(--bdc-outline_radius));
51
+ box-shadow: var(--number-input_shadow, var(--bdc-outline_shadow));
52
+ background-color: var(--number-input_color_bg, var(--bds-color_bg));
53
+ color: var(--number-input_color, var(--bds-color_on-bg));
54
+ text-align: center;
55
+ -moz-appearance: textfield;
56
+ min-inline-size: 0;
57
+ }
58
+
59
+ .input::-webkit-inner-spin-button,
60
+ .input::-webkit-outer-spin-button {
61
+ appearance: none;
62
+ margin: 0;
63
+ }
64
+
65
+ .input:focus {
66
+ --bdc_color: var(--bdc_color--focus);
67
+ }
68
+
69
+ .input:focus-visible {
70
+ outline: var(--bds-outline_default);
71
+ outline-offset: var(--bds-outline_offset);
72
+ }
73
+
74
+ .stepper {
75
+ all: unset;
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ inline-size: 2rem;
80
+ block-size: 2rem;
81
+ border-radius: var(--bds-border_radius--xs);
82
+ cursor: pointer;
83
+ flex-shrink: 0;
84
+ color: var(--bds-color_on-bg);
85
+ transition: var(--bds-animation_transition);
86
+ }
87
+
88
+ .stepper:focus-visible {
89
+ outline: var(--bds-outline_default);
90
+ outline-offset: var(--bds-outline_offset);
91
+ }
92
+
93
+ .stepper:disabled {
94
+ opacity: 0.4;
95
+ cursor: not-allowed;
96
+ }
97
+
98
+ @media (hover: hover) and (pointer: fine) {
99
+ .stepper:not(:disabled):hover {
100
+ background-color: var(--bds-color_bg--subtle);
101
+ }
102
+ }
103
+
104
+ .stepper svg {
105
+ inline-size: 1rem;
106
+ block-size: 1rem;
107
+ }
108
+ `;
109
+
110
+ static properties = {
111
+ name: { type: String, reflect: true },
112
+ value: { type: Number },
113
+ min: { type: Number },
114
+ max: { type: Number },
115
+ step: { type: Number },
116
+ disabled: { type: Boolean, reflect: true },
117
+ };
118
+
119
+ declare name: string;
120
+ declare value: number;
121
+ declare min: number | undefined;
122
+ declare max: number | undefined;
123
+ declare step: number;
124
+ declare disabled: boolean;
125
+
126
+ private _internals!: ElementInternals;
127
+
128
+ constructor() {
129
+ super();
130
+ this._internals = this.attachInternals();
131
+ this.name = '';
132
+ this.value = 0;
133
+ this.min = undefined;
134
+ this.max = undefined;
135
+ this.step = 1;
136
+ this.disabled = false;
137
+ }
138
+
139
+ updated(changed: Map<string, unknown>) {
140
+ super.updated(changed);
141
+ if (changed.has('value')) {
142
+ this._internals.setFormValue?.(String(this.value));
143
+ }
144
+ }
145
+
146
+ formResetCallback() {
147
+ this.value = 0;
148
+ this._internals.setFormValue?.('0');
149
+ }
150
+
151
+ private _clamp(v: number): number {
152
+ let result = v;
153
+ if (this.min !== undefined) result = Math.max(this.min, result);
154
+ if (this.max !== undefined) result = Math.min(this.max, result);
155
+ return result;
156
+ }
157
+
158
+ private _adjust(delta: number) {
159
+ const next = this._clamp(this.value + delta);
160
+ this.value = next;
161
+ this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
162
+ }
163
+
164
+ private _handleChange(e: Event) {
165
+ const v = parseFloat((e.target as HTMLInputElement).value);
166
+ this.value = this._clamp(isNaN(v) ? 0 : v);
167
+ }
168
+
169
+ render() {
170
+ const atMin = this.min !== undefined && this.value <= this.min;
171
+ const atMax = this.max !== undefined && this.value >= this.max;
172
+
173
+ return html`
174
+ <div class="row">
175
+ <button
176
+ type="button"
177
+ class="stepper"
178
+ aria-label="Decrease"
179
+ tabindex="-1"
180
+ ?disabled=${this.disabled || atMin}
181
+ @click=${() => this._adjust(-this.step)}
182
+ >
183
+ <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
184
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14" />
185
+ </svg>
186
+ </button>
187
+ <input
188
+ type="number"
189
+ class="input"
190
+ name=${this.name}
191
+ .value=${String(this.value)}
192
+ min=${this.min ?? ''}
193
+ max=${this.max ?? ''}
194
+ step=${this.step}
195
+ ?disabled=${this.disabled}
196
+ @change=${this._handleChange}
197
+ />
198
+ <button
199
+ type="button"
200
+ class="stepper"
201
+ aria-label="Increase"
202
+ tabindex="-1"
203
+ ?disabled=${this.disabled || atMax}
204
+ @click=${() => this._adjust(this.step)}
205
+ >
206
+ <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
207
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14M5 12h14" />
208
+ </svg>
209
+ </button>
210
+ </div>
211
+ `;
212
+ }
213
+ }
214
+
215
+ customElements.define('bds-number-input', BdsNumberInput);
216
+
217
+ declare global {
218
+ interface HTMLElementTagNameMap {
219
+ 'bds-number-input': BdsNumberInput;
220
+ }
221
+ }
@@ -0,0 +1,70 @@
1
+ import { fixture, cleanup } from '../../test/helpers';
2
+ import { BdsRadio } from './bds-radio';
3
+
4
+ describe('bds-radio', () => {
5
+ it('is registered as a custom element', () => {
6
+ expect(customElements.get('bds-radio')).toBe(BdsRadio);
7
+ });
8
+
9
+ it('renders a radio input in shadow DOM', async () => {
10
+ const el = await fixture('<bds-radio name="size" value="m">Medium</bds-radio>');
11
+ expect(el.shadowRoot!.querySelector('input[type="radio"]')).not.toBeNull();
12
+ cleanup(el);
13
+ });
14
+
15
+ it('is not checked by default', async () => {
16
+ const el = await fixture('<bds-radio name="size" value="m">Medium</bds-radio>');
17
+ const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
18
+ expect(input.checked).toBe(false);
19
+ cleanup(el);
20
+ });
21
+
22
+ it('is checked when checked attribute is set', async () => {
23
+ const el = await fixture('<bds-radio name="size" value="m" checked>Medium</bds-radio>');
24
+ const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
25
+ expect(input.checked).toBe(true);
26
+ cleanup(el);
27
+ });
28
+
29
+ it('input is disabled when disabled attribute is set', async () => {
30
+ const el = await fixture('<bds-radio name="size" value="m" disabled>Medium</bds-radio>');
31
+ const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
32
+ expect(input.disabled).toBe(true);
33
+ cleanup(el);
34
+ });
35
+
36
+ it('input has the correct name and value attributes', async () => {
37
+ const el = await fixture('<bds-radio name="size" value="m">Medium</bds-radio>');
38
+ const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
39
+ expect(input.name).toBe('size');
40
+ expect(input.value).toBe('m');
41
+ cleanup(el);
42
+ });
43
+
44
+ it('renders a label linked to the input', async () => {
45
+ const el = await fixture('<bds-radio name="size" value="m">Medium</bds-radio>');
46
+ const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
47
+ const label = el.shadowRoot!.querySelector('label')!;
48
+ expect(label.getAttribute('for')).toBe(input.id);
49
+ cleanup(el);
50
+ });
51
+
52
+ it('renders slotted label text', async () => {
53
+ const el = await fixture('<bds-radio name="size" value="m">Medium size</bds-radio>');
54
+ expect(el.textContent?.trim()).toBe('Medium size');
55
+ cleanup(el);
56
+ });
57
+
58
+ it('updates checked property on change', async () => {
59
+ const el = await fixture('<bds-radio name="size" value="m">Medium</bds-radio>') as BdsRadio;
60
+ const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
61
+ input.checked = true;
62
+ input.dispatchEvent(new Event('change', { bubbles: true }));
63
+ expect(el.checked).toBe(true);
64
+ cleanup(el);
65
+ });
66
+
67
+ it('is form-associated', () => {
68
+ expect(BdsRadio.formAssociated).toBe(true);
69
+ });
70
+ });
@@ -0,0 +1,157 @@
1
+ import { LitElement, css, html } from 'lit';
2
+
3
+ /**
4
+ * `<bds-radio>` — form-associated Radio custom element.
5
+ *
6
+ * Participates in HTML forms via the Form-Associated Custom Elements API.
7
+ * Use multiple `<bds-radio>` elements with the same `name` to form a group.
8
+ *
9
+ * Attributes:
10
+ * name — form field name (groups radio buttons)
11
+ * value — submitted value when selected
12
+ * checked — boolean; whether this option is selected
13
+ * required — boolean
14
+ * disabled — boolean
15
+ *
16
+ * Slots:
17
+ * (default) — label text
18
+ *
19
+ * @example
20
+ * <bds-radio name="size" value="s">Small</bds-radio>
21
+ * <bds-radio name="size" value="m" checked>Medium</bds-radio>
22
+ * <bds-radio name="size" value="l">Large</bds-radio>
23
+ */
24
+ export class BdsRadio extends LitElement {
25
+ static formAssociated = true;
26
+
27
+ static styles = css`
28
+ :host {
29
+ display: inline-flex;
30
+ align-items: flex-start;
31
+ gap: var(--bds-space_xs);
32
+ cursor: pointer;
33
+ font-size: var(--bds-font_size--body);
34
+ font-family: var(--bds-font_family--body);
35
+ color: var(--bds-color_on-bg);
36
+ }
37
+
38
+ :host([disabled]) {
39
+ opacity: 0.5;
40
+ cursor: not-allowed;
41
+ pointer-events: none;
42
+ }
43
+
44
+ .input {
45
+ --inputSize: calc(var(--bds-space_s) * 2);
46
+ margin-block-start: 0.25em;
47
+ inline-size: var(--inputSize);
48
+ block-size: var(--inputSize);
49
+ flex-shrink: 0;
50
+ --bdc_color: currentcolor;
51
+ --bdc-outline_radius: 50%;
52
+
53
+ outline: var(--bdc-outline_width) solid var(--bdc_color);
54
+ outline-offset: var(--bdc-outline_offset);
55
+ border-radius: 50%;
56
+ box-shadow: var(--radio_shadow, var(--bdc-outline_shadow));
57
+ border: none;
58
+ appearance: none;
59
+ background-color: var(--radio_color_bg, var(--bds-color_bg));
60
+ cursor: pointer;
61
+ position: relative;
62
+ transition: --bdc_color 0.3s ease, background-color 0.3s ease;
63
+ }
64
+
65
+ .input:checked {
66
+ background-color: var(--radio_color-active, var(--bdc-color_active));
67
+ --bdc_color: var(--radio_color-active, var(--bdc-color_active));
68
+ }
69
+
70
+ .input:checked::after {
71
+ content: '';
72
+ position: absolute;
73
+ inset: 25%;
74
+ border-radius: 50%;
75
+ background-color: var(--radio_color-on-active, var(--bdc-color_on-active));
76
+ }
77
+
78
+ .input:focus-visible {
79
+ outline: var(--bds-outline_default);
80
+ outline-offset: var(--bds-outline_offset);
81
+ }
82
+
83
+ label {
84
+ cursor: pointer;
85
+ flex: 1;
86
+ }
87
+ `;
88
+
89
+ static properties = {
90
+ name: { type: String, reflect: true },
91
+ value: { type: String },
92
+ checked: { type: Boolean, reflect: true },
93
+ required: { type: Boolean, reflect: true },
94
+ disabled: { type: Boolean, reflect: true },
95
+ };
96
+
97
+ declare name: string;
98
+ declare value: string;
99
+ declare checked: boolean;
100
+ declare required: boolean;
101
+ declare disabled: boolean;
102
+
103
+ private _internals!: ElementInternals;
104
+ private readonly _uid = Math.random().toString(36).slice(2, 8);
105
+
106
+ constructor() {
107
+ super();
108
+ this._internals = this.attachInternals();
109
+ this.name = '';
110
+ this.value = '';
111
+ this.checked = false;
112
+ this.required = false;
113
+ this.disabled = false;
114
+ }
115
+
116
+ updated(changed: Map<string, unknown>) {
117
+ super.updated(changed);
118
+ if (changed.has('checked') || changed.has('value')) {
119
+ this._internals.setFormValue?.(this.checked ? this.value : null);
120
+ }
121
+ }
122
+
123
+ formResetCallback() {
124
+ this.checked = false;
125
+ this._internals.setFormValue?.(null);
126
+ }
127
+
128
+ private _handleChange(e: Event) {
129
+ this.checked = (e.target as HTMLInputElement).checked;
130
+ }
131
+
132
+ render() {
133
+ const id = `bds-radio-${this._uid}`;
134
+ return html`
135
+ <input
136
+ id=${id}
137
+ type="radio"
138
+ class="input"
139
+ name=${this.name}
140
+ value=${this.value}
141
+ .checked=${this.checked}
142
+ ?required=${this.required}
143
+ ?disabled=${this.disabled}
144
+ @change=${this._handleChange}
145
+ />
146
+ <label for=${id}><slot></slot></label>
147
+ `;
148
+ }
149
+ }
150
+
151
+ customElements.define('bds-radio', BdsRadio);
152
+
153
+ declare global {
154
+ interface HTMLElementTagNameMap {
155
+ 'bds-radio': BdsRadio;
156
+ }
157
+ }
@@ -0,0 +1,74 @@
1
+ import { fixture, cleanup } from '../../test/helpers';
2
+ import { BdsSegmentedControl } from './bds-segmented-control';
3
+
4
+ const OPTIONS = JSON.stringify([
5
+ { value: 'list', label: 'List' },
6
+ { value: 'grid', label: 'Grid' },
7
+ { value: 'table', label: 'Table', disabled: true },
8
+ ]);
9
+
10
+ describe('bds-segmented-control', () => {
11
+ it('is registered as a custom element', () => {
12
+ expect(customElements.get('bds-segmented-control')).toBe(BdsSegmentedControl);
13
+ });
14
+
15
+ it('renders radio inputs for each option', async () => {
16
+ const el = await fixture(`<bds-segmented-control name="view" options='${OPTIONS}'></bds-segmented-control>`);
17
+ const radios = el.shadowRoot!.querySelectorAll('input[type="radio"]');
18
+ expect(radios.length).toBe(3);
19
+ cleanup(el);
20
+ });
21
+
22
+ it('renders a group role wrapper', async () => {
23
+ const el = await fixture(`<bds-segmented-control name="view" options='${OPTIONS}'></bds-segmented-control>`);
24
+ expect(el.shadowRoot!.querySelector('[role="group"]')).not.toBeNull();
25
+ cleanup(el);
26
+ });
27
+
28
+ it('marks the selected option as checked', async () => {
29
+ const el = await fixture(`<bds-segmented-control name="view" value="grid" options='${OPTIONS}'></bds-segmented-control>`);
30
+ await el.updateComplete;
31
+ const radios = [...el.shadowRoot!.querySelectorAll<HTMLInputElement>('input[type="radio"]')];
32
+ const gridRadio = radios.find(r => r.value === 'grid')!;
33
+ expect(gridRadio.checked).toBe(true);
34
+ cleanup(el);
35
+ });
36
+
37
+ it('marks a disabled option as disabled', async () => {
38
+ const el = await fixture(`<bds-segmented-control name="view" options='${OPTIONS}'></bds-segmented-control>`);
39
+ const radios = [...el.shadowRoot!.querySelectorAll<HTMLInputElement>('input[type="radio"]')];
40
+ const tableRadio = radios.find(r => r.value === 'table')!;
41
+ expect(tableRadio.disabled).toBe(true);
42
+ cleanup(el);
43
+ });
44
+
45
+ it('updates value property when a radio is changed', async () => {
46
+ const el = await fixture(`<bds-segmented-control name="view" options='${OPTIONS}'></bds-segmented-control>`) as BdsSegmentedControl;
47
+ const radios = [...el.shadowRoot!.querySelectorAll<HTMLInputElement>('input[type="radio"]')];
48
+ const gridRadio = radios.find(r => r.value === 'grid')!;
49
+ gridRadio.checked = true;
50
+ gridRadio.dispatchEvent(new Event('change', { bubbles: true }));
51
+ expect(el.value).toBe('grid');
52
+ cleanup(el);
53
+ });
54
+
55
+ it('fires a change event when an option is selected', async () => {
56
+ const el = await fixture(`<bds-segmented-control name="view" options='${OPTIONS}'></bds-segmented-control>`);
57
+ const handler = vi.fn();
58
+ el.addEventListener('change', handler);
59
+ const radios = [...el.shadowRoot!.querySelectorAll<HTMLInputElement>('input[type="radio"]')];
60
+ radios[0].dispatchEvent(new Event('change', { bubbles: true }));
61
+ expect(handler).toHaveBeenCalledOnce();
62
+ cleanup(el);
63
+ });
64
+
65
+ it('renders a sliding thumb element', async () => {
66
+ const el = await fixture(`<bds-segmented-control name="view" options='${OPTIONS}'></bds-segmented-control>`);
67
+ expect(el.shadowRoot!.querySelector('.thumb')).not.toBeNull();
68
+ cleanup(el);
69
+ });
70
+
71
+ it('is form-associated', () => {
72
+ expect(BdsSegmentedControl.formAssociated).toBe(true);
73
+ });
74
+ });