@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.
- package/package.json +27 -0
- package/src/ds-accordion/ds-accordion-item.js +288 -0
- package/src/ds-accordion/ds-accordion-item.stories.js +82 -0
- package/src/ds-accordion/ds-accordion.a11y.test.js +92 -0
- package/src/ds-accordion/ds-accordion.js +68 -0
- package/src/ds-accordion/ds-accordion.stories.js +118 -0
- package/src/ds-accordion/ds-accordion.test.js +146 -0
- package/src/ds-accordion/index.js +2 -0
- package/src/ds-action-bar/ds-action-bar.js +116 -0
- package/src/ds-action-bar/ds-action-bar.stories.js +86 -0
- package/src/ds-action-bar/ds-action-bar.test.js +64 -0
- package/src/ds-action-bar/index.js +1 -0
- package/src/ds-alert/ds-alert.a11y.test.js +151 -0
- package/src/ds-alert/ds-alert.js +223 -0
- package/src/ds-alert/ds-alert.mdx +142 -0
- package/src/ds-alert/ds-alert.stories.js +166 -0
- package/src/ds-alert/ds-alert.test.js +256 -0
- package/src/ds-alert/index.js +1 -0
- package/src/ds-avatar/ds-avatar.a11y.test.js +45 -0
- package/src/ds-avatar/ds-avatar.js +216 -0
- package/src/ds-avatar/ds-avatar.stories.js +120 -0
- package/src/ds-avatar/ds-avatar.test.js +83 -0
- package/src/ds-avatar/index.js +1 -0
- package/src/ds-avatar-extended/ds-avatar-extended.a11y.test.js +29 -0
- package/src/ds-avatar-extended/ds-avatar-extended.js +108 -0
- package/src/ds-avatar-extended/ds-avatar-extended.stories.js +93 -0
- package/src/ds-avatar-extended/ds-avatar-extended.test.js +66 -0
- package/src/ds-avatar-extended/index.js +1 -0
- package/src/ds-banner/ds-banner.a11y.test.js +51 -0
- package/src/ds-banner/ds-banner.js +233 -0
- package/src/ds-banner/ds-banner.stories.js +185 -0
- package/src/ds-banner/ds-banner.test.js +116 -0
- package/src/ds-banner/index.js +1 -0
- package/src/ds-breadcrumb-item/ds-breadcrumb-item.js +135 -0
- package/src/ds-breadcrumb-item/ds-breadcrumb-item.stories.js +49 -0
- package/src/ds-breadcrumb-item/ds-breadcrumb-item.test.js +55 -0
- package/src/ds-breadcrumbs/ds-breadcrumbs.js +194 -0
- package/src/ds-breadcrumbs/ds-breadcrumbs.stories.js +54 -0
- package/src/ds-breadcrumbs/ds-breadcrumbs.test.js +33 -0
- package/src/ds-button/ds-button.a11y.test.js +49 -0
- package/src/ds-button/ds-button.js +205 -0
- package/src/ds-button/ds-button.mdx +141 -0
- package/src/ds-button/ds-button.stories.js +152 -0
- package/src/ds-button/ds-button.test.js +62 -0
- package/src/ds-button/index.js +1 -0
- package/src/ds-button-group/ds-button-group.js +82 -0
- package/src/ds-button-group/ds-button-group.mdx +39 -0
- package/src/ds-button-group/ds-button-group.stories.js +47 -0
- package/src/ds-button-group/ds-button-group.test.js +47 -0
- package/src/ds-button-group/index.js +1 -0
- package/src/ds-checkbox/ds-checkbox.a11y.test.js +79 -0
- package/src/ds-checkbox/ds-checkbox.js +271 -0
- package/src/ds-checkbox/ds-checkbox.stories.js +77 -0
- package/src/ds-checkbox/ds-checkbox.test.js +191 -0
- package/src/ds-checkbox/index.js +1 -0
- package/src/ds-checkbox-group/ds-checkbox-group.a11y.test.js +146 -0
- package/src/ds-checkbox-group/ds-checkbox-group.js +235 -0
- package/src/ds-checkbox-group/ds-checkbox-group.stories.js +210 -0
- package/src/ds-checkbox-group/ds-checkbox-group.test.js +150 -0
- package/src/ds-checkbox-group/index.js +1 -0
- package/src/ds-dialog/ds-dialog.js +466 -0
- package/src/ds-dialog/ds-dialog.stories.js +274 -0
- package/src/ds-dialog/ds-dialog.test.js +441 -0
- package/src/ds-dialog/index.js +1 -0
- package/src/ds-dropdown/ds-dropdown.a11y.test.js +80 -0
- package/src/ds-dropdown/ds-dropdown.js +891 -0
- package/src/ds-dropdown/ds-dropdown.stories.js +259 -0
- package/src/ds-dropdown/ds-dropdown.test.js +268 -0
- package/src/ds-dropdown/index.js +1 -0
- package/src/ds-dropdown-group/ds-dropdown-group.js +55 -0
- package/src/ds-dropdown-panel/ds-dropdown-panel.js +34 -0
- package/src/ds-file-uploaded/ds-file-uploaded.a11y.test.js +40 -0
- package/src/ds-file-uploaded/ds-file-uploaded.js +135 -0
- package/src/ds-file-uploaded/ds-file-uploaded.mdx +33 -0
- package/src/ds-file-uploaded/ds-file-uploaded.stories.js +81 -0
- package/src/ds-file-uploaded/ds-file-uploaded.test.js +85 -0
- package/src/ds-file-uploader/ds-file-uploader.a11y.test.js +61 -0
- package/src/ds-file-uploader/ds-file-uploader.js +442 -0
- package/src/ds-file-uploader/ds-file-uploader.mdx +44 -0
- package/src/ds-file-uploader/ds-file-uploader.stories.js +76 -0
- package/src/ds-file-uploader/ds-file-uploader.test.js +142 -0
- package/src/ds-header/ds-header.a11y.test.js +38 -0
- package/src/ds-header/ds-header.js +149 -0
- package/src/ds-header/ds-header.stories.js +63 -0
- package/src/ds-header/ds-header.test.js +52 -0
- package/src/ds-header/index.js +1 -0
- package/src/ds-header-nav/ds-header-nav.a11y.test.js +69 -0
- package/src/ds-header-nav/ds-header-nav.js +114 -0
- package/src/ds-header-nav/ds-header-nav.stories.js +17 -0
- package/src/ds-header-nav/ds-header-nav.test.js +93 -0
- package/src/ds-header-nav-item/ds-header-nav-item.a11y.test.js +71 -0
- package/src/ds-header-nav-item/ds-header-nav-item.js +124 -0
- package/src/ds-header-nav-item/ds-header-nav-item.stories.js +43 -0
- package/src/ds-header-nav-item/ds-header-nav-item.test.js +61 -0
- package/src/ds-icon/ds-icon.a11y.test.js +49 -0
- package/src/ds-icon/ds-icon.js +75 -0
- package/src/ds-icon/ds-icon.mdx +36 -0
- package/src/ds-icon/ds-icon.stories.js +88 -0
- package/src/ds-icon/ds-icon.test.js +97 -0
- package/src/ds-icon/index.js +1 -0
- package/src/ds-icon-button/ds-icon-button.a11y.test.js +55 -0
- package/src/ds-icon-button/ds-icon-button.js +224 -0
- package/src/ds-icon-button/ds-icon-button.mdx +131 -0
- package/src/ds-icon-button/ds-icon-button.stories.js +128 -0
- package/src/ds-icon-button/ds-icon-button.test.js +90 -0
- package/src/ds-icon-button/index.js +1 -0
- package/src/ds-input/ds-input.a11y.test.js +145 -0
- package/src/ds-input/ds-input.js +645 -0
- package/src/ds-input/ds-input.mdx +251 -0
- package/src/ds-input/ds-input.stories.js +298 -0
- package/src/ds-input/ds-input.test.js +792 -0
- package/src/ds-input/index.js +1 -0
- package/src/ds-link/ds-link.js +111 -0
- package/src/ds-link/ds-link.stories.js +56 -0
- package/src/ds-link/ds-link.test.js +74 -0
- package/src/ds-list-item/ds-list-item.a11y.test.js +39 -0
- package/src/ds-list-item/ds-list-item.js +292 -0
- package/src/ds-list-item/ds-list-item.stories.js +101 -0
- package/src/ds-list-item/ds-list-item.test.js +63 -0
- package/src/ds-menu/ds-menu.js +30 -0
- package/src/ds-menu/ds-menu.stories.js +120 -0
- package/src/ds-menu/ds-menu.test.js +123 -0
- package/src/ds-menu-group/ds-menu-group.js +101 -0
- package/src/ds-menu-group/ds-menu-group.stories.js +99 -0
- package/src/ds-nav-item/ds-nav-item.a11y.test.js +91 -0
- package/src/ds-nav-item/ds-nav-item.js +307 -0
- package/src/ds-nav-item/ds-nav-item.stories.js +99 -0
- package/src/ds-nav-item/ds-nav-item.test.js +169 -0
- package/src/ds-nav-item/index.js +1 -0
- package/src/ds-nav-vertical/ds-nav-vertical.a11y.test.js +69 -0
- package/src/ds-nav-vertical/ds-nav-vertical.js +173 -0
- package/src/ds-nav-vertical/ds-nav-vertical.stories.js +124 -0
- package/src/ds-nav-vertical/ds-nav-vertical.test.js +176 -0
- package/src/ds-nav-vertical/index.js +1 -0
- package/src/ds-pagination/ds-pagination.a11y.test.js +50 -0
- package/src/ds-pagination/ds-pagination.js +232 -0
- package/src/ds-pagination/ds-pagination.stories.js +63 -0
- package/src/ds-pagination/ds-pagination.test.js +141 -0
- package/src/ds-pagination/index.js +1 -0
- package/src/ds-progress-bar/ds-progress-bar.a11y.test.js +25 -0
- package/src/ds-progress-bar/ds-progress-bar.js +81 -0
- package/src/ds-progress-bar/ds-progress-bar.stories.js +69 -0
- package/src/ds-progress-bar/ds-progress-bar.test.js +60 -0
- package/src/ds-radio/ds-radio.a11y.test.js +69 -0
- package/src/ds-radio/ds-radio.js +240 -0
- package/src/ds-radio/ds-radio.stories.js +102 -0
- package/src/ds-radio/ds-radio.test.js +114 -0
- package/src/ds-radio/index.js +1 -0
- package/src/ds-radio-group/ds-radio-group.a11y.test.js +164 -0
- package/src/ds-radio-group/ds-radio-group.js +257 -0
- package/src/ds-radio-group/ds-radio-group.stories.js +247 -0
- package/src/ds-radio-group/ds-radio-group.test.js +194 -0
- package/src/ds-radio-group/index.js +1 -0
- package/src/ds-rich-list/ds-rich-list.js +246 -0
- package/src/ds-rich-list/ds-rich-list.stories.js +368 -0
- package/src/ds-rich-list/ds-rich-list.test.js +293 -0
- package/src/ds-rich-list-item/ds-rich-list-item.js +579 -0
- package/src/ds-rich-list-item/ds-rich-list-item.stories.js +197 -0
- package/src/ds-rich-list-item/ds-rich-list-item.test.js +434 -0
- package/src/ds-slider/ds-slider.js +399 -0
- package/src/ds-slider/ds-slider.stories.js +107 -0
- package/src/ds-slider/ds-slider.test.js +308 -0
- package/src/ds-spinner/ds-spinner.js +173 -0
- package/src/ds-spinner/ds-spinner.stories.js +52 -0
- package/src/ds-spinner/ds-spinner.test.js +50 -0
- package/src/ds-status-border/ds-status-border.js +88 -0
- package/src/ds-status-border/ds-status-border.stories.js +242 -0
- package/src/ds-status-border/ds-status-border.test.js +168 -0
- package/src/ds-stepper/ds-stepper.a11y.test.js +198 -0
- package/src/ds-stepper/ds-stepper.js +207 -0
- package/src/ds-stepper/ds-stepper.stories.js +530 -0
- package/src/ds-stepper/ds-stepper.test.js +311 -0
- package/src/ds-stepper-item/ds-stepper-item.js +485 -0
- package/src/ds-stepper-item/ds-stepper-item.stories.js +288 -0
- package/src/ds-switch/ds-switch.js +348 -0
- package/src/ds-switch/ds-switch.stories.js +145 -0
- package/src/ds-switch/ds-switch.test.js +226 -0
- package/src/ds-switch/index.js +1 -0
- package/src/ds-tab-item/ds-tab-item.js +341 -0
- package/src/ds-tab-item/ds-tab-item.stories.js +69 -0
- package/src/ds-tabs/ds-tab-panel.js +48 -0
- package/src/ds-tabs/ds-tabs.a11y.test.js +56 -0
- package/src/ds-tabs/ds-tabs.js +180 -0
- package/src/ds-tabs/ds-tabs.stories.js +152 -0
- package/src/ds-tabs/ds-tabs.test.js +306 -0
- package/src/ds-tabs/index.js +3 -0
- package/src/ds-tag-action/ds-tag-action.a11y.test.js +32 -0
- package/src/ds-tag-action/ds-tag-action.js +185 -0
- package/src/ds-tag-action/ds-tag-action.stories.js +55 -0
- package/src/ds-tag-action/ds-tag-action.test.js +44 -0
- package/src/ds-tag-removable/ds-tag-removable.a11y.test.js +24 -0
- package/src/ds-tag-removable/ds-tag-removable.js +146 -0
- package/src/ds-tag-removable/ds-tag-removable.stories.js +52 -0
- package/src/ds-tag-removable/ds-tag-removable.test.js +46 -0
- package/src/ds-tag-status/ds-tag-status.a11y.test.js +93 -0
- package/src/ds-tag-status/ds-tag-status.js +164 -0
- package/src/ds-tag-status/ds-tag-status.stories.js +200 -0
- package/src/ds-tag-status/ds-tag-status.test.js +140 -0
- package/src/ds-tag-status/index.js +1 -0
- package/src/ds-textarea/ds-textarea-clearable.test.js +89 -0
- package/src/ds-textarea/ds-textarea.a11y.test.js +66 -0
- package/src/ds-textarea/ds-textarea.js +505 -0
- package/src/ds-textarea/ds-textarea.stories.js +335 -0
- package/src/ds-textarea/ds-textarea.test.js +218 -0
- package/src/ds-textarea/index.js +1 -0
- package/src/ds-thumbnail/ds-thumbnail.js +207 -0
- package/src/ds-thumbnail/ds-thumbnail.stories.js +217 -0
- package/src/ds-thumbnail/ds-thumbnail.test.js +220 -0
- package/src/ds-toast/ds-toast-provider.js +110 -0
- package/src/ds-toast/ds-toast.a11y.test.js +34 -0
- package/src/ds-toast/ds-toast.js +243 -0
- package/src/ds-toast/ds-toast.stories.js +143 -0
- package/src/ds-toast/ds-toast.test.js +93 -0
- package/src/ds-toast/index.js +2 -0
- package/src/ds-tooltip/ds-tooltip.a11y.test.js +110 -0
- package/src/ds-tooltip/ds-tooltip.js +217 -0
- package/src/ds-tooltip/ds-tooltip.mdx +75 -0
- package/src/ds-tooltip/ds-tooltip.stories.js +72 -0
- package/src/ds-tooltip/ds-tooltip.test.js +191 -0
- package/src/ds-tooltip/index.js +1 -0
- package/src/ds-tooltip/positioner.js +117 -0
- package/src/index.js +50 -0
- package/src/mixins/field-label.mixin.js +113 -0
- package/src/mixins/field-message.mixin.js +66 -0
- package/src/token-provider/index.js +1 -0
- package/src/token-provider/token-provider.a11y.test.js +44 -0
- package/src/token-provider/token-provider.js +85 -0
- package/src/token-provider/token-provider.stories.js +105 -0
- package/src/token-provider/token-provider.test.js +134 -0
- 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
|
+
});
|