@nuvia-ui/components 4.0.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 (230) hide show
  1. package/package.json +27 -0
  2. package/src/ds-accordion/ds-accordion-item.js +288 -0
  3. package/src/ds-accordion/ds-accordion-item.stories.js +82 -0
  4. package/src/ds-accordion/ds-accordion.a11y.test.js +92 -0
  5. package/src/ds-accordion/ds-accordion.js +68 -0
  6. package/src/ds-accordion/ds-accordion.stories.js +118 -0
  7. package/src/ds-accordion/ds-accordion.test.js +146 -0
  8. package/src/ds-accordion/index.js +2 -0
  9. package/src/ds-action-bar/ds-action-bar.js +116 -0
  10. package/src/ds-action-bar/ds-action-bar.stories.js +86 -0
  11. package/src/ds-action-bar/ds-action-bar.test.js +64 -0
  12. package/src/ds-action-bar/index.js +1 -0
  13. package/src/ds-alert/ds-alert.a11y.test.js +151 -0
  14. package/src/ds-alert/ds-alert.js +223 -0
  15. package/src/ds-alert/ds-alert.mdx +142 -0
  16. package/src/ds-alert/ds-alert.stories.js +166 -0
  17. package/src/ds-alert/ds-alert.test.js +256 -0
  18. package/src/ds-alert/index.js +1 -0
  19. package/src/ds-avatar/ds-avatar.a11y.test.js +45 -0
  20. package/src/ds-avatar/ds-avatar.js +216 -0
  21. package/src/ds-avatar/ds-avatar.stories.js +120 -0
  22. package/src/ds-avatar/ds-avatar.test.js +83 -0
  23. package/src/ds-avatar/index.js +1 -0
  24. package/src/ds-avatar-extended/ds-avatar-extended.a11y.test.js +29 -0
  25. package/src/ds-avatar-extended/ds-avatar-extended.js +108 -0
  26. package/src/ds-avatar-extended/ds-avatar-extended.stories.js +93 -0
  27. package/src/ds-avatar-extended/ds-avatar-extended.test.js +66 -0
  28. package/src/ds-avatar-extended/index.js +1 -0
  29. package/src/ds-banner/ds-banner.a11y.test.js +51 -0
  30. package/src/ds-banner/ds-banner.js +233 -0
  31. package/src/ds-banner/ds-banner.stories.js +185 -0
  32. package/src/ds-banner/ds-banner.test.js +116 -0
  33. package/src/ds-banner/index.js +1 -0
  34. package/src/ds-breadcrumb-item/ds-breadcrumb-item.js +135 -0
  35. package/src/ds-breadcrumb-item/ds-breadcrumb-item.stories.js +49 -0
  36. package/src/ds-breadcrumb-item/ds-breadcrumb-item.test.js +55 -0
  37. package/src/ds-breadcrumbs/ds-breadcrumbs.js +194 -0
  38. package/src/ds-breadcrumbs/ds-breadcrumbs.stories.js +54 -0
  39. package/src/ds-breadcrumbs/ds-breadcrumbs.test.js +33 -0
  40. package/src/ds-button/ds-button.a11y.test.js +49 -0
  41. package/src/ds-button/ds-button.js +205 -0
  42. package/src/ds-button/ds-button.mdx +141 -0
  43. package/src/ds-button/ds-button.stories.js +152 -0
  44. package/src/ds-button/ds-button.test.js +62 -0
  45. package/src/ds-button/index.js +1 -0
  46. package/src/ds-button-group/ds-button-group.js +82 -0
  47. package/src/ds-button-group/ds-button-group.mdx +39 -0
  48. package/src/ds-button-group/ds-button-group.stories.js +47 -0
  49. package/src/ds-button-group/ds-button-group.test.js +47 -0
  50. package/src/ds-button-group/index.js +1 -0
  51. package/src/ds-checkbox/ds-checkbox.a11y.test.js +79 -0
  52. package/src/ds-checkbox/ds-checkbox.js +271 -0
  53. package/src/ds-checkbox/ds-checkbox.stories.js +77 -0
  54. package/src/ds-checkbox/ds-checkbox.test.js +191 -0
  55. package/src/ds-checkbox/index.js +1 -0
  56. package/src/ds-checkbox-group/ds-checkbox-group.a11y.test.js +146 -0
  57. package/src/ds-checkbox-group/ds-checkbox-group.js +235 -0
  58. package/src/ds-checkbox-group/ds-checkbox-group.stories.js +210 -0
  59. package/src/ds-checkbox-group/ds-checkbox-group.test.js +150 -0
  60. package/src/ds-checkbox-group/index.js +1 -0
  61. package/src/ds-dialog/ds-dialog.js +466 -0
  62. package/src/ds-dialog/ds-dialog.stories.js +274 -0
  63. package/src/ds-dialog/ds-dialog.test.js +441 -0
  64. package/src/ds-dialog/index.js +1 -0
  65. package/src/ds-dropdown/ds-dropdown.a11y.test.js +80 -0
  66. package/src/ds-dropdown/ds-dropdown.js +891 -0
  67. package/src/ds-dropdown/ds-dropdown.stories.js +259 -0
  68. package/src/ds-dropdown/ds-dropdown.test.js +268 -0
  69. package/src/ds-dropdown/index.js +1 -0
  70. package/src/ds-dropdown-group/ds-dropdown-group.js +55 -0
  71. package/src/ds-dropdown-panel/ds-dropdown-panel.js +34 -0
  72. package/src/ds-file-uploaded/ds-file-uploaded.a11y.test.js +40 -0
  73. package/src/ds-file-uploaded/ds-file-uploaded.js +135 -0
  74. package/src/ds-file-uploaded/ds-file-uploaded.mdx +33 -0
  75. package/src/ds-file-uploaded/ds-file-uploaded.stories.js +81 -0
  76. package/src/ds-file-uploaded/ds-file-uploaded.test.js +85 -0
  77. package/src/ds-file-uploader/ds-file-uploader.a11y.test.js +61 -0
  78. package/src/ds-file-uploader/ds-file-uploader.js +442 -0
  79. package/src/ds-file-uploader/ds-file-uploader.mdx +44 -0
  80. package/src/ds-file-uploader/ds-file-uploader.stories.js +76 -0
  81. package/src/ds-file-uploader/ds-file-uploader.test.js +142 -0
  82. package/src/ds-header/ds-header.a11y.test.js +38 -0
  83. package/src/ds-header/ds-header.js +149 -0
  84. package/src/ds-header/ds-header.stories.js +63 -0
  85. package/src/ds-header/ds-header.test.js +52 -0
  86. package/src/ds-header/index.js +1 -0
  87. package/src/ds-header-nav/ds-header-nav.a11y.test.js +69 -0
  88. package/src/ds-header-nav/ds-header-nav.js +114 -0
  89. package/src/ds-header-nav/ds-header-nav.stories.js +17 -0
  90. package/src/ds-header-nav/ds-header-nav.test.js +93 -0
  91. package/src/ds-header-nav-item/ds-header-nav-item.a11y.test.js +71 -0
  92. package/src/ds-header-nav-item/ds-header-nav-item.js +124 -0
  93. package/src/ds-header-nav-item/ds-header-nav-item.stories.js +43 -0
  94. package/src/ds-header-nav-item/ds-header-nav-item.test.js +61 -0
  95. package/src/ds-icon/ds-icon.a11y.test.js +49 -0
  96. package/src/ds-icon/ds-icon.js +75 -0
  97. package/src/ds-icon/ds-icon.mdx +36 -0
  98. package/src/ds-icon/ds-icon.stories.js +88 -0
  99. package/src/ds-icon/ds-icon.test.js +97 -0
  100. package/src/ds-icon/index.js +1 -0
  101. package/src/ds-icon-button/ds-icon-button.a11y.test.js +55 -0
  102. package/src/ds-icon-button/ds-icon-button.js +224 -0
  103. package/src/ds-icon-button/ds-icon-button.mdx +131 -0
  104. package/src/ds-icon-button/ds-icon-button.stories.js +128 -0
  105. package/src/ds-icon-button/ds-icon-button.test.js +90 -0
  106. package/src/ds-icon-button/index.js +1 -0
  107. package/src/ds-input/ds-input.a11y.test.js +145 -0
  108. package/src/ds-input/ds-input.js +645 -0
  109. package/src/ds-input/ds-input.mdx +251 -0
  110. package/src/ds-input/ds-input.stories.js +298 -0
  111. package/src/ds-input/ds-input.test.js +792 -0
  112. package/src/ds-input/index.js +1 -0
  113. package/src/ds-link/ds-link.js +111 -0
  114. package/src/ds-link/ds-link.stories.js +56 -0
  115. package/src/ds-link/ds-link.test.js +74 -0
  116. package/src/ds-list-item/ds-list-item.a11y.test.js +39 -0
  117. package/src/ds-list-item/ds-list-item.js +292 -0
  118. package/src/ds-list-item/ds-list-item.stories.js +101 -0
  119. package/src/ds-list-item/ds-list-item.test.js +63 -0
  120. package/src/ds-menu/ds-menu.js +30 -0
  121. package/src/ds-menu/ds-menu.stories.js +120 -0
  122. package/src/ds-menu/ds-menu.test.js +123 -0
  123. package/src/ds-menu-group/ds-menu-group.js +101 -0
  124. package/src/ds-menu-group/ds-menu-group.stories.js +99 -0
  125. package/src/ds-nav-item/ds-nav-item.a11y.test.js +91 -0
  126. package/src/ds-nav-item/ds-nav-item.js +307 -0
  127. package/src/ds-nav-item/ds-nav-item.stories.js +99 -0
  128. package/src/ds-nav-item/ds-nav-item.test.js +169 -0
  129. package/src/ds-nav-item/index.js +1 -0
  130. package/src/ds-nav-vertical/ds-nav-vertical.a11y.test.js +69 -0
  131. package/src/ds-nav-vertical/ds-nav-vertical.js +173 -0
  132. package/src/ds-nav-vertical/ds-nav-vertical.stories.js +124 -0
  133. package/src/ds-nav-vertical/ds-nav-vertical.test.js +176 -0
  134. package/src/ds-nav-vertical/index.js +1 -0
  135. package/src/ds-pagination/ds-pagination.a11y.test.js +50 -0
  136. package/src/ds-pagination/ds-pagination.js +232 -0
  137. package/src/ds-pagination/ds-pagination.stories.js +63 -0
  138. package/src/ds-pagination/ds-pagination.test.js +141 -0
  139. package/src/ds-pagination/index.js +1 -0
  140. package/src/ds-progress-bar/ds-progress-bar.a11y.test.js +25 -0
  141. package/src/ds-progress-bar/ds-progress-bar.js +81 -0
  142. package/src/ds-progress-bar/ds-progress-bar.stories.js +69 -0
  143. package/src/ds-progress-bar/ds-progress-bar.test.js +60 -0
  144. package/src/ds-radio/ds-radio.a11y.test.js +69 -0
  145. package/src/ds-radio/ds-radio.js +240 -0
  146. package/src/ds-radio/ds-radio.stories.js +102 -0
  147. package/src/ds-radio/ds-radio.test.js +114 -0
  148. package/src/ds-radio/index.js +1 -0
  149. package/src/ds-radio-group/ds-radio-group.a11y.test.js +164 -0
  150. package/src/ds-radio-group/ds-radio-group.js +257 -0
  151. package/src/ds-radio-group/ds-radio-group.stories.js +247 -0
  152. package/src/ds-radio-group/ds-radio-group.test.js +194 -0
  153. package/src/ds-radio-group/index.js +1 -0
  154. package/src/ds-rich-list/ds-rich-list.js +246 -0
  155. package/src/ds-rich-list/ds-rich-list.stories.js +368 -0
  156. package/src/ds-rich-list/ds-rich-list.test.js +293 -0
  157. package/src/ds-rich-list-item/ds-rich-list-item.js +579 -0
  158. package/src/ds-rich-list-item/ds-rich-list-item.stories.js +197 -0
  159. package/src/ds-rich-list-item/ds-rich-list-item.test.js +434 -0
  160. package/src/ds-slider/ds-slider.js +399 -0
  161. package/src/ds-slider/ds-slider.stories.js +107 -0
  162. package/src/ds-slider/ds-slider.test.js +308 -0
  163. package/src/ds-spinner/ds-spinner.js +173 -0
  164. package/src/ds-spinner/ds-spinner.stories.js +52 -0
  165. package/src/ds-spinner/ds-spinner.test.js +50 -0
  166. package/src/ds-status-border/ds-status-border.js +88 -0
  167. package/src/ds-status-border/ds-status-border.stories.js +242 -0
  168. package/src/ds-status-border/ds-status-border.test.js +168 -0
  169. package/src/ds-stepper/ds-stepper.a11y.test.js +198 -0
  170. package/src/ds-stepper/ds-stepper.js +207 -0
  171. package/src/ds-stepper/ds-stepper.stories.js +530 -0
  172. package/src/ds-stepper/ds-stepper.test.js +311 -0
  173. package/src/ds-stepper-item/ds-stepper-item.js +485 -0
  174. package/src/ds-stepper-item/ds-stepper-item.stories.js +288 -0
  175. package/src/ds-switch/ds-switch.js +348 -0
  176. package/src/ds-switch/ds-switch.stories.js +145 -0
  177. package/src/ds-switch/ds-switch.test.js +226 -0
  178. package/src/ds-switch/index.js +1 -0
  179. package/src/ds-tab-item/ds-tab-item.js +341 -0
  180. package/src/ds-tab-item/ds-tab-item.stories.js +69 -0
  181. package/src/ds-tabs/ds-tab-panel.js +48 -0
  182. package/src/ds-tabs/ds-tabs.a11y.test.js +56 -0
  183. package/src/ds-tabs/ds-tabs.js +180 -0
  184. package/src/ds-tabs/ds-tabs.stories.js +152 -0
  185. package/src/ds-tabs/ds-tabs.test.js +306 -0
  186. package/src/ds-tabs/index.js +3 -0
  187. package/src/ds-tag-action/ds-tag-action.a11y.test.js +32 -0
  188. package/src/ds-tag-action/ds-tag-action.js +185 -0
  189. package/src/ds-tag-action/ds-tag-action.stories.js +55 -0
  190. package/src/ds-tag-action/ds-tag-action.test.js +44 -0
  191. package/src/ds-tag-removable/ds-tag-removable.a11y.test.js +24 -0
  192. package/src/ds-tag-removable/ds-tag-removable.js +146 -0
  193. package/src/ds-tag-removable/ds-tag-removable.stories.js +52 -0
  194. package/src/ds-tag-removable/ds-tag-removable.test.js +46 -0
  195. package/src/ds-tag-status/ds-tag-status.a11y.test.js +93 -0
  196. package/src/ds-tag-status/ds-tag-status.js +164 -0
  197. package/src/ds-tag-status/ds-tag-status.stories.js +200 -0
  198. package/src/ds-tag-status/ds-tag-status.test.js +140 -0
  199. package/src/ds-tag-status/index.js +1 -0
  200. package/src/ds-textarea/ds-textarea-clearable.test.js +89 -0
  201. package/src/ds-textarea/ds-textarea.a11y.test.js +66 -0
  202. package/src/ds-textarea/ds-textarea.js +505 -0
  203. package/src/ds-textarea/ds-textarea.stories.js +335 -0
  204. package/src/ds-textarea/ds-textarea.test.js +218 -0
  205. package/src/ds-textarea/index.js +1 -0
  206. package/src/ds-thumbnail/ds-thumbnail.js +207 -0
  207. package/src/ds-thumbnail/ds-thumbnail.stories.js +217 -0
  208. package/src/ds-thumbnail/ds-thumbnail.test.js +220 -0
  209. package/src/ds-toast/ds-toast-provider.js +110 -0
  210. package/src/ds-toast/ds-toast.a11y.test.js +34 -0
  211. package/src/ds-toast/ds-toast.js +243 -0
  212. package/src/ds-toast/ds-toast.stories.js +143 -0
  213. package/src/ds-toast/ds-toast.test.js +93 -0
  214. package/src/ds-toast/index.js +2 -0
  215. package/src/ds-tooltip/ds-tooltip.a11y.test.js +110 -0
  216. package/src/ds-tooltip/ds-tooltip.js +217 -0
  217. package/src/ds-tooltip/ds-tooltip.mdx +75 -0
  218. package/src/ds-tooltip/ds-tooltip.stories.js +72 -0
  219. package/src/ds-tooltip/ds-tooltip.test.js +191 -0
  220. package/src/ds-tooltip/index.js +1 -0
  221. package/src/ds-tooltip/positioner.js +117 -0
  222. package/src/index.js +50 -0
  223. package/src/mixins/field-label.mixin.js +113 -0
  224. package/src/mixins/field-message.mixin.js +66 -0
  225. package/src/token-provider/index.js +1 -0
  226. package/src/token-provider/token-provider.a11y.test.js +44 -0
  227. package/src/token-provider/token-provider.js +85 -0
  228. package/src/token-provider/token-provider.stories.js +105 -0
  229. package/src/token-provider/token-provider.test.js +134 -0
  230. package/src/utils/number-input.utils.js +42 -0
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import axe from 'axe-core';
3
+ import './ds-radio.js';
4
+
5
+ describe('ds-radio a11y', () => {
6
+ let container;
7
+
8
+ beforeEach(() => {
9
+ container = document.createElement('div');
10
+ document.body.appendChild(container);
11
+ });
12
+
13
+ afterEach(() => {
14
+ container.remove();
15
+ });
16
+
17
+ it('should pass axe accessibility checks in default state', async () => {
18
+ container.innerHTML = '<ds-radio label="Option one"></ds-radio>';
19
+ await new Promise(resolve => setTimeout(resolve, 100));
20
+
21
+ const results = await axe.run(container, {
22
+ rules: { 'color-contrast': { enabled: false } }
23
+ });
24
+ if (results.violations.length > 0) {
25
+ console.log('Violations:', JSON.stringify(results.violations, null, 2));
26
+ }
27
+ expect(results.violations).toHaveLength(0);
28
+ });
29
+
30
+ it('should pass axe accessibility checks when checked', async () => {
31
+ container.innerHTML = '<ds-radio label="Selected option" checked></ds-radio>';
32
+ await new Promise(resolve => setTimeout(resolve, 100));
33
+
34
+ const results = await axe.run(container, {
35
+ rules: { 'color-contrast': { enabled: false } }
36
+ });
37
+ expect(results.violations).toHaveLength(0);
38
+ });
39
+
40
+ it('should pass axe accessibility checks when disabled', async () => {
41
+ container.innerHTML = '<ds-radio label="Disabled option" disabled></ds-radio>';
42
+ await new Promise(resolve => setTimeout(resolve, 100));
43
+
44
+ const results = await axe.run(container, {
45
+ rules: { 'color-contrast': { enabled: false } }
46
+ });
47
+ expect(results.violations).toHaveLength(0);
48
+ });
49
+
50
+ it('should pass axe accessibility checks in error state', async () => {
51
+ container.innerHTML = '<ds-radio label="Error option" validation-status="error"></ds-radio>';
52
+ await new Promise(resolve => setTimeout(resolve, 100));
53
+
54
+ const results = await axe.run(container, {
55
+ rules: { 'color-contrast': { enabled: false } }
56
+ });
57
+ expect(results.violations).toHaveLength(0);
58
+ });
59
+
60
+ it('should pass axe accessibility checks when required', async () => {
61
+ container.innerHTML = '<ds-radio label="Required option" required></ds-radio>';
62
+ await new Promise(resolve => setTimeout(resolve, 100));
63
+
64
+ const results = await axe.run(container, {
65
+ rules: { 'color-contrast': { enabled: false } }
66
+ });
67
+ expect(results.violations).toHaveLength(0);
68
+ });
69
+ });
@@ -0,0 +1,240 @@
1
+ import { LitElement, html, css } from 'lit';
2
+
3
+ /**
4
+ * A radio button component for exclusive selection within a group.
5
+ *
6
+ * @element ds-radio
7
+ *
8
+ * @prop {Boolean} checked - Whether the radio is selected
9
+ * @prop {Boolean} disabled - Whether the radio is disabled
10
+ * @prop {Boolean} required - Whether the radio is required
11
+ * @prop {String} name - Name for form association (should match group)
12
+ * @prop {String} value - Value for form submission
13
+ * @prop {String} label - Label text displayed next to the radio
14
+ * @prop {String} validation-status - Validation state: 'error' or empty
15
+ *
16
+ * @fires change - Fired when the radio is selected
17
+ */
18
+ export class DsRadio extends LitElement {
19
+ static properties = {
20
+ checked: { type: Boolean, reflect: true },
21
+ disabled: { type: Boolean, reflect: true },
22
+ required: { type: Boolean, reflect: true },
23
+ name: { type: String },
24
+ value: { type: String },
25
+ label: { type: String },
26
+ validationStatus: { type: String, attribute: 'validation-status', reflect: true }
27
+ };
28
+
29
+ static styles = css`
30
+ :host {
31
+ display: inline-flex;
32
+ align-items: flex-start;
33
+ }
34
+
35
+ /* Wrapper */
36
+ .radio-wrapper {
37
+ display: flex;
38
+ align-items: flex-start; /* Align to top to handle multiline labels if needed */
39
+ cursor: pointer;
40
+ padding: var(--ds-size-6) var(--ds-space-sm); /* 6px vertical (to get 32px total with 20px content), 8px horizontal */
41
+ min-height: var(--ds-size-32);
42
+ box-sizing: border-box;
43
+ border-radius: var(--ds-radius-container); /* 0px */
44
+ transition: background-color 0.2s;
45
+ position: relative;
46
+ }
47
+
48
+ :host([disabled]) .radio-wrapper {
49
+ cursor: not-allowed;
50
+ }
51
+
52
+ /* Hover state - Covers control and label */
53
+ :host(:not([disabled])) .radio-wrapper:hover {
54
+ background-color: var(--ds-color-bg-hover);
55
+ }
56
+
57
+ /* Control Container - 20px height to match label line-height */
58
+ .control-container {
59
+ display: flex;
60
+ align-items: center;
61
+ justify-content: center;
62
+ height: var(--ds-size-20);
63
+ flex-shrink: 0;
64
+ }
65
+
66
+ /* Control (The Circle) - 16px to match checkbox */
67
+ .control {
68
+ width: var(--ds-size-16);
69
+ height: var(--ds-size-16);
70
+ border: 2px solid var(--ds-color-border-strongest);
71
+ border-radius: var(--ds-size-999);
72
+ background-color: transparent;
73
+ display: flex;
74
+ align-items: center;
75
+ justify-content: center;
76
+ box-sizing: border-box;
77
+ transition: background-color 0.15s ease, border-color 0.15s ease;
78
+ }
79
+
80
+ /* Inner Dot (shown when checked) - 8px for 16px control */
81
+ .dot {
82
+ width: 8px;
83
+ height: 8px;
84
+ border-radius: var(--ds-size-999);
85
+ background-color: var(--ds-color-bg-brand);
86
+ opacity: 0;
87
+ transform: scale(0);
88
+ transition: opacity 0.15s ease, transform 0.15s ease;
89
+ }
90
+
91
+ :host([checked]) .dot {
92
+ opacity: 1;
93
+ transform: scale(1);
94
+ }
95
+
96
+ /* Checked State - keep border visible, dot handles the visual indicator */
97
+ :host([checked]) .control {
98
+ border-color: var(--ds-color-border-brand);
99
+ }
100
+
101
+ /* Label Styles */
102
+ .label {
103
+ margin-inline-start: var(--ds-space-sm);
104
+ font: var(--ds-typo-content-body-regular);
105
+ color: var(--ds-color-text-default);
106
+ user-select: none;
107
+ }
108
+
109
+ /* Focus Visible */
110
+ .radio-wrapper:has(input:focus-visible) {
111
+ outline: 2px solid var(--ds-color-border-focus);
112
+ outline-offset: 0;
113
+ }
114
+
115
+ /* Native Input (Hidden but accessible) */
116
+ input {
117
+ position: absolute;
118
+ opacity: 0;
119
+ width: 100%;
120
+ height: 100%;
121
+ top: 0;
122
+ left: 0;
123
+ margin: 0;
124
+ cursor: inherit;
125
+ }
126
+
127
+ /* Disabled State */
128
+ :host([disabled]) .radio-wrapper:hover {
129
+ background-color: transparent;
130
+ }
131
+
132
+ :host([disabled]) .label {
133
+ color: var(--ds-color-text-disabled);
134
+ }
135
+
136
+ /* Disabled State Control */
137
+ :host([disabled]) .control {
138
+ background-color: transparent;
139
+ border-color: var(--ds-color-icon-disabled);
140
+ }
141
+
142
+ :host([disabled][checked]) .control {
143
+ background-color: var(--ds-color-bg-disabled);
144
+ }
145
+
146
+ /* Disabled Dot Color */
147
+ :host([disabled]) .dot {
148
+ background-color: var(--ds-color-icon-disabled);
149
+ }
150
+
151
+ /* Error State */
152
+ :host([validation-status="error"]) .control {
153
+ border-color: var(--ds-color-border-error);
154
+ }
155
+
156
+ :host([validation-status="error"][checked]) .control {
157
+ border-color: var(--ds-color-border-error);
158
+ }
159
+
160
+ /* Error State Dot Color */
161
+ :host([validation-status="error"][checked]) .dot {
162
+ background-color: var(--ds-color-bg-error);
163
+ }
164
+ `;
165
+
166
+ constructor() {
167
+ super();
168
+ this.checked = false;
169
+ this.disabled = false;
170
+ this.required = false;
171
+ this.name = '';
172
+ this.value = '';
173
+ this.label = '';
174
+ this.validationStatus = '';
175
+ }
176
+
177
+ _handleClick(e) {
178
+ if (this.disabled) return;
179
+ if (this.checked) return; // Radio buttons don't uncheck on click
180
+
181
+ e.preventDefault();
182
+
183
+ // Handle mutual exclusivity within the same root (Document or ShadowRoot)
184
+ const root = this.getRootNode();
185
+ const name = this.getAttribute('name');
186
+
187
+ if (name) {
188
+ const siblings = root.querySelectorAll(`ds-radio[name="${name}"]`);
189
+ siblings.forEach(sibling => {
190
+ if (sibling !== this) {
191
+ sibling.checked = false;
192
+ }
193
+ });
194
+ }
195
+
196
+ this.checked = true;
197
+ this._dispatchChange();
198
+ }
199
+
200
+ _dispatchChange() {
201
+ this.dispatchEvent(new CustomEvent('change', {
202
+ detail: {
203
+ checked: this.checked,
204
+ value: this.value
205
+ },
206
+ bubbles: true,
207
+ composed: true
208
+ }));
209
+ }
210
+
211
+ _getAriaLabel() {
212
+ return this.getAttribute('aria-label') || this.label || '';
213
+ }
214
+
215
+ render() {
216
+ return html`
217
+ <div class="radio-wrapper" @click="${this._handleClick}">
218
+ <input
219
+ type="radio"
220
+ .checked="${this.checked}"
221
+ ?disabled="${this.disabled}"
222
+ ?required="${this.required}"
223
+ name="${this.name}"
224
+ value="${this.value}"
225
+ aria-label="${this._getAriaLabel()}"
226
+ >
227
+
228
+ <div class="control-container">
229
+ <div class="control">
230
+ <div class="dot"></div>
231
+ </div>
232
+ </div>
233
+
234
+ ${this.label ? html`<span class="label">${this.label}</span>` : ''}
235
+ </div>
236
+ `;
237
+ }
238
+ }
239
+
240
+ customElements.define('ds-radio', DsRadio);
@@ -0,0 +1,102 @@
1
+ import { html } from 'lit';
2
+ import './ds-radio.js';
3
+ import '../token-provider/token-provider.js';
4
+
5
+ export default {
6
+ title: 'Components/Radio',
7
+ component: 'ds-radio',
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ label: { control: 'text' },
11
+ checked: { control: 'boolean' },
12
+ disabled: { control: 'boolean' },
13
+ required: { control: 'boolean' },
14
+ name: { control: 'text' },
15
+ value: { control: 'text' },
16
+ validationStatus: {
17
+ control: 'select',
18
+ options: ['', 'error']
19
+ }
20
+ },
21
+ args: {
22
+ label: 'Option label',
23
+ checked: false,
24
+ disabled: false,
25
+ required: false,
26
+ name: 'radio-group',
27
+ value: 'option-1',
28
+ validationStatus: ''
29
+ }
30
+ };
31
+
32
+ export const Default = {
33
+ args: {
34
+ label: 'Default option'
35
+ }
36
+ };
37
+
38
+ export const Checked = {
39
+ args: {
40
+ label: 'Selected option',
41
+ checked: true
42
+ }
43
+ };
44
+
45
+ export const Disabled = {
46
+ args: {
47
+ label: 'This option is disabled',
48
+ disabled: true
49
+ }
50
+ };
51
+
52
+ export const DisabledChecked = {
53
+ args: {
54
+ label: 'Disabled selected option',
55
+ checked: true,
56
+ disabled: true
57
+ }
58
+ };
59
+
60
+ export const WithError = {
61
+ args: {
62
+ label: 'Option with validation error',
63
+ validationStatus: 'error'
64
+ }
65
+ };
66
+
67
+ export const WithErrorChecked = {
68
+ args: {
69
+ label: 'Selected option with error',
70
+ checked: true,
71
+ validationStatus: 'error'
72
+ }
73
+ };
74
+
75
+ export const RadioGroup = {
76
+ render: () => html`
77
+ <div style="display: flex; flex-direction: column; gap: 8px;">
78
+ <ds-radio name="group-demo" value="option-1" label="First option" checked></ds-radio>
79
+ <ds-radio name="group-demo" value="option-2" label="Second option"></ds-radio>
80
+ <ds-radio name="group-demo" value="option-3" label="Third option"></ds-radio>
81
+ </div>
82
+ `
83
+ };
84
+
85
+ export const AllStates = {
86
+ render: () => html`
87
+ <div style="display: flex; flex-direction: column; gap: 16px;">
88
+ <div style="display: flex; gap: 24px; align-items: center;">
89
+ <ds-radio label="Default"></ds-radio>
90
+ <ds-radio label="Checked" checked></ds-radio>
91
+ </div>
92
+ <div style="display: flex; gap: 24px; align-items: center;">
93
+ <ds-radio label="Disabled" disabled></ds-radio>
94
+ <ds-radio label="Disabled checked" disabled checked></ds-radio>
95
+ </div>
96
+ <div style="display: flex; gap: 24px; align-items: center;">
97
+ <ds-radio label="Error" validation-status="error"></ds-radio>
98
+ <ds-radio label="Error checked" validation-status="error" checked></ds-radio>
99
+ </div>
100
+ </div>
101
+ `
102
+ };
@@ -0,0 +1,114 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import './ds-radio.js';
3
+
4
+ describe('ds-radio', () => {
5
+ let container;
6
+
7
+ beforeEach(() => {
8
+ container = document.createElement('div');
9
+ document.body.appendChild(container);
10
+ });
11
+
12
+ afterEach(() => {
13
+ container.remove();
14
+ });
15
+
16
+ it('renders with default values', async () => {
17
+ container.innerHTML = '<ds-radio></ds-radio>';
18
+ const element = container.querySelector('ds-radio');
19
+ await new Promise(resolve => setTimeout(resolve, 0));
20
+
21
+ expect(element.checked).toBe(false);
22
+ expect(element.disabled).toBe(false);
23
+ expect(element.shadowRoot.querySelector('.control')).toBeTruthy();
24
+ expect(element.shadowRoot.querySelector('.dot')).toBeTruthy();
25
+ });
26
+
27
+ it('renders label when provided', async () => {
28
+ container.innerHTML = '<ds-radio label="Test Label"></ds-radio>';
29
+ const element = container.querySelector('ds-radio');
30
+ await new Promise(resolve => setTimeout(resolve, 0));
31
+
32
+ const label = element.shadowRoot.querySelector('.label');
33
+ expect(label).toBeTruthy();
34
+ expect(label.textContent).toBe('Test Label');
35
+ });
36
+
37
+ it('reflects checked property to attribute', async () => {
38
+ container.innerHTML = '<ds-radio></ds-radio>';
39
+ const element = container.querySelector('ds-radio');
40
+ await new Promise(resolve => setTimeout(resolve, 0));
41
+
42
+ element.checked = true;
43
+ await new Promise(resolve => setTimeout(resolve, 0));
44
+ expect(element.hasAttribute('checked')).toBe(true);
45
+ });
46
+
47
+ it('handles click events to select', async () => {
48
+ container.innerHTML = '<ds-radio></ds-radio>';
49
+ const element = container.querySelector('ds-radio');
50
+ await new Promise(resolve => setTimeout(resolve, 0));
51
+
52
+ const wrapper = element.shadowRoot.querySelector('.radio-wrapper');
53
+
54
+ // Click to check
55
+ wrapper.click();
56
+ await new Promise(resolve => setTimeout(resolve, 0));
57
+ expect(element.checked).toBe(true);
58
+
59
+ // Click again should NOT uncheck (radio behavior)
60
+ wrapper.click();
61
+ await new Promise(resolve => setTimeout(resolve, 0));
62
+ expect(element.checked).toBe(true);
63
+ });
64
+
65
+ it('dispatches change event on interaction', async () => {
66
+ container.innerHTML = '<ds-radio value="test-value"></ds-radio>';
67
+ const element = container.querySelector('ds-radio');
68
+ await new Promise(resolve => setTimeout(resolve, 0));
69
+
70
+ let eventDetail = null;
71
+ element.addEventListener('change', (e) => {
72
+ eventDetail = e.detail;
73
+ });
74
+
75
+ const wrapper = element.shadowRoot.querySelector('.radio-wrapper');
76
+ wrapper.click();
77
+ await new Promise(resolve => setTimeout(resolve, 0));
78
+
79
+ expect(eventDetail).toBeTruthy();
80
+ expect(eventDetail.checked).toBe(true);
81
+ expect(eventDetail.value).toBe('test-value');
82
+ });
83
+
84
+ it('does not toggle when disabled', async () => {
85
+ container.innerHTML = '<ds-radio disabled></ds-radio>';
86
+ const element = container.querySelector('ds-radio');
87
+ await new Promise(resolve => setTimeout(resolve, 0));
88
+
89
+ const wrapper = element.shadowRoot.querySelector('.radio-wrapper');
90
+ wrapper.click();
91
+ await new Promise(resolve => setTimeout(resolve, 0));
92
+
93
+ expect(element.checked).toBe(false);
94
+ });
95
+
96
+ it('sets name and value on internal input', async () => {
97
+ container.innerHTML = '<ds-radio name="test-radio" value="accepted"></ds-radio>';
98
+ const element = container.querySelector('ds-radio');
99
+ await new Promise(resolve => setTimeout(resolve, 0));
100
+
101
+ const input = element.shadowRoot.querySelector('input');
102
+ expect(input.name).toBe('test-radio');
103
+ expect(input.value).toBe('accepted');
104
+ });
105
+
106
+ it('uses aria-label from host if provided', async () => {
107
+ container.innerHTML = '<ds-radio aria-label="custom accessible name"></ds-radio>';
108
+ const element = container.querySelector('ds-radio');
109
+ await new Promise(resolve => setTimeout(resolve, 0));
110
+
111
+ const input = element.shadowRoot.querySelector('input');
112
+ expect(input.getAttribute('aria-label')).toBe('custom accessible name');
113
+ });
114
+ });
@@ -0,0 +1 @@
1
+ export { DsRadio } from './ds-radio.js';
@@ -0,0 +1,164 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import axe from 'axe-core';
3
+ import './ds-radio-group.js';
4
+ import '../ds-radio/ds-radio.js';
5
+
6
+ describe('ds-radio-group a11y', () => {
7
+ let container;
8
+
9
+ beforeEach(() => {
10
+ container = document.createElement('div');
11
+ document.body.appendChild(container);
12
+ });
13
+
14
+ afterEach(() => {
15
+ container.remove();
16
+ });
17
+
18
+ it('should pass axe accessibility checks with label and children', async () => {
19
+ container.innerHTML = `
20
+ <ds-radio-group label="Payment method" name="payment">
21
+ <ds-radio label="Credit card" value="credit"></ds-radio>
22
+ <ds-radio label="Debit card" value="debit"></ds-radio>
23
+ <ds-radio label="PayPal" value="paypal"></ds-radio>
24
+ </ds-radio-group>
25
+ `;
26
+ await new Promise(resolve => setTimeout(resolve, 100));
27
+
28
+ const results = await axe.run(container, {
29
+ rules: { 'color-contrast': { enabled: false } }
30
+ });
31
+ if (results.violations.length > 0) {
32
+ console.log('Violations:', JSON.stringify(results.violations, null, 2));
33
+ }
34
+ expect(results.violations).toHaveLength(0);
35
+ });
36
+
37
+ it('should pass axe accessibility checks with helper text', async () => {
38
+ container.innerHTML = `
39
+ <ds-radio-group label="Shipping speed" name="shipping" helper="Select your preferred delivery option">
40
+ <ds-radio label="Standard (5-7 days)" value="standard"></ds-radio>
41
+ <ds-radio label="Express (2-3 days)" value="express"></ds-radio>
42
+ </ds-radio-group>
43
+ `;
44
+ await new Promise(resolve => setTimeout(resolve, 100));
45
+
46
+ const results = await axe.run(container, {
47
+ rules: { 'color-contrast': { enabled: false } }
48
+ });
49
+ expect(results.violations).toHaveLength(0);
50
+ });
51
+
52
+ it('should pass axe accessibility checks in error state', async () => {
53
+ container.innerHTML = `
54
+ <ds-radio-group
55
+ label="Required selection"
56
+ name="required"
57
+ validation-status="error"
58
+ validation-message="Please select an option"
59
+ >
60
+ <ds-radio label="Option A" value="a"></ds-radio>
61
+ <ds-radio label="Option B" value="b"></ds-radio>
62
+ </ds-radio-group>
63
+ `;
64
+ await new Promise(resolve => setTimeout(resolve, 100));
65
+
66
+ const results = await axe.run(container, {
67
+ rules: { 'color-contrast': { enabled: false } }
68
+ });
69
+ expect(results.violations).toHaveLength(0);
70
+ });
71
+
72
+ it('should pass axe accessibility checks when disabled', async () => {
73
+ container.innerHTML = `
74
+ <ds-radio-group label="Disabled options" name="disabled" disabled>
75
+ <ds-radio label="Option A" value="a"></ds-radio>
76
+ <ds-radio label="Option B" value="b"></ds-radio>
77
+ </ds-radio-group>
78
+ `;
79
+ await new Promise(resolve => setTimeout(resolve, 100));
80
+
81
+ const results = await axe.run(container, {
82
+ rules: { 'color-contrast': { enabled: false } }
83
+ });
84
+ expect(results.violations).toHaveLength(0);
85
+ });
86
+
87
+ it('should pass axe accessibility checks with horizontal orientation', async () => {
88
+ container.innerHTML = `
89
+ <ds-radio-group label="Size" name="size" orientation="horizontal">
90
+ <ds-radio label="Small" value="s"></ds-radio>
91
+ <ds-radio label="Medium" value="m"></ds-radio>
92
+ <ds-radio label="Large" value="l"></ds-radio>
93
+ </ds-radio-group>
94
+ `;
95
+ await new Promise(resolve => setTimeout(resolve, 100));
96
+
97
+ const results = await axe.run(container, {
98
+ rules: { 'color-contrast': { enabled: false } }
99
+ });
100
+ expect(results.violations).toHaveLength(0);
101
+ });
102
+
103
+ it('should pass axe accessibility checks with pre-selected value', async () => {
104
+ container.innerHTML = `
105
+ <ds-radio-group label="Language" name="lang" value="en">
106
+ <ds-radio label="English" value="en"></ds-radio>
107
+ <ds-radio label="Portuguese" value="pt"></ds-radio>
108
+ <ds-radio label="Spanish" value="es"></ds-radio>
109
+ </ds-radio-group>
110
+ `;
111
+ await new Promise(resolve => setTimeout(resolve, 100));
112
+
113
+ const results = await axe.run(container, {
114
+ rules: { 'color-contrast': { enabled: false } }
115
+ });
116
+ expect(results.violations).toHaveLength(0);
117
+ });
118
+
119
+ it('should have proper radiogroup role attribute', async () => {
120
+ container.innerHTML = `
121
+ <ds-radio-group label="Test group" name="test">
122
+ <ds-radio label="Option A" value="a"></ds-radio>
123
+ </ds-radio-group>
124
+ `;
125
+ await new Promise(resolve => setTimeout(resolve, 100));
126
+
127
+ const group = container.querySelector('ds-radio-group');
128
+ const groupContent = group.shadowRoot.querySelector('.group-content');
129
+
130
+ expect(groupContent.getAttribute('role')).toBe('radiogroup');
131
+ });
132
+
133
+ it('should have aria-labelledby referencing the label', async () => {
134
+ container.innerHTML = `
135
+ <ds-radio-group label="Labeled group" name="labeled">
136
+ <ds-radio label="Option A" value="a"></ds-radio>
137
+ </ds-radio-group>
138
+ `;
139
+ await new Promise(resolve => setTimeout(resolve, 100));
140
+
141
+ const group = container.querySelector('ds-radio-group');
142
+ const groupContent = group.shadowRoot.querySelector('.group-content');
143
+ const labelId = groupContent.getAttribute('aria-labelledby');
144
+
145
+ expect(labelId).toBeTruthy();
146
+ expect(group.shadowRoot.getElementById(labelId)).toBeTruthy();
147
+ });
148
+
149
+ it('should propagate name to child radios for proper grouping', async () => {
150
+ container.innerHTML = `
151
+ <ds-radio-group label="Test group" name="test-group">
152
+ <ds-radio label="Option A" value="a"></ds-radio>
153
+ <ds-radio label="Option B" value="b"></ds-radio>
154
+ </ds-radio-group>
155
+ `;
156
+ await new Promise(resolve => setTimeout(resolve, 200));
157
+
158
+ const radios = container.querySelectorAll('ds-radio');
159
+
160
+ radios.forEach(radio => {
161
+ expect(radio.getAttribute('name')).toBe('test-group');
162
+ });
163
+ });
164
+ });