@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,71 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import axe from 'axe-core';
|
|
3
|
+
import './ds-header-nav-item.js';
|
|
4
|
+
|
|
5
|
+
describe('ds-header-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 in default state (button)', async () => {
|
|
18
|
+
container.innerHTML = `
|
|
19
|
+
<ds-header-nav-item>Dashboard</ds-header-nav-item>
|
|
20
|
+
`;
|
|
21
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
22
|
+
|
|
23
|
+
const results = await axe.run(container, {
|
|
24
|
+
rules: { 'color-contrast': { enabled: false } }
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (results.violations.length > 0) {
|
|
28
|
+
console.log('Violations:', JSON.stringify(results.violations, null, 2));
|
|
29
|
+
}
|
|
30
|
+
expect(results.violations).toHaveLength(0);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should pass axe accessibility checks when selected', async () => {
|
|
34
|
+
container.innerHTML = `
|
|
35
|
+
<ds-header-nav-item selected>Dashboard</ds-header-nav-item>
|
|
36
|
+
`;
|
|
37
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
38
|
+
|
|
39
|
+
const results = await axe.run(container, {
|
|
40
|
+
rules: { 'color-contrast': { enabled: false } }
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(results.violations).toHaveLength(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should pass axe accessibility checks as link', async () => {
|
|
47
|
+
container.innerHTML = `
|
|
48
|
+
<ds-header-nav-item href="/dashboard">Dashboard</ds-header-nav-item>
|
|
49
|
+
`;
|
|
50
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
51
|
+
|
|
52
|
+
const results = await axe.run(container, {
|
|
53
|
+
rules: { 'color-contrast': { enabled: false } }
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(results.violations).toHaveLength(0);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should pass axe accessibility checks with icon', async () => {
|
|
60
|
+
container.innerHTML = `
|
|
61
|
+
<ds-header-nav-item icon="home">Dashboard</ds-header-nav-item>
|
|
62
|
+
`;
|
|
63
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
64
|
+
|
|
65
|
+
const results = await axe.run(container, {
|
|
66
|
+
rules: { 'color-contrast': { enabled: false } }
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(results.violations).toHaveLength(0);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
import '../ds-icon/ds-icon.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Navigation item for the Header component.
|
|
6
|
+
*
|
|
7
|
+
* @element ds-header-nav-item
|
|
8
|
+
*
|
|
9
|
+
* @slot default - The label text
|
|
10
|
+
*
|
|
11
|
+
* @prop {string} icon - Icon name (optional)
|
|
12
|
+
* @prop {boolean} selected - Whether the item is active
|
|
13
|
+
* @prop {string} href - Link destination
|
|
14
|
+
*/
|
|
15
|
+
export class DsHeaderNavItem extends LitElement {
|
|
16
|
+
static properties = {
|
|
17
|
+
icon: { type: String },
|
|
18
|
+
selected: { type: Boolean, reflect: true },
|
|
19
|
+
href: { type: String }
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
static styles = css`
|
|
23
|
+
:host {
|
|
24
|
+
display: inline-flex;
|
|
25
|
+
/* height: 100%; Removed to allow 32px sizing */
|
|
26
|
+
align-items: center;
|
|
27
|
+
position: relative;
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
text-decoration: none;
|
|
30
|
+
color: var(--ds-color-text-default); /* Expects context mapping from Header */
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.item {
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
box-sizing: border-box;
|
|
37
|
+
/* Height 32px calculation:
|
|
38
|
+
Line-height (approx 20px) + Padding-top (6px) + Padding-bottom (6px) = 32px
|
|
39
|
+
*/
|
|
40
|
+
padding: var(--ds-size-6) var(--ds-space-md);
|
|
41
|
+
gap: var(--ds-size-4);
|
|
42
|
+
border-radius: var(--ds-radius-container);
|
|
43
|
+
transition: all 0.2s;
|
|
44
|
+
text-decoration: none;
|
|
45
|
+
color: inherit;
|
|
46
|
+
font: var(--ds-typo-content-body-regular);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Reset button styles */
|
|
50
|
+
button.item {
|
|
51
|
+
appearance: none;
|
|
52
|
+
background: transparent;
|
|
53
|
+
border: none;
|
|
54
|
+
font-family: inherit;
|
|
55
|
+
font-size: inherit;
|
|
56
|
+
font: var(--ds-typo-content-body-regular);
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
text-align: left;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Hover */
|
|
62
|
+
:host(:hover) .item {
|
|
63
|
+
background-color: var(--ds-color-bg-hover);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Pressed / Selected */
|
|
67
|
+
:host([selected]) .item,
|
|
68
|
+
:host(:active) .item {
|
|
69
|
+
background-color: var(--ds-color-bg-pressed);
|
|
70
|
+
font-weight: var(--ds-font-weight-bold);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* Focus */
|
|
74
|
+
.item:focus-visible {
|
|
75
|
+
outline: 2px solid var(--ds-color-border-focus);
|
|
76
|
+
outline-offset: 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Selected Top Bar */
|
|
80
|
+
:host([selected])::after {
|
|
81
|
+
content: '';
|
|
82
|
+
position: absolute;
|
|
83
|
+
top: 0;
|
|
84
|
+
left: 0;
|
|
85
|
+
right: 0;
|
|
86
|
+
height: 2px;
|
|
87
|
+
background-color: var(--ds-color-bg-brand);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* Ensure icon size matches specs (20px) */
|
|
91
|
+
ds-icon {
|
|
92
|
+
--size: 20px;
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
constructor() {
|
|
97
|
+
super();
|
|
98
|
+
this.selected = false;
|
|
99
|
+
this.href = '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
render() {
|
|
103
|
+
const content = html`
|
|
104
|
+
${this.icon ? html`<ds-icon name="${this.icon}"></ds-icon>` : ''}
|
|
105
|
+
<slot></slot>
|
|
106
|
+
`;
|
|
107
|
+
|
|
108
|
+
if (this.href) {
|
|
109
|
+
return html`
|
|
110
|
+
<a href="${this.href}" class="item" aria-current="${this.selected ? 'page' : undefined}">
|
|
111
|
+
${content}
|
|
112
|
+
</a>
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return html`
|
|
117
|
+
<button type="button" class="item" aria-current="${this.selected ? 'page' : undefined}">
|
|
118
|
+
${content}
|
|
119
|
+
</button>
|
|
120
|
+
`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
customElements.define('ds-header-nav-item', DsHeaderNavItem);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import './ds-header-nav-item.js';
|
|
3
|
+
import '../ds-header-nav/ds-header-nav.js';
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
title: 'Components/Header Nav Item',
|
|
7
|
+
component: 'ds-header-nav-item',
|
|
8
|
+
argTypes: {
|
|
9
|
+
label: { control: 'text' },
|
|
10
|
+
icon: { control: 'text' },
|
|
11
|
+
selected: { control: 'boolean' },
|
|
12
|
+
href: { control: 'text' }
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const Template = (args) => html`
|
|
17
|
+
<ds-header-nav-item
|
|
18
|
+
.icon=${args.icon}
|
|
19
|
+
.href=${args.href}
|
|
20
|
+
?selected=${args.selected}
|
|
21
|
+
>
|
|
22
|
+
${args.label || 'Nav Item'}
|
|
23
|
+
</ds-header-nav-item>
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
export const Default = Template.bind({});
|
|
27
|
+
Default.args = {
|
|
28
|
+
label: 'Dashboard',
|
|
29
|
+
selected: false
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const Selected = Template.bind({});
|
|
33
|
+
Selected.args = {
|
|
34
|
+
label: 'Dashboard',
|
|
35
|
+
selected: true
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const WithIcon = Template.bind({});
|
|
39
|
+
WithIcon.args = {
|
|
40
|
+
label: 'Settings',
|
|
41
|
+
icon: 'settings',
|
|
42
|
+
selected: false
|
|
43
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import './ds-header-nav-item.js';
|
|
3
|
+
|
|
4
|
+
describe('ds-header-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 label slot (as button by default)', async () => {
|
|
17
|
+
const element = document.createElement('ds-header-nav-item');
|
|
18
|
+
container.appendChild(element);
|
|
19
|
+
element.textContent = 'My Label';
|
|
20
|
+
await element.updateComplete;
|
|
21
|
+
|
|
22
|
+
expect(element.textContent).toContain('My Label');
|
|
23
|
+
expect(element.shadowRoot.querySelector('button')).toBeTruthy();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('renders as anchor when href is provided', async () => {
|
|
27
|
+
const element = document.createElement('ds-header-nav-item');
|
|
28
|
+
container.appendChild(element);
|
|
29
|
+
element.href = '#';
|
|
30
|
+
element.textContent = 'My Link';
|
|
31
|
+
await element.updateComplete;
|
|
32
|
+
|
|
33
|
+
expect(element.shadowRoot.querySelector('a')).toBeTruthy();
|
|
34
|
+
expect(element.shadowRoot.querySelector('button')).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('reflects selected attribute on anchor', async () => {
|
|
38
|
+
const element = document.createElement('ds-header-nav-item');
|
|
39
|
+
container.appendChild(element);
|
|
40
|
+
element.href = '#';
|
|
41
|
+
element.selected = true;
|
|
42
|
+
await element.updateComplete;
|
|
43
|
+
|
|
44
|
+
expect(element.hasAttribute('selected')).toBe(true);
|
|
45
|
+
const link = element.shadowRoot.querySelector('a');
|
|
46
|
+
expect(link).toBeTruthy();
|
|
47
|
+
expect(link.getAttribute('aria-current')).toBe('page');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('reflects selected attribute on button', async () => {
|
|
51
|
+
const element = document.createElement('ds-header-nav-item');
|
|
52
|
+
container.appendChild(element);
|
|
53
|
+
element.selected = true;
|
|
54
|
+
await element.updateComplete;
|
|
55
|
+
|
|
56
|
+
expect(element.hasAttribute('selected')).toBe(true);
|
|
57
|
+
const btn = element.shadowRoot.querySelector('button');
|
|
58
|
+
expect(btn).toBeTruthy();
|
|
59
|
+
expect(btn.getAttribute('aria-current')).toBe('page');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import axe from 'axe-core';
|
|
3
|
+
import './ds-icon.js';
|
|
4
|
+
|
|
5
|
+
describe('ds-icon accessibility', () => {
|
|
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 have aria-hidden on decorative icons', async () => {
|
|
18
|
+
container.innerHTML = '<ds-icon name="star" size="md"></ds-icon>';
|
|
19
|
+
const icon = container.querySelector('ds-icon');
|
|
20
|
+
|
|
21
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
22
|
+
|
|
23
|
+
const svg = icon.shadowRoot.querySelector('svg');
|
|
24
|
+
expect(svg.getAttribute('aria-hidden')).toBe('true');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should pass axe accessibility checks', async () => {
|
|
28
|
+
container.innerHTML = `
|
|
29
|
+
<ds-icon name="home" size="sm"></ds-icon>
|
|
30
|
+
<ds-icon name="star" size="md"></ds-icon>
|
|
31
|
+
<ds-icon name="settings" size="lg"></ds-icon>
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
35
|
+
|
|
36
|
+
const results = await axe.run(container);
|
|
37
|
+
expect(results.violations).toHaveLength(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should be focusable only when interactive', async () => {
|
|
41
|
+
container.innerHTML = '<ds-icon name="star"></ds-icon>';
|
|
42
|
+
const icon = container.querySelector('ds-icon');
|
|
43
|
+
|
|
44
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
45
|
+
|
|
46
|
+
// Decorative icons should not be focusable by default
|
|
47
|
+
expect(icon.tabIndex).toBe(-1);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Icon component using Material Symbols Sharp
|
|
5
|
+
*
|
|
6
|
+
* @element ds-icon
|
|
7
|
+
* @prop {string} name - Icon name (e.g., 'star', 'home', 'search')
|
|
8
|
+
* @prop {string} size - Icon size: 'xs' | 'sm' | 'md' | 'lg' (default: 'md')
|
|
9
|
+
* @prop {string} spritePath - Path to the SVG sprite file (default: '/sprite.svg')
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* <ds-icon name="star" size="sm"></ds-icon>
|
|
13
|
+
* <ds-icon name="home" sprite-path="/assets/icons.svg"></ds-icon>
|
|
14
|
+
*/
|
|
15
|
+
export class DsIcon extends LitElement {
|
|
16
|
+
static properties = {
|
|
17
|
+
name: { type: String },
|
|
18
|
+
size: { type: String, reflect: true },
|
|
19
|
+
spritePath: { type: String, attribute: 'sprite-path' }
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
static styles = css`
|
|
23
|
+
:host {
|
|
24
|
+
display: inline-block;
|
|
25
|
+
vertical-align: middle;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
svg {
|
|
29
|
+
fill: currentColor;
|
|
30
|
+
width: var(--size);
|
|
31
|
+
height: var(--size);
|
|
32
|
+
display: block;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Size variants using design tokens */
|
|
36
|
+
:host([size="xs"]) {
|
|
37
|
+
--size: var(--ds-icon-size-xs, 16px);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
:host([size="sm"]) {
|
|
41
|
+
--size: var(--ds-icon-size-sm, 20px);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
:host([size="md"]),
|
|
45
|
+
:host(:not([size])) {
|
|
46
|
+
--size: var(--ds-icon-size-md, 32px);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
:host([size="lg"]) {
|
|
50
|
+
--size: var(--ds-icon-size-lg, 48px);
|
|
51
|
+
}
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
constructor() {
|
|
55
|
+
super();
|
|
56
|
+
this.size = 'md';
|
|
57
|
+
this.name = '';
|
|
58
|
+
this.spritePath = '/sprite.svg';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
render() {
|
|
62
|
+
if (!this.name) {
|
|
63
|
+
console.warn('ds-icon: "name" prop is required');
|
|
64
|
+
return html``;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return html`
|
|
68
|
+
<svg aria-hidden="true">
|
|
69
|
+
<use href="${this.spritePath}#${this.name}"></use>
|
|
70
|
+
</svg>
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
customElements.define('ds-icon', DsIcon);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Meta, Canvas, Controls } from '@storybook/blocks';
|
|
2
|
+
import * as DsIconStories from './ds-icon.stories';
|
|
3
|
+
|
|
4
|
+
<Meta of={DsIconStories} />
|
|
5
|
+
|
|
6
|
+
# Icon
|
|
7
|
+
|
|
8
|
+
A visual element that represents an action, object, or concept.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
### When to use
|
|
13
|
+
- To reinforce the meaning of a label (e.g., a "Save" floppy disk icon next to the text).
|
|
14
|
+
- To represent an action in a compact way (e.g., inside an Icon Button).
|
|
15
|
+
- To categorize items (e.g., file types).
|
|
16
|
+
|
|
17
|
+
### When not to use
|
|
18
|
+
- To replace text labels completely, unless the meaning is universally understood (like a trash can or magnifying glass).
|
|
19
|
+
- As decoration without semantic value if it clutters the interface.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Best Practices
|
|
24
|
+
|
|
25
|
+
### Accessibility
|
|
26
|
+
- ✅ **Decorative Icons:** If an icon is purely decorative and accompanied by text, ensure it is hidden from screen readers (the component handles this via `aria-hidden="true"` by default if no label is provided).
|
|
27
|
+
- ✅ **Standalone Icons:** If an icon stands alone (without text), it **must** have an accessible label provided via context or the parent component (like `ds-icon-button`).
|
|
28
|
+
|
|
29
|
+
### Color & Sizing
|
|
30
|
+
- ✅ **Inherit Color:** Icons default to `currentColor`, meaning they inherit the text color of their parent. Use utility classes or token CSS variables on the parent to style them.
|
|
31
|
+
- ✅ **Standard Sizes:** Always use the defined size props (`xs`, `sm`, `md`, `lg`) to ensure consistency across the UI.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
<Canvas of={DsIconStories.Default} />
|
|
36
|
+
<Controls />
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import './ds-icon.js';
|
|
2
|
+
import { iconNames } from '../../../icons/build/icons.js';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
title: 'Components/Icon',
|
|
7
|
+
component: 'ds-icon',
|
|
8
|
+
argTypes: {
|
|
9
|
+
name: {
|
|
10
|
+
control: 'select',
|
|
11
|
+
options: iconNames,
|
|
12
|
+
description: 'Icon name from Material Symbols Sharp',
|
|
13
|
+
},
|
|
14
|
+
size: {
|
|
15
|
+
control: 'radio',
|
|
16
|
+
options: ['xs', 'sm', 'md', 'lg'],
|
|
17
|
+
description: 'Icon size',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const Default = {
|
|
23
|
+
args: {
|
|
24
|
+
name: 'star',
|
|
25
|
+
size: 'md',
|
|
26
|
+
},
|
|
27
|
+
render: ({ name, size }) => {
|
|
28
|
+
const icon = document.createElement('ds-icon');
|
|
29
|
+
icon.setAttribute('name', name);
|
|
30
|
+
icon.setAttribute('size', size);
|
|
31
|
+
return icon;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const AllSizes = {
|
|
36
|
+
render: () => {
|
|
37
|
+
const container = document.createElement('div');
|
|
38
|
+
container.style.cssText = 'display: flex; align-items: center; gap: 2rem;';
|
|
39
|
+
|
|
40
|
+
['xs', 'sm', 'md', 'lg'].forEach(size => {
|
|
41
|
+
const wrapper = document.createElement('div');
|
|
42
|
+
wrapper.style.cssText = 'display: flex; flex-direction: column; align-items: center; gap: 0.5rem;';
|
|
43
|
+
|
|
44
|
+
const icon = document.createElement('ds-icon');
|
|
45
|
+
icon.setAttribute('name', 'star');
|
|
46
|
+
icon.setAttribute('size', size);
|
|
47
|
+
|
|
48
|
+
const label = document.createElement('div');
|
|
49
|
+
label.style.cssText = 'font: var(--ds-typo-content-caption-regular); color: var(--ds-color-text-secondary);';
|
|
50
|
+
label.textContent = size.toUpperCase();
|
|
51
|
+
|
|
52
|
+
wrapper.appendChild(icon);
|
|
53
|
+
wrapper.appendChild(label);
|
|
54
|
+
container.appendChild(wrapper);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return container;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
export const CommonIcons = {
|
|
63
|
+
render: () => {
|
|
64
|
+
const container = document.createElement('div');
|
|
65
|
+
container.style.cssText = 'display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 1.5rem;';
|
|
66
|
+
|
|
67
|
+
const commonIcons = ['home', 'star', 'settings', 'search', 'menu', 'close', 'check', 'delete', 'edit', 'add', 'remove', 'help'];
|
|
68
|
+
|
|
69
|
+
commonIcons.forEach(iconName => {
|
|
70
|
+
const wrapper = document.createElement('div');
|
|
71
|
+
wrapper.style.cssText = 'display: flex; flex-direction: column; align-items: center; gap: 0.5rem;';
|
|
72
|
+
|
|
73
|
+
const icon = document.createElement('ds-icon');
|
|
74
|
+
icon.setAttribute('name', iconName);
|
|
75
|
+
icon.setAttribute('size', 'md');
|
|
76
|
+
|
|
77
|
+
const label = document.createElement('div');
|
|
78
|
+
label.style.cssText = 'font: var(--ds-typo-content-caption-regular); color: var(--ds-color-text-secondary); text-align: center;';
|
|
79
|
+
label.textContent = iconName;
|
|
80
|
+
|
|
81
|
+
wrapper.appendChild(icon);
|
|
82
|
+
wrapper.appendChild(label);
|
|
83
|
+
container.appendChild(wrapper);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return container;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import './ds-icon.js';
|
|
3
|
+
|
|
4
|
+
describe('ds-icon', () => {
|
|
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('should render with valid name prop', async () => {
|
|
17
|
+
container.innerHTML = '<ds-icon name="home"></ds-icon>';
|
|
18
|
+
const icon = container.querySelector('ds-icon');
|
|
19
|
+
|
|
20
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
21
|
+
|
|
22
|
+
const svg = icon.shadowRoot.querySelector('svg');
|
|
23
|
+
expect(svg).toBeTruthy();
|
|
24
|
+
|
|
25
|
+
const use = svg.querySelector('use');
|
|
26
|
+
expect(use.getAttribute('href')).toContain('#home');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should not render when name is empty', async () => {
|
|
30
|
+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
31
|
+
|
|
32
|
+
container.innerHTML = '<ds-icon></ds-icon>';
|
|
33
|
+
const icon = container.querySelector('ds-icon');
|
|
34
|
+
|
|
35
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
36
|
+
|
|
37
|
+
const svg = icon.shadowRoot.querySelector('svg');
|
|
38
|
+
expect(svg).toBeFalsy();
|
|
39
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith('ds-icon: "name" prop is required');
|
|
40
|
+
|
|
41
|
+
consoleWarnSpy.mockRestore();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should apply size attribute correctly', async () => {
|
|
45
|
+
const sizes = ['xs', 'sm', 'md', 'lg'];
|
|
46
|
+
|
|
47
|
+
for (const size of sizes) {
|
|
48
|
+
container.innerHTML = `<ds-icon name="star" size="${size}"></ds-icon>`;
|
|
49
|
+
const icon = container.querySelector('ds-icon');
|
|
50
|
+
|
|
51
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
52
|
+
|
|
53
|
+
expect(icon.getAttribute('size')).toBe(size);
|
|
54
|
+
expect(icon.size).toBe(size);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should default to md size when size is not specified', async () => {
|
|
59
|
+
container.innerHTML = '<ds-icon name="star"></ds-icon>';
|
|
60
|
+
const icon = container.querySelector('ds-icon');
|
|
61
|
+
|
|
62
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
63
|
+
|
|
64
|
+
expect(icon.size).toBe('md');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should inherit color from parent (currentColor)', async () => {
|
|
68
|
+
container.style.color = 'rgb(255, 0, 0)';
|
|
69
|
+
container.innerHTML = '<ds-icon name="star"></ds-icon>';
|
|
70
|
+
const icon = container.querySelector('ds-icon');
|
|
71
|
+
|
|
72
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
73
|
+
|
|
74
|
+
const svg = icon.shadowRoot.querySelector('svg');
|
|
75
|
+
const computedStyle = window.getComputedStyle(svg);
|
|
76
|
+
|
|
77
|
+
// SVG should use currentColor which inherits from parent
|
|
78
|
+
expect(computedStyle.fill).toBe('rgb(255, 0, 0)');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should update when name prop changes', async () => {
|
|
82
|
+
container.innerHTML = '<ds-icon name="home"></ds-icon>';
|
|
83
|
+
const icon = container.querySelector('ds-icon');
|
|
84
|
+
|
|
85
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
86
|
+
|
|
87
|
+
let use = icon.shadowRoot.querySelector('use');
|
|
88
|
+
expect(use.getAttribute('href')).toContain('#home');
|
|
89
|
+
|
|
90
|
+
// Change the name
|
|
91
|
+
icon.setAttribute('name', 'star');
|
|
92
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
93
|
+
|
|
94
|
+
use = icon.shadowRoot.querySelector('use');
|
|
95
|
+
expect(use.getAttribute('href')).toContain('#star');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DsIcon } from './ds-icon.js';
|