@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,110 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
import './ds-toast.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Toast Provider component.
|
|
6
|
+
* Manages the display and stacking of toast notifications.
|
|
7
|
+
* Listens for 'ds-toast-show' events on the window.
|
|
8
|
+
*
|
|
9
|
+
* @element ds-toast-provider
|
|
10
|
+
*/
|
|
11
|
+
export class DsToastProvider extends LitElement {
|
|
12
|
+
static properties = {
|
|
13
|
+
_toasts: { type: Array, state: true }
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
static styles = css`
|
|
17
|
+
:host {
|
|
18
|
+
position: fixed;
|
|
19
|
+
top: 0;
|
|
20
|
+
right: 0;
|
|
21
|
+
z-index: var(--ds-zindex-toast, 100);
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
gap: var(--ds-space-sm, 8px); /* 8px gap between toasts */
|
|
25
|
+
padding: var(--ds-space-md, 16px);
|
|
26
|
+
max-height: 100vh;
|
|
27
|
+
overflow-y: auto;
|
|
28
|
+
pointer-events: none; /* Allow clicks to pass through empty space */
|
|
29
|
+
box-sizing: border-box;
|
|
30
|
+
|
|
31
|
+
/* Scrollbar styling */
|
|
32
|
+
scrollbar-width: thin;
|
|
33
|
+
scrollbar-color: var(--ds-color-border-default, #ccc) transparent;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* Webkit scrollbar */
|
|
37
|
+
:host::-webkit-scrollbar {
|
|
38
|
+
width: 6px;
|
|
39
|
+
}
|
|
40
|
+
:host::-webkit-scrollbar-track {
|
|
41
|
+
background: transparent;
|
|
42
|
+
}
|
|
43
|
+
:host::-webkit-scrollbar-thumb {
|
|
44
|
+
background-color: var(--ds-color-border-default, #ccc);
|
|
45
|
+
border-radius: 3px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
ds-toast {
|
|
49
|
+
pointer-events: auto; /* Re-enable clicks on toasts */
|
|
50
|
+
flex-shrink: 0;
|
|
51
|
+
transition: all 0.3s ease;
|
|
52
|
+
}
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
constructor() {
|
|
56
|
+
super();
|
|
57
|
+
this._toasts = [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
connectedCallback() {
|
|
61
|
+
super.connectedCallback();
|
|
62
|
+
window.addEventListener('ds-toast-show', this._handleToastShow);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
disconnectedCallback() {
|
|
66
|
+
super.disconnectedCallback();
|
|
67
|
+
window.removeEventListener('ds-toast-show', this._handleToastShow);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_handleToastShow = (event) => {
|
|
71
|
+
const { detail } = event;
|
|
72
|
+
const id = Date.now().toString(36) + Math.random().toString(36).substr(2);
|
|
73
|
+
|
|
74
|
+
// Add new toast to the beginning of the array (top of stack)
|
|
75
|
+
this._toasts = [
|
|
76
|
+
{
|
|
77
|
+
id,
|
|
78
|
+
...detail
|
|
79
|
+
},
|
|
80
|
+
...this._toasts
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
_handleDismiss(id) {
|
|
85
|
+
this._toasts = this._toasts.filter(toast => toast.id !== id);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
render() {
|
|
89
|
+
return html`
|
|
90
|
+
${this._toasts.map(toast => html`
|
|
91
|
+
<ds-toast
|
|
92
|
+
.status="${toast.status || 'info'}"
|
|
93
|
+
.title="${toast.title || ''}"
|
|
94
|
+
.message="${toast.message}"
|
|
95
|
+
.dismissible="${toast.dismissible !== false}"
|
|
96
|
+
.duration="${toast.duration ?? 5000}"
|
|
97
|
+
@ds-dismiss="${() => this._handleDismiss(toast.id)}"
|
|
98
|
+
>
|
|
99
|
+
${toast.actions ? html`
|
|
100
|
+
<div slot="actions">
|
|
101
|
+
${toast.actions}
|
|
102
|
+
</div>
|
|
103
|
+
` : ''}
|
|
104
|
+
</ds-toast>
|
|
105
|
+
`)}
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
customElements.define('ds-toast-provider', DsToastProvider);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import axe from 'axe-core';
|
|
3
|
+
import './ds-toast.js';
|
|
4
|
+
|
|
5
|
+
describe('ds-toast 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', async () => {
|
|
18
|
+
container.innerHTML = '<ds-toast message="Accessible toast message"></ds-toast>';
|
|
19
|
+
const element = container.querySelector('ds-toast');
|
|
20
|
+
await element.updateComplete;
|
|
21
|
+
|
|
22
|
+
const results = await axe.run(element);
|
|
23
|
+
expect(results.violations.length).toBe(0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should pass axe checks with status variants', async () => {
|
|
27
|
+
container.innerHTML = '<ds-toast status="error" title="Error" message="Something went wrong"></ds-toast>';
|
|
28
|
+
const element = container.querySelector('ds-toast');
|
|
29
|
+
await element.updateComplete;
|
|
30
|
+
|
|
31
|
+
const results = await axe.run(element);
|
|
32
|
+
expect(results.violations.length).toBe(0);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
import '../ds-icon/ds-icon.js';
|
|
3
|
+
import '../ds-icon-button/ds-icon-button.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Toast component for temporary notifications.
|
|
7
|
+
* Designed to be used within ds-toast-provider, but can be used standalone.
|
|
8
|
+
*
|
|
9
|
+
* @element ds-toast
|
|
10
|
+
*
|
|
11
|
+
* @prop {string} status - Status type: 'error' | 'warning' | 'success' | 'info' (default: 'info')
|
|
12
|
+
* @prop {string} title - Optional title
|
|
13
|
+
* @prop {string} message - Message text
|
|
14
|
+
* @prop {boolean} dismissible - Show/hide close button (default: false)
|
|
15
|
+
* @prop {number} duration - Auto-dismiss duration in ms. 0 to disable. (default: 5000)
|
|
16
|
+
*
|
|
17
|
+
* @fires ds-dismiss - Dispatched when closed (manually or auto)
|
|
18
|
+
*/
|
|
19
|
+
export class DsToast extends LitElement {
|
|
20
|
+
static properties = {
|
|
21
|
+
status: { type: String, reflect: true },
|
|
22
|
+
title: { type: String },
|
|
23
|
+
message: { type: String },
|
|
24
|
+
dismissible: { type: Boolean },
|
|
25
|
+
duration: { type: Number },
|
|
26
|
+
_hasActions: { type: Boolean, state: true }
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
static styles = css`
|
|
30
|
+
:host {
|
|
31
|
+
display: block;
|
|
32
|
+
width: 344px; /* Fixed width as requested */
|
|
33
|
+
box-sizing: border-box;
|
|
34
|
+
/* Animation entry is handled by the provider usually, or host animation */
|
|
35
|
+
animation: slide-in 0.3s cubic-bezier(0.2, 0, 0, 1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@keyframes slide-in {
|
|
39
|
+
from {
|
|
40
|
+
transform: translateX(100%);
|
|
41
|
+
opacity: 0;
|
|
42
|
+
}
|
|
43
|
+
to {
|
|
44
|
+
transform: translateX(0);
|
|
45
|
+
opacity: 1;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.toast {
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: flex-start;
|
|
52
|
+
gap: var(--ds-space-sm); /* 8px */
|
|
53
|
+
padding: 10px 8px; /* Matching ds-alert padding */
|
|
54
|
+
border-radius: var(--ds-radius-container); /* 0px - sharp */
|
|
55
|
+
box-sizing: border-box;
|
|
56
|
+
box-shadow: var(--ds-shadow-overlay, 0 4px 12px rgba(0, 0, 0, 0.15));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Side containers matching ds-alert strategy for alignment */
|
|
60
|
+
.icon-container,
|
|
61
|
+
.dismiss-container {
|
|
62
|
+
flex-shrink: 0;
|
|
63
|
+
display: flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
justify-content: center;
|
|
66
|
+
height: 20px; /* text line-height matching */
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Status icon */
|
|
70
|
+
.icon {
|
|
71
|
+
color: var(--ds-color-icon-default);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Content Area */
|
|
75
|
+
.content {
|
|
76
|
+
flex: 1;
|
|
77
|
+
display: flex;
|
|
78
|
+
flex-direction: column;
|
|
79
|
+
min-width: 0;
|
|
80
|
+
font: var(--ds-typo-content-body-regular);
|
|
81
|
+
color: var(--ds-color-text-default);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.title {
|
|
85
|
+
font: var(--ds-typo-content-body-bold);
|
|
86
|
+
margin-bottom: var(--ds-space-xs); /* 4px */
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.message {
|
|
90
|
+
word-wrap: break-word; /* Ensure text wraps */
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.actions {
|
|
94
|
+
display: flex;
|
|
95
|
+
gap: var(--ds-space-sm); /* 8px gap */
|
|
96
|
+
margin-top: var(--ds-space-sm);
|
|
97
|
+
flex-wrap: wrap;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.actions[hidden] {
|
|
101
|
+
display: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Dismiss button container needs to be pushed to the right if we want it fully responsive,
|
|
105
|
+
but in this flex layout, flex:1 on message area handles it. */
|
|
106
|
+
.dismiss-container {
|
|
107
|
+
margin-left: auto; /* Push to the far right if content doesn't fill */
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* Close button */
|
|
111
|
+
.close-button {
|
|
112
|
+
/* No absolute sizing here, handled by parent flex centering */
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Status Styles - Background Colors */
|
|
116
|
+
:host([status="error"]) .toast {
|
|
117
|
+
background-color: var(--ds-color-bg-error-subtle);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
:host([status="warning"]) .toast {
|
|
121
|
+
background-color: var(--ds-color-bg-warning-subtle);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
:host([status="success"]) .toast {
|
|
125
|
+
background-color: var(--ds-color-bg-success-subtle);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
:host([status="info"]) .toast,
|
|
129
|
+
:host(:not([status])) .toast {
|
|
130
|
+
background-color: var(--ds-color-bg-info-subtle);
|
|
131
|
+
}
|
|
132
|
+
`;
|
|
133
|
+
|
|
134
|
+
constructor() {
|
|
135
|
+
super();
|
|
136
|
+
this.status = 'info';
|
|
137
|
+
this.title = '';
|
|
138
|
+
this.message = '';
|
|
139
|
+
this.dismissible = true; /* Default true for toasts usually, unless strictly programmatic */
|
|
140
|
+
this.duration = 5000; /* Default 5s */
|
|
141
|
+
this._hasActions = false;
|
|
142
|
+
this._timer = null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
connectedCallback() {
|
|
146
|
+
super.connectedCallback();
|
|
147
|
+
this._startTimer();
|
|
148
|
+
|
|
149
|
+
// Pause on hover
|
|
150
|
+
this.addEventListener('mouseenter', this._pauseTimer);
|
|
151
|
+
this.addEventListener('mouseleave', this._resumeTimer);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
disconnectedCallback() {
|
|
155
|
+
super.disconnectedCallback();
|
|
156
|
+
this._clearTimer();
|
|
157
|
+
this.removeEventListener('mouseenter', this._pauseTimer);
|
|
158
|
+
this.removeEventListener('mouseleave', this._resumeTimer);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
_startTimer() {
|
|
162
|
+
if (this.duration > 0) {
|
|
163
|
+
this._timer = setTimeout(() => {
|
|
164
|
+
this._dismiss();
|
|
165
|
+
}, this.duration);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
_clearTimer() {
|
|
170
|
+
if (this._timer) {
|
|
171
|
+
clearTimeout(this._timer);
|
|
172
|
+
this._timer = null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
_pauseTimer = () => {
|
|
177
|
+
this._clearTimer();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
_resumeTimer = () => {
|
|
181
|
+
// Ideally we would track remaining time, but resetting to full duration upon simple hover exit
|
|
182
|
+
// is a common and acceptable simplifcation for toasts (gives user time to read).
|
|
183
|
+
// Or we could implement strict remaining time logic. For now, restarting is safer UX.
|
|
184
|
+
this._startTimer();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
_dismiss() {
|
|
188
|
+
this.dispatchEvent(new CustomEvent('ds-dismiss', { bubbles: true, composed: true }));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
_getIconName() {
|
|
192
|
+
const iconMap = {
|
|
193
|
+
error: 'cancel',
|
|
194
|
+
warning: 'warning',
|
|
195
|
+
success: 'check-circle',
|
|
196
|
+
info: 'info'
|
|
197
|
+
};
|
|
198
|
+
return iconMap[this.status] || iconMap.info;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
_handleSlotChange(e) {
|
|
202
|
+
const assignedNodes = e.target.assignedNodes({ flatten: true });
|
|
203
|
+
this._hasActions = assignedNodes.length > 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
render() {
|
|
207
|
+
return html`
|
|
208
|
+
<div class="toast" role="alert">
|
|
209
|
+
<div class="icon-container">
|
|
210
|
+
<ds-icon
|
|
211
|
+
class="icon"
|
|
212
|
+
name="${this._getIconName()}"
|
|
213
|
+
size="sm"
|
|
214
|
+
></ds-icon>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div class="content">
|
|
218
|
+
${this.title ? html`<div class="title">${this.title}</div>` : ''}
|
|
219
|
+
<div class="message">${this.message}</div>
|
|
220
|
+
|
|
221
|
+
<div class="actions" ?hidden="${!this._hasActions}">
|
|
222
|
+
<slot name="actions" @slotchange="${this._handleSlotChange}"></slot>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
${this.dismissible ? html`
|
|
227
|
+
<div class="dismiss-container">
|
|
228
|
+
<ds-icon-button
|
|
229
|
+
class="close-button"
|
|
230
|
+
icon="close"
|
|
231
|
+
variant="action"
|
|
232
|
+
size="m"
|
|
233
|
+
aria-label="Fechar notificação"
|
|
234
|
+
@click="${this._dismiss}"
|
|
235
|
+
></ds-icon-button>
|
|
236
|
+
</div>
|
|
237
|
+
` : ''}
|
|
238
|
+
</div>
|
|
239
|
+
`;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
customElements.define('ds-toast', DsToast);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import './ds-toast.js';
|
|
3
|
+
import './ds-toast-provider.js';
|
|
4
|
+
import '../ds-button/ds-button.js';
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
title: 'Components/Toast',
|
|
8
|
+
component: 'ds-toast',
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
argTypes: {
|
|
11
|
+
status: {
|
|
12
|
+
control: 'select',
|
|
13
|
+
options: ['error', 'warning', 'success', 'info']
|
|
14
|
+
},
|
|
15
|
+
duration: {
|
|
16
|
+
control: { type: 'number', min: 0 }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const Template = (args) => html`
|
|
22
|
+
<div style="position: relative; height: 150px; width: 400px; border: 1px dashed #ccc; padding: 20px;">
|
|
23
|
+
<ds-toast
|
|
24
|
+
.status=${args.status}
|
|
25
|
+
.title=${args.title}
|
|
26
|
+
.message=${args.message}
|
|
27
|
+
.dismissible=${args.dismissible}
|
|
28
|
+
.duration=${args.duration}
|
|
29
|
+
>
|
|
30
|
+
${args.slotContent ? html`<div slot="actions">${args.slotContent}</div>` : ''}
|
|
31
|
+
</ds-toast>
|
|
32
|
+
</div>
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
export const Default = {
|
|
36
|
+
args: {
|
|
37
|
+
status: 'info',
|
|
38
|
+
message: 'Your changes have been saved.',
|
|
39
|
+
dismissible: true,
|
|
40
|
+
duration: 0
|
|
41
|
+
},
|
|
42
|
+
render: (args) => Template(args)
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const WithTitle = {
|
|
46
|
+
args: {
|
|
47
|
+
status: 'success',
|
|
48
|
+
title: 'Success',
|
|
49
|
+
message: 'Profile updated successfully.',
|
|
50
|
+
dismissible: true,
|
|
51
|
+
duration: 0
|
|
52
|
+
},
|
|
53
|
+
render: (args) => Template(args)
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const NotDismissible = {
|
|
57
|
+
args: {
|
|
58
|
+
status: 'info',
|
|
59
|
+
message: 'This notification cannot be dismissed by the user.',
|
|
60
|
+
dismissible: false,
|
|
61
|
+
duration: 0
|
|
62
|
+
},
|
|
63
|
+
render: (args) => Template(args)
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const WithActions = {
|
|
67
|
+
args: {
|
|
68
|
+
status: 'warning',
|
|
69
|
+
message: 'Connection lost. Retrying...',
|
|
70
|
+
dismissible: true,
|
|
71
|
+
duration: 0
|
|
72
|
+
},
|
|
73
|
+
render: (args) => html`
|
|
74
|
+
<div style="position: relative; height: 150px; width: 400px; border: 1px dashed #ccc; padding: 20px;">
|
|
75
|
+
<ds-toast
|
|
76
|
+
.status=${args.status}
|
|
77
|
+
.title=${args.title}
|
|
78
|
+
.message=${args.message}
|
|
79
|
+
.dismissible=${args.dismissible}
|
|
80
|
+
.duration=${args.duration}
|
|
81
|
+
>
|
|
82
|
+
<div slot="actions" style="display: flex; gap: 8px; align-items: center;">
|
|
83
|
+
<ds-button variant="action">Retry</ds-button>
|
|
84
|
+
<ds-button variant="action">Dismiss</ds-button>
|
|
85
|
+
</div>
|
|
86
|
+
</ds-toast>
|
|
87
|
+
</div>
|
|
88
|
+
`
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Demonstrates the stacking behavior using ds-toast-provider.
|
|
93
|
+
* Click the buttons to trigger toasts.
|
|
94
|
+
*/
|
|
95
|
+
export const ToastSystem = {
|
|
96
|
+
render: () => html`
|
|
97
|
+
<div>
|
|
98
|
+
<ds-toast-provider></ds-toast-provider>
|
|
99
|
+
|
|
100
|
+
<div style="display: flex; gap: 8px; flex-direction: column; align-items: flex-start;">
|
|
101
|
+
<p>Click buttons to trigger toasts (Fixed Top-Right)</p>
|
|
102
|
+
|
|
103
|
+
<ds-button variant="secondary" @click=${() => dispatchToast('success', 'Saved', 'Changes saved successfully!')}>
|
|
104
|
+
Trigger Success
|
|
105
|
+
</ds-button>
|
|
106
|
+
|
|
107
|
+
<ds-button variant="secondary" @click=${() => dispatchToast('error', 'Error', 'Failed to save changes.', 0)}>
|
|
108
|
+
Trigger Error (Persistent)
|
|
109
|
+
</ds-button>
|
|
110
|
+
|
|
111
|
+
<ds-button variant="secondary" @click=${() => dispatchToast('info', '', 'Simple info message without title')}>
|
|
112
|
+
Trigger Info (No Title)
|
|
113
|
+
</ds-button>
|
|
114
|
+
|
|
115
|
+
<ds-button variant="secondary" @click=${() => dispatchToast('warning', 'Warning', 'System overload!', 3000)}>
|
|
116
|
+
Trigger Warning
|
|
117
|
+
</ds-button>
|
|
118
|
+
|
|
119
|
+
<ds-button variant="secondary" @click=${() => dispatchMany()}>
|
|
120
|
+
Trigger Many (Overflow Test)
|
|
121
|
+
</ds-button>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
`
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
function dispatchToast(status, title, message, duration = 3000) {
|
|
128
|
+
const event = new CustomEvent('ds-toast-show', {
|
|
129
|
+
detail: { status, title, message, duration, dismissible: true },
|
|
130
|
+
bubbles: true,
|
|
131
|
+
composed: true
|
|
132
|
+
});
|
|
133
|
+
window.dispatchEvent(event);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function dispatchMany() {
|
|
137
|
+
let count = 0;
|
|
138
|
+
const interval = setInterval(() => {
|
|
139
|
+
dispatchToast('info', `Notification ${count + 1}`, `This is toast number ${count + 1}`);
|
|
140
|
+
count++;
|
|
141
|
+
if (count >= 5) clearInterval(interval);
|
|
142
|
+
}, 200);
|
|
143
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import './ds-toast.js';
|
|
3
|
+
|
|
4
|
+
describe('ds-toast', () => {
|
|
5
|
+
let container;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
container = document.createElement('div');
|
|
9
|
+
document.body.appendChild(container);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
container.remove();
|
|
14
|
+
vi.useRealTimers();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('renders with default values', async () => {
|
|
18
|
+
container.innerHTML = '<ds-toast message="Test message"></ds-toast>';
|
|
19
|
+
const element = container.querySelector('ds-toast');
|
|
20
|
+
await element.updateComplete;
|
|
21
|
+
|
|
22
|
+
expect(element.status).toBe('info');
|
|
23
|
+
expect(element.message).toBe('Test message');
|
|
24
|
+
expect(element.dismissible).toBe(true);
|
|
25
|
+
expect(element.duration).toBe(5000);
|
|
26
|
+
|
|
27
|
+
const toast = element.shadowRoot.querySelector('.toast');
|
|
28
|
+
expect(toast).toBeTruthy();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('renders with custom status and title', async () => {
|
|
32
|
+
container.innerHTML = '<ds-toast status="success" title="Success Title" message="Success Message"></ds-toast>';
|
|
33
|
+
const element = container.querySelector('ds-toast');
|
|
34
|
+
await element.updateComplete;
|
|
35
|
+
|
|
36
|
+
expect(element.status).toBe('success');
|
|
37
|
+
expect(element.title).toBe('Success Title');
|
|
38
|
+
|
|
39
|
+
const titleEl = element.shadowRoot.querySelector('.title');
|
|
40
|
+
expect(titleEl.textContent).toBe('Success Title');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('dispatches ds-dismiss event on close button click', async () => {
|
|
44
|
+
container.innerHTML = '<ds-toast message="Dismiss me"></ds-toast>';
|
|
45
|
+
const element = container.querySelector('ds-toast');
|
|
46
|
+
await element.updateComplete;
|
|
47
|
+
|
|
48
|
+
const dismissSpy = vi.fn();
|
|
49
|
+
element.addEventListener('ds-dismiss', dismissSpy);
|
|
50
|
+
|
|
51
|
+
const closeBtn = element.shadowRoot.querySelector('.close-button');
|
|
52
|
+
closeBtn.click();
|
|
53
|
+
|
|
54
|
+
expect(dismissSpy).toHaveBeenCalled();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('auto-dismisses after duration', async () => {
|
|
58
|
+
vi.useFakeTimers();
|
|
59
|
+
container.innerHTML = '<ds-toast message="Auto dismiss" duration="1000"></ds-toast>';
|
|
60
|
+
const element = container.querySelector('ds-toast');
|
|
61
|
+
await element.updateComplete;
|
|
62
|
+
|
|
63
|
+
const dismissSpy = vi.fn();
|
|
64
|
+
element.addEventListener('ds-dismiss', dismissSpy);
|
|
65
|
+
|
|
66
|
+
vi.advanceTimersByTime(1000);
|
|
67
|
+
expect(dismissSpy).toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('pauses timer on hover', async () => {
|
|
71
|
+
vi.useFakeTimers();
|
|
72
|
+
container.innerHTML = '<ds-toast message="Hover test" duration="1000"></ds-toast>';
|
|
73
|
+
const element = container.querySelector('ds-toast');
|
|
74
|
+
await element.updateComplete;
|
|
75
|
+
|
|
76
|
+
const dismissSpy = vi.fn();
|
|
77
|
+
element.addEventListener('ds-dismiss', dismissSpy);
|
|
78
|
+
|
|
79
|
+
// Simulate hover
|
|
80
|
+
element.dispatchEvent(new Event('mouseenter'));
|
|
81
|
+
|
|
82
|
+
// Advance time past duration
|
|
83
|
+
vi.advanceTimersByTime(1500);
|
|
84
|
+
expect(dismissSpy).not.toHaveBeenCalled();
|
|
85
|
+
|
|
86
|
+
// Simulate mouse leave
|
|
87
|
+
element.dispatchEvent(new Event('mouseleave'));
|
|
88
|
+
|
|
89
|
+
// Advance timer again
|
|
90
|
+
vi.advanceTimersByTime(1000);
|
|
91
|
+
expect(dismissSpy).toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
});
|