@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 { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import axe from 'axe-core';
|
|
3
|
+
import './ds-tooltip.js';
|
|
4
|
+
|
|
5
|
+
describe('ds-tooltip 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 for simple tooltip', async () => {
|
|
18
|
+
container.innerHTML = '<ds-tooltip content="Accessible tooltip"><button>Trigger</button></ds-tooltip>';
|
|
19
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
20
|
+
|
|
21
|
+
const options = {
|
|
22
|
+
rules: {
|
|
23
|
+
'color-contrast': { enabled: false }
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const results = await axe.run(container, options);
|
|
28
|
+
if (results.violations.length > 0) {
|
|
29
|
+
console.log('Violations:', JSON.stringify(results.violations, null, 2));
|
|
30
|
+
}
|
|
31
|
+
expect(results.violations).toHaveLength(0);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should pass axe accessibility checks for custom tooltip with header and body', async () => {
|
|
35
|
+
container.innerHTML = `
|
|
36
|
+
<ds-tooltip>
|
|
37
|
+
<button>Trigger</button>
|
|
38
|
+
<div slot="header">Header Text</div>
|
|
39
|
+
<div slot="body">Body Text</div>
|
|
40
|
+
</ds-tooltip>
|
|
41
|
+
`;
|
|
42
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
43
|
+
|
|
44
|
+
const options = {
|
|
45
|
+
rules: {
|
|
46
|
+
'color-contrast': { enabled: false }
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const results = await axe.run(container, options);
|
|
51
|
+
if (results.violations.length > 0) {
|
|
52
|
+
console.log('Violations:', JSON.stringify(results.violations, null, 2));
|
|
53
|
+
}
|
|
54
|
+
expect(results.violations).toHaveLength(0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should pass axe accessibility checks for all placement variants', async () => {
|
|
58
|
+
const placements = ['top', 'bottom', 'left', 'right', 'top-start', 'bottom-end'];
|
|
59
|
+
|
|
60
|
+
const options = {
|
|
61
|
+
rules: {
|
|
62
|
+
'color-contrast': { enabled: false }
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
for (const placement of placements) {
|
|
67
|
+
container.innerHTML = `<ds-tooltip placement="${placement}" content="Test tooltip"><button>Test</button></ds-tooltip>`;
|
|
68
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
69
|
+
|
|
70
|
+
const results = await axe.run(container, options);
|
|
71
|
+
if (results.violations.length > 0) {
|
|
72
|
+
console.log(`Violations for placement ${placement}:`, JSON.stringify(results.violations, null, 2));
|
|
73
|
+
}
|
|
74
|
+
expect(results.violations).toHaveLength(0);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should have proper ARIA attributes', async () => {
|
|
79
|
+
container.innerHTML = '<ds-tooltip content="Test tooltip"><button id="trigger">Trigger</button></ds-tooltip>';
|
|
80
|
+
const el = container.querySelector('ds-tooltip');
|
|
81
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
82
|
+
|
|
83
|
+
const tooltip = el.shadowRoot.querySelector('#tooltip');
|
|
84
|
+
const slot = el.shadowRoot.querySelector('slot:not([name])');
|
|
85
|
+
|
|
86
|
+
// Check that tooltip has role="tooltip"
|
|
87
|
+
expect(tooltip.getAttribute('role')).toBe('tooltip');
|
|
88
|
+
|
|
89
|
+
// Check that slot has aria-describedby
|
|
90
|
+
expect(slot.getAttribute('aria-describedby')).toBe('tooltip');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should be keyboard accessible', async () => {
|
|
94
|
+
container.innerHTML = '<ds-tooltip content="Test tooltip"><button tabindex="0">Focus me</button></ds-tooltip>';
|
|
95
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
96
|
+
|
|
97
|
+
const options = {
|
|
98
|
+
rules: {
|
|
99
|
+
'color-contrast': { enabled: false }
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const results = await axe.run(container, options);
|
|
104
|
+
expect(results.violations).toHaveLength(0);
|
|
105
|
+
|
|
106
|
+
// Verify button is focusable
|
|
107
|
+
const button = container.querySelector('button');
|
|
108
|
+
expect(button.tabIndex).toBe(0);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
import { PositionerController } from './positioner.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @summary Tooltip component for displaying contextual information.
|
|
6
|
+
*
|
|
7
|
+
* @slot - The trigger element (e.g., a button or icon).
|
|
8
|
+
* @slot header - Optional header content for the custom tooltip variant.
|
|
9
|
+
* @slot body - Optional body content for the custom tooltip variant.
|
|
10
|
+
*
|
|
11
|
+
* @cssprop --ds-tooltip-z-index - Z-index for the tooltip (default: var(--ds-zindex-tooltip))
|
|
12
|
+
*/
|
|
13
|
+
export class DsTooltip extends LitElement {
|
|
14
|
+
static properties = {
|
|
15
|
+
placement: { type: String },
|
|
16
|
+
content: { type: String },
|
|
17
|
+
manual: { type: Boolean }, // When true, tooltip is controlled programmatically
|
|
18
|
+
_open: { state: true },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
super();
|
|
23
|
+
this.placement = 'top';
|
|
24
|
+
this.content = '';
|
|
25
|
+
this.manual = false;
|
|
26
|
+
this._open = false;
|
|
27
|
+
this.positioner = new PositionerController(this);
|
|
28
|
+
this._triggerElements = [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
disconnectedCallback() {
|
|
32
|
+
super.disconnectedCallback();
|
|
33
|
+
this._removeEventListeners();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static styles = css`
|
|
37
|
+
:host {
|
|
38
|
+
display: inline-block;
|
|
39
|
+
position: relative;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* The tooltip container */
|
|
43
|
+
.tooltip {
|
|
44
|
+
position: absolute; /* Absolute within the host container */
|
|
45
|
+
top: 0;
|
|
46
|
+
left: 0;
|
|
47
|
+
width: max-content;
|
|
48
|
+
max-width: 300px;
|
|
49
|
+
|
|
50
|
+
background-color: var(--ds-color-bg-default);
|
|
51
|
+
color: var(--ds-color-text-default);
|
|
52
|
+
|
|
53
|
+
/* Typography */
|
|
54
|
+
font: var(--ds-typo-content-body-regular);
|
|
55
|
+
|
|
56
|
+
/* Spacing & Shape */
|
|
57
|
+
padding: var(--ds-space-md); /* 16px */
|
|
58
|
+
border-radius: var(--ds-radius-container);
|
|
59
|
+
|
|
60
|
+
/* Shadow */
|
|
61
|
+
box-shadow: var(--ds-elevation-floating);
|
|
62
|
+
|
|
63
|
+
/* Visibility */
|
|
64
|
+
display: none;
|
|
65
|
+
z-index: var(--ds-zindex-tooltip, 1000);
|
|
66
|
+
|
|
67
|
+
/* Prevent mouse interaction with tooltip itself (optional, but good for simple tooltips)
|
|
68
|
+
Use pointer-events: auto if we want interactive tooltips (e.g. links inside)
|
|
69
|
+
*/
|
|
70
|
+
pointer-events: none;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.tooltip[data-show] {
|
|
74
|
+
display: block;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Content Layout */
|
|
78
|
+
.content-wrapper {
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-direction: column;
|
|
81
|
+
gap: var(--ds-space-xs); /* Inner spacing if needed, but requirements say logic for divider */
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Custom variant: Header & Body handling */
|
|
85
|
+
.header {
|
|
86
|
+
font: var(--ds-typo-content-body-bold);
|
|
87
|
+
margin-bottom: var(--ds-space-xs);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.divider {
|
|
91
|
+
height: 1px;
|
|
92
|
+
background-color: var(--ds-color-border-default);
|
|
93
|
+
width: 100%;
|
|
94
|
+
/* Negative margins to span full width including padding?
|
|
95
|
+
Requirement: "divider que vai de uma ponta a ponta. Mantendo o espaço de 16 pixels."
|
|
96
|
+
This implies the divider should be inside the padding or the padding is around the content?
|
|
97
|
+
"divisão entre o header e o body... divider vai de ponta a ponta" often means full bleed.
|
|
98
|
+
If padding is on container, full bleed needs negative margins.
|
|
99
|
+
*/
|
|
100
|
+
margin-left: calc(var(--ds-space-md) * -1);
|
|
101
|
+
width: calc(100% + (var(--ds-space-md) * 2));
|
|
102
|
+
margin-top: var(--ds-space-xs);
|
|
103
|
+
margin-bottom: var(--ds-space-xs);
|
|
104
|
+
}
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
firstUpdated() {
|
|
108
|
+
// Set up trigger and target for positioner
|
|
109
|
+
const tooltipEl = this.shadowRoot.getElementById('tooltip');
|
|
110
|
+
|
|
111
|
+
// Use the host element as reference. This ensures correct positioning
|
|
112
|
+
// even when the tooltip is inside another Shadow DOM (e.g., ds-input).
|
|
113
|
+
// Floating UI will calculate coordinates relative to the host's bounding box.
|
|
114
|
+
this.positioner.target = this;
|
|
115
|
+
this.positioner.floating = tooltipEl;
|
|
116
|
+
this.positioner.placement = this.placement;
|
|
117
|
+
|
|
118
|
+
// Only add automatic event listeners if not in manual mode
|
|
119
|
+
if (!this.manual) {
|
|
120
|
+
this._addEventListeners();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
updated(changedProperties) {
|
|
125
|
+
if (changedProperties.has('placement')) {
|
|
126
|
+
this.positioner.placement = this.placement;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_addEventListeners() {
|
|
131
|
+
// Get the default slot to find the trigger element(s)
|
|
132
|
+
const slot = this.shadowRoot.querySelector('slot:not([name])');
|
|
133
|
+
this._triggerElements = slot.assignedElements({ flatten: true });
|
|
134
|
+
|
|
135
|
+
// Attach events to each slotted trigger element
|
|
136
|
+
this._triggerElements.forEach(trigger => {
|
|
137
|
+
trigger.addEventListener('mouseenter', this._show);
|
|
138
|
+
trigger.addEventListener('mouseleave', this._hide);
|
|
139
|
+
trigger.addEventListener('focusin', this._show);
|
|
140
|
+
trigger.addEventListener('focusout', this._hide);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Also listen on the host as fallback for empty slots or direct interactions
|
|
144
|
+
this.addEventListener('mouseenter', this._show);
|
|
145
|
+
this.addEventListener('mouseleave', this._hide);
|
|
146
|
+
this.addEventListener('focusin', this._show);
|
|
147
|
+
this.addEventListener('focusout', this._hide);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
_removeEventListeners() {
|
|
151
|
+
// Remove listeners from trigger elements
|
|
152
|
+
this._triggerElements.forEach(trigger => {
|
|
153
|
+
trigger.removeEventListener('mouseenter', this._show);
|
|
154
|
+
trigger.removeEventListener('mouseleave', this._hide);
|
|
155
|
+
trigger.removeEventListener('focusin', this._show);
|
|
156
|
+
trigger.removeEventListener('focusout', this._hide);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Remove listeners from host
|
|
160
|
+
this.removeEventListener('mouseenter', this._show);
|
|
161
|
+
this.removeEventListener('mouseleave', this._hide);
|
|
162
|
+
this.removeEventListener('focusin', this._show);
|
|
163
|
+
this.removeEventListener('focusout', this._hide);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
_show = () => {
|
|
167
|
+
this._open = true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
_hide = () => {
|
|
171
|
+
this._open = false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
render() {
|
|
175
|
+
return html`
|
|
176
|
+
<!-- Trigger Slot -->
|
|
177
|
+
<slot aria-describedby="tooltip"></slot>
|
|
178
|
+
|
|
179
|
+
<!-- Tooltip Container -->
|
|
180
|
+
<div
|
|
181
|
+
id="tooltip"
|
|
182
|
+
class="tooltip"
|
|
183
|
+
role="tooltip"
|
|
184
|
+
?data-show=${this._open}
|
|
185
|
+
>
|
|
186
|
+
${this.content
|
|
187
|
+
? html`${this.content}`
|
|
188
|
+
: html`
|
|
189
|
+
<div class="content-wrapper">
|
|
190
|
+
<slot name="header" @slotchange=${this._handleSlotChange}></slot>
|
|
191
|
+
${this._hasHeader && this._hasBody
|
|
192
|
+
? html`<div class="divider"></div>`
|
|
193
|
+
: ''}
|
|
194
|
+
<slot name="body" @slotchange=${this._handleSlotChange}></slot>
|
|
195
|
+
</div>
|
|
196
|
+
`
|
|
197
|
+
}
|
|
198
|
+
</div>
|
|
199
|
+
`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
get _hasHeader() {
|
|
203
|
+
const slot = this.shadowRoot.querySelector('slot[name="header"]');
|
|
204
|
+
return slot && slot.assignedNodes().length > 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
get _hasBody() {
|
|
208
|
+
const slot = this.shadowRoot.querySelector('slot[name="body"]');
|
|
209
|
+
return slot && slot.assignedNodes().length > 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
_handleSlotChange() {
|
|
213
|
+
this.requestUpdate();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
customElements.define('ds-tooltip', DsTooltip);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Meta, Canvas, Controls } from '@storybook/blocks';
|
|
2
|
+
import * as TooltipStories from './ds-tooltip.stories';
|
|
3
|
+
|
|
4
|
+
<Meta of={TooltipStories} />
|
|
5
|
+
|
|
6
|
+
# Tooltip
|
|
7
|
+
|
|
8
|
+
Tooltips provide additional, contextual information when users hover over or focus on an element.
|
|
9
|
+
|
|
10
|
+
## Playground
|
|
11
|
+
|
|
12
|
+
<Canvas of={TooltipStories.Simple} />
|
|
13
|
+
<Controls of={TooltipStories.Simple} />
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
Use tooltips to clarify actions, icons, or truncated text. Do not use them for essential information that users need to complete a task.
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<ds-tooltip content="Copy to clipboard">
|
|
21
|
+
<ds-button icon="copy"></ds-button>
|
|
22
|
+
</ds-tooltip>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Variants
|
|
26
|
+
|
|
27
|
+
### Simple (Text Only)
|
|
28
|
+
|
|
29
|
+
For short descriptions, use the `content` attribute.
|
|
30
|
+
|
|
31
|
+
<Canvas of={TooltipStories.Simple} />
|
|
32
|
+
|
|
33
|
+
### Custom (Header + Body)
|
|
34
|
+
|
|
35
|
+
For more complex information, use the `header` and `body` slots. A divider will automatically appear between them if both are present.
|
|
36
|
+
|
|
37
|
+
<Canvas of={TooltipStories.Custom} />
|
|
38
|
+
|
|
39
|
+
## Positioning
|
|
40
|
+
|
|
41
|
+
The tooltip supports robust positioning options using the `placement` attribute. You can specify main directions (`top`, `bottom`, `left`, `right`) and combine them with alignment suffixes (`-start`, `-end`) to precisely control where the tooltip appears relative to the trigger.
|
|
42
|
+
|
|
43
|
+
- **Main Directions**: `top`, `bottom`, `left`, `right`
|
|
44
|
+
- **Alignments**:
|
|
45
|
+
- `-start`: Aligns to the start (left/top) of the trigger.
|
|
46
|
+
- `-end`: Aligns to the end (right/bottom) of the trigger.
|
|
47
|
+
- **Auto**: `auto` (automatically finds the best position).
|
|
48
|
+
|
|
49
|
+
### Examples
|
|
50
|
+
|
|
51
|
+
- `right-start`: Places tooltip to the right, aligned to the top edge.
|
|
52
|
+
- `top-end`: Places tooltip above, aligned to the right edge.
|
|
53
|
+
|
|
54
|
+
<Canvas of={TooltipStories.AlignmentVariations} />
|
|
55
|
+
|
|
56
|
+
## Accessibility
|
|
57
|
+
|
|
58
|
+
- The tooltip triggers on both `hover` and `keyboard focus`.
|
|
59
|
+
- It automatically handles `ARIA` attributes (`role="tooltip"`).
|
|
60
|
+
- Dismissal occurs on `mouseleave` or `focusout`.
|
|
61
|
+
|
|
62
|
+
## API
|
|
63
|
+
|
|
64
|
+
| Property | Type | Default | Description |
|
|
65
|
+
|----------|------|---------|-------------|
|
|
66
|
+
| `placement` | string | `'top'` | Position of tooltip relative to trigger |
|
|
67
|
+
| `content` | string | `''` | Text content for simple tooltips |
|
|
68
|
+
|
|
69
|
+
### Slots
|
|
70
|
+
|
|
71
|
+
| Name | Description |
|
|
72
|
+
|------|-------------|
|
|
73
|
+
| (default) | The trigger element |
|
|
74
|
+
| `header` | Optional header content (styled with Body Bold) |
|
|
75
|
+
| `body` | Optional body content |
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import './index.js';
|
|
3
|
+
import '../ds-button/index.js'; // Assuming we have a button to use as trigger
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
title: 'Components/Tooltip',
|
|
7
|
+
component: 'ds-tooltip',
|
|
8
|
+
argTypes: {
|
|
9
|
+
placement: {
|
|
10
|
+
control: { type: 'select' },
|
|
11
|
+
options: [
|
|
12
|
+
'top', 'top-start', 'top-end',
|
|
13
|
+
'bottom', 'bottom-start', 'bottom-end',
|
|
14
|
+
'left', 'left-start', 'left-end',
|
|
15
|
+
'right', 'right-start', 'right-end',
|
|
16
|
+
'auto'
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
content: { control: 'text' },
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const Template = ({ placement, content }) => html`
|
|
24
|
+
<div style="display: flex; justify-content: center; align-items: center; height: 300px;">
|
|
25
|
+
<ds-tooltip placement=${placement} content=${content}>
|
|
26
|
+
<ds-button>Hover me</ds-button>
|
|
27
|
+
</ds-tooltip>
|
|
28
|
+
</div>
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
export const Simple = Template.bind({});
|
|
32
|
+
Simple.args = {
|
|
33
|
+
placement: 'top',
|
|
34
|
+
content: 'This is a simple tooltip',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const Custom = () => html`
|
|
38
|
+
<div style="display: flex; justify-content: center; align-items: center; height: 300px;">
|
|
39
|
+
<ds-tooltip placement="bottom">
|
|
40
|
+
<ds-button variant="secondary">Hover for Details</ds-button>
|
|
41
|
+
<div slot="header">Tooltip Header</div>
|
|
42
|
+
<div slot="body">
|
|
43
|
+
This is the body content of the tooltip. It has more details.
|
|
44
|
+
</div>
|
|
45
|
+
</ds-tooltip>
|
|
46
|
+
</div>
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
export const AlignmentVariations = () => html`
|
|
50
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 80px; padding: 100px; justify-items: center;">
|
|
51
|
+
|
|
52
|
+
<!-- Right Start: To the right, aligned top -->
|
|
53
|
+
<ds-tooltip placement="right-start" content="Right Start">
|
|
54
|
+
<ds-button>Right-Start</ds-button>
|
|
55
|
+
</ds-tooltip>
|
|
56
|
+
|
|
57
|
+
<!-- Right End: To the right, aligned bottom -->
|
|
58
|
+
<ds-tooltip placement="right-end" content="Right End">
|
|
59
|
+
<ds-button>Right-End</ds-button>
|
|
60
|
+
</ds-tooltip>
|
|
61
|
+
|
|
62
|
+
<!-- Top Start: Above, aligned left -->
|
|
63
|
+
<ds-tooltip placement="top-start" content="Top Start">
|
|
64
|
+
<ds-button>Top-Start</ds-button>
|
|
65
|
+
</ds-tooltip>
|
|
66
|
+
|
|
67
|
+
<!-- Bottom End: Below, aligned right -->
|
|
68
|
+
<ds-tooltip placement="bottom-end" content="Bottom End">
|
|
69
|
+
<ds-button>Bottom-End</ds-button>
|
|
70
|
+
</ds-tooltip>
|
|
71
|
+
</div>
|
|
72
|
+
`;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import './ds-tooltip.js';
|
|
3
|
+
|
|
4
|
+
describe('ds-tooltip', () => {
|
|
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 placement top', async () => {
|
|
17
|
+
container.innerHTML = '<ds-tooltip content="Test tooltip"><button>Hover me</button></ds-tooltip>';
|
|
18
|
+
const el = container.querySelector('ds-tooltip');
|
|
19
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
20
|
+
|
|
21
|
+
expect(el.placement).toBe('top');
|
|
22
|
+
const tooltip = el.shadowRoot.querySelector('#tooltip');
|
|
23
|
+
expect(tooltip).toBeTruthy();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should reflect content attribute', async () => {
|
|
27
|
+
container.innerHTML = '<ds-tooltip content="Simple tooltip"><button>Trigger</button></ds-tooltip>';
|
|
28
|
+
const el = container.querySelector('ds-tooltip');
|
|
29
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
30
|
+
|
|
31
|
+
expect(el.content).toBe('Simple tooltip');
|
|
32
|
+
expect(el.getAttribute('content')).toBe('Simple tooltip');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should support different placements', async () => {
|
|
36
|
+
const placements = ['top', 'bottom', 'left', 'right', 'top-start', 'top-end', 'auto'];
|
|
37
|
+
|
|
38
|
+
for (const placement of placements) {
|
|
39
|
+
container.innerHTML = `<ds-tooltip placement="${placement}" content="Test"><button>Test</button></ds-tooltip>`;
|
|
40
|
+
const el = container.querySelector('ds-tooltip');
|
|
41
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
42
|
+
|
|
43
|
+
expect(el.placement).toBe(placement);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should show tooltip when _open is true', async () => {
|
|
48
|
+
container.innerHTML = '<ds-tooltip content="Test tooltip"><button>Hover me</button></ds-tooltip>';
|
|
49
|
+
const el = container.querySelector('ds-tooltip');
|
|
50
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
51
|
+
|
|
52
|
+
const tooltip = el.shadowRoot.querySelector('#tooltip');
|
|
53
|
+
expect(tooltip.hasAttribute('data-show')).toBe(false);
|
|
54
|
+
|
|
55
|
+
// Manually trigger open state
|
|
56
|
+
el._open = true;
|
|
57
|
+
await el.updateComplete;
|
|
58
|
+
|
|
59
|
+
expect(tooltip.hasAttribute('data-show')).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should hide tooltip when _open is false', async () => {
|
|
63
|
+
container.innerHTML = '<ds-tooltip content="Test tooltip"><button>Hover me</button></ds-tooltip>';
|
|
64
|
+
const el = container.querySelector('ds-tooltip');
|
|
65
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
66
|
+
|
|
67
|
+
el._open = true;
|
|
68
|
+
await el.updateComplete;
|
|
69
|
+
|
|
70
|
+
const tooltip = el.shadowRoot.querySelector('#tooltip');
|
|
71
|
+
expect(tooltip.hasAttribute('data-show')).toBe(true);
|
|
72
|
+
|
|
73
|
+
el._open = false;
|
|
74
|
+
await el.updateComplete;
|
|
75
|
+
|
|
76
|
+
expect(tooltip.hasAttribute('data-show')).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should render custom variant with header and body slots', async () => {
|
|
80
|
+
container.innerHTML = `
|
|
81
|
+
<ds-tooltip>
|
|
82
|
+
<button>Trigger</button>
|
|
83
|
+
<div slot="header">Header Text</div>
|
|
84
|
+
<div slot="body">Body Text</div>
|
|
85
|
+
</ds-tooltip>
|
|
86
|
+
`;
|
|
87
|
+
const el = container.querySelector('ds-tooltip');
|
|
88
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
89
|
+
|
|
90
|
+
const slots = el.shadowRoot.querySelectorAll('slot');
|
|
91
|
+
const headerSlot = el.shadowRoot.querySelector('slot[name="header"]');
|
|
92
|
+
const bodySlot = el.shadowRoot.querySelector('slot[name="body"]');
|
|
93
|
+
|
|
94
|
+
expect(headerSlot).toBeTruthy();
|
|
95
|
+
expect(bodySlot).toBeTruthy();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should show divider when both header and body are present', async () => {
|
|
99
|
+
container.innerHTML = `
|
|
100
|
+
<ds-tooltip>
|
|
101
|
+
<button>Trigger</button>
|
|
102
|
+
<div slot="header">Header</div>
|
|
103
|
+
<div slot="body">Body</div>
|
|
104
|
+
</ds-tooltip>
|
|
105
|
+
`;
|
|
106
|
+
const el = container.querySelector('ds-tooltip');
|
|
107
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
108
|
+
|
|
109
|
+
// Force open to check divider
|
|
110
|
+
el._open = true;
|
|
111
|
+
await el.updateComplete;
|
|
112
|
+
|
|
113
|
+
const divider = el.shadowRoot.querySelector('.divider');
|
|
114
|
+
expect(divider).toBeTruthy();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should have role="tooltip" on tooltip element', async () => {
|
|
118
|
+
container.innerHTML = '<ds-tooltip content="Test"><button>Test</button></ds-tooltip>';
|
|
119
|
+
const el = container.querySelector('ds-tooltip');
|
|
120
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
121
|
+
|
|
122
|
+
const tooltip = el.shadowRoot.querySelector('#tooltip');
|
|
123
|
+
expect(tooltip.getAttribute('role')).toBe('tooltip');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should trigger tooltip on mouseenter event', async () => {
|
|
127
|
+
container.innerHTML = '<ds-tooltip content="Test tooltip"><button id="trigger">Hover me</button></ds-tooltip>';
|
|
128
|
+
const el = container.querySelector('ds-tooltip');
|
|
129
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
130
|
+
|
|
131
|
+
const button = container.querySelector('#trigger');
|
|
132
|
+
expect(el._open).toBe(false);
|
|
133
|
+
|
|
134
|
+
// Simulate mouseenter
|
|
135
|
+
button.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
|
136
|
+
await el.updateComplete;
|
|
137
|
+
|
|
138
|
+
expect(el._open).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should hide tooltip on mouseleave event', async () => {
|
|
142
|
+
container.innerHTML = '<ds-tooltip content="Test tooltip"><button id="trigger">Hover me</button></ds-tooltip>';
|
|
143
|
+
const el = container.querySelector('ds-tooltip');
|
|
144
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
145
|
+
|
|
146
|
+
const button = container.querySelector('#trigger');
|
|
147
|
+
|
|
148
|
+
// Open tooltip
|
|
149
|
+
button.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
|
150
|
+
await el.updateComplete;
|
|
151
|
+
expect(el._open).toBe(true);
|
|
152
|
+
|
|
153
|
+
// Close tooltip
|
|
154
|
+
button.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }));
|
|
155
|
+
await el.updateComplete;
|
|
156
|
+
expect(el._open).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should trigger tooltip on focusin event', async () => {
|
|
160
|
+
container.innerHTML = '<ds-tooltip content="Test tooltip"><button id="trigger">Focus me</button></ds-tooltip>';
|
|
161
|
+
const el = container.querySelector('ds-tooltip');
|
|
162
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
163
|
+
|
|
164
|
+
const button = container.querySelector('#trigger');
|
|
165
|
+
expect(el._open).toBe(false);
|
|
166
|
+
|
|
167
|
+
// Simulate focusin
|
|
168
|
+
button.dispatchEvent(new FocusEvent('focusin', { bubbles: true }));
|
|
169
|
+
await el.updateComplete;
|
|
170
|
+
|
|
171
|
+
expect(el._open).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should hide tooltip on focusout event', async () => {
|
|
175
|
+
container.innerHTML = '<ds-tooltip content="Test tooltip"><button id="trigger">Focus me</button></ds-tooltip>';
|
|
176
|
+
const el = container.querySelector('ds-tooltip');
|
|
177
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
178
|
+
|
|
179
|
+
const button = container.querySelector('#trigger');
|
|
180
|
+
|
|
181
|
+
// Open tooltip
|
|
182
|
+
button.dispatchEvent(new FocusEvent('focusin', { bubbles: true }));
|
|
183
|
+
await el.updateComplete;
|
|
184
|
+
expect(el._open).toBe(true);
|
|
185
|
+
|
|
186
|
+
// Close tooltip
|
|
187
|
+
button.dispatchEvent(new FocusEvent('focusout', { bubbles: true }));
|
|
188
|
+
await el.updateComplete;
|
|
189
|
+
expect(el._open).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DsTooltip } from './ds-tooltip.js';
|