@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,307 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import '../ds-icon/ds-icon.js';
3
+
4
+ /**
5
+ * Navigation item component for vertical navigation menus
6
+ *
7
+ * @element ds-nav-item
8
+ *
9
+ * @prop {string} label - Label text to display
10
+ * @prop {string} value - Unique value identifier for this item
11
+ * @prop {string} icon - Optional decorative icon name (left side)
12
+ * @prop {boolean} selected - Whether this item is currently selected/active
13
+ * @prop {boolean} expanded - For parent items, whether children are visible
14
+ * @prop {number} level - Indentation level (0 = root, each level adds 32px padding)
15
+ * @prop {string} href - Optional URL for navigation (makes item a link)
16
+ *
17
+ * @slot - Default slot for child nav items (makes this a parent/collapsible item)
18
+ *
19
+ * @fires ds-nav-select - Fired when a stand-alone item is clicked
20
+ * @fires ds-nav-toggle - Fired when a parent item is expanded/collapsed
21
+ *
22
+ * @csspart container - The main container element
23
+ * @csspart icon - The decorative icon
24
+ * @csspart label - The label text
25
+ * @csspart chevron - The expand/collapse chevron (parent items only)
26
+ * @csspart children - The container for child items
27
+ */
28
+ export class DsNavItem extends LitElement {
29
+ static properties = {
30
+ label: { type: String },
31
+ value: { type: String, reflect: true },
32
+ icon: { type: String },
33
+ selected: { type: Boolean, reflect: true },
34
+ expanded: { type: Boolean, reflect: true },
35
+ level: { type: Number, reflect: true },
36
+ href: { type: String },
37
+ childSelected: { type: Boolean, reflect: true, attribute: 'child-selected' },
38
+ _hasChildren: { type: Boolean, state: true }
39
+ };
40
+
41
+ static styles = css`
42
+ :host {
43
+ display: block;
44
+ box-sizing: border-box;
45
+ }
46
+
47
+ /* Wrapper handles indentation */
48
+ .nav-wrapper {
49
+ display: flex;
50
+ align-items: center;
51
+ box-sizing: border-box;
52
+ padding-left: 0;
53
+ }
54
+
55
+ /* Level indentation: 32px per level on wrapper */
56
+ :host([level="1"]) .nav-wrapper { padding-left: 32px; }
57
+ :host([level="2"]) .nav-wrapper { padding-left: 64px; }
58
+ :host([level="3"]) .nav-wrapper { padding-left: 96px; }
59
+ :host([level="4"]) .nav-wrapper { padding-left: 128px; }
60
+
61
+ /* Main clickable/interactive container - only content area */
62
+ .nav-item {
63
+ display: flex;
64
+ align-items: center;
65
+ flex: 1;
66
+ height: 32px;
67
+ padding: 6px var(--ds-space-sm); /* 6px vertical, 8px horizontal */
68
+ gap: var(--ds-space-xs); /* 4px */
69
+ box-sizing: border-box;
70
+ border-radius: var(--ds-radius-container); /* 0px - sharp */
71
+ border: none;
72
+ background: transparent;
73
+ color: var(--ds-color-text-default);
74
+ font: var(--ds-typo-content-body-regular);
75
+ text-decoration: none;
76
+ cursor: pointer;
77
+ position: relative;
78
+ text-align: left;
79
+ }
80
+
81
+ /* Remove default button/anchor styles */
82
+ button.nav-item {
83
+ appearance: none;
84
+ font-family: var(--ds-font-family-content);
85
+ font: var(--ds-typo-content-body-regular);
86
+ }
87
+
88
+ a.nav-item {
89
+ display: flex;
90
+ }
91
+
92
+ /* Icon styling */
93
+ .icon {
94
+ flex-shrink: 0;
95
+ color: var(--ds-color-icon-default);
96
+ }
97
+
98
+ /* Label */
99
+ .label {
100
+ flex: 1;
101
+ white-space: nowrap;
102
+ overflow: hidden;
103
+ text-overflow: ellipsis;
104
+ }
105
+
106
+ /* Chevron for parent items */
107
+ .chevron {
108
+ flex-shrink: 0;
109
+ color: var(--ds-color-icon-default);
110
+ transition: transform 0.2s ease;
111
+ }
112
+
113
+ /* Hover state - only on content area */
114
+ :host(:not([selected])) .nav-item:hover {
115
+ background-color: var(--ds-color-bg-hover);
116
+ }
117
+
118
+ /* Pressed/Active state */
119
+ :host(:not([selected])) .nav-item:active {
120
+ background-color: var(--ds-color-bg-pressed);
121
+ }
122
+
123
+ /* Focus state */
124
+ .nav-item:focus-visible {
125
+ outline: 2px solid var(--ds-color-border-focus);
126
+ outline-offset: 0;
127
+ }
128
+
129
+ /* Selected state - only on content area */
130
+ :host([selected]) .nav-item {
131
+ background-color: var(--ds-color-bg-selected);
132
+ font-family: var(--ds-font-family-content);
133
+ font-weight: bold;
134
+ }
135
+
136
+ /* Parent bold state when a child is selected and parent is collapsed */
137
+ :host([child-selected]:not([expanded])) .label {
138
+ font-weight: bold;
139
+ }
140
+
141
+ /* Selected indicator line - starts at content area, not at indentation */
142
+ :host([selected]) .nav-item::before {
143
+ content: '';
144
+ position: absolute;
145
+ left: 0;
146
+ top: 0;
147
+ bottom: 0;
148
+ width: 2px;
149
+ background-color: var(--ds-color-border-brand);
150
+ }
151
+
152
+
153
+
154
+ /* Children container */
155
+ .children {
156
+ display: none;
157
+ flex-direction: column;
158
+ gap: var(--ds-space-sm);
159
+ }
160
+
161
+ :host([expanded]) .children {
162
+ display: flex;
163
+ margin-top: var(--ds-space-sm); /* 8px gap between parent and first child */
164
+ }
165
+
166
+ /* Hidden slot for detecting children */
167
+ .hidden-slot {
168
+ display: none;
169
+ }
170
+ `;
171
+
172
+ constructor() {
173
+ super();
174
+ this.label = '';
175
+ this.value = '';
176
+ this.icon = '';
177
+ this.selected = false;
178
+ this.expanded = false;
179
+ this.level = 0;
180
+ this.href = '';
181
+ this.childSelected = false;
182
+ this._hasChildren = false;
183
+ }
184
+
185
+ connectedCallback() {
186
+ super.connectedCallback();
187
+ // Check for slotted children to determine if this is a parent item
188
+ this._checkForChildren();
189
+ }
190
+
191
+ disconnectedCallback() {
192
+ super.disconnectedCallback();
193
+ }
194
+
195
+ _checkForChildren() {
196
+ // Check if there are any ds-nav-item children
197
+ const children = this.querySelectorAll(':scope > ds-nav-item');
198
+ this._hasChildren = children.length > 0;
199
+ }
200
+
201
+ _handleSlotChange() {
202
+ this._checkForChildren();
203
+ }
204
+
205
+ _handleClick(e) {
206
+
207
+ if (this._hasChildren) {
208
+ // Parent item: toggle expand/collapse
209
+ e.preventDefault();
210
+ this.expanded = !this.expanded;
211
+ this.dispatchEvent(new CustomEvent('ds-nav-toggle', {
212
+ detail: { expanded: this.expanded },
213
+ bubbles: true,
214
+ composed: true
215
+ }));
216
+ } else {
217
+ // Stand-alone item: dispatch select event (container will handle selection)
218
+ this.dispatchEvent(new CustomEvent('ds-nav-select', {
219
+ detail: {
220
+ value: this.value,
221
+ label: this.label,
222
+ href: this.href,
223
+ item: this
224
+ },
225
+ bubbles: true,
226
+ composed: true
227
+ }));
228
+ }
229
+ }
230
+
231
+ _handleKeyDown(e) {
232
+ if (e.key === 'Enter' || e.key === ' ') {
233
+ e.preventDefault();
234
+ this._handleClick(e);
235
+ }
236
+ }
237
+
238
+ _getChevronIcon() {
239
+ return this.expanded ? 'expand-more' : 'chevron-right';
240
+ }
241
+
242
+ render() {
243
+ const isParent = this._hasChildren;
244
+ const Tag = this.href && !isParent ? 'a' : 'button';
245
+
246
+ const content = html`
247
+ ${this.icon ? html`
248
+ <ds-icon
249
+ name="${this.icon}"
250
+ size="sm"
251
+ class="icon"
252
+ part="icon"
253
+ ></ds-icon>
254
+ ` : ''}
255
+ <span class="label" part="label">${this.label}</span>
256
+ ${isParent ? html`
257
+ <ds-icon
258
+ name="${this._getChevronIcon()}"
259
+ size="sm"
260
+ class="chevron"
261
+ part="chevron"
262
+ ></ds-icon>
263
+ ` : ''}
264
+ `;
265
+
266
+ return html`
267
+ <div class="nav-wrapper">
268
+ ${Tag === 'a' ? html`
269
+ <a
270
+ class="nav-item"
271
+ href="${this.href}"
272
+ part="container"
273
+ aria-current="${this.selected ? 'page' : undefined}"
274
+ @click="${this._handleClick}"
275
+ @keydown="${this._handleKeyDown}"
276
+ >
277
+ ${content}
278
+ </a>
279
+ ` : html`
280
+ <button
281
+ class="nav-item"
282
+ part="container"
283
+ type="button"
284
+ aria-expanded="${isParent ? String(this.expanded) : undefined}"
285
+ aria-current="${this.selected ? 'page' : undefined}"
286
+ @click="${this._handleClick}"
287
+ @keydown="${this._handleKeyDown}"
288
+ >
289
+ ${content}
290
+ </button>
291
+ `}
292
+ </div>
293
+
294
+ ${isParent ? html`
295
+ <div class="children" part="children">
296
+ <slot @slotchange="${this._handleSlotChange}"></slot>
297
+ </div>
298
+ ` : html`
299
+ <div class="hidden-slot">
300
+ <slot @slotchange="${this._handleSlotChange}"></slot>
301
+ </div>
302
+ `}
303
+ `;
304
+ }
305
+ }
306
+
307
+ customElements.define('ds-nav-item', DsNavItem);
@@ -0,0 +1,99 @@
1
+ import { html } from 'lit';
2
+ import './ds-nav-item.js';
3
+
4
+ export default {
5
+ title: 'Components/Nav Item',
6
+ component: 'ds-nav-item',
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ label: {
10
+ control: 'text',
11
+ description: 'Label text to display'
12
+ },
13
+ icon: {
14
+ control: 'text',
15
+ description: 'Optional decorative icon name'
16
+ },
17
+ selected: {
18
+ control: 'boolean',
19
+ description: 'Whether this item is currently selected'
20
+ },
21
+
22
+ expanded: {
23
+ control: 'boolean',
24
+ description: 'For parent items, whether children are visible'
25
+ },
26
+ level: {
27
+ control: 'number',
28
+ description: 'Indentation level (each level adds 32px padding)'
29
+ },
30
+ href: {
31
+ control: 'text',
32
+ description: 'Optional URL for navigation'
33
+ }
34
+ }
35
+ };
36
+
37
+ /**
38
+ * Default stand-alone navigation item
39
+ */
40
+ export const Default = {
41
+ args: {
42
+ label: 'Dashboard',
43
+ icon: 'apps'
44
+ }
45
+ };
46
+
47
+ /**
48
+ * Selected/active state with brand indicator
49
+ */
50
+ export const Selected = {
51
+ args: {
52
+ label: 'Dashboard',
53
+ icon: 'apps',
54
+ selected: true
55
+ }
56
+ };
57
+
58
+
59
+
60
+ /**
61
+ * Without icon
62
+ */
63
+ export const WithoutIcon = {
64
+ args: {
65
+ label: 'Overview'
66
+ }
67
+ };
68
+
69
+ /**
70
+ * As a link
71
+ */
72
+ export const AsLink = {
73
+ args: {
74
+ label: 'External Link',
75
+ icon: 'open-in-new',
76
+ href: '/external'
77
+ }
78
+ };
79
+
80
+ /**
81
+ * Simple parent item example
82
+ */
83
+ export const Parent = {
84
+ args: {
85
+ label: 'Parent Item',
86
+ icon: 'folder',
87
+ expanded: false
88
+ },
89
+ render: (args) => html`
90
+ <ds-nav-item
91
+ label="${args.label}"
92
+ icon="${args.icon}"
93
+ ?expanded="${args.expanded}"
94
+ ?selected="${args.selected}"
95
+ >
96
+ <ds-nav-item label="Child Item"></ds-nav-item>
97
+ </ds-nav-item>
98
+ `
99
+ };
@@ -0,0 +1,169 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import './ds-nav-item.js';
3
+
4
+ describe('ds-nav-item', () => {
5
+ let container;
6
+
7
+ beforeEach(() => {
8
+ container = document.createElement('div');
9
+ document.body.appendChild(container);
10
+ });
11
+
12
+ afterEach(() => {
13
+ container.remove();
14
+ });
15
+
16
+ it('renders with default values', async () => {
17
+ container.innerHTML = '<ds-nav-item label="Home"></ds-nav-item>';
18
+ const element = container.querySelector('ds-nav-item');
19
+ await new Promise(resolve => setTimeout(resolve, 50));
20
+
21
+ expect(element.label).toBe('Home');
22
+ expect(element.selected).toBe(false);
23
+ expect(element.expanded).toBe(false);
24
+ expect(element.level).toBe(0);
25
+ });
26
+
27
+ it('renders label correctly', async () => {
28
+ container.innerHTML = '<ds-nav-item label="Dashboard"></ds-nav-item>';
29
+ const element = container.querySelector('ds-nav-item');
30
+ await new Promise(resolve => setTimeout(resolve, 50));
31
+
32
+ const label = element.shadowRoot.querySelector('.label');
33
+ expect(label.textContent).toBe('Dashboard');
34
+ });
35
+
36
+ it('renders icon when provided', async () => {
37
+ container.innerHTML = '<ds-nav-item label="Settings" icon="settings"></ds-nav-item>';
38
+ const element = container.querySelector('ds-nav-item');
39
+ await new Promise(resolve => setTimeout(resolve, 50));
40
+
41
+ const icon = element.shadowRoot.querySelector('ds-icon.icon');
42
+ expect(icon).toBeTruthy();
43
+ expect(icon.getAttribute('name')).toBe('settings');
44
+ });
45
+
46
+ it('does not render icon when not provided', async () => {
47
+ container.innerHTML = '<ds-nav-item label="Home"></ds-nav-item>';
48
+ const element = container.querySelector('ds-nav-item');
49
+ await new Promise(resolve => setTimeout(resolve, 50));
50
+
51
+ const icon = element.shadowRoot.querySelector('ds-icon.icon');
52
+ expect(icon).toBeFalsy();
53
+ });
54
+
55
+ it('reflects selected attribute', async () => {
56
+ container.innerHTML = '<ds-nav-item label="Home" selected></ds-nav-item>';
57
+ const element = container.querySelector('ds-nav-item');
58
+ await new Promise(resolve => setTimeout(resolve, 50));
59
+
60
+ expect(element.hasAttribute('selected')).toBe(true);
61
+ expect(element.selected).toBe(true);
62
+ });
63
+
64
+ it('dispatches ds-nav-select event on click for stand-alone item', async () => {
65
+ container.innerHTML = '<ds-nav-item label="Home" icon="home"></ds-nav-item>';
66
+ const element = container.querySelector('ds-nav-item');
67
+ await new Promise(resolve => setTimeout(resolve, 50));
68
+
69
+ const selectSpy = vi.fn();
70
+ element.addEventListener('ds-nav-select', selectSpy);
71
+
72
+ const button = element.shadowRoot.querySelector('.nav-item');
73
+ button.click();
74
+
75
+ expect(selectSpy).toHaveBeenCalledTimes(1);
76
+ expect(selectSpy.mock.calls[0][0].detail.label).toBe('Home');
77
+ });
78
+
79
+ it('renders as link when href is provided', async () => {
80
+ container.innerHTML = '<ds-nav-item label="External" href="/dashboard"></ds-nav-item>';
81
+ const element = container.querySelector('ds-nav-item');
82
+ await new Promise(resolve => setTimeout(resolve, 50));
83
+
84
+ const link = element.shadowRoot.querySelector('a.nav-item');
85
+ expect(link).toBeTruthy();
86
+ expect(link.getAttribute('href')).toBe('/dashboard');
87
+ });
88
+
89
+ it('renders chevron for parent items with children', async () => {
90
+ container.innerHTML = `
91
+ <ds-nav-item label="Parent" icon="folder">
92
+ <ds-nav-item label="Child 1"></ds-nav-item>
93
+ <ds-nav-item label="Child 2"></ds-nav-item>
94
+ </ds-nav-item>
95
+ `;
96
+ const parent = container.querySelector('ds-nav-item');
97
+ await new Promise(resolve => setTimeout(resolve, 100));
98
+
99
+ const chevron = parent.shadowRoot.querySelector('ds-icon.chevron');
100
+ expect(chevron).toBeTruthy();
101
+ expect(chevron.getAttribute('name')).toBe('chevron-right');
102
+ });
103
+
104
+ it('toggles expanded state on parent click', async () => {
105
+ container.innerHTML = `
106
+ <ds-nav-item label="Parent">
107
+ <ds-nav-item label="Child"></ds-nav-item>
108
+ </ds-nav-item>
109
+ `;
110
+ const parent = container.querySelector('ds-nav-item');
111
+ await new Promise(resolve => setTimeout(resolve, 100));
112
+
113
+ expect(parent.expanded).toBe(false);
114
+
115
+ const button = parent.shadowRoot.querySelector('.nav-item');
116
+ button.click();
117
+ await new Promise(resolve => setTimeout(resolve, 50));
118
+
119
+ expect(parent.expanded).toBe(true);
120
+ });
121
+
122
+ it('dispatches ds-nav-toggle event on parent click', async () => {
123
+ container.innerHTML = `
124
+ <ds-nav-item label="Parent">
125
+ <ds-nav-item label="Child"></ds-nav-item>
126
+ </ds-nav-item>
127
+ `;
128
+ const parent = container.querySelector('ds-nav-item');
129
+ await new Promise(resolve => setTimeout(resolve, 100));
130
+
131
+ const toggleSpy = vi.fn();
132
+ parent.addEventListener('ds-nav-toggle', toggleSpy);
133
+
134
+ const button = parent.shadowRoot.querySelector('.nav-item');
135
+ button.click();
136
+
137
+ expect(toggleSpy).toHaveBeenCalledTimes(1);
138
+ expect(toggleSpy.mock.calls[0][0].detail.expanded).toBe(true);
139
+ });
140
+
141
+ it('shows expand-more chevron when expanded', async () => {
142
+ container.innerHTML = `
143
+ <ds-nav-item label="Parent" expanded>
144
+ <ds-nav-item label="Child"></ds-nav-item>
145
+ </ds-nav-item>
146
+ `;
147
+ const parent = container.querySelector('ds-nav-item');
148
+ await new Promise(resolve => setTimeout(resolve, 100));
149
+
150
+ const chevron = parent.shadowRoot.querySelector('ds-icon.chevron');
151
+ expect(chevron.getAttribute('name')).toBe('expand-more');
152
+ });
153
+
154
+ it('respects level attribute for indentation', async () => {
155
+ container.innerHTML = `
156
+ <ds-nav-item label="Level 0" level="0"></ds-nav-item>
157
+ <ds-nav-item label="Level 1" level="1"></ds-nav-item>
158
+ <ds-nav-item label="Level 2" level="2"></ds-nav-item>
159
+ `;
160
+ await new Promise(resolve => setTimeout(resolve, 100));
161
+
162
+ const items = container.querySelectorAll('ds-nav-item');
163
+ expect(items[0].level).toBe(0);
164
+ expect(items[1].level).toBe(1);
165
+ expect(items[2].level).toBe(2);
166
+ });
167
+
168
+
169
+ });
@@ -0,0 +1 @@
1
+ export { DsNavItem } from './ds-nav-item.js';
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import axe from 'axe-core';
3
+ import './ds-nav-vertical.js';
4
+ import '../ds-nav-item/ds-nav-item.js';
5
+
6
+ describe('ds-nav-vertical a11y', () => {
7
+ let container;
8
+
9
+ beforeEach(() => {
10
+ container = document.createElement('div');
11
+ document.body.appendChild(container);
12
+ });
13
+
14
+ afterEach(() => {
15
+ container.remove();
16
+ });
17
+
18
+ it('should pass axe accessibility checks with basic nav', async () => {
19
+ container.innerHTML = `
20
+ <ds-nav-vertical>
21
+ <ds-nav-item value="home" label="Home" icon="home"></ds-nav-item>
22
+ <ds-nav-item value="settings" label="Settings" icon="settings"></ds-nav-item>
23
+ </ds-nav-vertical>
24
+ `;
25
+ await new Promise(resolve => setTimeout(resolve, 100));
26
+
27
+ const results = await axe.run(container, {
28
+ rules: { 'color-contrast': { enabled: false } }
29
+ });
30
+ if (results.violations.length > 0) {
31
+ console.log('Violations:', JSON.stringify(results.violations, null, 2));
32
+ }
33
+ expect(results.violations).toHaveLength(0);
34
+ });
35
+
36
+ it('should pass axe accessibility checks with selected item', async () => {
37
+ container.innerHTML = `
38
+ <ds-nav-vertical value="home">
39
+ <ds-nav-item value="home" label="Home" icon="home"></ds-nav-item>
40
+ <ds-nav-item value="settings" label="Settings" icon="settings"></ds-nav-item>
41
+ </ds-nav-vertical>
42
+ `;
43
+ await new Promise(resolve => setTimeout(resolve, 100));
44
+
45
+ const results = await axe.run(container, {
46
+ rules: { 'color-contrast': { enabled: false } }
47
+ });
48
+ expect(results.violations).toHaveLength(0);
49
+ });
50
+
51
+ it('should pass axe accessibility checks with nested items', async () => {
52
+ container.innerHTML = `
53
+ <ds-nav-vertical>
54
+ <ds-nav-item value="products" label="Products" icon="folder" expanded>
55
+ <ds-nav-item value="all" label="All Products"></ds-nav-item>
56
+ <ds-nav-item value="categories" label="Categories"></ds-nav-item>
57
+ </ds-nav-item>
58
+ </ds-nav-vertical>
59
+ `;
60
+ await new Promise(resolve => setTimeout(resolve, 150));
61
+
62
+ const results = await axe.run(container, {
63
+ rules: { 'color-contrast': { enabled: false } }
64
+ });
65
+ expect(results.violations).toHaveLength(0);
66
+ });
67
+
68
+
69
+ });