@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,63 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import './ds-list-item.js';
3
+
4
+ describe('ds-list-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 with label', async () => {
17
+ container.innerHTML = '<ds-list-item label="Test Label"></ds-list-item>';
18
+ const el = container.querySelector('ds-list-item');
19
+ await new Promise(resolve => setTimeout(resolve, 0));
20
+ const label = el.shadowRoot.querySelector('.label');
21
+ expect(label.textContent.trim()).toBe('Test Label');
22
+ });
23
+
24
+ it('renders slot content', async () => {
25
+ container.innerHTML = '<ds-list-item>Slotted Content</ds-list-item>';
26
+ const el = container.querySelector('ds-list-item');
27
+ await new Promise(resolve => setTimeout(resolve, 0));
28
+ const slot = el.shadowRoot.querySelector('slot:not([name])');
29
+ expect(slot).toBeTruthy();
30
+ });
31
+
32
+ it('handles checkbox selection variant', async () => {
33
+ container.innerHTML = '<ds-list-item variant="select-checkbox" selected></ds-list-item>';
34
+ const el = container.querySelector('ds-list-item');
35
+ await new Promise(resolve => setTimeout(resolve, 0));
36
+
37
+ // Check if checkbox exists and is checked
38
+ const checkbox = el.shadowRoot.querySelector('ds-checkbox');
39
+ expect(checkbox).toBeTruthy();
40
+ expect(checkbox.hasAttribute('checked')).toBe(true);
41
+ });
42
+
43
+ it('handles simple select variant', async () => {
44
+ container.innerHTML = '<ds-list-item variant="select-simple" selected></ds-list-item>';
45
+ const el = container.querySelector('ds-list-item');
46
+ await new Promise(resolve => setTimeout(resolve, 0));
47
+
48
+ // Check if check icon exists
49
+ const icon = el.shadowRoot.querySelector('ds-icon[name="check"]');
50
+ expect(icon).toBeTruthy();
51
+ });
52
+
53
+ it('handles section variant styling', async () => {
54
+ container.innerHTML = '<ds-list-item variant="section" label="Header"></ds-list-item>';
55
+ const el = container.querySelector('ds-list-item');
56
+ await new Promise(resolve => setTimeout(resolve, 0));
57
+
58
+ // Section variant (presentation role) should NOT have tabindex
59
+ expect(el.hasAttribute('tabindex')).toBe(false);
60
+ });
61
+
62
+
63
+ });
@@ -0,0 +1,30 @@
1
+ import { LitElement, html, css } from 'lit';
2
+
3
+ /**
4
+ * Menu container component.
5
+ *
6
+ * @element ds-menu
7
+ */
8
+ export class DsMenu extends LitElement {
9
+ static styles = css`
10
+ :host {
11
+ display: flex;
12
+ flex-direction: column;
13
+ padding: 16px; /* 16px all around per request */
14
+ gap: 0; /* Gap removed - children handle spacing */
15
+ box-shadow: var(--ds-elevation-floating);
16
+ background-color: var(--ds-color-bg-default);
17
+ border-radius: var(--ds-radius-container);
18
+ box-sizing: border-box;
19
+ width: 100%; /* Fill the popup container */
20
+ min-width: unset; /* Allow container to dictate size */
21
+ overflow: hidden; /* Clip content to rounded corners */
22
+ }
23
+ `;
24
+
25
+ render() {
26
+ return html`<slot></slot>`;
27
+ }
28
+ }
29
+
30
+ customElements.define('ds-menu', DsMenu);
@@ -0,0 +1,120 @@
1
+ import { html } from 'lit';
2
+ import './ds-menu.js';
3
+ import '../ds-menu-group/ds-menu-group.js';
4
+ import '../ds-list-item/ds-list-item.js';
5
+ import '../ds-icon/ds-icon.js';
6
+ import '../ds-avatar/ds-avatar.js';
7
+
8
+ export default {
9
+ title: 'Components/Menu',
10
+ component: 'ds-menu',
11
+ argTypes: {
12
+ padding: { control: 'text' }
13
+ },
14
+ decorators: [
15
+ (story) => html`
16
+ <div style="padding: 24px; min-height: 400px; display: flex; align-items: flex-start; justify-content: flex-start;">
17
+ <div style="width: 280px;">
18
+ ${story()}
19
+ </div>
20
+ </div>
21
+ `
22
+ ]
23
+ };
24
+
25
+ export const SimpleActions = {
26
+ render: () => html`
27
+ <ds-menu>
28
+ <ds-list-item label="Edit Profile">
29
+ <ds-icon slot="start" name="edit" size="sm"></ds-icon>
30
+ </ds-list-item>
31
+ <ds-list-item label="Preferences">
32
+ <ds-icon slot="start" name="settings" size="sm"></ds-icon>
33
+ </ds-list-item>
34
+ <ds-list-item label="Sign Out" variant="action" additional-text="Log out of your account">
35
+ <ds-icon slot="start" name="logout" size="sm"></ds-icon>
36
+ </ds-list-item>
37
+ </ds-menu>
38
+ `
39
+ };
40
+
41
+ export const GroupedActions = {
42
+ render: () => html`
43
+ <ds-menu>
44
+ <!-- First section: hide divider to avoid line at top of menu -->
45
+ <ds-list-item variant="section" label="Account" hide-divider></ds-list-item>
46
+ <ds-menu-group>
47
+ <ds-list-item label="Profile">
48
+ <ds-avatar slot="start" size="sm" initials="MS"></ds-avatar>
49
+ </ds-list-item>
50
+ <ds-list-item label="Billing"></ds-list-item>
51
+ </ds-menu-group>
52
+
53
+ <ds-list-item variant="section" label="Support"></ds-list-item>
54
+ <ds-menu-group>
55
+ <ds-list-item label="Help Center"></ds-list-item>
56
+ <ds-list-item label="Contact Us"></ds-list-item>
57
+ </ds-menu-group>
58
+ </ds-menu>
59
+ `
60
+ };
61
+
62
+ export const SingleSelect = {
63
+ render: () => html`
64
+ <ds-menu>
65
+ <ds-list-item variant="section" label="Sort By" hide-divider></ds-list-item>
66
+ <ds-menu-group type="single">
67
+ <ds-list-item label="Newest First" variant="select-simple" selected></ds-list-item>
68
+ <ds-list-item label="Oldest First" variant="select-simple"></ds-list-item>
69
+ <ds-list-item label="Alphabetical" variant="select-simple"></ds-list-item>
70
+ </ds-menu-group>
71
+ </ds-menu>
72
+ `
73
+ };
74
+
75
+ export const MultiSelect = {
76
+ render: () => html`
77
+ <ds-menu>
78
+ <ds-list-item variant="section" label="Filter Status" hide-divider></ds-list-item>
79
+ <ds-menu-group type="multiple">
80
+ <ds-list-item label="Completed" variant="select-simple" selected></ds-list-item>
81
+ <ds-list-item label="Pending" variant="select-simple" selected></ds-list-item>
82
+ <ds-list-item label="Archived" variant="select-simple"></ds-list-item>
83
+ </ds-menu-group>
84
+ </ds-menu>
85
+ `
86
+ };
87
+
88
+ export const MixedComplex = {
89
+ render: () => html`
90
+ <ds-menu>
91
+ <ds-menu-group>
92
+ <ds-list-item label="View Details">
93
+ <ds-icon slot="start" name="visibility" size="sm"></ds-icon>
94
+ </ds-list-item>
95
+ </ds-menu-group>
96
+
97
+ <!-- Section Title (with default divider above) -->
98
+ <ds-list-item variant="section" label="Display Options"></ds-list-item>
99
+
100
+ <ds-menu-group type="single">
101
+ <ds-list-item label="Compact View" variant="select-simple"></ds-list-item>
102
+ <ds-list-item label="Comfortable View" variant="select-simple" selected></ds-list-item>
103
+ </ds-menu-group>
104
+
105
+ <ds-list-item variant="section" label="Visibility"></ds-list-item>
106
+
107
+ <ds-menu-group type="multiple">
108
+ <ds-list-item label="Show Hidden Files" variant="select-simple"></ds-list-item>
109
+ <ds-list-item label="Show Extensions" variant="select-simple"></ds-list-item>
110
+ </ds-menu-group>
111
+
112
+ <!-- Visual Separator only (Hide Label) -->
113
+ <ds-list-item variant="section" hide-label></ds-list-item>
114
+
115
+ <ds-list-item label="Delete" style="--ds-color-text-default: var(--ds-color-text-danger);">
116
+ <ds-icon slot="start" name="delete" size="sm" style="color: var(--ds-color-icon-danger);"></ds-icon>
117
+ </ds-list-item>
118
+ </ds-menu>
119
+ `
120
+ };
@@ -0,0 +1,123 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import '../ds-menu/ds-menu.js';
3
+ import '../ds-menu-group/ds-menu-group.js';
4
+ import '../ds-list-item/ds-list-item.js';
5
+
6
+ describe('DsMenu', () => {
7
+ let container;
8
+
9
+ beforeEach(() => {
10
+ container = document.createElement('div');
11
+ document.body.appendChild(container);
12
+ });
13
+
14
+ afterEach(() => {
15
+ container.remove();
16
+ });
17
+
18
+ it('renders with correct default styles', async () => {
19
+ container.innerHTML = '<ds-menu>Content</ds-menu>';
20
+ const el = container.querySelector('ds-menu');
21
+ await new Promise(resolve => setTimeout(resolve, 0));
22
+
23
+ const style = getComputedStyle(el);
24
+ expect(style.display).toBe('flex');
25
+ expect(style.flexDirection).toBe('column');
26
+ expect(style.padding).toBe('16px');
27
+ expect(style.gap).toBe('0px');
28
+ });
29
+ });
30
+
31
+ describe('DsMenuGroup', () => {
32
+ let container;
33
+
34
+ beforeEach(() => {
35
+ container = document.createElement('div');
36
+ document.body.appendChild(container);
37
+ });
38
+
39
+ afterEach(() => {
40
+ container.remove();
41
+ });
42
+
43
+ it('handles single selection (radio behavior)', async () => {
44
+ container.innerHTML = `
45
+ <ds-menu>
46
+ <ds-menu-group type="single">
47
+ <ds-list-item id="item1" variant="select-simple" selected></ds-list-item>
48
+ <ds-list-item id="item2" variant="select-simple"></ds-list-item>
49
+ </ds-menu-group>
50
+ </ds-menu>
51
+ `;
52
+
53
+ const group = container.querySelector('ds-menu-group');
54
+ const item1 = container.querySelector('#item1');
55
+ const item2 = container.querySelector('#item2');
56
+
57
+ await new Promise(resolve => setTimeout(resolve, 0));
58
+
59
+ expect(item1.selected).toBe(true);
60
+ expect(item2.selected).toBe(false);
61
+
62
+ // click item2
63
+ item2.click(); // Group listens for click bubbling
64
+ await new Promise(resolve => setTimeout(resolve, 0));
65
+
66
+ expect(item1.selected).toBe(false);
67
+ expect(item2.selected).toBe(true);
68
+ });
69
+
70
+ it('handles multiple selection (checkbox behavior)', async () => {
71
+ container.innerHTML = `
72
+ <ds-menu>
73
+ <ds-menu-group type="multiple">
74
+ <ds-list-item id="item1" variant="select-simple"></ds-list-item>
75
+ <ds-list-item id="item2" variant="select-simple"></ds-list-item>
76
+ </ds-menu-group>
77
+ </ds-menu>
78
+ `;
79
+
80
+ const item1 = container.querySelector('#item1');
81
+ const item2 = container.querySelector('#item2');
82
+
83
+ await new Promise(resolve => setTimeout(resolve, 0));
84
+
85
+ expect(item1.selected).toBe(false);
86
+ expect(item2.selected).toBe(false);
87
+
88
+ item1.click();
89
+ await new Promise(resolve => setTimeout(resolve, 0));
90
+
91
+ expect(item1.selected).toBe(true);
92
+ expect(item2.selected).toBe(false);
93
+
94
+ item2.click();
95
+ await new Promise(resolve => setTimeout(resolve, 0));
96
+
97
+ expect(item1.selected).toBe(true);
98
+ expect(item2.selected).toBe(true);
99
+
100
+ item1.click(); // Toggle off
101
+ await new Promise(resolve => setTimeout(resolve, 0));
102
+
103
+ expect(item1.selected).toBe(false);
104
+ expect(item2.selected).toBe(true);
105
+ });
106
+
107
+ it('ignores clicks on disabled items', async () => {
108
+ container.innerHTML = `
109
+ <ds-menu>
110
+ <ds-menu-group type="single">
111
+ <ds-list-item id="item1" variant="select-simple"></ds-list-item>
112
+ <ds-list-item id="item2" variant="select-simple" disabled></ds-list-item>
113
+ </ds-menu-group>
114
+ </ds-menu>
115
+ `;
116
+
117
+ const item2 = container.querySelector('#item2');
118
+ item2.click();
119
+ await new Promise(resolve => setTimeout(resolve, 0));
120
+
121
+ expect(item2.selected).toBe(false);
122
+ });
123
+ });
@@ -0,0 +1,101 @@
1
+ import { LitElement, html, css } from 'lit';
2
+
3
+ /**
4
+ * Logical group for menu items.
5
+ *
6
+ * @element ds-menu-group
7
+ * @prop {string} type - 'action' | 'single' | 'multiple'
8
+ * @prop {string} variant - Optional variant
9
+ */
10
+ export class DsMenuGroup extends LitElement {
11
+ static properties = {
12
+ type: { type: String },
13
+ variant: { type: String }
14
+ };
15
+
16
+ static styles = css`
17
+ :host {
18
+ display: block;
19
+ padding: 8px 0; /* Standard vertical spacing for a group */
20
+ }
21
+ `;
22
+
23
+ constructor() {
24
+ super();
25
+ this.type = 'action';
26
+ }
27
+
28
+ /* Keep logic methods if they are useful for other menus, or revert them too?
29
+ User said "voltem ao plano inicial".
30
+ I will keep the logic (variant handling, click handling) as that's arguably part of the "component"
31
+ but revert the Render/Styles to simple.
32
+ */
33
+ connectedCallback() {
34
+ super.connectedCallback();
35
+ this.addEventListener('click', this._handleClick);
36
+ }
37
+
38
+ disconnectedCallback() {
39
+ super.disconnectedCallback();
40
+ this.removeEventListener('click', this._handleClick);
41
+ }
42
+
43
+ _handleSlotChange() {
44
+ this._updateChildrenVariants();
45
+ }
46
+
47
+ updated(changedProperties) {
48
+ if (changedProperties.has('variant')) {
49
+ this._updateChildrenVariants();
50
+ }
51
+ }
52
+
53
+ _updateChildrenVariants() {
54
+ if (!this.variant) return;
55
+ const slot = this.shadowRoot.querySelector('slot');
56
+ if (!slot) return;
57
+ const nodes = slot.assignedElements({ flatten: true });
58
+ nodes.forEach(node => {
59
+ if (node.localName === 'ds-list-item') {
60
+ const currentVariant = node.getAttribute('variant');
61
+ if (!currentVariant || currentVariant === 'action') {
62
+ node.setAttribute('variant', this.variant);
63
+ }
64
+ }
65
+ });
66
+ }
67
+
68
+ _handleClick(e) {
69
+ if (this.type === 'action') return;
70
+ const path = e.composedPath();
71
+ const item = path.find(el => el.localName === 'ds-list-item');
72
+ if (!item || item.hasAttribute('disabled') || item.getAttribute('variant') === 'section') return;
73
+
74
+ if (this.type === 'single') {
75
+ this._handleSingleSelect(item);
76
+ } else if (this.type === 'multiple') {
77
+ this._handleMultiSelect(item);
78
+ }
79
+ }
80
+
81
+ _handleSingleSelect(selectedItem) {
82
+ selectedItem.selected = true;
83
+ const slot = this.shadowRoot.querySelector('slot');
84
+ const nodes = slot.assignedNodes({ flatten: true });
85
+ nodes.forEach(node => {
86
+ if (node.localName === 'ds-list-item' && node !== selectedItem) {
87
+ node.selected = false;
88
+ }
89
+ });
90
+ }
91
+
92
+ _handleMultiSelect(item) {
93
+ item.selected = !item.selected;
94
+ }
95
+
96
+ render() {
97
+ return html`<slot @slotchange=${this._handleSlotChange}></slot>`;
98
+ }
99
+ }
100
+
101
+ customElements.define('ds-menu-group', DsMenuGroup);
@@ -0,0 +1,99 @@
1
+ import { html } from 'lit';
2
+ import './ds-menu-group.js';
3
+ import '../ds-list-item/ds-list-item.js';
4
+
5
+ export default {
6
+ title: 'Components/Menu Group',
7
+ component: 'ds-menu-group',
8
+ argTypes: {
9
+ type: {
10
+ control: 'radio',
11
+ options: ['action', 'single', 'multiple'],
12
+ description: 'Defines the selection logic for the group.'
13
+ },
14
+ variant: {
15
+ control: 'text',
16
+ description: 'Optional: Overrides the variant of all child items (e.g. "select-checkbox").'
17
+ }
18
+ },
19
+ decorators: [
20
+ (story) => html`
21
+ <div style="padding: 24px; min-height: 400px; display: flex; justify-content: flex-start; align-items: flex-start;">
22
+ <div style="width: 280px; display: flex; flex-direction: column; gap: 4px;">
23
+ ${story()}
24
+ </div>
25
+ </div>
26
+ `
27
+ ]
28
+ };
29
+
30
+ const Template = (args) => html`
31
+ <ds-menu-group .type=${args.type} .variant=${args.variant}>
32
+ <ds-list-item label="Option 1" selected></ds-list-item>
33
+ <ds-list-item label="Option 2"></ds-list-item>
34
+ <ds-list-item label="Option 3"></ds-list-item>
35
+ </ds-menu-group>
36
+ `;
37
+
38
+ export const SingleSelect = {
39
+ render: Template,
40
+ args: {
41
+ type: 'single',
42
+ variant: 'select-simple'
43
+ },
44
+ parameters: {
45
+ docs: {
46
+ description: {
47
+ story: 'Use `type="single"` for radio-button-like behavior within the group.'
48
+ }
49
+ }
50
+ }
51
+ };
52
+
53
+ export const MultiSelect = {
54
+ render: Template,
55
+ args: {
56
+ type: 'multiple',
57
+ variant: 'select-simple'
58
+ },
59
+ parameters: {
60
+ docs: {
61
+ description: {
62
+ story: 'Use `type="multiple"` for independent toggling of items.'
63
+ }
64
+ }
65
+ }
66
+ };
67
+
68
+ export const ActionGroup = {
69
+ render: () => html`
70
+ <ds-menu-group type="action">
71
+ <ds-list-item label="Action 1" variant="action"></ds-list-item>
72
+ <ds-list-item label="Action 2" variant="action"></ds-list-item>
73
+ </ds-menu-group>
74
+ `,
75
+ parameters: {
76
+ docs: {
77
+ description: {
78
+ story: 'Use `type="action"` (default) for grouping simple logical actions without selection state.'
79
+ }
80
+ }
81
+ }
82
+ };
83
+
84
+ export const CheckboxGroup = {
85
+ render: () => html`
86
+ <ds-menu-group type="multiple" variant="select-checkbox">
87
+ <ds-list-item label="Checkbox 1" selected></ds-list-item>
88
+ <ds-list-item label="Checkbox 2"></ds-list-item>
89
+ <ds-list-item label="Checkbox 3"></ds-list-item>
90
+ </ds-menu-group>
91
+ `,
92
+ parameters: {
93
+ docs: {
94
+ description: {
95
+ story: 'Use `variant="select-checkbox"` on the `ds-menu-group` to automatically style all children as checkboxes. This demonstrates the **cascading variant** feature.'
96
+ }
97
+ }
98
+ }
99
+ };
@@ -0,0 +1,91 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import axe from 'axe-core';
3
+ import './ds-nav-item.js';
4
+
5
+ describe('ds-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 for stand-alone item', async () => {
18
+ container.innerHTML = '<ds-nav-item label="Home" icon="home"></ds-nav-item>';
19
+ await new Promise(resolve => setTimeout(resolve, 100));
20
+
21
+ const results = await axe.run(container, {
22
+ rules: { 'color-contrast': { enabled: false } }
23
+ });
24
+ if (results.violations.length > 0) {
25
+ console.log('Violations:', JSON.stringify(results.violations, null, 2));
26
+ }
27
+ expect(results.violations).toHaveLength(0);
28
+ });
29
+
30
+ it('should pass axe accessibility checks for selected item', async () => {
31
+ container.innerHTML = '<ds-nav-item label="Dashboard" icon="apps" selected></ds-nav-item>';
32
+ await new Promise(resolve => setTimeout(resolve, 100));
33
+
34
+ const results = await axe.run(container, {
35
+ rules: { 'color-contrast': { enabled: false } }
36
+ });
37
+ expect(results.violations).toHaveLength(0);
38
+ });
39
+
40
+
41
+
42
+ it('should pass axe accessibility checks for parent item', async () => {
43
+ container.innerHTML = `
44
+ <ds-nav-item label="Products" icon="folder">
45
+ <ds-nav-item label="All Products"></ds-nav-item>
46
+ <ds-nav-item label="Categories"></ds-nav-item>
47
+ </ds-nav-item>
48
+ `;
49
+ await new Promise(resolve => setTimeout(resolve, 150));
50
+
51
+ const results = await axe.run(container, {
52
+ rules: { 'color-contrast': { enabled: false } }
53
+ });
54
+ expect(results.violations).toHaveLength(0);
55
+ });
56
+
57
+ it('should pass axe accessibility checks for expanded parent', async () => {
58
+ container.innerHTML = `
59
+ <ds-nav-item label="Products" icon="folder" expanded>
60
+ <ds-nav-item label="All Products"></ds-nav-item>
61
+ <ds-nav-item label="Categories"></ds-nav-item>
62
+ </ds-nav-item>
63
+ `;
64
+ await new Promise(resolve => setTimeout(resolve, 150));
65
+
66
+ const results = await axe.run(container, {
67
+ rules: { 'color-contrast': { enabled: false } }
68
+ });
69
+ expect(results.violations).toHaveLength(0);
70
+ });
71
+
72
+ it('should pass axe accessibility checks for link item', async () => {
73
+ container.innerHTML = '<ds-nav-item label="External Link" href="/external"></ds-nav-item>';
74
+ await new Promise(resolve => setTimeout(resolve, 100));
75
+
76
+ const results = await axe.run(container, {
77
+ rules: { 'color-contrast': { enabled: false } }
78
+ });
79
+ expect(results.violations).toHaveLength(0);
80
+ });
81
+
82
+ it('should pass axe accessibility checks without icon', async () => {
83
+ container.innerHTML = '<ds-nav-item label="Text Only"></ds-nav-item>';
84
+ await new Promise(resolve => setTimeout(resolve, 100));
85
+
86
+ const results = await axe.run(container, {
87
+ rules: { 'color-contrast': { enabled: false } }
88
+ });
89
+ expect(results.violations).toHaveLength(0);
90
+ });
91
+ });