@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,71 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import axe from 'axe-core';
3
+ import './ds-header-nav-item.js';
4
+
5
+ describe('ds-header-nav-item 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 (button)', async () => {
18
+ container.innerHTML = `
19
+ <ds-header-nav-item>Dashboard</ds-header-nav-item>
20
+ `;
21
+ await new Promise(resolve => setTimeout(resolve, 100));
22
+
23
+ const results = await axe.run(container, {
24
+ rules: { 'color-contrast': { enabled: false } }
25
+ });
26
+
27
+ if (results.violations.length > 0) {
28
+ console.log('Violations:', JSON.stringify(results.violations, null, 2));
29
+ }
30
+ expect(results.violations).toHaveLength(0);
31
+ });
32
+
33
+ it('should pass axe accessibility checks when selected', async () => {
34
+ container.innerHTML = `
35
+ <ds-header-nav-item selected>Dashboard</ds-header-nav-item>
36
+ `;
37
+ await new Promise(resolve => setTimeout(resolve, 100));
38
+
39
+ const results = await axe.run(container, {
40
+ rules: { 'color-contrast': { enabled: false } }
41
+ });
42
+
43
+ expect(results.violations).toHaveLength(0);
44
+ });
45
+
46
+ it('should pass axe accessibility checks as link', async () => {
47
+ container.innerHTML = `
48
+ <ds-header-nav-item href="/dashboard">Dashboard</ds-header-nav-item>
49
+ `;
50
+ await new Promise(resolve => setTimeout(resolve, 100));
51
+
52
+ const results = await axe.run(container, {
53
+ rules: { 'color-contrast': { enabled: false } }
54
+ });
55
+
56
+ expect(results.violations).toHaveLength(0);
57
+ });
58
+
59
+ it('should pass axe accessibility checks with icon', async () => {
60
+ container.innerHTML = `
61
+ <ds-header-nav-item icon="home">Dashboard</ds-header-nav-item>
62
+ `;
63
+ await new Promise(resolve => setTimeout(resolve, 100));
64
+
65
+ const results = await axe.run(container, {
66
+ rules: { 'color-contrast': { enabled: false } }
67
+ });
68
+
69
+ expect(results.violations).toHaveLength(0);
70
+ });
71
+ });
@@ -0,0 +1,124 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import '../ds-icon/ds-icon.js';
3
+
4
+ /**
5
+ * Navigation item for the Header component.
6
+ *
7
+ * @element ds-header-nav-item
8
+ *
9
+ * @slot default - The label text
10
+ *
11
+ * @prop {string} icon - Icon name (optional)
12
+ * @prop {boolean} selected - Whether the item is active
13
+ * @prop {string} href - Link destination
14
+ */
15
+ export class DsHeaderNavItem extends LitElement {
16
+ static properties = {
17
+ icon: { type: String },
18
+ selected: { type: Boolean, reflect: true },
19
+ href: { type: String }
20
+ };
21
+
22
+ static styles = css`
23
+ :host {
24
+ display: inline-flex;
25
+ /* height: 100%; Removed to allow 32px sizing */
26
+ align-items: center;
27
+ position: relative;
28
+ cursor: pointer;
29
+ text-decoration: none;
30
+ color: var(--ds-color-text-default); /* Expects context mapping from Header */
31
+ }
32
+
33
+ .item {
34
+ display: flex;
35
+ align-items: center;
36
+ box-sizing: border-box;
37
+ /* Height 32px calculation:
38
+ Line-height (approx 20px) + Padding-top (6px) + Padding-bottom (6px) = 32px
39
+ */
40
+ padding: var(--ds-size-6) var(--ds-space-md);
41
+ gap: var(--ds-size-4);
42
+ border-radius: var(--ds-radius-container);
43
+ transition: all 0.2s;
44
+ text-decoration: none;
45
+ color: inherit;
46
+ font: var(--ds-typo-content-body-regular);
47
+ }
48
+
49
+ /* Reset button styles */
50
+ button.item {
51
+ appearance: none;
52
+ background: transparent;
53
+ border: none;
54
+ font-family: inherit;
55
+ font-size: inherit;
56
+ font: var(--ds-typo-content-body-regular);
57
+ cursor: pointer;
58
+ text-align: left;
59
+ }
60
+
61
+ /* Hover */
62
+ :host(:hover) .item {
63
+ background-color: var(--ds-color-bg-hover);
64
+ }
65
+
66
+ /* Pressed / Selected */
67
+ :host([selected]) .item,
68
+ :host(:active) .item {
69
+ background-color: var(--ds-color-bg-pressed);
70
+ font-weight: var(--ds-font-weight-bold);
71
+ }
72
+
73
+ /* Focus */
74
+ .item:focus-visible {
75
+ outline: 2px solid var(--ds-color-border-focus);
76
+ outline-offset: 0;
77
+ }
78
+
79
+ /* Selected Top Bar */
80
+ :host([selected])::after {
81
+ content: '';
82
+ position: absolute;
83
+ top: 0;
84
+ left: 0;
85
+ right: 0;
86
+ height: 2px;
87
+ background-color: var(--ds-color-bg-brand);
88
+ }
89
+
90
+ /* Ensure icon size matches specs (20px) */
91
+ ds-icon {
92
+ --size: 20px;
93
+ }
94
+ `;
95
+
96
+ constructor() {
97
+ super();
98
+ this.selected = false;
99
+ this.href = '';
100
+ }
101
+
102
+ render() {
103
+ const content = html`
104
+ ${this.icon ? html`<ds-icon name="${this.icon}"></ds-icon>` : ''}
105
+ <slot></slot>
106
+ `;
107
+
108
+ if (this.href) {
109
+ return html`
110
+ <a href="${this.href}" class="item" aria-current="${this.selected ? 'page' : undefined}">
111
+ ${content}
112
+ </a>
113
+ `;
114
+ }
115
+
116
+ return html`
117
+ <button type="button" class="item" aria-current="${this.selected ? 'page' : undefined}">
118
+ ${content}
119
+ </button>
120
+ `;
121
+ }
122
+ }
123
+
124
+ customElements.define('ds-header-nav-item', DsHeaderNavItem);
@@ -0,0 +1,43 @@
1
+ import { html } from 'lit';
2
+ import './ds-header-nav-item.js';
3
+ import '../ds-header-nav/ds-header-nav.js';
4
+
5
+ export default {
6
+ title: 'Components/Header Nav Item',
7
+ component: 'ds-header-nav-item',
8
+ argTypes: {
9
+ label: { control: 'text' },
10
+ icon: { control: 'text' },
11
+ selected: { control: 'boolean' },
12
+ href: { control: 'text' }
13
+ }
14
+ };
15
+
16
+ const Template = (args) => html`
17
+ <ds-header-nav-item
18
+ .icon=${args.icon}
19
+ .href=${args.href}
20
+ ?selected=${args.selected}
21
+ >
22
+ ${args.label || 'Nav Item'}
23
+ </ds-header-nav-item>
24
+ `;
25
+
26
+ export const Default = Template.bind({});
27
+ Default.args = {
28
+ label: 'Dashboard',
29
+ selected: false
30
+ };
31
+
32
+ export const Selected = Template.bind({});
33
+ Selected.args = {
34
+ label: 'Dashboard',
35
+ selected: true
36
+ };
37
+
38
+ export const WithIcon = Template.bind({});
39
+ WithIcon.args = {
40
+ label: 'Settings',
41
+ icon: 'settings',
42
+ selected: false
43
+ };
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import './ds-header-nav-item.js';
3
+
4
+ describe('ds-header-nav-item', () => {
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 label slot (as button by default)', async () => {
17
+ const element = document.createElement('ds-header-nav-item');
18
+ container.appendChild(element);
19
+ element.textContent = 'My Label';
20
+ await element.updateComplete;
21
+
22
+ expect(element.textContent).toContain('My Label');
23
+ expect(element.shadowRoot.querySelector('button')).toBeTruthy();
24
+ });
25
+
26
+ it('renders as anchor when href is provided', async () => {
27
+ const element = document.createElement('ds-header-nav-item');
28
+ container.appendChild(element);
29
+ element.href = '#';
30
+ element.textContent = 'My Link';
31
+ await element.updateComplete;
32
+
33
+ expect(element.shadowRoot.querySelector('a')).toBeTruthy();
34
+ expect(element.shadowRoot.querySelector('button')).toBeNull();
35
+ });
36
+
37
+ it('reflects selected attribute on anchor', async () => {
38
+ const element = document.createElement('ds-header-nav-item');
39
+ container.appendChild(element);
40
+ element.href = '#';
41
+ element.selected = true;
42
+ await element.updateComplete;
43
+
44
+ expect(element.hasAttribute('selected')).toBe(true);
45
+ const link = element.shadowRoot.querySelector('a');
46
+ expect(link).toBeTruthy();
47
+ expect(link.getAttribute('aria-current')).toBe('page');
48
+ });
49
+
50
+ it('reflects selected attribute on button', async () => {
51
+ const element = document.createElement('ds-header-nav-item');
52
+ container.appendChild(element);
53
+ element.selected = true;
54
+ await element.updateComplete;
55
+
56
+ expect(element.hasAttribute('selected')).toBe(true);
57
+ const btn = element.shadowRoot.querySelector('button');
58
+ expect(btn).toBeTruthy();
59
+ expect(btn.getAttribute('aria-current')).toBe('page');
60
+ });
61
+ });
@@ -0,0 +1,49 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import axe from 'axe-core';
3
+ import './ds-icon.js';
4
+
5
+ describe('ds-icon accessibility', () => {
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 have aria-hidden on decorative icons', async () => {
18
+ container.innerHTML = '<ds-icon name="star" size="md"></ds-icon>';
19
+ const icon = container.querySelector('ds-icon');
20
+
21
+ await new Promise(resolve => setTimeout(resolve, 100));
22
+
23
+ const svg = icon.shadowRoot.querySelector('svg');
24
+ expect(svg.getAttribute('aria-hidden')).toBe('true');
25
+ });
26
+
27
+ it('should pass axe accessibility checks', async () => {
28
+ container.innerHTML = `
29
+ <ds-icon name="home" size="sm"></ds-icon>
30
+ <ds-icon name="star" size="md"></ds-icon>
31
+ <ds-icon name="settings" size="lg"></ds-icon>
32
+ `;
33
+
34
+ await new Promise(resolve => setTimeout(resolve, 100));
35
+
36
+ const results = await axe.run(container);
37
+ expect(results.violations).toHaveLength(0);
38
+ });
39
+
40
+ it('should be focusable only when interactive', async () => {
41
+ container.innerHTML = '<ds-icon name="star"></ds-icon>';
42
+ const icon = container.querySelector('ds-icon');
43
+
44
+ await new Promise(resolve => setTimeout(resolve, 100));
45
+
46
+ // Decorative icons should not be focusable by default
47
+ expect(icon.tabIndex).toBe(-1);
48
+ });
49
+ });
@@ -0,0 +1,75 @@
1
+ import { LitElement, html, css } from 'lit';
2
+
3
+ /**
4
+ * Icon component using Material Symbols Sharp
5
+ *
6
+ * @element ds-icon
7
+ * @prop {string} name - Icon name (e.g., 'star', 'home', 'search')
8
+ * @prop {string} size - Icon size: 'xs' | 'sm' | 'md' | 'lg' (default: 'md')
9
+ * @prop {string} spritePath - Path to the SVG sprite file (default: '/sprite.svg')
10
+ *
11
+ * @example
12
+ * <ds-icon name="star" size="sm"></ds-icon>
13
+ * <ds-icon name="home" sprite-path="/assets/icons.svg"></ds-icon>
14
+ */
15
+ export class DsIcon extends LitElement {
16
+ static properties = {
17
+ name: { type: String },
18
+ size: { type: String, reflect: true },
19
+ spritePath: { type: String, attribute: 'sprite-path' }
20
+ };
21
+
22
+ static styles = css`
23
+ :host {
24
+ display: inline-block;
25
+ vertical-align: middle;
26
+ }
27
+
28
+ svg {
29
+ fill: currentColor;
30
+ width: var(--size);
31
+ height: var(--size);
32
+ display: block;
33
+ }
34
+
35
+ /* Size variants using design tokens */
36
+ :host([size="xs"]) {
37
+ --size: var(--ds-icon-size-xs, 16px);
38
+ }
39
+
40
+ :host([size="sm"]) {
41
+ --size: var(--ds-icon-size-sm, 20px);
42
+ }
43
+
44
+ :host([size="md"]),
45
+ :host(:not([size])) {
46
+ --size: var(--ds-icon-size-md, 32px);
47
+ }
48
+
49
+ :host([size="lg"]) {
50
+ --size: var(--ds-icon-size-lg, 48px);
51
+ }
52
+ `;
53
+
54
+ constructor() {
55
+ super();
56
+ this.size = 'md';
57
+ this.name = '';
58
+ this.spritePath = '/sprite.svg';
59
+ }
60
+
61
+ render() {
62
+ if (!this.name) {
63
+ console.warn('ds-icon: "name" prop is required');
64
+ return html``;
65
+ }
66
+
67
+ return html`
68
+ <svg aria-hidden="true">
69
+ <use href="${this.spritePath}#${this.name}"></use>
70
+ </svg>
71
+ `;
72
+ }
73
+ }
74
+
75
+ customElements.define('ds-icon', DsIcon);
@@ -0,0 +1,36 @@
1
+ import { Meta, Canvas, Controls } from '@storybook/blocks';
2
+ import * as DsIconStories from './ds-icon.stories';
3
+
4
+ <Meta of={DsIconStories} />
5
+
6
+ # Icon
7
+
8
+ A visual element that represents an action, object, or concept.
9
+
10
+ ## Usage
11
+
12
+ ### When to use
13
+ - To reinforce the meaning of a label (e.g., a "Save" floppy disk icon next to the text).
14
+ - To represent an action in a compact way (e.g., inside an Icon Button).
15
+ - To categorize items (e.g., file types).
16
+
17
+ ### When not to use
18
+ - To replace text labels completely, unless the meaning is universally understood (like a trash can or magnifying glass).
19
+ - As decoration without semantic value if it clutters the interface.
20
+
21
+ ---
22
+
23
+ ## Best Practices
24
+
25
+ ### Accessibility
26
+ - ✅ **Decorative Icons:** If an icon is purely decorative and accompanied by text, ensure it is hidden from screen readers (the component handles this via `aria-hidden="true"` by default if no label is provided).
27
+ - ✅ **Standalone Icons:** If an icon stands alone (without text), it **must** have an accessible label provided via context or the parent component (like `ds-icon-button`).
28
+
29
+ ### Color & Sizing
30
+ - ✅ **Inherit Color:** Icons default to `currentColor`, meaning they inherit the text color of their parent. Use utility classes or token CSS variables on the parent to style them.
31
+ - ✅ **Standard Sizes:** Always use the defined size props (`xs`, `sm`, `md`, `lg`) to ensure consistency across the UI.
32
+
33
+ ---
34
+
35
+ <Canvas of={DsIconStories.Default} />
36
+ <Controls />
@@ -0,0 +1,88 @@
1
+ import './ds-icon.js';
2
+ import { iconNames } from '../../../icons/build/icons.js';
3
+
4
+
5
+ export default {
6
+ title: 'Components/Icon',
7
+ component: 'ds-icon',
8
+ argTypes: {
9
+ name: {
10
+ control: 'select',
11
+ options: iconNames,
12
+ description: 'Icon name from Material Symbols Sharp',
13
+ },
14
+ size: {
15
+ control: 'radio',
16
+ options: ['xs', 'sm', 'md', 'lg'],
17
+ description: 'Icon size',
18
+ },
19
+ },
20
+ };
21
+
22
+ export const Default = {
23
+ args: {
24
+ name: 'star',
25
+ size: 'md',
26
+ },
27
+ render: ({ name, size }) => {
28
+ const icon = document.createElement('ds-icon');
29
+ icon.setAttribute('name', name);
30
+ icon.setAttribute('size', size);
31
+ return icon;
32
+ },
33
+ };
34
+
35
+ export const AllSizes = {
36
+ render: () => {
37
+ const container = document.createElement('div');
38
+ container.style.cssText = 'display: flex; align-items: center; gap: 2rem;';
39
+
40
+ ['xs', 'sm', 'md', 'lg'].forEach(size => {
41
+ const wrapper = document.createElement('div');
42
+ wrapper.style.cssText = 'display: flex; flex-direction: column; align-items: center; gap: 0.5rem;';
43
+
44
+ const icon = document.createElement('ds-icon');
45
+ icon.setAttribute('name', 'star');
46
+ icon.setAttribute('size', size);
47
+
48
+ const label = document.createElement('div');
49
+ label.style.cssText = 'font: var(--ds-typo-content-caption-regular); color: var(--ds-color-text-secondary);';
50
+ label.textContent = size.toUpperCase();
51
+
52
+ wrapper.appendChild(icon);
53
+ wrapper.appendChild(label);
54
+ container.appendChild(wrapper);
55
+ });
56
+
57
+ return container;
58
+ },
59
+ };
60
+
61
+
62
+ export const CommonIcons = {
63
+ render: () => {
64
+ const container = document.createElement('div');
65
+ container.style.cssText = 'display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 1.5rem;';
66
+
67
+ const commonIcons = ['home', 'star', 'settings', 'search', 'menu', 'close', 'check', 'delete', 'edit', 'add', 'remove', 'help'];
68
+
69
+ commonIcons.forEach(iconName => {
70
+ const wrapper = document.createElement('div');
71
+ wrapper.style.cssText = 'display: flex; flex-direction: column; align-items: center; gap: 0.5rem;';
72
+
73
+ const icon = document.createElement('ds-icon');
74
+ icon.setAttribute('name', iconName);
75
+ icon.setAttribute('size', 'md');
76
+
77
+ const label = document.createElement('div');
78
+ label.style.cssText = 'font: var(--ds-typo-content-caption-regular); color: var(--ds-color-text-secondary); text-align: center;';
79
+ label.textContent = iconName;
80
+
81
+ wrapper.appendChild(icon);
82
+ wrapper.appendChild(label);
83
+ container.appendChild(wrapper);
84
+ });
85
+
86
+ return container;
87
+ },
88
+ };
@@ -0,0 +1,97 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import './ds-icon.js';
3
+
4
+ describe('ds-icon', () => {
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('should render with valid name prop', async () => {
17
+ container.innerHTML = '<ds-icon name="home"></ds-icon>';
18
+ const icon = container.querySelector('ds-icon');
19
+
20
+ await new Promise(resolve => setTimeout(resolve, 100));
21
+
22
+ const svg = icon.shadowRoot.querySelector('svg');
23
+ expect(svg).toBeTruthy();
24
+
25
+ const use = svg.querySelector('use');
26
+ expect(use.getAttribute('href')).toContain('#home');
27
+ });
28
+
29
+ it('should not render when name is empty', async () => {
30
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
31
+
32
+ container.innerHTML = '<ds-icon></ds-icon>';
33
+ const icon = container.querySelector('ds-icon');
34
+
35
+ await new Promise(resolve => setTimeout(resolve, 100));
36
+
37
+ const svg = icon.shadowRoot.querySelector('svg');
38
+ expect(svg).toBeFalsy();
39
+ expect(consoleWarnSpy).toHaveBeenCalledWith('ds-icon: "name" prop is required');
40
+
41
+ consoleWarnSpy.mockRestore();
42
+ });
43
+
44
+ it('should apply size attribute correctly', async () => {
45
+ const sizes = ['xs', 'sm', 'md', 'lg'];
46
+
47
+ for (const size of sizes) {
48
+ container.innerHTML = `<ds-icon name="star" size="${size}"></ds-icon>`;
49
+ const icon = container.querySelector('ds-icon');
50
+
51
+ await new Promise(resolve => setTimeout(resolve, 100));
52
+
53
+ expect(icon.getAttribute('size')).toBe(size);
54
+ expect(icon.size).toBe(size);
55
+ }
56
+ });
57
+
58
+ it('should default to md size when size is not specified', async () => {
59
+ container.innerHTML = '<ds-icon name="star"></ds-icon>';
60
+ const icon = container.querySelector('ds-icon');
61
+
62
+ await new Promise(resolve => setTimeout(resolve, 100));
63
+
64
+ expect(icon.size).toBe('md');
65
+ });
66
+
67
+ it('should inherit color from parent (currentColor)', async () => {
68
+ container.style.color = 'rgb(255, 0, 0)';
69
+ container.innerHTML = '<ds-icon name="star"></ds-icon>';
70
+ const icon = container.querySelector('ds-icon');
71
+
72
+ await new Promise(resolve => setTimeout(resolve, 100));
73
+
74
+ const svg = icon.shadowRoot.querySelector('svg');
75
+ const computedStyle = window.getComputedStyle(svg);
76
+
77
+ // SVG should use currentColor which inherits from parent
78
+ expect(computedStyle.fill).toBe('rgb(255, 0, 0)');
79
+ });
80
+
81
+ it('should update when name prop changes', async () => {
82
+ container.innerHTML = '<ds-icon name="home"></ds-icon>';
83
+ const icon = container.querySelector('ds-icon');
84
+
85
+ await new Promise(resolve => setTimeout(resolve, 100));
86
+
87
+ let use = icon.shadowRoot.querySelector('use');
88
+ expect(use.getAttribute('href')).toContain('#home');
89
+
90
+ // Change the name
91
+ icon.setAttribute('name', 'star');
92
+ await new Promise(resolve => setTimeout(resolve, 100));
93
+
94
+ use = icon.shadowRoot.querySelector('use');
95
+ expect(use.getAttribute('href')).toContain('#star');
96
+ });
97
+ });
@@ -0,0 +1 @@
1
+ export { DsIcon } from './ds-icon.js';