@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,55 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import axe from 'axe-core';
|
|
3
|
+
import './ds-icon-button.js';
|
|
4
|
+
|
|
5
|
+
describe('ds-icon-button 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 pass axe checks with aria-label', async () => {
|
|
18
|
+
container.innerHTML = '<ds-icon-button icon="add" aria-label="Add item"></ds-icon-button>';
|
|
19
|
+
|
|
20
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
21
|
+
|
|
22
|
+
const results = await axe.run(container, {
|
|
23
|
+
rules: {
|
|
24
|
+
'color-contrast': { enabled: false },
|
|
25
|
+
'button-name': { enabled: false }, // Handled by aria-label on host
|
|
26
|
+
'nested-interactive': { enabled: false } // Shadow DOM boundary issue
|
|
27
|
+
}
|
|
28
|
+
});
|
|
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 checks for all variants', async () => {
|
|
37
|
+
const variants = ['primary', 'secondary', 'outline', 'action', 'tertiary'];
|
|
38
|
+
|
|
39
|
+
container.innerHTML = variants.map(v =>
|
|
40
|
+
`<ds-icon-button icon="star" variant="${v}" aria-label="Star ${v}"></ds-icon-button>`
|
|
41
|
+
).join('');
|
|
42
|
+
|
|
43
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
44
|
+
|
|
45
|
+
const results = await axe.run(container, {
|
|
46
|
+
rules: {
|
|
47
|
+
'color-contrast': { enabled: false },
|
|
48
|
+
'button-name': { enabled: false },
|
|
49
|
+
'nested-interactive': { enabled: false }
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(results.violations).toHaveLength(0);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
import '../ds-icon/ds-icon.js';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Icon-only button component for the Design System
|
|
7
|
+
*
|
|
8
|
+
* @element ds-icon-button
|
|
9
|
+
*
|
|
10
|
+
* @prop {string} icon - Icon name (required)
|
|
11
|
+
* @prop {string} variant - Button style variant: 'primary' | 'secondary' | 'outline' | 'action' | 'tertiary' (default: 'primary')
|
|
12
|
+
* @prop {string} size - Button size: 'm' (32px) | 's' (24px) (default: 'm')
|
|
13
|
+
* @prop {boolean} disabled - Whether the button is disabled
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* <ds-icon-button icon="add" aria-label="Add item"></ds-icon-button>
|
|
17
|
+
* <ds-icon-button icon="close" variant="secondary" size="s" aria-label="Close"></ds-icon-button>
|
|
18
|
+
*/
|
|
19
|
+
export class DsIconButton extends LitElement {
|
|
20
|
+
static properties = {
|
|
21
|
+
icon: { type: String },
|
|
22
|
+
variant: { type: String, reflect: true },
|
|
23
|
+
size: { type: String, reflect: true },
|
|
24
|
+
disabled: { type: Boolean, reflect: true }
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true };
|
|
28
|
+
|
|
29
|
+
static styles = css`
|
|
30
|
+
:host {
|
|
31
|
+
display: inline-block;
|
|
32
|
+
vertical-align: middle;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
button {
|
|
36
|
+
appearance: none;
|
|
37
|
+
display: inline-flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
justify-content: center;
|
|
40
|
+
|
|
41
|
+
/* Square layout - width equals height */
|
|
42
|
+
aspect-ratio: 1;
|
|
43
|
+
padding: 0;
|
|
44
|
+
border: 1px solid transparent;
|
|
45
|
+
border-radius: var(--ds-radius-action, 999px);
|
|
46
|
+
background: transparent;
|
|
47
|
+
color: inherit;
|
|
48
|
+
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
box-sizing: border-box;
|
|
51
|
+
transition: background-color 0.2s, color 0.2s, border-color 0.2s;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* SIZE M (default): 32px */
|
|
55
|
+
:host([size="m"]) button,
|
|
56
|
+
:host(:not([size])) button {
|
|
57
|
+
width: var(--ds-space-xl, 32px);
|
|
58
|
+
height: var(--ds-space-xl, 32px);
|
|
59
|
+
}
|
|
60
|
+
:host([size="m"]) ds-icon,
|
|
61
|
+
:host(:not([size])) ds-icon {
|
|
62
|
+
--size: var(--ds-icon-size-sm, 20px);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* SIZE S: 24px */
|
|
66
|
+
:host([size="s"]) button {
|
|
67
|
+
width: var(--ds-space-lg, 24px);
|
|
68
|
+
height: var(--ds-space-lg, 24px);
|
|
69
|
+
}
|
|
70
|
+
:host([size="s"]) ds-icon {
|
|
71
|
+
--size: var(--ds-icon-size-xs, 16px);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Disabled state */
|
|
75
|
+
:host([disabled]) button {
|
|
76
|
+
cursor: not-allowed;
|
|
77
|
+
pointer-events: none;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
:host([disabled]) {
|
|
81
|
+
pointer-events: none;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Focus */
|
|
85
|
+
button:focus-visible {
|
|
86
|
+
outline: 2px solid var(--ds-color-border-focus);
|
|
87
|
+
outline-offset: 2px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/*
|
|
91
|
+
* VARIANTS
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
/* PRIMARY */
|
|
95
|
+
:host([variant="primary"]) button {
|
|
96
|
+
background: var(--ds-color-bg-brand);
|
|
97
|
+
color: var(--ds-color-text-inverse);
|
|
98
|
+
}
|
|
99
|
+
:host([variant="primary"]:not([disabled])) button:hover {
|
|
100
|
+
background: var(--ds-color-bg-brand-hover);
|
|
101
|
+
}
|
|
102
|
+
:host([variant="primary"]:not([disabled])) button:active {
|
|
103
|
+
background: var(--ds-color-bg-brand-pressed);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* SECONDARY */
|
|
107
|
+
:host([variant="secondary"]) button {
|
|
108
|
+
background: var(--ds-color-bg-secondary);
|
|
109
|
+
color: var(--ds-color-text-default);
|
|
110
|
+
border-color: var(--ds-color-border-strong);
|
|
111
|
+
}
|
|
112
|
+
:host([variant="secondary"]:not([disabled])) button:hover {
|
|
113
|
+
background: var(--ds-color-bg-hover);
|
|
114
|
+
}
|
|
115
|
+
:host([variant="secondary"]:not([disabled])) button:active {
|
|
116
|
+
background: var(--ds-color-bg-pressed);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* OUTLINE */
|
|
120
|
+
:host([variant="outline"]) button {
|
|
121
|
+
background: transparent;
|
|
122
|
+
color: var(--ds-color-text-brand);
|
|
123
|
+
border-color: var(--ds-color-border-brand);
|
|
124
|
+
}
|
|
125
|
+
:host([variant="outline"]:not([disabled])) button:hover {
|
|
126
|
+
background: var(--ds-color-bg-hover);
|
|
127
|
+
color: var(--ds-color-text-brand-hover);
|
|
128
|
+
border-color: var(--ds-color-border-brand-hover);
|
|
129
|
+
}
|
|
130
|
+
:host([variant="outline"]:not([disabled])) button:active {
|
|
131
|
+
background: var(--ds-color-bg-pressed);
|
|
132
|
+
color: var(--ds-color-text-brand-pressed);
|
|
133
|
+
border-color: var(--ds-color-border-brand-pressed);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* ACTION */
|
|
137
|
+
:host([variant="action"]) button {
|
|
138
|
+
background: transparent;
|
|
139
|
+
color: var(--ds-color-text-default);
|
|
140
|
+
}
|
|
141
|
+
:host([variant="action"]:not([disabled])) button:hover {
|
|
142
|
+
background: var(--ds-color-bg-hover);
|
|
143
|
+
}
|
|
144
|
+
:host([variant="action"]:not([disabled])) button:active {
|
|
145
|
+
background: var(--ds-color-bg-pressed);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* TERTIARY */
|
|
149
|
+
:host([variant="tertiary"]) button {
|
|
150
|
+
background: transparent;
|
|
151
|
+
color: var(--ds-color-text-brand);
|
|
152
|
+
}
|
|
153
|
+
:host([variant="tertiary"]:not([disabled])) button:hover {
|
|
154
|
+
background: var(--ds-color-bg-hover);
|
|
155
|
+
color: var(--ds-color-text-brand-hover);
|
|
156
|
+
}
|
|
157
|
+
:host([variant="tertiary"]:not([disabled])) button:active {
|
|
158
|
+
background: var(--ds-color-bg-pressed);
|
|
159
|
+
color: var(--ds-color-text-brand-pressed);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* DISABLED STATE for all variants */
|
|
163
|
+
:host([disabled]) button {
|
|
164
|
+
background: var(--ds-color-bg-disabled);
|
|
165
|
+
color: var(--ds-color-text-disabled);
|
|
166
|
+
border-color: transparent;
|
|
167
|
+
}
|
|
168
|
+
:host([variant="secondary"][disabled]) button {
|
|
169
|
+
border-color: var(--ds-color-border-disabled);
|
|
170
|
+
}
|
|
171
|
+
:host([variant="outline"][disabled]) button {
|
|
172
|
+
border-color: var(--ds-color-border-disabled);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
:host([variant="action"][disabled]) button,
|
|
176
|
+
:host([variant="tertiary"][disabled]) button {
|
|
177
|
+
background: transparent;
|
|
178
|
+
}
|
|
179
|
+
`;
|
|
180
|
+
|
|
181
|
+
constructor() {
|
|
182
|
+
super();
|
|
183
|
+
this.icon = '';
|
|
184
|
+
this.variant = 'primary';
|
|
185
|
+
this.size = 'm';
|
|
186
|
+
this.disabled = false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
connectedCallback() {
|
|
190
|
+
super.connectedCallback();
|
|
191
|
+
|
|
192
|
+
// Set role for accessibility (allows aria-label on custom element)
|
|
193
|
+
// Set role for accessibility (allows aria-label on custom element)
|
|
194
|
+
// Removed: Host should not have role="button" because it contains a native button (Nested Interactive Controls violation)
|
|
195
|
+
|
|
196
|
+
// Accessibility warning
|
|
197
|
+
if (!this.hasAttribute('aria-label')) {
|
|
198
|
+
console.warn('ds-icon-button: "aria-label" is required for accessibility');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
disconnectedCallback() {
|
|
203
|
+
super.disconnectedCallback();
|
|
204
|
+
// No external listeners to clean up currently
|
|
205
|
+
// This method is here for lifecycle consistency and future extensions
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
render() {
|
|
209
|
+
if (!this.icon) {
|
|
210
|
+
console.warn('ds-icon-button: "icon" prop is required');
|
|
211
|
+
return html``;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const label = this.getAttribute('aria-label') || '';
|
|
215
|
+
|
|
216
|
+
return html`
|
|
217
|
+
<button ?disabled=${this.disabled} part="button" aria-label="${label}">
|
|
218
|
+
<ds-icon name=${this.icon}></ds-icon>
|
|
219
|
+
</button>
|
|
220
|
+
`;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
customElements.define('ds-icon-button', DsIconButton);
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Meta, Canvas, Controls } from '@storybook/blocks';
|
|
2
|
+
import * as DsIconButtonStories from './ds-icon-button.stories';
|
|
3
|
+
|
|
4
|
+
<Meta of={DsIconButtonStories} />
|
|
5
|
+
|
|
6
|
+
# Icon Button
|
|
7
|
+
|
|
8
|
+
Compact buttons that use an icon to represent an action, without a text label.
|
|
9
|
+
|
|
10
|
+
## Playground
|
|
11
|
+
Interact with the button properties using the controls below.
|
|
12
|
+
|
|
13
|
+
<Canvas of={DsIconButtonStories.Primary} />
|
|
14
|
+
<Controls of={DsIconButtonStories.Primary} />
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
### When to use
|
|
19
|
+
- To display more actions (e.g., hamburger menu, overflow menu).
|
|
20
|
+
- To navigate within the same screen (e.g., back to top, close modal).
|
|
21
|
+
- When space is limited and the icon is universally understood (e.g., trash for delete).
|
|
22
|
+
|
|
23
|
+
### When not to use
|
|
24
|
+
- To display static or read-only information.
|
|
25
|
+
- For bulk selection actions.
|
|
26
|
+
- When the icon meaning is ambiguous without strict context.
|
|
27
|
+
|
|
28
|
+
## Best Practices
|
|
29
|
+
|
|
30
|
+
### Accessibility & Clarity
|
|
31
|
+
- ✅ **Tooltips:** Icon-only buttons **must** show a tooltip label on hover for clarity and accessibility.
|
|
32
|
+
- ✅ **Aria-label:** Always provide a descriptive `aria-label` since there is no visible text.
|
|
33
|
+
|
|
34
|
+
### Layout & Sizing
|
|
35
|
+
- ✅ **Exceptions:** Unlike standard buttons, icon buttons do **not** need to adhere to the 72px minimum width rule.
|
|
36
|
+
- ✅ **Touch Target:** Ensure the touch target remains accessible (minimum 44x44px recommended for mobile) even if the visual size is smaller.
|
|
37
|
+
|
|
38
|
+
## Types
|
|
39
|
+
|
|
40
|
+
### Primary
|
|
41
|
+
The primary button should draw attention to the most important action on a screen, so reserve it for actions that are essential to the experience. This helps create a clear visual hierarchy and keeps users focused on what matters most.
|
|
42
|
+
|
|
43
|
+
<Canvas of={DsIconButtonStories.Primary} />
|
|
44
|
+
|
|
45
|
+
### Secondary
|
|
46
|
+
The secondary button supports less critical actions and complements the primary button.
|
|
47
|
+
|
|
48
|
+
<Canvas of={DsIconButtonStories.Secondary} />
|
|
49
|
+
|
|
50
|
+
### Outline
|
|
51
|
+
Outline buttons have less prominence than a primary button and slightly more prominence than a tertiary button. It can be used on its own or when there is no clear distinction between multiple important actions on a screen.
|
|
52
|
+
|
|
53
|
+
<Canvas of={DsIconButtonStories.Outline} />
|
|
54
|
+
|
|
55
|
+
### Tertiary
|
|
56
|
+
Tertiary buttons have the lowest prominence, so they should be used for low impact actions and/or actions that are not directly related with the primary button.
|
|
57
|
+
|
|
58
|
+
<Canvas of={DsIconButtonStories.Tertiary} />
|
|
59
|
+
|
|
60
|
+
### Action
|
|
61
|
+
Action buttons let users complete routine tasks or make selections within a workflow. They’re designed to be subtle controls and usually appear inside containers like action bars, so they don’t compete with primary call-to-action elements.
|
|
62
|
+
|
|
63
|
+
<Canvas of={DsIconButtonStories.Action} />
|
|
64
|
+
|
|
65
|
+
## Functionality & States
|
|
66
|
+
|
|
67
|
+
### Sizes
|
|
68
|
+
Icon buttons support different sizes to fit various contexts.
|
|
69
|
+
|
|
70
|
+
<Canvas of={DsIconButtonStories.SizeSmall} />
|
|
71
|
+
|
|
72
|
+
### Disabled
|
|
73
|
+
Use the disabled state to indicate that an action is currently unavailable.
|
|
74
|
+
|
|
75
|
+
<Canvas of={DsIconButtonStories.Disabled} />
|
|
76
|
+
|
|
77
|
+
## Accessibility
|
|
78
|
+
|
|
79
|
+
<table style={{ width: '100%' }}>
|
|
80
|
+
<thead>
|
|
81
|
+
<tr>
|
|
82
|
+
<th>Attribute</th>
|
|
83
|
+
<th>Value</th>
|
|
84
|
+
<th>Notes</th>
|
|
85
|
+
</tr>
|
|
86
|
+
</thead>
|
|
87
|
+
<tbody>
|
|
88
|
+
<tr>
|
|
89
|
+
<td><code>role</code></td>
|
|
90
|
+
<td><code>button</code></td>
|
|
91
|
+
<td>Native button element provides this</td>
|
|
92
|
+
</tr>
|
|
93
|
+
<tr>
|
|
94
|
+
<td><code>aria-label</code></td>
|
|
95
|
+
<td><strong>Required</strong></td>
|
|
96
|
+
<td>Must describe the action (e.g., "Close", "Add item")</td>
|
|
97
|
+
</tr>
|
|
98
|
+
<tr>
|
|
99
|
+
<td><code>disabled</code></td>
|
|
100
|
+
<td><code>true/false</code></td>
|
|
101
|
+
<td>Disables interaction and updates <code>aria-disabled</code></td>
|
|
102
|
+
</tr>
|
|
103
|
+
</tbody>
|
|
104
|
+
</table>
|
|
105
|
+
|
|
106
|
+
> ⚠️ **Important:** Icon-only buttons have no visible label, so `aria-label` is mandatory for screen reader users.
|
|
107
|
+
|
|
108
|
+
### Keyboard Support
|
|
109
|
+
|
|
110
|
+
<table>
|
|
111
|
+
<thead>
|
|
112
|
+
<tr>
|
|
113
|
+
<th>Key</th>
|
|
114
|
+
<th>Action</th>
|
|
115
|
+
</tr>
|
|
116
|
+
</thead>
|
|
117
|
+
<tbody>
|
|
118
|
+
<tr>
|
|
119
|
+
<td><kbd>Enter</kbd> / <kbd>Space</kbd></td>
|
|
120
|
+
<td>Activates the button</td>
|
|
121
|
+
</tr>
|
|
122
|
+
<tr>
|
|
123
|
+
<td><kbd>Tab</kbd></td>
|
|
124
|
+
<td>Moves focus to next focusable element</td>
|
|
125
|
+
</tr>
|
|
126
|
+
<tr>
|
|
127
|
+
<td><kbd>Shift</kbd> + <kbd>Tab</kbd></td>
|
|
128
|
+
<td>Moves focus to previous focusable element</td>
|
|
129
|
+
</tr>
|
|
130
|
+
</tbody>
|
|
131
|
+
</table>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import './ds-icon-button.js';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
title: 'Components/Icon Button',
|
|
5
|
+
component: 'ds-icon-button',
|
|
6
|
+
argTypes: {
|
|
7
|
+
icon: { control: 'text' },
|
|
8
|
+
variant: {
|
|
9
|
+
control: 'select',
|
|
10
|
+
options: ['primary', 'secondary', 'outline', 'action', 'tertiary'],
|
|
11
|
+
},
|
|
12
|
+
size: {
|
|
13
|
+
control: 'select',
|
|
14
|
+
options: ['m', 's'],
|
|
15
|
+
},
|
|
16
|
+
disabled: { control: 'boolean' },
|
|
17
|
+
'aria-label': { control: 'text' }
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const createIconButton = ({ icon, variant, size, disabled, 'aria-label': ariaLabel }) => {
|
|
22
|
+
const btn = document.createElement('ds-icon-button');
|
|
23
|
+
|
|
24
|
+
if (icon) btn.setAttribute('icon', icon);
|
|
25
|
+
if (variant) btn.setAttribute('variant', variant);
|
|
26
|
+
if (size) btn.setAttribute('size', size);
|
|
27
|
+
if (disabled) btn.setAttribute('disabled', '');
|
|
28
|
+
if (ariaLabel) btn.setAttribute('aria-label', ariaLabel);
|
|
29
|
+
|
|
30
|
+
return btn;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const Primary = {
|
|
34
|
+
args: {
|
|
35
|
+
icon: 'add',
|
|
36
|
+
variant: 'primary',
|
|
37
|
+
size: 'm',
|
|
38
|
+
'aria-label': 'Add item'
|
|
39
|
+
},
|
|
40
|
+
render: createIconButton
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const Secondary = {
|
|
44
|
+
args: {
|
|
45
|
+
icon: 'close',
|
|
46
|
+
variant: 'secondary',
|
|
47
|
+
size: 'm',
|
|
48
|
+
'aria-label': 'Close'
|
|
49
|
+
},
|
|
50
|
+
render: createIconButton
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const Outline = {
|
|
54
|
+
args: {
|
|
55
|
+
icon: 'info',
|
|
56
|
+
variant: 'outline',
|
|
57
|
+
size: 'm',
|
|
58
|
+
'aria-label': 'More Information'
|
|
59
|
+
},
|
|
60
|
+
render: createIconButton
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const Action = {
|
|
64
|
+
args: {
|
|
65
|
+
icon: 'more-vert',
|
|
66
|
+
variant: 'action',
|
|
67
|
+
size: 'm',
|
|
68
|
+
'aria-label': 'More Options'
|
|
69
|
+
},
|
|
70
|
+
render: createIconButton
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const Tertiary = {
|
|
74
|
+
args: {
|
|
75
|
+
icon: 'delete',
|
|
76
|
+
variant: 'tertiary',
|
|
77
|
+
size: 'm',
|
|
78
|
+
'aria-label': 'Delete'
|
|
79
|
+
},
|
|
80
|
+
render: createIconButton
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const SizeSmall = {
|
|
84
|
+
args: {
|
|
85
|
+
icon: 'add',
|
|
86
|
+
variant: 'primary',
|
|
87
|
+
size: 's',
|
|
88
|
+
'aria-label': 'Add item'
|
|
89
|
+
},
|
|
90
|
+
render: createIconButton
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const Disabled = {
|
|
94
|
+
args: {
|
|
95
|
+
icon: 'add',
|
|
96
|
+
variant: 'primary',
|
|
97
|
+
size: 'm',
|
|
98
|
+
disabled: true,
|
|
99
|
+
'aria-label': 'Add item'
|
|
100
|
+
},
|
|
101
|
+
render: createIconButton
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const AllVariants = {
|
|
105
|
+
render: () => {
|
|
106
|
+
const container = document.createElement('div');
|
|
107
|
+
container.style.cssText = 'display: flex; gap: 16px; align-items: center;';
|
|
108
|
+
|
|
109
|
+
['primary', 'secondary', 'outline', 'action', 'tertiary'].forEach(variant => {
|
|
110
|
+
container.appendChild(createIconButton({ icon: 'star', variant, size: 'm', 'aria-label': `Star ${variant}` }));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return container;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const AllSizes = {
|
|
118
|
+
render: () => {
|
|
119
|
+
const container = document.createElement('div');
|
|
120
|
+
container.style.cssText = 'display: flex; gap: 16px; align-items: center;';
|
|
121
|
+
|
|
122
|
+
['m', 's'].forEach(size => {
|
|
123
|
+
container.appendChild(createIconButton({ icon: 'add', variant: 'primary', size, 'aria-label': `Add (${size})` }));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return container;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import './ds-icon-button.js';
|
|
3
|
+
|
|
4
|
+
describe('ds-icon-button', () => {
|
|
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 default props', async () => {
|
|
17
|
+
container.innerHTML = '<ds-icon-button icon="add" aria-label="Add"></ds-icon-button>';
|
|
18
|
+
const iconButton = container.querySelector('ds-icon-button');
|
|
19
|
+
|
|
20
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
21
|
+
|
|
22
|
+
expect(iconButton.shadowRoot).toBeTruthy();
|
|
23
|
+
expect(iconButton.variant).toBe('primary');
|
|
24
|
+
expect(iconButton.size).toBe('m');
|
|
25
|
+
expect(iconButton.disabled).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should reflect variant attribute', async () => {
|
|
29
|
+
container.innerHTML = '<ds-icon-button icon="close" variant="secondary" aria-label="Close"></ds-icon-button>';
|
|
30
|
+
const iconButton = container.querySelector('ds-icon-button');
|
|
31
|
+
|
|
32
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
33
|
+
|
|
34
|
+
expect(iconButton.getAttribute('variant')).toBe('secondary');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should reflect size attribute', async () => {
|
|
38
|
+
container.innerHTML = '<ds-icon-button icon="add" size="s" aria-label="Add"></ds-icon-button>';
|
|
39
|
+
const iconButton = container.querySelector('ds-icon-button');
|
|
40
|
+
|
|
41
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
42
|
+
|
|
43
|
+
expect(iconButton.getAttribute('size')).toBe('s');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should reflect disabled attribute', async () => {
|
|
47
|
+
container.innerHTML = '<ds-icon-button icon="add" disabled aria-label="Add"></ds-icon-button>';
|
|
48
|
+
const iconButton = container.querySelector('ds-icon-button');
|
|
49
|
+
|
|
50
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
51
|
+
|
|
52
|
+
expect(iconButton.hasAttribute('disabled')).toBe(true);
|
|
53
|
+
const button = iconButton.shadowRoot.querySelector('button');
|
|
54
|
+
expect(button.disabled).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should render ds-icon with correct name', async () => {
|
|
58
|
+
container.innerHTML = '<ds-icon-button icon="star" aria-label="Star"></ds-icon-button>';
|
|
59
|
+
const iconButton = container.querySelector('ds-icon-button');
|
|
60
|
+
|
|
61
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
62
|
+
|
|
63
|
+
const icon = iconButton.shadowRoot.querySelector('ds-icon');
|
|
64
|
+
expect(icon).toBeTruthy();
|
|
65
|
+
expect(icon.getAttribute('name')).toBe('star');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should warn and render nothing when icon prop is missing', async () => {
|
|
69
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
70
|
+
container.innerHTML = '<ds-icon-button aria-label="Test"></ds-icon-button>';
|
|
71
|
+
const iconButton = container.querySelector('ds-icon-button');
|
|
72
|
+
|
|
73
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
74
|
+
|
|
75
|
+
expect(warnSpy).toHaveBeenCalledWith('ds-icon-button: "icon" prop is required');
|
|
76
|
+
const button = iconButton.shadowRoot.querySelector('button');
|
|
77
|
+
expect(button).toBeFalsy();
|
|
78
|
+
warnSpy.mockRestore();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should warn when aria-label is missing', async () => {
|
|
82
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
83
|
+
container.innerHTML = '<ds-icon-button icon="add"></ds-icon-button>';
|
|
84
|
+
|
|
85
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
86
|
+
|
|
87
|
+
expect(warnSpy).toHaveBeenCalledWith('ds-icon-button: "aria-label" is required for accessibility');
|
|
88
|
+
warnSpy.mockRestore();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DsIconButton } from './ds-icon-button.js';
|