@boostdev/design-system-components 1.1.2 → 1.2.1

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 (213) hide show
  1. package/AGENTS.md +38 -14
  2. package/dist/client.cjs +85 -103
  3. package/dist/client.css +610 -601
  4. package/dist/client.d.cts +13 -16
  5. package/dist/client.d.ts +13 -16
  6. package/dist/client.js +95 -113
  7. package/dist/index.cjs +85 -103
  8. package/dist/index.css +610 -601
  9. package/dist/index.d.cts +13 -16
  10. package/dist/index.d.ts +13 -16
  11. package/dist/index.js +95 -113
  12. package/dist/native/index.cjs +9 -8
  13. package/dist/native/index.d.cts +1 -1
  14. package/dist/native/index.d.ts +1 -1
  15. package/dist/native/index.js +9 -8
  16. package/dist/web-components/{chunk-K5CE6HSB.js → chunk-2FGATTGT.js} +79 -17
  17. package/dist/web-components/{chunk-O4UFI2RX.js → chunk-3REOIRDW.js} +13 -11
  18. package/dist/web-components/{chunk-5IPHEONG.js → chunk-6I2DBFQ7.js} +1 -0
  19. package/dist/web-components/{chunk-VM3YHGQ2.js → chunk-AJSXNDAP.js} +1 -2
  20. package/dist/web-components/{chunk-LSC4N56Q.js → chunk-BX3IPVOB.js} +1 -2
  21. package/dist/web-components/{chunk-CAPI25CU.js → chunk-DZRSJGPB.js} +3 -3
  22. package/dist/web-components/{chunk-VSS5ITNE.js → chunk-EBJM3VD4.js} +1 -3
  23. package/dist/web-components/{chunk-O7I63SQX.js → chunk-HHHRF2PS.js} +6 -12
  24. package/dist/web-components/{chunk-OTX4COSV.js → chunk-JUKB3BUP.js} +3 -4
  25. package/dist/web-components/{chunk-IQGLIQSO.js → chunk-N6IMUOY4.js} +2 -5
  26. package/dist/web-components/{chunk-CJ5ZOO42.js → chunk-OCODKRVZ.js} +68 -44
  27. package/dist/web-components/{chunk-QDRH46TF.js → chunk-QLZ2362S.js} +1 -3
  28. package/dist/web-components/{chunk-IK3PTFZ4.js → chunk-T6AETZRP.js} +1 -3
  29. package/dist/web-components/{chunk-4BX6AWXX.js → chunk-X3FKVHLK.js} +2 -4
  30. package/dist/web-components/{chunk-A67LCT7C.js → chunk-XNA6WTXG.js} +3 -4
  31. package/dist/web-components/{chunk-XIENXR7H.js → chunk-YRXCVKHV.js} +1 -3
  32. package/dist/web-components/{chunk-6T3RKUKG.js → chunk-ZTC6GRP7.js} +1 -2
  33. package/dist/web-components/globals.js +1 -1
  34. package/dist/web-components/index.d.ts +571 -1
  35. package/dist/web-components/index.js +1515 -18
  36. package/dist/web-components/interaction/bds-accordion.js +1 -1
  37. package/dist/web-components/interaction/bds-button.d.ts +2 -2
  38. package/dist/web-components/interaction/bds-button.js +1 -1
  39. package/dist/web-components/interaction/bds-collapsible.js +1 -1
  40. package/dist/web-components/interaction/bds-dialog.js +1 -1
  41. package/dist/web-components/interaction/bds-drawer.js +1 -1
  42. package/dist/web-components/interaction/form/bds-checkbox.js +1 -1
  43. package/dist/web-components/interaction/form/bds-combobox.js +1 -1
  44. package/dist/web-components/interaction/form/bds-number-input.js +1 -1
  45. package/dist/web-components/interaction/form/bds-radio.js +1 -1
  46. package/dist/web-components/interaction/form/bds-segmented-control.d.ts +20 -3
  47. package/dist/web-components/interaction/form/bds-segmented-control.js +1 -1
  48. package/dist/web-components/interaction/form/bds-select.js +1 -1
  49. package/dist/web-components/interaction/form/bds-switch.js +1 -1
  50. package/dist/web-components/interaction/form/bds-textarea.js +1 -1
  51. package/dist/web-components/ui/bds-alert.js +1 -1
  52. package/dist/web-components/ui/bds-card.js +1 -1
  53. package/dist/web-components/ui/bds-notification-banner.js +1 -1
  54. package/package.json +2 -2
  55. package/src/components/interaction/Button/Button.module.css +12 -11
  56. package/src/components/interaction/Button/Button.native.tsx +10 -9
  57. package/src/components/interaction/Button/Button.tsx +1 -1
  58. package/src/components/interaction/Command/Command.module.css +3 -7
  59. package/src/components/interaction/Dialog/Dialog.module.css +7 -5
  60. package/src/components/interaction/Drawer/Drawer.module.css +48 -25
  61. package/src/components/interaction/Drawer/Drawer.tsx +21 -17
  62. package/src/components/interaction/DropdownMenu/DropdownMenu.module.css +1 -3
  63. package/src/components/interaction/Popover/Popover.module.css +1 -3
  64. package/src/components/interaction/form/Checkbox/Checkbox.module.css +1 -3
  65. package/src/components/interaction/form/Combobox/Combobox.module.css +2 -6
  66. package/src/components/interaction/form/FormInput/FormInput.module.css +1 -3
  67. package/src/components/interaction/form/NumberInput/NumberInput.module.css +16 -4
  68. package/src/components/interaction/form/Radio/Radio.module.css +6 -16
  69. package/src/components/interaction/form/SegmentedControl/SegmentedControl.mdx +74 -25
  70. package/src/components/interaction/form/SegmentedControl/SegmentedControl.module.css +65 -36
  71. package/src/components/interaction/form/SegmentedControl/SegmentedControl.spec.tsx +127 -56
  72. package/src/components/interaction/form/SegmentedControl/SegmentedControl.stories.tsx +89 -72
  73. package/src/components/interaction/form/SegmentedControl/SegmentedControl.tsx +40 -62
  74. package/src/components/interaction/form/SegmentedControl/index.ts +1 -1
  75. package/src/components/interaction/form/Select/Select.module.css +2 -4
  76. package/src/components/interaction/form/Switch/Switch.module.css +5 -7
  77. package/src/components/interaction/form/Switch/Switch.native.spec.tsx +9 -9
  78. package/src/components/interaction/form/Switch/Switch.native.tsx +2 -2
  79. package/src/components/interaction/form/Textarea/Textarea.module.css +1 -3
  80. package/src/components/interaction/form/atoms/InputContainer.stories.tsx +64 -0
  81. package/src/components/interaction/form/atoms/Label.stories.tsx +33 -0
  82. package/src/components/interaction/form/atoms/Message.stories.tsx +33 -0
  83. package/src/components/layout/Card/Card.module.css +1 -3
  84. package/src/components/ui/Accordion/Accordion.module.css +1 -2
  85. package/src/components/ui/Alert/Alert.module.css +1 -2
  86. package/src/components/ui/Calendar/Calendar.module.css +2 -5
  87. package/src/components/ui/Carousel/Carousel.module.css +1 -3
  88. package/src/components/ui/NotificationBanner/NotificationBanner.module.css +1 -2
  89. package/src/components/ui/Pagination/Pagination.module.css +1 -2
  90. package/src/components/ui/SkipLink/SkipLink.module.css +1 -2
  91. package/src/components/ui/Table/Table.module.css +1 -2
  92. package/src/css/bdc.css +16 -11
  93. package/src/index.ts +1 -1
  94. package/src/stories/Introduction.mdx +2 -1
  95. package/src/web-components/globals.ts +3 -3
  96. package/src/web-components/index.ts +12 -0
  97. package/src/web-components/interaction/BdsAccordion.mdx +103 -0
  98. package/src/web-components/interaction/BdsAccordion.stories.tsx +94 -0
  99. package/src/web-components/interaction/BdsCollapsible.mdx +116 -0
  100. package/src/web-components/interaction/BdsCollapsible.stories.tsx +68 -0
  101. package/src/web-components/interaction/BdsDialog.mdx +110 -0
  102. package/src/web-components/interaction/BdsDialog.stories.tsx +157 -0
  103. package/src/web-components/interaction/BdsDrawer.mdx +108 -0
  104. package/src/web-components/interaction/BdsDrawer.stories.tsx +196 -0
  105. package/src/web-components/interaction/BdsDropdownMenu.mdx +108 -0
  106. package/src/web-components/interaction/BdsDropdownMenu.stories.tsx +91 -0
  107. package/src/web-components/interaction/BdsSkipLink.mdx +90 -0
  108. package/src/web-components/interaction/BdsSkipLink.stories.tsx +68 -0
  109. package/src/web-components/interaction/BdsTabs.mdx +117 -0
  110. package/src/web-components/interaction/BdsTabs.stories.tsx +94 -0
  111. package/src/web-components/interaction/BdsTooltip.mdx +104 -0
  112. package/src/web-components/interaction/BdsTooltip.stories.tsx +87 -0
  113. package/src/web-components/interaction/bds-accordion.ts +1 -2
  114. package/src/web-components/interaction/bds-button.ts +15 -13
  115. package/src/web-components/interaction/bds-collapsible.ts +1 -0
  116. package/src/web-components/interaction/bds-dialog.ts +2 -5
  117. package/src/web-components/interaction/bds-drawer.ts +70 -44
  118. package/src/web-components/interaction/bds-dropdown-menu-item.ts +124 -0
  119. package/src/web-components/interaction/bds-dropdown-menu.spec.ts +102 -0
  120. package/src/web-components/interaction/bds-dropdown-menu.ts +200 -0
  121. package/src/web-components/interaction/form/BdsCheckbox.mdx +76 -0
  122. package/src/web-components/interaction/form/BdsCheckbox.stories.tsx +72 -0
  123. package/src/web-components/interaction/form/BdsCheckboxGroup.mdx +70 -0
  124. package/src/web-components/interaction/form/BdsCheckboxGroup.stories.tsx +81 -0
  125. package/src/web-components/interaction/form/BdsCombobox.mdx +71 -0
  126. package/src/web-components/interaction/form/BdsCombobox.stories.tsx +93 -0
  127. package/src/web-components/interaction/form/BdsFileInput.mdx +79 -0
  128. package/src/web-components/interaction/form/BdsFileInput.stories.tsx +69 -0
  129. package/src/web-components/interaction/form/BdsFormInput.mdx +98 -0
  130. package/src/web-components/interaction/form/BdsFormInput.stories.tsx +139 -0
  131. package/src/web-components/interaction/form/BdsNumberInput.mdx +70 -0
  132. package/src/web-components/interaction/form/BdsNumberInput.stories.tsx +68 -0
  133. package/src/web-components/interaction/form/BdsRadio.mdx +72 -0
  134. package/src/web-components/interaction/form/BdsRadio.stories.tsx +58 -0
  135. package/src/web-components/interaction/form/BdsRadioGroup.mdx +73 -0
  136. package/src/web-components/interaction/form/BdsRadioGroup.stories.tsx +88 -0
  137. package/src/web-components/interaction/form/BdsSegmentedControl.mdx +95 -0
  138. package/src/web-components/interaction/form/BdsSegmentedControl.stories.tsx +102 -0
  139. package/src/web-components/interaction/form/BdsSelect.mdx +76 -0
  140. package/src/web-components/interaction/form/BdsSelect.stories.tsx +84 -0
  141. package/src/web-components/interaction/form/BdsSlider.mdx +77 -0
  142. package/src/web-components/interaction/form/BdsSlider.stories.tsx +68 -0
  143. package/src/web-components/interaction/form/BdsSwitch.mdx +72 -0
  144. package/src/web-components/interaction/form/BdsSwitch.stories.tsx +58 -0
  145. package/src/web-components/interaction/form/BdsTextarea.mdx +73 -0
  146. package/src/web-components/interaction/form/BdsTextarea.stories.tsx +70 -0
  147. package/src/web-components/interaction/form/bds-checkbox-group.spec.ts +55 -0
  148. package/src/web-components/interaction/form/bds-checkbox-group.ts +117 -0
  149. package/src/web-components/interaction/form/bds-checkbox.ts +1 -3
  150. package/src/web-components/interaction/form/bds-combobox.ts +1 -3
  151. package/src/web-components/interaction/form/bds-form-input.spec.ts +83 -0
  152. package/src/web-components/interaction/form/bds-form-input.ts +268 -0
  153. package/src/web-components/interaction/form/bds-number-input.ts +3 -4
  154. package/src/web-components/interaction/form/bds-radio-group.spec.ts +62 -0
  155. package/src/web-components/interaction/form/bds-radio-group.ts +142 -0
  156. package/src/web-components/interaction/form/bds-radio.ts +6 -12
  157. package/src/web-components/interaction/form/bds-segmented-control.ts +96 -20
  158. package/src/web-components/interaction/form/bds-select.ts +2 -4
  159. package/src/web-components/interaction/form/bds-switch.ts +3 -4
  160. package/src/web-components/interaction/form/bds-textarea.ts +1 -3
  161. package/src/web-components/ui/BdsAvatar.mdx +76 -0
  162. package/src/web-components/ui/BdsAvatar.stories.tsx +48 -0
  163. package/src/web-components/ui/BdsBreadcrumb.mdx +66 -0
  164. package/src/web-components/ui/BdsBreadcrumb.stories.tsx +49 -0
  165. package/src/web-components/ui/BdsButtonGroup.mdx +69 -0
  166. package/src/web-components/ui/BdsButtonGroup.stories.tsx +66 -0
  167. package/src/web-components/ui/BdsCalendar.mdx +93 -0
  168. package/src/web-components/ui/BdsCalendar.stories.tsx +63 -0
  169. package/src/web-components/ui/BdsCard.mdx +88 -0
  170. package/src/web-components/ui/BdsCard.stories.tsx +67 -0
  171. package/src/web-components/ui/BdsCarousel.mdx +83 -0
  172. package/src/web-components/ui/BdsCarousel.stories.tsx +96 -0
  173. package/src/web-components/ui/BdsDescriptionList.mdx +64 -0
  174. package/src/web-components/ui/BdsDescriptionList.stories.tsx +64 -0
  175. package/src/web-components/ui/BdsIconWrapper.mdx +70 -0
  176. package/src/web-components/ui/BdsIconWrapper.stories.tsx +81 -0
  177. package/src/web-components/ui/BdsLink.mdx +79 -0
  178. package/src/web-components/ui/BdsLink.stories.tsx +63 -0
  179. package/src/web-components/ui/BdsLoading.mdx +58 -0
  180. package/src/web-components/ui/BdsLoading.stories.tsx +35 -0
  181. package/src/web-components/ui/BdsNotificationBanner.mdx +85 -0
  182. package/src/web-components/ui/BdsNotificationBanner.stories.tsx +84 -0
  183. package/src/web-components/ui/BdsPagination.mdx +74 -0
  184. package/src/web-components/ui/BdsPagination.stories.tsx +36 -0
  185. package/src/web-components/ui/BdsProgress.mdx +70 -0
  186. package/src/web-components/ui/BdsProgress.stories.tsx +96 -0
  187. package/src/web-components/ui/BdsProgressCircle.mdx +70 -0
  188. package/src/web-components/ui/BdsProgressCircle.stories.tsx +57 -0
  189. package/src/web-components/ui/BdsRating.mdx +61 -0
  190. package/src/web-components/ui/BdsRating.stories.tsx +45 -0
  191. package/src/web-components/ui/BdsSectionHeader.mdx +72 -0
  192. package/src/web-components/ui/BdsSectionHeader.stories.tsx +54 -0
  193. package/src/web-components/ui/BdsSeparator.mdx +77 -0
  194. package/src/web-components/ui/BdsSeparator.stories.tsx +55 -0
  195. package/src/web-components/ui/BdsSkeleton.mdx +61 -0
  196. package/src/web-components/ui/BdsSkeleton.stories.tsx +50 -0
  197. package/src/web-components/ui/BdsTable.mdx +81 -0
  198. package/src/web-components/ui/BdsTable.stories.tsx +83 -0
  199. package/src/web-components/ui/BdsTypography.mdx +74 -0
  200. package/src/web-components/ui/BdsTypography.stories.tsx +52 -0
  201. package/src/web-components/ui/bds-alert.ts +1 -2
  202. package/src/web-components/ui/bds-button-group.spec.ts +40 -0
  203. package/src/web-components/ui/bds-button-group.ts +78 -0
  204. package/src/web-components/ui/bds-calendar.spec.ts +91 -0
  205. package/src/web-components/ui/bds-calendar.ts +427 -0
  206. package/src/web-components/ui/bds-card.ts +1 -3
  207. package/src/web-components/ui/bds-carousel.spec.ts +64 -0
  208. package/src/web-components/ui/bds-carousel.ts +296 -0
  209. package/src/web-components/ui/bds-notification-banner.ts +1 -2
  210. package/src/web-components/ui/bds-pagination.spec.ts +67 -0
  211. package/src/web-components/ui/bds-pagination.ts +197 -0
  212. package/src/web-components/ui/bds-table.spec.ts +45 -0
  213. package/src/web-components/ui/bds-table.ts +96 -0
@@ -0,0 +1,55 @@
1
+ import { fixture, cleanup } from '../../test/helpers';
2
+ import { BdsCheckboxGroup } from './bds-checkbox-group';
3
+
4
+ describe('bds-checkbox-group', () => {
5
+ it('is registered as a custom element', () => {
6
+ expect(customElements.get('bds-checkbox-group')).toBe(BdsCheckboxGroup);
7
+ });
8
+
9
+ it('renders a fieldset', async () => {
10
+ const el = await fixture('<bds-checkbox-group label="Interests"></bds-checkbox-group>');
11
+ expect(el.shadowRoot!.querySelector('fieldset')).not.toBeNull();
12
+ cleanup(el);
13
+ });
14
+
15
+ it('renders the label as a legend', async () => {
16
+ const el = await fixture('<bds-checkbox-group label="My Group"></bds-checkbox-group>');
17
+ const legend = el.shadowRoot!.querySelector('legend');
18
+ expect(legend?.textContent?.trim()).toContain('My Group');
19
+ cleanup(el);
20
+ });
21
+
22
+ it('renders slotted children', async () => {
23
+ const el = await fixture('<bds-checkbox-group label="Test"><span id="child">item</span></bds-checkbox-group>');
24
+ expect(el.shadowRoot!.querySelector('slot')).not.toBeNull();
25
+ cleanup(el);
26
+ });
27
+
28
+ it('disables the fieldset when disabled attribute is set', async () => {
29
+ const el = await fixture('<bds-checkbox-group label="Test" disabled></bds-checkbox-group>');
30
+ const fieldset = el.shadowRoot!.querySelector('fieldset')!;
31
+ expect(fieldset.disabled).toBe(true);
32
+ cleanup(el);
33
+ });
34
+
35
+ it('shows required asterisk when required', async () => {
36
+ const el = await fixture('<bds-checkbox-group label="Test" required></bds-checkbox-group>');
37
+ const required = el.shadowRoot!.querySelector('.required');
38
+ expect(required).not.toBeNull();
39
+ cleanup(el);
40
+ });
41
+
42
+ it('renders error message when error attribute is set', async () => {
43
+ const el = await fixture('<bds-checkbox-group label="Test" error="Select at least one"></bds-checkbox-group>');
44
+ const message = el.shadowRoot!.querySelector('.message--error');
45
+ expect(message?.textContent?.trim()).toBe('Select at least one');
46
+ cleanup(el);
47
+ });
48
+
49
+ it('renders hint message when hint attribute is set', async () => {
50
+ const el = await fixture('<bds-checkbox-group label="Test" hint="Select all that apply"></bds-checkbox-group>');
51
+ const message = el.shadowRoot!.querySelector('.message--hint');
52
+ expect(message?.textContent?.trim()).toBe('Select all that apply');
53
+ cleanup(el);
54
+ });
55
+ });
@@ -0,0 +1,117 @@
1
+ import { LitElement, css, html } from 'lit';
2
+
3
+ /**
4
+ * `<bds-checkbox-group>` — groups related checkboxes with a shared label.
5
+ *
6
+ * Renders a `<fieldset>` with a `<legend>` for accessible grouping.
7
+ *
8
+ * Attributes:
9
+ * label — visible group label (rendered as `<legend>`)
10
+ * error — error message shown below items
11
+ * hint — hint text shown below items
12
+ * disabled — disables the entire fieldset
13
+ * required — marks the group as required
14
+ *
15
+ * Slots:
16
+ * (default) — `<bds-checkbox>` elements
17
+ *
18
+ * @example
19
+ * <bds-checkbox-group label="Interests" required>
20
+ * <bds-checkbox name="sports" value="sports">Sports</bds-checkbox>
21
+ * <bds-checkbox name="music" value="music">Music</bds-checkbox>
22
+ * </bds-checkbox-group>
23
+ */
24
+ export class BdsCheckboxGroup extends LitElement {
25
+ static styles = css`
26
+ :host {
27
+ display: block;
28
+ }
29
+
30
+ fieldset {
31
+ border: none;
32
+ margin: 0;
33
+ padding: 0;
34
+ }
35
+
36
+ .legend {
37
+ font-size: var(--bds-font_size--body--s);
38
+ font-family: var(--bds-font_family--body);
39
+ font-weight: var(--bds-font_weight--semibold);
40
+ color: var(--bds-color_on-bg);
41
+ margin-block-end: var(--bds-space_xs);
42
+ }
43
+
44
+ .required {
45
+ color: var(--bds-color_error);
46
+ margin-inline-start: var(--bds-space_xxxs);
47
+ }
48
+
49
+ .items {
50
+ display: flex;
51
+ flex-direction: column;
52
+ gap: var(--bds-space_xs);
53
+ }
54
+
55
+ .message {
56
+ margin-block-start: var(--bds-space_xs);
57
+ font-size: var(--bds-font_size--body--s);
58
+ font-family: var(--bds-font_family--body);
59
+ }
60
+
61
+ .message--error {
62
+ color: var(--bds-color_error);
63
+ }
64
+
65
+ .message--hint {
66
+ color: var(--bds-color_on-bg--subtle);
67
+ }
68
+ `;
69
+
70
+ static properties = {
71
+ label: { type: String },
72
+ error: { type: String },
73
+ hint: { type: String },
74
+ disabled: { type: Boolean, reflect: true },
75
+ required: { type: Boolean, reflect: true },
76
+ };
77
+
78
+ declare label: string;
79
+ declare error: string | undefined;
80
+ declare hint: string | undefined;
81
+ declare disabled: boolean;
82
+ declare required: boolean;
83
+
84
+ constructor() {
85
+ super();
86
+ this.label = '';
87
+ this.disabled = false;
88
+ this.required = false;
89
+ }
90
+
91
+ render() {
92
+ return html`
93
+ <fieldset ?disabled=${this.disabled}>
94
+ <legend class="legend">
95
+ ${this.label}
96
+ ${this.required ? html`<span class="required" aria-hidden="true"> *</span>` : ''}
97
+ </legend>
98
+ <div class="items">
99
+ <slot></slot>
100
+ </div>
101
+ ${this.error
102
+ ? html`<p class="message message--error" role="alert">${this.error}</p>`
103
+ : this.hint
104
+ ? html`<p class="message message--hint">${this.hint}</p>`
105
+ : ''}
106
+ </fieldset>
107
+ `;
108
+ }
109
+ }
110
+
111
+ customElements.define('bds-checkbox-group', BdsCheckboxGroup);
112
+
113
+ declare global {
114
+ interface HTMLElementTagNameMap {
115
+ 'bds-checkbox-group': BdsCheckboxGroup;
116
+ }
117
+ }
@@ -52,10 +52,8 @@ export class BdsCheckbox extends LitElement {
52
52
  --bdc_color: currentcolor;
53
53
  --bdc-outline_radius: var(--bds-border_radius--xs);
54
54
 
55
- outline: var(--bdc-outline_width) solid var(--bdc_color);
56
- outline-offset: var(--bdc-outline_offset);
57
55
  border-radius: var(--checkbox_radius, var(--bdc-outline_radius));
58
- box-shadow: var(--checkbox_shadow, var(--bdc-outline_shadow));
56
+ box-shadow: inset 0 0 0 var(--bdc-outline_width) var(--bdc_color), var(--checkbox_shadow, var(--bdc-outline_shadow));
59
57
  border: none;
60
58
  appearance: none;
61
59
  background-color: var(--checkbox_color_bg, var(--bds-color_bg));
@@ -57,10 +57,8 @@ export class BdsCombobox extends LitElement {
57
57
  --bdc_color: currentcolor;
58
58
  --bdc-outline_radius: var(--bds-border_radius--xs);
59
59
 
60
- outline: var(--bdc-outline_width) solid var(--bdc_color);
61
- outline-offset: var(--bdc-outline_offset);
62
60
  border-radius: var(--combobox_radius, var(--bdc-outline_radius));
63
- box-shadow: var(--combobox_shadow, var(--bdc-outline_shadow));
61
+ box-shadow: inset 0 0 0 var(--bdc-outline_width) var(--bdc_color), var(--combobox_shadow, var(--bdc-outline_shadow));
64
62
  background-color: var(--combobox_color_bg, var(--bds-color_bg));
65
63
  color: var(--combobox_color, var(--bds-color_on-bg));
66
64
  box-sizing: border-box;
@@ -0,0 +1,83 @@
1
+ import { fixture, cleanup } from '../../test/helpers';
2
+ import { BdsFormInput } from './bds-form-input';
3
+
4
+ describe('bds-form-input', () => {
5
+ it('is registered as a custom element', () => {
6
+ expect(customElements.get('bds-form-input')).toBe(BdsFormInput);
7
+ });
8
+
9
+ it('is form-associated', () => {
10
+ expect(BdsFormInput.formAssociated).toBe(true);
11
+ });
12
+
13
+ it('renders an input element', async () => {
14
+ const el = await fixture('<bds-form-input name="email"></bds-form-input>');
15
+ expect(el.shadowRoot!.querySelector('input')).not.toBeNull();
16
+ cleanup(el);
17
+ });
18
+
19
+ it('renders a label when label attribute is set', async () => {
20
+ const el = await fixture('<bds-form-input name="email" label="Email"></bds-form-input>');
21
+ expect(el.shadowRoot!.querySelector('label')?.textContent?.trim()).toContain('Email');
22
+ cleanup(el);
23
+ });
24
+
25
+ it('input id matches label for attribute', async () => {
26
+ const el = await fixture('<bds-form-input name="email" label="Email"></bds-form-input>');
27
+ const input = el.shadowRoot!.querySelector('input')!;
28
+ const label = el.shadowRoot!.querySelector('label')!;
29
+ expect(input.id).toBeTruthy();
30
+ expect(label.getAttribute('for')).toBe(input.id);
31
+ cleanup(el);
32
+ });
33
+
34
+ it('defaults type to text', async () => {
35
+ const el = await fixture('<bds-form-input name="search"></bds-form-input>') as BdsFormInput;
36
+ const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
37
+ expect(input.type).toBe('text');
38
+ cleanup(el);
39
+ });
40
+
41
+ it('applies the specified type', async () => {
42
+ const el = await fixture('<bds-form-input name="pass" type="password"></bds-form-input>');
43
+ const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
44
+ expect(input.type).toBe('password');
45
+ cleanup(el);
46
+ });
47
+
48
+ it('disables input when disabled attribute is set', async () => {
49
+ const el = await fixture('<bds-form-input name="email" disabled></bds-form-input>');
50
+ const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
51
+ expect(input.disabled).toBe(true);
52
+ cleanup(el);
53
+ });
54
+
55
+ it('renders error message and sets aria-invalid when error is set', async () => {
56
+ const el = await fixture('<bds-form-input name="email" error="Invalid email"></bds-form-input>');
57
+ const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
58
+ const errorMsg = el.shadowRoot!.querySelector('.message--error');
59
+ expect(input.getAttribute('aria-invalid')).toBe('true');
60
+ expect(errorMsg?.textContent?.trim()).toBe('Invalid email');
61
+ cleanup(el);
62
+ });
63
+
64
+ it('renders hint message when hint is set (no error)', async () => {
65
+ const el = await fixture('<bds-form-input name="email" hint="We will not share your email"></bds-form-input>');
66
+ const hint = el.shadowRoot!.querySelector('.message--hint');
67
+ expect(hint?.textContent?.trim()).toBe('We will not share your email');
68
+ cleanup(el);
69
+ });
70
+
71
+ it('fires bds-change on change', async () => {
72
+ const el = await fixture('<bds-form-input name="email"></bds-form-input>');
73
+ const values: string[] = [];
74
+ el.addEventListener('bds-change', (e: Event) => {
75
+ values.push((e as CustomEvent).detail.value);
76
+ });
77
+ const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
78
+ Object.defineProperty(input, 'value', { value: 'test@example.com', writable: true });
79
+ input.dispatchEvent(new Event('change', { bubbles: true }));
80
+ expect(values).toEqual(['test@example.com']);
81
+ cleanup(el);
82
+ });
83
+ });
@@ -0,0 +1,268 @@
1
+ import { LitElement, css, html } from 'lit';
2
+
3
+ /**
4
+ * `<bds-form-input>` — form-associated text input with label, hint, and error support.
5
+ *
6
+ * Participates in HTML forms via the Form-Associated Custom Elements API.
7
+ *
8
+ * Attributes:
9
+ * name — form field name
10
+ * type — input type: text | email | password | search | url | tel (default: "text")
11
+ * value — current value
12
+ * placeholder — placeholder text
13
+ * label — visible label text
14
+ * hint — helper text shown below the input
15
+ * error — error message; when set the input enters error state
16
+ * disabled — disables the input
17
+ * required — marks the field as required
18
+ *
19
+ * Slots:
20
+ * prefix — icon or text rendered before the input
21
+ * suffix — icon or text rendered after the input
22
+ *
23
+ * Events:
24
+ * bds-change — fired on value change; `detail: { value: string }`
25
+ * bds-input — fired on every keystroke; `detail: { value: string }`
26
+ *
27
+ * @example
28
+ * <bds-form-input name="email" type="email" label="Email address" required></bds-form-input>
29
+ */
30
+ export class BdsFormInput extends LitElement {
31
+ static formAssociated = true;
32
+
33
+ static styles = css`
34
+ :host {
35
+ display: block;
36
+ }
37
+
38
+ :host([disabled]) {
39
+ opacity: 0.5;
40
+ pointer-events: none;
41
+ }
42
+
43
+ .label {
44
+ display: block;
45
+ font-size: var(--bds-font_size--body--s);
46
+ font-family: var(--bds-font_family--body);
47
+ font-weight: var(--bds-font_weight--semibold);
48
+ color: var(--bds-color_on-bg);
49
+ margin-block-end: var(--bds-space_xxs);
50
+ }
51
+
52
+ .required {
53
+ color: var(--bds-color_error);
54
+ margin-inline-start: var(--bds-space_xxxs);
55
+ }
56
+
57
+ .wrapper {
58
+ position: relative;
59
+ display: flex;
60
+ align-items: center;
61
+ --bdc_color: currentcolor;
62
+ --bdc-outline_radius: var(--bds-border_radius--xs);
63
+
64
+ border-radius: var(--formInput_radius, var(--bdc-outline_radius));
65
+ box-shadow: inset 0 0 0 var(--bdc-outline_width) var(--bdc_color), var(--formInput_shadow, var(--bdc-outline_shadow));
66
+ background-color: var(--bds-color_bg);
67
+ transition: --bdc_color var(--bds-animation_transition-duration) var(--bds-animation_easing);
68
+ }
69
+
70
+ :host([error]) .wrapper {
71
+ --bdc_color: var(--bds-color_error);
72
+ }
73
+
74
+ .wrapper:focus-within {
75
+ --bdc_color: var(--bdc_color--focus);
76
+ }
77
+
78
+ :host([error]) .wrapper:focus-within {
79
+ --bdc_color: var(--bds-color_error);
80
+ }
81
+
82
+ .input {
83
+ flex: 1;
84
+ min-inline-size: 0;
85
+ border: none;
86
+ background: transparent;
87
+ padding: var(--bds-space_s);
88
+ font-size: var(--bds-font_size--body);
89
+ font-family: var(--bds-font_family--body);
90
+ color: var(--bds-color_on-bg);
91
+ outline: none;
92
+ }
93
+
94
+ .input::placeholder {
95
+ color: var(--bds-color_on-bg--subtle);
96
+ }
97
+
98
+ .input:focus-visible {
99
+ outline: none;
100
+ }
101
+
102
+ .wrapper:focus-within {
103
+ outline: var(--bds-outline_default);
104
+ outline-offset: var(--bds-outline_offset);
105
+ border-radius: var(--formInput_radius, var(--bdc-outline_radius));
106
+ }
107
+
108
+ .prefix,
109
+ .suffix {
110
+ display: flex;
111
+ align-items: center;
112
+ color: var(--bds-color_on-bg--subtle);
113
+ flex-shrink: 0;
114
+ }
115
+
116
+ .prefix {
117
+ padding-inline-start: var(--bds-space_s);
118
+ }
119
+
120
+ .suffix {
121
+ padding-inline-end: var(--bds-space_s);
122
+ }
123
+
124
+ .prefix:empty,
125
+ .suffix:empty {
126
+ display: none;
127
+ }
128
+
129
+ .message {
130
+ margin-block-start: var(--bds-space_xxs);
131
+ font-size: var(--bds-font_size--body--s);
132
+ font-family: var(--bds-font_family--body);
133
+ }
134
+
135
+ .message--error {
136
+ color: var(--bds-color_error);
137
+ }
138
+
139
+ .message--hint {
140
+ color: var(--bds-color_on-bg--subtle);
141
+ }
142
+ `;
143
+
144
+ static properties = {
145
+ name: { type: String, reflect: true },
146
+ type: { type: String, reflect: true },
147
+ value: { type: String },
148
+ placeholder: { type: String },
149
+ label: { type: String },
150
+ hint: { type: String },
151
+ error: { type: String, reflect: true },
152
+ disabled: { type: Boolean, reflect: true },
153
+ required: { type: Boolean, reflect: true },
154
+ };
155
+
156
+ declare name: string;
157
+ declare type: string;
158
+ declare value: string;
159
+ declare placeholder: string;
160
+ declare label: string;
161
+ declare hint: string | undefined;
162
+ declare error: string | undefined;
163
+ declare disabled: boolean;
164
+ declare required: boolean;
165
+
166
+ private _internals!: ElementInternals;
167
+ private readonly _uid = Math.random().toString(36).slice(2, 8);
168
+
169
+ constructor() {
170
+ super();
171
+ this._internals = this.attachInternals();
172
+ this.name = '';
173
+ this.type = 'text';
174
+ this.value = '';
175
+ this.placeholder = '';
176
+ this.label = '';
177
+ this.disabled = false;
178
+ this.required = false;
179
+ }
180
+
181
+ updated(changed: Map<string, unknown>) {
182
+ super.updated(changed);
183
+ if (changed.has('value')) {
184
+ this._internals.setFormValue?.(this.value);
185
+ }
186
+ if (changed.has('required') || changed.has('value')) {
187
+ if (this.required && !this.value) {
188
+ this._internals.setValidity?.({ valueMissing: true }, 'Required');
189
+ } else {
190
+ this._internals.setValidity?.({});
191
+ }
192
+ }
193
+ }
194
+
195
+ formResetCallback() {
196
+ this.value = '';
197
+ this._internals.setFormValue?.('');
198
+ }
199
+
200
+ private _handleInput(e: Event) {
201
+ const input = e.target as HTMLInputElement;
202
+ this.value = input.value;
203
+ this.dispatchEvent(new CustomEvent('bds-input', {
204
+ detail: { value: this.value },
205
+ bubbles: true,
206
+ composed: true,
207
+ }));
208
+ }
209
+
210
+ private _handleChange(e: Event) {
211
+ const input = e.target as HTMLInputElement;
212
+ this.value = input.value;
213
+ this.dispatchEvent(new CustomEvent('bds-change', {
214
+ detail: { value: this.value },
215
+ bubbles: true,
216
+ composed: true,
217
+ }));
218
+ }
219
+
220
+ render() {
221
+ const id = `bds-form-input-${this._uid}`;
222
+ const hintId = `${id}-hint`;
223
+ const errorId = `${id}-error`;
224
+ const describedBy = [this.error && errorId, this.hint && hintId].filter(Boolean).join(' ') || undefined;
225
+
226
+ return html`
227
+ ${this.label
228
+ ? html`
229
+ <label class="label" for=${id}>
230
+ ${this.label}
231
+ ${this.required ? html`<span class="required" aria-hidden="true">*</span>` : ''}
232
+ </label>
233
+ `
234
+ : ''}
235
+ <div class="wrapper">
236
+ <span class="prefix"><slot name="prefix"></slot></span>
237
+ <input
238
+ id=${id}
239
+ class="input"
240
+ type=${this.type}
241
+ .value=${this.value}
242
+ placeholder=${this.placeholder}
243
+ ?disabled=${this.disabled}
244
+ ?required=${this.required}
245
+ aria-invalid=${this.error ? 'true' : 'false'}
246
+ aria-describedby=${describedBy ?? ''}
247
+ aria-required=${this.required ? 'true' : 'false'}
248
+ @input=${this._handleInput}
249
+ @change=${this._handleChange}
250
+ />
251
+ <span class="suffix"><slot name="suffix"></slot></span>
252
+ </div>
253
+ ${this.error
254
+ ? html`<p id=${errorId} class="message message--error" role="alert">${this.error}</p>`
255
+ : this.hint
256
+ ? html`<p id=${hintId} class="message message--hint">${this.hint}</p>`
257
+ : ''}
258
+ `;
259
+ }
260
+ }
261
+
262
+ customElements.define('bds-form-input', BdsFormInput);
263
+
264
+ declare global {
265
+ interface HTMLElementTagNameMap {
266
+ 'bds-form-input': BdsFormInput;
267
+ }
268
+ }
@@ -45,10 +45,8 @@ export class BdsNumberInput extends LitElement {
45
45
  --bdc_color: currentcolor;
46
46
  --bdc-outline_radius: var(--bds-border_radius--xs);
47
47
 
48
- outline: var(--bdc-outline_width) solid var(--bdc_color);
49
- outline-offset: var(--bdc-outline_offset);
50
48
  border-radius: var(--number-input_radius, var(--bdc-outline_radius));
51
- box-shadow: var(--number-input_shadow, var(--bdc-outline_shadow));
49
+ box-shadow: inset 0 0 0 var(--bdc-outline_width) var(--bdc_color), var(--number-input_shadow, var(--bdc-outline_shadow));
52
50
  background-color: var(--number-input_color_bg, var(--bds-color_bg));
53
51
  color: var(--number-input_color, var(--bds-color_on-bg));
54
52
  text-align: center;
@@ -76,12 +74,13 @@ export class BdsNumberInput extends LitElement {
76
74
  display: flex;
77
75
  align-items: center;
78
76
  justify-content: center;
77
+ margin: 1px;
79
78
  inline-size: 2rem;
80
79
  block-size: 2rem;
81
80
  border-radius: var(--bds-border_radius--xs);
82
81
  cursor: pointer;
83
82
  flex-shrink: 0;
84
- color: var(--bds-color_on-bg);
83
+ color: var(--bds-color_interactive);
85
84
  transition: var(--bds-animation_transition);
86
85
  }
87
86
 
@@ -0,0 +1,62 @@
1
+ import { fixture, cleanup } from '../../test/helpers';
2
+ import { BdsRadioGroup } from './bds-radio-group';
3
+
4
+ describe('bds-radio-group', () => {
5
+ it('is registered as a custom element', () => {
6
+ expect(customElements.get('bds-radio-group')).toBe(BdsRadioGroup);
7
+ });
8
+
9
+ it('renders a fieldset', async () => {
10
+ const el = await fixture('<bds-radio-group label="Options"></bds-radio-group>');
11
+ expect(el.shadowRoot!.querySelector('fieldset')).not.toBeNull();
12
+ cleanup(el);
13
+ });
14
+
15
+ it('renders the label as a legend', async () => {
16
+ const el = await fixture('<bds-radio-group label="Select one"></bds-radio-group>');
17
+ const legend = el.shadowRoot!.querySelector('legend');
18
+ expect(legend?.textContent?.trim()).toContain('Select one');
19
+ cleanup(el);
20
+ });
21
+
22
+ it('disables the fieldset when disabled', async () => {
23
+ const el = await fixture('<bds-radio-group label="Test" disabled></bds-radio-group>');
24
+ const fieldset = el.shadowRoot!.querySelector('fieldset')!;
25
+ expect(fieldset.disabled).toBe(true);
26
+ cleanup(el);
27
+ });
28
+
29
+ it('renders a slot for radio children', async () => {
30
+ const el = await fixture('<bds-radio-group label="Test"></bds-radio-group>');
31
+ expect(el.shadowRoot!.querySelector('slot')).not.toBeNull();
32
+ cleanup(el);
33
+ });
34
+
35
+ it('renders error message when error attribute is set', async () => {
36
+ const el = await fixture('<bds-radio-group label="Test" error="Required field"></bds-radio-group>');
37
+ const message = el.shadowRoot!.querySelector('.message--error');
38
+ expect(message?.textContent?.trim()).toBe('Required field');
39
+ cleanup(el);
40
+ });
41
+
42
+ it('reflects name attribute', async () => {
43
+ const el = await fixture('<bds-radio-group label="Test" name="contact"></bds-radio-group>') as BdsRadioGroup;
44
+ expect(el.name).toBe('contact');
45
+ cleanup(el);
46
+ });
47
+
48
+ it('propagates name to slotted bds-radio elements', async () => {
49
+ // Define a minimal bds-radio stub so querySelectorAll finds it
50
+ if (!customElements.get('bds-radio-stub')) {
51
+ customElements.define('bds-radio-stub', class extends HTMLElement {});
52
+ }
53
+ const el = await fixture(
54
+ '<bds-radio-group label="Contact" name="contact"><bds-radio value="email">Email</bds-radio></bds-radio-group>',
55
+ ) as BdsRadioGroup;
56
+ // Wait for propagation promise
57
+ await Promise.resolve();
58
+ const radio = el.querySelector('bds-radio');
59
+ expect(radio?.getAttribute('name')).toBe('contact');
60
+ cleanup(el);
61
+ });
62
+ });