@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,256 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import './ds-alert.js';
3
+
4
+ describe('ds-alert', () => {
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-alert>Test message</ds-alert>';
18
+ const element = container.querySelector('ds-alert');
19
+ await new Promise(resolve => setTimeout(resolve, 50));
20
+
21
+ expect(element.status).toBe('info');
22
+ expect(element.dismissible).toBe(false);
23
+ });
24
+
25
+ it('renders message in default slot', async () => {
26
+ container.innerHTML = '<ds-alert>This is a test message</ds-alert>';
27
+ const element = container.querySelector('ds-alert');
28
+ await new Promise(resolve => setTimeout(resolve, 50));
29
+
30
+ const slot = element.shadowRoot.querySelector('slot:not([name])');
31
+ const assignedNodes = slot.assignedNodes();
32
+ expect(assignedNodes.length).toBeGreaterThan(0);
33
+ expect(assignedNodes[0].textContent).toBe('This is a test message');
34
+ });
35
+
36
+ it('renders title in title slot', async () => {
37
+ container.innerHTML = '<ds-alert><strong slot="title">Alert Title</strong>Message</ds-alert>';
38
+ const element = container.querySelector('ds-alert');
39
+ await new Promise(resolve => setTimeout(resolve, 50));
40
+
41
+ const titleSlot = element.shadowRoot.querySelector('slot[name="title"]');
42
+ const assignedNodes = titleSlot.assignedNodes();
43
+ expect(assignedNodes.length).toBeGreaterThan(0);
44
+ expect(assignedNodes[0].textContent).toBe('Alert Title');
45
+ });
46
+
47
+ it('renders correct icon for each status', async () => {
48
+ const statusIcons = {
49
+ error: 'cancel',
50
+ warning: 'warning',
51
+ success: 'check-circle',
52
+ info: 'info'
53
+ };
54
+
55
+ for (const [status, iconName] of Object.entries(statusIcons)) {
56
+ container.innerHTML = `<ds-alert status="${status}">Test</ds-alert>`;
57
+ const element = container.querySelector('ds-alert');
58
+ await new Promise(resolve => setTimeout(resolve, 50));
59
+
60
+ const icon = element.shadowRoot.querySelector('ds-icon');
61
+ expect(icon.getAttribute('name')).toBe(iconName);
62
+ }
63
+ });
64
+
65
+ it('renders message from property', async () => {
66
+ container.innerHTML = '<ds-alert message="Property message"></ds-alert>';
67
+ const element = container.querySelector('ds-alert');
68
+ await new Promise(resolve => setTimeout(resolve, 50));
69
+
70
+ const messageDiv = element.shadowRoot.querySelector('.message');
71
+ expect(messageDiv.textContent).toBe('Property message');
72
+ });
73
+
74
+ it('renders title from property', async () => {
75
+ container.innerHTML = '<ds-alert title="Property title"></ds-alert>';
76
+ const element = container.querySelector('ds-alert');
77
+ await new Promise(resolve => setTimeout(resolve, 50));
78
+
79
+ const titleDiv = element.shadowRoot.querySelector('.title');
80
+ expect(titleDiv.textContent).toBe('Property title');
81
+ });
82
+
83
+ it('hides actions container when slot is empty', async () => {
84
+ container.innerHTML = '<ds-alert>No actions</ds-alert>';
85
+ const element = container.querySelector('ds-alert');
86
+ await new Promise(resolve => setTimeout(resolve, 50));
87
+
88
+ const actionsDiv = element.shadowRoot.querySelector('.actions');
89
+ expect(actionsDiv.hasAttribute('hidden')).toBe(true);
90
+ });
91
+
92
+ it('shows actions container when slot has content', async () => {
93
+ container.innerHTML = '<ds-alert>With actions <button slot="actions">Click</button></ds-alert>';
94
+ const element = container.querySelector('ds-alert');
95
+ await new Promise(resolve => setTimeout(resolve, 50));
96
+
97
+ const actionsDiv = element.shadowRoot.querySelector('.actions');
98
+ expect(actionsDiv.hasAttribute('hidden')).toBe(false);
99
+ });
100
+
101
+ it('reflects status attribute', async () => {
102
+ container.innerHTML = '<ds-alert status="error">Error message</ds-alert>';
103
+ const element = container.querySelector('ds-alert');
104
+ await new Promise(resolve => setTimeout(resolve, 50));
105
+
106
+ expect(element.getAttribute('status')).toBe('error');
107
+ });
108
+
109
+ it('hides close button by default', async () => {
110
+ container.innerHTML = '<ds-alert>Message</ds-alert>';
111
+ const element = container.querySelector('ds-alert');
112
+ await new Promise(resolve => setTimeout(resolve, 50));
113
+
114
+ const closeButton = element.shadowRoot.querySelector('ds-icon-button');
115
+ expect(closeButton).toBeFalsy();
116
+ });
117
+
118
+ it('shows close button when dismissible is true', async () => {
119
+ container.innerHTML = '<ds-alert dismissible>Message</ds-alert>';
120
+ const element = container.querySelector('ds-alert');
121
+ await new Promise(resolve => setTimeout(resolve, 50));
122
+
123
+ const closeButton = element.shadowRoot.querySelector('ds-icon-button');
124
+ expect(closeButton).toBeTruthy();
125
+ });
126
+
127
+ it('close button has correct properties', async () => {
128
+ container.innerHTML = '<ds-alert dismissible>Message</ds-alert>';
129
+ const element = container.querySelector('ds-alert');
130
+ await new Promise(resolve => setTimeout(resolve, 50));
131
+
132
+ const closeButton = element.shadowRoot.querySelector('ds-icon-button');
133
+ expect(closeButton.getAttribute('icon')).toBe('close');
134
+ expect(closeButton.getAttribute('variant')).toBe('action');
135
+ expect(closeButton.getAttribute('size')).toBe('m');
136
+ });
137
+
138
+ it('dispatches ds-dismiss event when close button is clicked', async () => {
139
+ container.innerHTML = '<ds-alert dismissible>Message</ds-alert>';
140
+ const element = container.querySelector('ds-alert');
141
+ await new Promise(resolve => setTimeout(resolve, 50));
142
+
143
+ let eventFired = false;
144
+ element.addEventListener('ds-dismiss', () => {
145
+ eventFired = true;
146
+ });
147
+
148
+ const closeButton = element.shadowRoot.querySelector('ds-icon-button');
149
+ closeButton.click();
150
+ await new Promise(resolve => setTimeout(resolve, 50));
151
+
152
+ expect(eventFired).toBe(true);
153
+ });
154
+
155
+ it('renders actions slot', async () => {
156
+ container.innerHTML = `
157
+ <ds-alert>
158
+ Message
159
+ <div slot="actions">
160
+ <button>Action 1</button>
161
+ <button>Action 2</button>
162
+ </div>
163
+ </ds-alert>
164
+ `;
165
+ const element = container.querySelector('ds-alert');
166
+ await new Promise(resolve => setTimeout(resolve, 50));
167
+
168
+ const actionsSlot = element.shadowRoot.querySelector('slot[name="actions"]');
169
+ const assignedNodes = actionsSlot.assignedNodes();
170
+ expect(assignedNodes.length).toBeGreaterThan(0);
171
+ });
172
+
173
+ it('applies correct background color for error status', async () => {
174
+ container.innerHTML = '<ds-alert status="error">Error</ds-alert>';
175
+ const element = container.querySelector('ds-alert');
176
+ await new Promise(resolve => setTimeout(resolve, 50));
177
+
178
+ const alertContainer = element.shadowRoot.querySelector('.alert');
179
+ const styles = getComputedStyle(alertContainer);
180
+ expect(styles.backgroundColor).toBeTruthy();
181
+ });
182
+
183
+ it('applies correct background color for warning status', async () => {
184
+ container.innerHTML = '<ds-alert status="warning">Warning</ds-alert>';
185
+ const element = container.querySelector('ds-alert');
186
+ await new Promise(resolve => setTimeout(resolve, 50));
187
+
188
+ const alertContainer = element.shadowRoot.querySelector('.alert');
189
+ const styles = getComputedStyle(alertContainer);
190
+ expect(styles.backgroundColor).toBeTruthy();
191
+ });
192
+
193
+ it('applies correct background color for success status', async () => {
194
+ container.innerHTML = '<ds-alert status="success">Success</ds-alert>';
195
+ const element = container.querySelector('ds-alert');
196
+ await new Promise(resolve => setTimeout(resolve, 50));
197
+
198
+ const alertContainer = element.shadowRoot.querySelector('.alert');
199
+ const styles = getComputedStyle(alertContainer);
200
+ expect(styles.backgroundColor).toBeTruthy();
201
+ });
202
+
203
+ it('applies correct background color for info status', async () => {
204
+ container.innerHTML = '<ds-alert status="info">Info</ds-alert>';
205
+ const element = container.querySelector('ds-alert');
206
+ await new Promise(resolve => setTimeout(resolve, 50));
207
+
208
+ const alertContainer = element.shadowRoot.querySelector('.alert');
209
+ const styles = getComputedStyle(alertContainer);
210
+ expect(styles.backgroundColor).toBeTruthy();
211
+ });
212
+
213
+ it('has role="alert" for accessibility', async () => {
214
+ container.innerHTML = '<ds-alert>Message</ds-alert>';
215
+ const element = container.querySelector('ds-alert');
216
+ await new Promise(resolve => setTimeout(resolve, 50));
217
+
218
+ const alertContainer = element.shadowRoot.querySelector('.alert');
219
+ expect(alertContainer.getAttribute('role')).toBe('alert');
220
+ });
221
+
222
+ it('icon has correct size', async () => {
223
+ container.innerHTML = '<ds-alert>Message</ds-alert>';
224
+ const element = container.querySelector('ds-alert');
225
+ await new Promise(resolve => setTimeout(resolve, 50));
226
+
227
+ const icon = element.shadowRoot.querySelector('ds-icon');
228
+ expect(icon.getAttribute('size')).toBe('sm');
229
+ });
230
+
231
+ it('can update status dynamically', async () => {
232
+ container.innerHTML = '<ds-alert status="info">Message</ds-alert>';
233
+ const element = container.querySelector('ds-alert');
234
+ await new Promise(resolve => setTimeout(resolve, 50));
235
+
236
+ element.status = 'error';
237
+ await new Promise(resolve => setTimeout(resolve, 50));
238
+
239
+ expect(element.getAttribute('status')).toBe('error');
240
+ const icon = element.shadowRoot.querySelector('ds-icon');
241
+ expect(icon.getAttribute('name')).toBe('cancel');
242
+ });
243
+
244
+ it('can toggle dismissible dynamically', async () => {
245
+ container.innerHTML = '<ds-alert>Message</ds-alert>';
246
+ const element = container.querySelector('ds-alert');
247
+ await new Promise(resolve => setTimeout(resolve, 50));
248
+
249
+ expect(element.shadowRoot.querySelector('ds-icon-button')).toBeFalsy();
250
+
251
+ element.dismissible = true;
252
+ await new Promise(resolve => setTimeout(resolve, 50));
253
+
254
+ expect(element.shadowRoot.querySelector('ds-icon-button')).toBeTruthy();
255
+ });
256
+ });
@@ -0,0 +1 @@
1
+ export { DsAlert } from './ds-alert.js';
@@ -0,0 +1,45 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import axe from 'axe-core';
3
+ import './ds-avatar.js';
4
+
5
+ describe('ds-avatar 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 be accessible with image', async () => {
18
+ container.innerHTML = '<ds-avatar src="image.jpg" alt="User Profile"></ds-avatar>';
19
+ await new Promise(resolve => setTimeout(resolve, 0));
20
+
21
+ // Use custom axe config if needed, or default
22
+ const results = await axe.run(container);
23
+ expect(results.violations).toHaveLength(0);
24
+ });
25
+
26
+ it('should be accessible with initials', async () => {
27
+ container.innerHTML = '<ds-avatar initials="MS"></ds-avatar>';
28
+ await new Promise(resolve => setTimeout(resolve, 0));
29
+
30
+ const results = await axe.run(container);
31
+ expect(results.violations).toHaveLength(0);
32
+ });
33
+
34
+ it('should be accessible as clickable button', async () => {
35
+ container.innerHTML = '<ds-avatar clickable initials="MS" aria-label="Open Profile"></ds-avatar>';
36
+ await new Promise(resolve => setTimeout(resolve, 0));
37
+
38
+ const el = container.querySelector('ds-avatar');
39
+ expect(el.getAttribute('role')).toBe('button');
40
+ expect(el.getAttribute('tabindex')).toBe('0');
41
+
42
+ const results = await axe.run(container);
43
+ expect(results.violations).toHaveLength(0);
44
+ });
45
+ });
@@ -0,0 +1,216 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import '../ds-icon/ds-icon.js';
3
+
4
+ /**
5
+ * Avatar component for displaying user representation.
6
+ *
7
+ * @element ds-avatar
8
+ *
9
+ * @prop {string} size - Avatar size: 'sm' (24px) | 'md' (32px) | 'lg' (40px) (default: 'md')
10
+ * @prop {string} src - Image source URL. Highest priority.
11
+ * @prop {string} alt - Alt text for image. Required if src is provided.
12
+ * @prop {string} initials - Initials to display if no image. 2nd priority.
13
+ * @prop {string} icon - Icon name to display if no image/initials. 3rd priority. Default: 'account_circle'
14
+ * @prop {string} backgroundColor - Custom background color (CSS value)
15
+ * @prop {boolean} clickable - Whether the avatar is interactive
16
+ *
17
+ * @event click - Emitted when clickable and clicked/activated
18
+ */
19
+ export class DsAvatar extends LitElement {
20
+ static properties = {
21
+ size: { type: String, reflect: true },
22
+ src: { type: String },
23
+ alt: { type: String },
24
+ initials: { type: String },
25
+ icon: { type: String },
26
+ backgroundColor: { type: String, attribute: 'background-color' },
27
+ clickable: { type: Boolean, reflect: true }
28
+ };
29
+
30
+ static styles = css`
31
+ :host {
32
+ display: inline-block;
33
+ vertical-align: middle;
34
+ --avatar-size: var(--ds-space-xl); /* Default md = 32px */
35
+ --avatar-font-size: var(--ds-typo-content-caption-regular); /* Default md = caption regular */
36
+ --avatar-bg: var(--ds-color-bg-neutral-subtle); /* Updated to subtle */
37
+ --avatar-color: var(--ds-color-text-default);
38
+ }
39
+
40
+ /* Sizes */
41
+ :host([size="sm"]) {
42
+ --avatar-size: var(--ds-space-lg); /* 24px */
43
+ --avatar-font-size: var(--ds-typo-content-viz-regular); /* viz regular */
44
+ }
45
+
46
+ :host([size="md"]) {
47
+ --avatar-size: var(--ds-space-xl); /* 32px */
48
+ --avatar-font-size: var(--ds-typo-content-caption-regular); /* caption regular */
49
+ }
50
+
51
+ :host([size="lg"]) {
52
+ --avatar-size: 40px;
53
+ --avatar-font-size: var(--ds-typo-content-body-regular); /* body regular */
54
+ }
55
+
56
+ .avatar {
57
+ display: flex;
58
+ align-items: center;
59
+ justify-content: center;
60
+ width: var(--avatar-size);
61
+ height: var(--avatar-size);
62
+ border-radius: var(--ds-radius-avatar, 999px);
63
+ background: var(--avatar-bg);
64
+ color: var(--avatar-color);
65
+ overflow: hidden;
66
+ user-select: none;
67
+ box-sizing: border-box;
68
+ position: relative;
69
+ }
70
+
71
+ /* Image Variant */
72
+ .avatar img {
73
+ width: 100%;
74
+ height: 100%;
75
+ object-fit: cover;
76
+ }
77
+
78
+ /* Initials Variant */
79
+ .avatar__initials {
80
+ font: var(--avatar-font-size);
81
+ text-transform: uppercase;
82
+ line-height: 1;
83
+ }
84
+
85
+ /* Icon Variant */
86
+ .avatar__icon {
87
+ display: flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ }
91
+
92
+ /* Clickable State */
93
+ :host([clickable]) .avatar {
94
+ cursor: pointer;
95
+ transition: background-color 0.2s;
96
+ }
97
+
98
+ :host([clickable]) {
99
+ outline: none;
100
+ }
101
+
102
+ :host([clickable]:focus-visible) .avatar {
103
+ outline: 2px solid var(--ds-color-border-focus);
104
+ outline-offset: 2px;
105
+ }
106
+ `;
107
+
108
+ constructor() {
109
+ super();
110
+ this.size = 'md';
111
+ this.initials = '';
112
+ this.icon = 'account_circle';
113
+ this.clickable = false;
114
+ }
115
+
116
+ updated(changedProperties) {
117
+ if (changedProperties.has('clickable')) {
118
+ if (this.clickable) {
119
+ this.setAttribute('tabindex', '0');
120
+ this.setAttribute('role', 'button');
121
+ this.addEventListener('keydown', this._handleKeyDown);
122
+ this.addEventListener('click', this._handleClick);
123
+ } else {
124
+ this.removeAttribute('tabindex');
125
+ this.removeAttribute('role');
126
+ this.removeEventListener('keydown', this._handleKeyDown);
127
+ this.removeEventListener('click', this._handleClick);
128
+ }
129
+ }
130
+ }
131
+
132
+ _handleKeyDown(e) {
133
+ if (this.clickable && (e.key === 'Enter' || e.key === ' ')) {
134
+ // Prevent scrolling for Space
135
+ e.preventDefault();
136
+ // Dispatch click event
137
+ this.click();
138
+ }
139
+ }
140
+
141
+ _handleClick(e) {
142
+ // Optional: add ripple or analytics here
143
+ }
144
+
145
+ render() {
146
+ // Custom background
147
+ const styles = {};
148
+ if (this.backgroundColor) {
149
+ styles['background-color'] = this.backgroundColor;
150
+ }
151
+
152
+ // Determine content based on priority
153
+ let content;
154
+
155
+ // 1. Image
156
+ if (this.src) {
157
+ content = html`
158
+ <img
159
+ src="${this.src}"
160
+ alt="${this.alt || ''}"
161
+ @error="${this._handleImageError}"
162
+ />
163
+ `;
164
+ }
165
+ // 2. Initials
166
+ else if (this.initials) {
167
+ content = html`
168
+ <span class="avatar__initials">${this.initials.substring(0, 2)}</span>
169
+ `;
170
+ }
171
+ // 3. Icon (Default)
172
+ else {
173
+ // Determine icon size based on avatar size
174
+ const iconSize = this.size === 'sm' ? 'xs' : 'sm'; // sm(24px)->xs(16px), md(32px)->sm(20px), lg(40px)->sm(20px)
175
+ // Actually for lg(40px), maybe md(32px) icon?
176
+ // Checking design tokens: xs=16, sm=20, md=32.
177
+ // If avatar is 32px (md), icon 20px (sm) is good.
178
+ // If avatar is 24px (sm), icon 16px (xs) is good.
179
+ // If avatar is 40px (lg), icon 20px (sm) or maybe 24px? we don't have 24px icon token yet?
180
+ // Let's stick to sm icon for lg avatar for now unless specced otherwise.
181
+
182
+ const computedIconSize = this.size === 'sm' ? 'xs' : 'sm';
183
+
184
+ content = html`
185
+ <div class="avatar__icon">
186
+ <ds-icon
187
+ name="${this.icon || 'account_circle'}"
188
+ size="${computedIconSize}">
189
+ </ds-icon>
190
+ </div>
191
+ `;
192
+ }
193
+
194
+ return html`
195
+ <div class="avatar" style="${this._getStyleString(styles)}">
196
+ ${content}
197
+ </div>
198
+ `;
199
+ }
200
+
201
+ _handleImageError(e) {
202
+ // Fallback if image fails to load?
203
+ // Simple way: clear src so it re-renders with fallback?
204
+ // For now, let's keep it simple, maybe just log or let it show broken image?
205
+ // Ideally we would trigger a state change to show fallback.
206
+ this.src = ''; // This will trigger re-render and show initials/icon
207
+ }
208
+
209
+ _getStyleString(styleObj) {
210
+ return Object.entries(styleObj)
211
+ .map(([k, v]) => `${k}:${v}`)
212
+ .join(';');
213
+ }
214
+ }
215
+
216
+ customElements.define('ds-avatar', DsAvatar);
@@ -0,0 +1,120 @@
1
+ import './ds-avatar.js';
2
+
3
+ export default {
4
+ title: 'Components/Avatar',
5
+ component: 'ds-avatar',
6
+ argTypes: {
7
+ size: {
8
+ control: 'select',
9
+ options: ['sm', 'md', 'lg'],
10
+ },
11
+ src: { control: 'text' },
12
+ alt: { control: 'text' },
13
+ initials: { control: 'text' },
14
+ icon: { control: 'text' },
15
+ backgroundColor: { control: 'color' },
16
+ clickable: { control: 'boolean' }
17
+ },
18
+ };
19
+
20
+ const createAvatar = ({ size, src, alt, initials, icon, backgroundColor, clickable }) => {
21
+ const el = document.createElement('ds-avatar');
22
+
23
+ if (size) el.setAttribute('size', size);
24
+ if (src) el.setAttribute('src', src);
25
+ if (alt) el.setAttribute('alt', alt);
26
+ if (initials) el.setAttribute('initials', initials);
27
+ if (icon) el.setAttribute('icon', icon);
28
+ if (backgroundColor) el.setAttribute('background-color', backgroundColor);
29
+ if (clickable) el.setAttribute('clickable', '');
30
+
31
+ return el;
32
+ };
33
+
34
+ export const Image = {
35
+ args: {
36
+ src: '/avatar-default.png',
37
+ alt: 'Default User',
38
+ size: 'md'
39
+ },
40
+ render: createAvatar
41
+ };
42
+
43
+ export const Initials = {
44
+ args: {
45
+ initials: 'MS',
46
+ size: 'md',
47
+ backgroundColor: '#823cdd'
48
+ },
49
+ render: createAvatar
50
+ };
51
+
52
+ export const Icon = {
53
+ args: {
54
+ icon: 'account_circle',
55
+ size: 'md'
56
+ },
57
+ render: createAvatar
58
+ };
59
+
60
+ export const CustomIcon = {
61
+ args: {
62
+ icon: 'star',
63
+ size: 'md',
64
+ backgroundColor: '#36a061'
65
+ },
66
+ render: createAvatar
67
+ };
68
+
69
+ export const Sizes = {
70
+ render: () => {
71
+ const container = document.createElement('div');
72
+ container.style.cssText = 'display: flex; align-items: center; gap: 16px;';
73
+
74
+ ['sm', 'md', 'lg'].forEach(size => {
75
+ const el = createAvatar({
76
+ size,
77
+ initials: size.toUpperCase(),
78
+ backgroundColor: '#0060ff'
79
+ });
80
+ container.appendChild(el);
81
+ });
82
+
83
+ return container;
84
+ }
85
+ };
86
+
87
+ export const FallbackChain = {
88
+ render: () => {
89
+ const container = document.createElement('div');
90
+ container.style.cssText = 'display: flex; flex-direction: column; gap: 20px;';
91
+
92
+ // 1. Image
93
+ const row1 = document.createElement('div');
94
+ row1.innerHTML = '<strong>1. Image (Highest Priority)</strong><br>';
95
+ row1.appendChild(createAvatar({ src: '/avatar-default.png', initials: 'MS', icon: 'star' }));
96
+ container.appendChild(row1);
97
+
98
+ // 2. Initials
99
+ const row2 = document.createElement('div');
100
+ row2.innerHTML = '<strong>2. Initials (Fallback from Image)</strong><br>';
101
+ row2.appendChild(createAvatar({ initials: 'MS', icon: 'star' }));
102
+ container.appendChild(row2);
103
+
104
+ // 3. Icon
105
+ const row3 = document.createElement('div');
106
+ row3.innerHTML = '<strong>3. Icon (Fallback from Initials)</strong><br>';
107
+ row3.appendChild(createAvatar({ icon: 'star' }));
108
+ container.appendChild(row3);
109
+
110
+ return container;
111
+ }
112
+ };
113
+
114
+ export const Clickable = {
115
+ args: {
116
+ src: '/avatar-default.png',
117
+ clickable: true
118
+ },
119
+ render: createAvatar
120
+ };