@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,152 @@
1
+ import { html } from 'lit';
2
+ import './ds-tabs.js';
3
+ import '../ds-tab-item/ds-tab-item.js';
4
+ import '../ds-icon/ds-icon.js';
5
+
6
+ export default {
7
+ title: 'Components/Tabs',
8
+ component: 'ds-tabs',
9
+ argTypes: {
10
+ variant: {
11
+ control: { type: 'select' },
12
+ options: ['line', 'contained'],
13
+ defaultValue: 'line',
14
+ },
15
+ },
16
+ };
17
+
18
+ const Template = (args) => html`
19
+ <style>
20
+ /* Demo container styles */
21
+ .demo-container {
22
+ font-family: var(--ds-font-family-content);
23
+ }
24
+ p {
25
+ padding: 16px;
26
+ color: var(--ds-color-text-secondary);
27
+ }
28
+ </style>
29
+ <div class="demo-container">
30
+ <ds-tabs variant=${args.variant || 'line'}>
31
+ <ds-tab-item label="First Tab" selected></ds-tab-item>
32
+ <ds-tab-item label="Second Tab"></ds-tab-item>
33
+ <ds-tab-item label="Third Tab"></ds-tab-item>
34
+ <ds-tab-item label="Disabled Tab" disabled></ds-tab-item>
35
+ </ds-tabs>
36
+ <p>Content for the selected tab would go here.</p>
37
+ </div>
38
+ `;
39
+
40
+ export const Default = Template.bind({});
41
+ Default.args = {
42
+ variant: 'line',
43
+ };
44
+
45
+ export const WithIcons = () => html`
46
+ <ds-tabs>
47
+ <ds-tab-item label="Home" selected>
48
+ <ds-icon slot="icon" name="home" size="sm"></ds-icon>
49
+ </ds-tab-item>
50
+ <ds-tab-item label="Settings">
51
+ <ds-icon slot="icon" name="settings" size="sm"></ds-icon>
52
+ </ds-tab-item>
53
+ <ds-tab-item label="Profile">
54
+ <ds-icon slot="icon" name="person" size="sm"></ds-icon>
55
+ </ds-tab-item>
56
+ </ds-tabs>
57
+ `;
58
+
59
+ export const InteractiveDemo = () => html`
60
+ <script>
61
+ // Simple script to handle tab switching for the demo
62
+ document.addEventListener('ds-tab-selected', (e) => {
63
+ // Find parent tabs
64
+ const tabs = e.target.closest('ds-tabs');
65
+ if(tabs && tabs.contains(document.currentScript?.parentElement)) {
66
+ // Logic is mainly handled by ds-tabs internal handler for selection state visualization
67
+ console.log('Tab selected:', e.target.label);
68
+ }
69
+ });
70
+ </script>
71
+ <ds-tabs>
72
+ <ds-tab-item label="Overview" selected></ds-tab-item>
73
+ <ds-tab-item label="Details"></ds-tab-item>
74
+ <ds-tab-item label="Reviews"></ds-tab-item>
75
+ </ds-tabs>
76
+ `;
77
+
78
+ export const Overflow = () => html`
79
+ <div style="max-width: 300px; border: 1px solid #ccc;">
80
+ <ds-tabs>
81
+ <ds-tab-item label="Tab 1" selected></ds-tab-item>
82
+ <ds-tab-item label="Tab 2"></ds-tab-item>
83
+ <ds-tab-item label="Tab 3"></ds-tab-item>
84
+ <ds-tab-item label="Tab 4 long label"></ds-tab-item>
85
+ <ds-tab-item label="Tab 5"></ds-tab-item>
86
+ <ds-tab-item label="Tab 6"></ds-tab-item>
87
+ </ds-tabs>
88
+ </div>
89
+ `;
90
+
91
+ import './ds-tab-panel.js';
92
+
93
+ // ... (existing exports)
94
+
95
+ // Full Example with Panels
96
+ export const Contained = Template.bind({});
97
+ Contained.args = {
98
+ variant: 'contained',
99
+ };
100
+
101
+ export const WithPanels = (args) => {
102
+ const handleTabSelected = (e) => {
103
+ console.log('Event ds-tab-selected captured in Story:', e);
104
+ const selectedTab = e.target;
105
+ const targetId = selectedTab.getAttribute('aria-controls');
106
+
107
+ // robustly find the wrapper: e.currentTarget is the ds-tabs list
108
+ const wrapper = e.currentTarget.parentElement;
109
+
110
+ if (!wrapper) {
111
+ console.warn('Wrapper not found');
112
+ return;
113
+ }
114
+
115
+ const panels = wrapper.querySelectorAll('ds-tab-panel');
116
+ panels.forEach(panel => {
117
+ const shouldSelect = panel.id === targetId;
118
+ // Use property setting AND attribute for robustness
119
+ panel.selected = shouldSelect;
120
+ if (shouldSelect) {
121
+ console.log('Activating panel:', panel.id);
122
+ }
123
+ });
124
+ };
125
+
126
+ return html`
127
+ <div class="tabs-wrapper">
128
+ <ds-tabs id="tabs-demo" @ds-tab-selected=${handleTabSelected}>
129
+ <ds-tab-item label="Account" selected aria-controls="panel-1"></ds-tab-item>
130
+ <ds-tab-item label="Notifications" aria-controls="panel-2"></ds-tab-item>
131
+ <ds-tab-item label="Security" aria-controls="panel-3"></ds-tab-item>
132
+ </ds-tabs>
133
+
134
+ <ds-tab-panel id="panel-1" aria-labelledby="tab-1" selected>
135
+ <h3>Account Settings</h3>
136
+ <p>Manage your account details properly here.</p>
137
+ </ds-tab-panel>
138
+
139
+ <ds-tab-panel id="panel-2" aria-labelledby="tab-2">
140
+ <h3>Notifications</h3>
141
+ <p>Choose your notification preferences.</p>
142
+ </ds-tab-panel>
143
+
144
+ <ds-tab-panel id="panel-3" aria-labelledby="tab-3">
145
+ <h3>Security</h3>
146
+ <p>Update your password and security settings.</p>
147
+ </ds-tab-panel>
148
+ </div>
149
+ `;
150
+ };
151
+
152
+ // Isolated TabItem story moved to ds-tab-item.stories.js
@@ -0,0 +1,306 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import './ds-tabs.js';
3
+ import '../ds-tab-item/ds-tab-item.js';
4
+
5
+ describe('ds-tabs', () => {
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('renders tabs correctly', async () => {
18
+ container.innerHTML = `
19
+ <ds-tabs>
20
+ <ds-tab-item label="Tab 1" selected></ds-tab-item>
21
+ <ds-tab-item label="Tab 2"></ds-tab-item>
22
+ </ds-tabs>
23
+ `;
24
+
25
+ // Wait for elements to be defined and rendered
26
+ await new Promise(resolve => setTimeout(resolve, 50));
27
+
28
+ const tabs = container.querySelectorAll('ds-tab-item');
29
+ expect(tabs.length).toBe(2);
30
+ expect(tabs[0].getAttribute('selected')).toBe('');
31
+ });
32
+
33
+ it('handles click selection', async () => {
34
+ container.innerHTML = `
35
+ <ds-tabs>
36
+ <ds-tab-item label="Tab 1" selected></ds-tab-item>
37
+ <ds-tab-item label="Tab 2" id="tab2"></ds-tab-item>
38
+ </ds-tabs>
39
+ `;
40
+
41
+ await new Promise(resolve => setTimeout(resolve, 50));
42
+ const tab2 = container.querySelector('#tab2');
43
+
44
+ tab2.click();
45
+
46
+ await new Promise(resolve => setTimeout(resolve, 50));
47
+
48
+ const tabs = container.querySelectorAll('ds-tab-item');
49
+ expect(tabs[0].hasAttribute('selected')).toBe(false);
50
+ expect(tabs[1].hasAttribute('selected')).toBe(true);
51
+ });
52
+
53
+ it('handles keyboard navigation (ArrowRight)', async () => {
54
+ container.innerHTML = `
55
+ <ds-tabs>
56
+ <ds-tab-item label="Tab 1" selected></ds-tab-item>
57
+ <ds-tab-item label="Tab 2"></ds-tab-item>
58
+ <ds-tab-item label="Tab 3"></ds-tab-item>
59
+ </ds-tabs>
60
+ `;
61
+
62
+ await new Promise(resolve => setTimeout(resolve, 50));
63
+ const tabs = container.querySelectorAll('ds-tab-item');
64
+ tabs[0].focus();
65
+
66
+ // Simulate ArrowRight keydown
67
+ tabs[0].dispatchEvent(new KeyboardEvent('keydown', {
68
+ key: 'ArrowRight',
69
+ bubbles: true,
70
+ composed: true
71
+ }));
72
+
73
+ await new Promise(resolve => setTimeout(resolve, 50));
74
+
75
+ // Check if next tab is selected
76
+ expect(tabs[1].selected).toBe(true);
77
+ expect(document.activeElement === tabs[1]).toBe(true);
78
+ });
79
+
80
+ // KEYBOARD NAVIGATION COMPLETE
81
+ it('ArrowLeft moves to previous tab (wraps around)', async () => {
82
+ container.innerHTML = `
83
+ <ds-tabs>
84
+ <ds-tab-item label="Tab 1" selected></ds-tab-item>
85
+ <ds-tab-item label="Tab 2"></ds-tab-item>
86
+ <ds-tab-item label="Tab 3"></ds-tab-item>
87
+ </ds-tabs>
88
+ `;
89
+ await new Promise(resolve => setTimeout(resolve, 50));
90
+ const tabs = container.querySelectorAll('ds-tab-item');
91
+ tabs[0].focus();
92
+
93
+ tabs[0].dispatchEvent(new KeyboardEvent('keydown', {
94
+ key: 'ArrowLeft',
95
+ bubbles: true,
96
+ composed: true
97
+ }));
98
+
99
+ await new Promise(resolve => setTimeout(resolve, 50));
100
+
101
+ // Should wrap to last tab
102
+ expect(tabs[2].selected).toBe(true);
103
+ });
104
+
105
+ it('Home key selects first tab', async () => {
106
+ container.innerHTML = `
107
+ <ds-tabs>
108
+ <ds-tab-item label="Tab 1"></ds-tab-item>
109
+ <ds-tab-item label="Tab 2" selected></ds-tab-item>
110
+ <ds-tab-item label="Tab 3"></ds-tab-item>
111
+ </ds-tabs>
112
+ `;
113
+ await new Promise(resolve => setTimeout(resolve, 50));
114
+ const tabs = container.querySelectorAll('ds-tab-item');
115
+ tabs[1].focus();
116
+
117
+ tabs[1].dispatchEvent(new KeyboardEvent('keydown', {
118
+ key: 'Home',
119
+ bubbles: true,
120
+ composed: true
121
+ }));
122
+
123
+ await new Promise(resolve => setTimeout(resolve, 50));
124
+
125
+ expect(tabs[0].selected).toBe(true);
126
+ });
127
+
128
+ it('End key selects last tab', async () => {
129
+ container.innerHTML = `
130
+ <ds-tabs>
131
+ <ds-tab-item label="Tab 1" selected></ds-tab-item>
132
+ <ds-tab-item label="Tab 2"></ds-tab-item>
133
+ <ds-tab-item label="Tab 3"></ds-tab-item>
134
+ </ds-tabs>
135
+ `;
136
+ await new Promise(resolve => setTimeout(resolve, 50));
137
+ const tabs = container.querySelectorAll('ds-tab-item');
138
+ tabs[0].focus();
139
+
140
+ tabs[0].dispatchEvent(new KeyboardEvent('keydown', {
141
+ key: 'End',
142
+ bubbles: true,
143
+ composed: true
144
+ }));
145
+
146
+ await new Promise(resolve => setTimeout(resolve, 50));
147
+
148
+ expect(tabs[2].selected).toBe(true);
149
+ });
150
+
151
+ it('skips disabled tabs during keyboard navigation', async () => {
152
+ container.innerHTML = `
153
+ <ds-tabs>
154
+ <ds-tab-item label="Tab 1" selected></ds-tab-item>
155
+ <ds-tab-item label="Tab 2" disabled></ds-tab-item>
156
+ <ds-tab-item label="Tab 3"></ds-tab-item>
157
+ </ds-tabs>
158
+ `;
159
+ await new Promise(resolve => setTimeout(resolve, 50));
160
+ const tabs = container.querySelectorAll('ds-tab-item');
161
+ tabs[0].focus();
162
+
163
+ tabs[0].dispatchEvent(new KeyboardEvent('keydown', {
164
+ key: 'ArrowRight',
165
+ bubbles: true,
166
+ composed: true
167
+ }));
168
+
169
+ await new Promise(resolve => setTimeout(resolve, 50));
170
+
171
+ // Should skip disabled tab 2 and go to tab 3
172
+ expect(tabs[2].selected).toBe(true);
173
+ });
174
+
175
+ // VARIANT TESTS
176
+ it('propagates variant="line" to all tab items', async () => {
177
+ container.innerHTML = `
178
+ <ds-tabs variant="line">
179
+ <ds-tab-item label="Tab 1"></ds-tab-item>
180
+ <ds-tab-item label="Tab 2"></ds-tab-item>
181
+ </ds-tabs>
182
+ `;
183
+ await new Promise(resolve => setTimeout(resolve, 50));
184
+
185
+ const tabsEl = container.querySelector('ds-tabs');
186
+ const tabs = container.querySelectorAll('ds-tab-item');
187
+
188
+ expect(tabsEl.variant).toBe('line');
189
+ tabs.forEach(tab => {
190
+ expect(tab.variant).toBe('line');
191
+ });
192
+ });
193
+
194
+ it('propagates variant="contained" to all tab items', async () => {
195
+ container.innerHTML = `
196
+ <ds-tabs variant="contained">
197
+ <ds-tab-item label="Tab 1"></ds-tab-item>
198
+ <ds-tab-item label="Tab 2"></ds-tab-item>
199
+ </ds-tabs>
200
+ `;
201
+ await new Promise(resolve => setTimeout(resolve, 50));
202
+
203
+ const tabsEl = container.querySelector('ds-tabs');
204
+ const tabs = container.querySelectorAll('ds-tab-item');
205
+
206
+ expect(tabsEl.variant).toBe('contained');
207
+ tabs.forEach(tab => {
208
+ expect(tab.variant).toBe('contained');
209
+ });
210
+ });
211
+
212
+ it('updates variant when prop changes', async () => {
213
+ container.innerHTML = `
214
+ <ds-tabs variant="line">
215
+ <ds-tab-item label="Tab 1"></ds-tab-item>
216
+ <ds-tab-item label="Tab 2"></ds-tab-item>
217
+ </ds-tabs>
218
+ `;
219
+ await new Promise(resolve => setTimeout(resolve, 50));
220
+
221
+ const tabsEl = container.querySelector('ds-tabs');
222
+ tabsEl.variant = 'contained';
223
+ await new Promise(resolve => setTimeout(resolve, 50));
224
+
225
+ const tabs = container.querySelectorAll('ds-tab-item');
226
+ tabs.forEach(tab => {
227
+ expect(tab.variant).toBe('contained');
228
+ });
229
+ });
230
+
231
+ // SELECTION TESTS
232
+ it('only one tab selected at a time', async () => {
233
+ container.innerHTML = `
234
+ <ds-tabs>
235
+ <ds-tab-item label="Tab 1" selected></ds-tab-item>
236
+ <ds-tab-item label="Tab 2" id="tab2"></ds-tab-item>
237
+ </ds-tabs>
238
+ `;
239
+ await new Promise(resolve => setTimeout(resolve, 50));
240
+
241
+ const tab2 = container.querySelector('#tab2');
242
+ tab2.click();
243
+ await new Promise(resolve => setTimeout(resolve, 50));
244
+
245
+ const tabs = container.querySelectorAll('ds-tab-item');
246
+ const selectedTabs = [...tabs].filter(t => t.selected);
247
+ expect(selectedTabs.length).toBe(1);
248
+ expect(selectedTabs[0]).toBe(tab2);
249
+ });
250
+
251
+ it('activates tab on click', async () => {
252
+ container.innerHTML = `
253
+ <ds-tabs>
254
+ <ds-tab-item label="Tab 1" selected></ds-tab-item>
255
+ <ds-tab-item label="Tab 2" id="tab2"></ds-tab-item>
256
+ </ds-tabs>
257
+ `;
258
+ await new Promise(resolve => setTimeout(resolve, 50));
259
+
260
+ const tab2 = container.querySelector('#tab2');
261
+ expect(tab2.selected).toBe(false);
262
+
263
+ tab2.click();
264
+ await new Promise(resolve => setTimeout(resolve, 50));
265
+
266
+ expect(tab2.selected).toBe(true);
267
+ });
268
+
269
+ // EDGE CASES
270
+ it('handles empty slot (no tabs)', async () => {
271
+ container.innerHTML = `<ds-tabs></ds-tabs>`;
272
+ await new Promise(resolve => setTimeout(resolve, 50));
273
+
274
+ const tabsEl = container.querySelector('ds-tabs');
275
+ expect(tabsEl).toBeTruthy();
276
+
277
+ // No tabs, no errors
278
+ const tabs = container.querySelectorAll('ds-tab-item');
279
+ expect(tabs.length).toBe(0);
280
+ });
281
+
282
+ it('handles all tabs disabled', async () => {
283
+ container.innerHTML = `
284
+ <ds-tabs>
285
+ <ds-tab-item label="Tab 1" disabled selected></ds-tab-item>
286
+ <ds-tab-item label="Tab 2" disabled></ds-tab-item>
287
+ </ds-tabs>
288
+ `;
289
+ await new Promise(resolve => setTimeout(resolve, 50));
290
+
291
+ const tabsEl = container.querySelector('ds-tabs');
292
+ const tabs = container.querySelectorAll('ds-tab-item');
293
+
294
+ // All disabled, keyboard nav should do nothing
295
+ tabs[0].dispatchEvent(new KeyboardEvent('keydown', {
296
+ key: 'ArrowRight',
297
+ bubbles: true,
298
+ composed: true
299
+ }));
300
+
301
+ await new Promise(resolve => setTimeout(resolve, 50));
302
+
303
+ // Nothing should change
304
+ expect(tabs[0].selected).toBe(true);
305
+ });
306
+ });
@@ -0,0 +1,3 @@
1
+ export { DsTabs } from './ds-tabs.js';
2
+ export { DsTabItem } from './ds-tab-item.js';
3
+ export { DsTabPanel } from './ds-tab-panel.js';
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import './ds-tag-action.js';
3
+
4
+ describe('DsTagAction A11y', () => {
5
+ let container;
6
+ beforeEach(() => { container = document.createElement('div'); document.body.appendChild(container); });
7
+ afterEach(() => { container.remove(); });
8
+
9
+ it('renders internal button', async () => {
10
+ container.innerHTML = '<ds-tag-action label="Action"></ds-tag-action>';
11
+ await new Promise(r => setTimeout(r, 0));
12
+ const el = container.querySelector('ds-tag-action');
13
+ const btn = el.shadowRoot.querySelector('button');
14
+ expect(btn).to.exist;
15
+ });
16
+
17
+ it('sets aria-pressed when selected', async () => {
18
+ container.innerHTML = '<ds-tag-action label="Selected" selected></ds-tag-action>';
19
+ await new Promise(r => setTimeout(r, 0));
20
+ const el = container.querySelector('ds-tag-action');
21
+ const btn = el.shadowRoot.querySelector('button');
22
+ expect(btn.getAttribute('aria-pressed')).toBe('true');
23
+ });
24
+
25
+ it('sets disabled attribute on button', async () => {
26
+ container.innerHTML = '<ds-tag-action label="Disabled" disabled></ds-tag-action>';
27
+ await new Promise(r => setTimeout(r, 0));
28
+ const el = container.querySelector('ds-tag-action');
29
+ const btn = el.shadowRoot.querySelector('button');
30
+ expect(btn.hasAttribute('disabled')).toBe(true);
31
+ });
32
+ });
@@ -0,0 +1,185 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import '../ds-icon/ds-icon.js';
3
+
4
+ /**
5
+ * @element ds-tag-action
6
+ * @summary A rounded, interactive tag (chip) that can be selected.
7
+ *
8
+ * @prop {string} label - Text label.
9
+ * @prop {string} icon - Optional icon name (left).
10
+ * @prop {boolean} selected - Whether the tag is selected.
11
+ * @prop {boolean} disabled - Whether the tag is disabled.
12
+ * @prop {string} size - 's' (24px) | 'm' (32px). Default: 'm'.
13
+ * @prop {string} color - 'neutral' | 'info'. Default: 'neutral'.
14
+ */
15
+ export class DsTagAction extends LitElement {
16
+ static properties = {
17
+ label: { type: String },
18
+ icon: { type: String },
19
+ selected: { type: Boolean, reflect: true },
20
+ disabled: { type: Boolean, reflect: true },
21
+ size: { type: String, reflect: true },
22
+ color: { type: String, reflect: true },
23
+ };
24
+
25
+ static styles = css`
26
+ :host {
27
+ display: inline-flex; /* Fix ghost space from inline-block */
28
+ vertical-align: middle;
29
+ outline: none;
30
+ }
31
+
32
+ button {
33
+ appearance: none;
34
+ display: inline-flex;
35
+ align-items: center;
36
+ justify-content: center;
37
+ box-sizing: border-box;
38
+
39
+ border-radius: var(--ds-radius-action, 999px);
40
+ border: 1px solid transparent; /* Reserve space for border to avoid shift */
41
+ cursor: pointer;
42
+ position: relative;
43
+
44
+ font: var(--ds-typo-content-body-regular); /* Correct token */
45
+ white-space: nowrap;
46
+
47
+ transition: background-color 0.2s, border-color 0.2s, color 0.2s;
48
+ }
49
+
50
+ /* Gap Logic */
51
+ button {
52
+ gap: 4px; /* "gap de 4px entre eles" */
53
+ }
54
+
55
+ /*
56
+ * SIZES
57
+ */
58
+ /*
59
+ * SIZES
60
+ */
61
+ /* Size M (default): 32px height */
62
+ :host([size="m"]) button,
63
+ :host(:not([size])) button {
64
+ height: 32px;
65
+ padding: 0 var(--ds-space-sm);
66
+ }
67
+
68
+ /* Size S: 24px height */
69
+ :host([size="s"]) button {
70
+ height: 24px;
71
+ padding: 0 var(--ds-space-sm);
72
+ box-sizing: border-box;
73
+ line-height: 1;
74
+ }
75
+
76
+ /*
77
+ * COLORS / VARIANTS
78
+ */
79
+
80
+ /* NEUTRAL (Default) */
81
+ :host([color="neutral"]) button,
82
+ :host(:not([color])) button {
83
+ background-color: var(--ds-color-bg-neutral-subtle);
84
+ color: var(--ds-color-text-default);
85
+ }
86
+
87
+ /* Neutral Hover */
88
+ :host([color="neutral"]:not([disabled])) button:hover,
89
+ :host(:not([color]):not([disabled])) button:hover {
90
+ background-color: var(--ds-color-bg-hover);
91
+ }
92
+
93
+ /* Neutral Selected */
94
+ :host([color="neutral"][selected]) button,
95
+ :host(:not([color])[selected]) button {
96
+ border-color: var(--ds-color-border-selected-secondary, #949494);
97
+ }
98
+
99
+ /* INFO */
100
+ :host([color="info"]) button {
101
+ background-color: var(--ds-color-bg-brand-subtle);
102
+ color: var(--ds-color-text-default);
103
+ }
104
+
105
+ /* Info Hover */
106
+ :host([color="info"]:not([disabled])) button:hover {
107
+ background-color: var(--ds-color-bg-brand-subtle-hover);
108
+ }
109
+
110
+ /* Info Selected */
111
+ :host([color="info"][selected]) button {
112
+ border-color: var(--ds-color-border-selected);
113
+ }
114
+
115
+ /*
116
+ * DISABLED
117
+ */
118
+ :host([disabled]) button {
119
+ cursor: not-allowed;
120
+ background-color: var(--ds-color-bg-disabled);
121
+ color: var(--ds-color-text-disabled);
122
+ border-color: transparent;
123
+ opacity: 1;
124
+ pointer-events: none;
125
+ }
126
+ :host([disabled][selected]) button {
127
+ border-color: var(--ds-color-border-disabled);
128
+ }
129
+
130
+ :host([disabled]) {
131
+ pointer-events: none;
132
+ }
133
+
134
+ /* FOCUS */
135
+ button:focus-visible {
136
+ outline: 2px solid var(--ds-color-border-focus);
137
+ outline-offset: 2px;
138
+ }
139
+
140
+ /* ICON styling */
141
+ ds-icon {
142
+ /* User requested size S consistently */
143
+ --size: var(--ds-icon-size-sm, 20px);
144
+ color: currentColor;
145
+ }
146
+ `;
147
+
148
+ constructor() {
149
+ super();
150
+ this.selected = false;
151
+ this.disabled = false;
152
+ this.size = 'm';
153
+ this.color = 'neutral';
154
+ this.label = '';
155
+ }
156
+
157
+ _handleClick() {
158
+ if (this.disabled) return;
159
+ this.selected = !this.selected;
160
+ this.dispatchEvent(new CustomEvent('ds-selected-change', {
161
+ detail: { selected: this.selected },
162
+ bubbles: true,
163
+ composed: true
164
+ }));
165
+ }
166
+
167
+ render() {
168
+ return html`
169
+ <button
170
+ type="button"
171
+ part="container"
172
+ ?disabled=${this.disabled}
173
+ aria-pressed=${this.selected}
174
+ @click=${this._handleClick}
175
+ >
176
+ ${this.icon ? html`<ds-icon name=${this.icon} size="sm" part="icon"></ds-icon>` : ''}
177
+ <span>${this.label}</span>
178
+ </button>
179
+ `;
180
+ }
181
+ }
182
+
183
+ if (!customElements.get('ds-tag-action')) {
184
+ customElements.define('ds-tag-action', DsTagAction);
185
+ }