@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.
Files changed (230) hide show
  1. package/package.json +27 -0
  2. package/src/ds-accordion/ds-accordion-item.js +288 -0
  3. package/src/ds-accordion/ds-accordion-item.stories.js +82 -0
  4. package/src/ds-accordion/ds-accordion.a11y.test.js +92 -0
  5. package/src/ds-accordion/ds-accordion.js +68 -0
  6. package/src/ds-accordion/ds-accordion.stories.js +118 -0
  7. package/src/ds-accordion/ds-accordion.test.js +146 -0
  8. package/src/ds-accordion/index.js +2 -0
  9. package/src/ds-action-bar/ds-action-bar.js +116 -0
  10. package/src/ds-action-bar/ds-action-bar.stories.js +86 -0
  11. package/src/ds-action-bar/ds-action-bar.test.js +64 -0
  12. package/src/ds-action-bar/index.js +1 -0
  13. package/src/ds-alert/ds-alert.a11y.test.js +151 -0
  14. package/src/ds-alert/ds-alert.js +223 -0
  15. package/src/ds-alert/ds-alert.mdx +142 -0
  16. package/src/ds-alert/ds-alert.stories.js +166 -0
  17. package/src/ds-alert/ds-alert.test.js +256 -0
  18. package/src/ds-alert/index.js +1 -0
  19. package/src/ds-avatar/ds-avatar.a11y.test.js +45 -0
  20. package/src/ds-avatar/ds-avatar.js +216 -0
  21. package/src/ds-avatar/ds-avatar.stories.js +120 -0
  22. package/src/ds-avatar/ds-avatar.test.js +83 -0
  23. package/src/ds-avatar/index.js +1 -0
  24. package/src/ds-avatar-extended/ds-avatar-extended.a11y.test.js +29 -0
  25. package/src/ds-avatar-extended/ds-avatar-extended.js +108 -0
  26. package/src/ds-avatar-extended/ds-avatar-extended.stories.js +93 -0
  27. package/src/ds-avatar-extended/ds-avatar-extended.test.js +66 -0
  28. package/src/ds-avatar-extended/index.js +1 -0
  29. package/src/ds-banner/ds-banner.a11y.test.js +51 -0
  30. package/src/ds-banner/ds-banner.js +233 -0
  31. package/src/ds-banner/ds-banner.stories.js +185 -0
  32. package/src/ds-banner/ds-banner.test.js +116 -0
  33. package/src/ds-banner/index.js +1 -0
  34. package/src/ds-breadcrumb-item/ds-breadcrumb-item.js +135 -0
  35. package/src/ds-breadcrumb-item/ds-breadcrumb-item.stories.js +49 -0
  36. package/src/ds-breadcrumb-item/ds-breadcrumb-item.test.js +55 -0
  37. package/src/ds-breadcrumbs/ds-breadcrumbs.js +194 -0
  38. package/src/ds-breadcrumbs/ds-breadcrumbs.stories.js +54 -0
  39. package/src/ds-breadcrumbs/ds-breadcrumbs.test.js +33 -0
  40. package/src/ds-button/ds-button.a11y.test.js +49 -0
  41. package/src/ds-button/ds-button.js +205 -0
  42. package/src/ds-button/ds-button.mdx +141 -0
  43. package/src/ds-button/ds-button.stories.js +152 -0
  44. package/src/ds-button/ds-button.test.js +62 -0
  45. package/src/ds-button/index.js +1 -0
  46. package/src/ds-button-group/ds-button-group.js +82 -0
  47. package/src/ds-button-group/ds-button-group.mdx +39 -0
  48. package/src/ds-button-group/ds-button-group.stories.js +47 -0
  49. package/src/ds-button-group/ds-button-group.test.js +47 -0
  50. package/src/ds-button-group/index.js +1 -0
  51. package/src/ds-checkbox/ds-checkbox.a11y.test.js +79 -0
  52. package/src/ds-checkbox/ds-checkbox.js +271 -0
  53. package/src/ds-checkbox/ds-checkbox.stories.js +77 -0
  54. package/src/ds-checkbox/ds-checkbox.test.js +191 -0
  55. package/src/ds-checkbox/index.js +1 -0
  56. package/src/ds-checkbox-group/ds-checkbox-group.a11y.test.js +146 -0
  57. package/src/ds-checkbox-group/ds-checkbox-group.js +235 -0
  58. package/src/ds-checkbox-group/ds-checkbox-group.stories.js +210 -0
  59. package/src/ds-checkbox-group/ds-checkbox-group.test.js +150 -0
  60. package/src/ds-checkbox-group/index.js +1 -0
  61. package/src/ds-dialog/ds-dialog.js +466 -0
  62. package/src/ds-dialog/ds-dialog.stories.js +274 -0
  63. package/src/ds-dialog/ds-dialog.test.js +441 -0
  64. package/src/ds-dialog/index.js +1 -0
  65. package/src/ds-dropdown/ds-dropdown.a11y.test.js +80 -0
  66. package/src/ds-dropdown/ds-dropdown.js +891 -0
  67. package/src/ds-dropdown/ds-dropdown.stories.js +259 -0
  68. package/src/ds-dropdown/ds-dropdown.test.js +268 -0
  69. package/src/ds-dropdown/index.js +1 -0
  70. package/src/ds-dropdown-group/ds-dropdown-group.js +55 -0
  71. package/src/ds-dropdown-panel/ds-dropdown-panel.js +34 -0
  72. package/src/ds-file-uploaded/ds-file-uploaded.a11y.test.js +40 -0
  73. package/src/ds-file-uploaded/ds-file-uploaded.js +135 -0
  74. package/src/ds-file-uploaded/ds-file-uploaded.mdx +33 -0
  75. package/src/ds-file-uploaded/ds-file-uploaded.stories.js +81 -0
  76. package/src/ds-file-uploaded/ds-file-uploaded.test.js +85 -0
  77. package/src/ds-file-uploader/ds-file-uploader.a11y.test.js +61 -0
  78. package/src/ds-file-uploader/ds-file-uploader.js +442 -0
  79. package/src/ds-file-uploader/ds-file-uploader.mdx +44 -0
  80. package/src/ds-file-uploader/ds-file-uploader.stories.js +76 -0
  81. package/src/ds-file-uploader/ds-file-uploader.test.js +142 -0
  82. package/src/ds-header/ds-header.a11y.test.js +38 -0
  83. package/src/ds-header/ds-header.js +149 -0
  84. package/src/ds-header/ds-header.stories.js +63 -0
  85. package/src/ds-header/ds-header.test.js +52 -0
  86. package/src/ds-header/index.js +1 -0
  87. package/src/ds-header-nav/ds-header-nav.a11y.test.js +69 -0
  88. package/src/ds-header-nav/ds-header-nav.js +114 -0
  89. package/src/ds-header-nav/ds-header-nav.stories.js +17 -0
  90. package/src/ds-header-nav/ds-header-nav.test.js +93 -0
  91. package/src/ds-header-nav-item/ds-header-nav-item.a11y.test.js +71 -0
  92. package/src/ds-header-nav-item/ds-header-nav-item.js +124 -0
  93. package/src/ds-header-nav-item/ds-header-nav-item.stories.js +43 -0
  94. package/src/ds-header-nav-item/ds-header-nav-item.test.js +61 -0
  95. package/src/ds-icon/ds-icon.a11y.test.js +49 -0
  96. package/src/ds-icon/ds-icon.js +75 -0
  97. package/src/ds-icon/ds-icon.mdx +36 -0
  98. package/src/ds-icon/ds-icon.stories.js +88 -0
  99. package/src/ds-icon/ds-icon.test.js +97 -0
  100. package/src/ds-icon/index.js +1 -0
  101. package/src/ds-icon-button/ds-icon-button.a11y.test.js +55 -0
  102. package/src/ds-icon-button/ds-icon-button.js +224 -0
  103. package/src/ds-icon-button/ds-icon-button.mdx +131 -0
  104. package/src/ds-icon-button/ds-icon-button.stories.js +128 -0
  105. package/src/ds-icon-button/ds-icon-button.test.js +90 -0
  106. package/src/ds-icon-button/index.js +1 -0
  107. package/src/ds-input/ds-input.a11y.test.js +145 -0
  108. package/src/ds-input/ds-input.js +645 -0
  109. package/src/ds-input/ds-input.mdx +251 -0
  110. package/src/ds-input/ds-input.stories.js +298 -0
  111. package/src/ds-input/ds-input.test.js +792 -0
  112. package/src/ds-input/index.js +1 -0
  113. package/src/ds-link/ds-link.js +111 -0
  114. package/src/ds-link/ds-link.stories.js +56 -0
  115. package/src/ds-link/ds-link.test.js +74 -0
  116. package/src/ds-list-item/ds-list-item.a11y.test.js +39 -0
  117. package/src/ds-list-item/ds-list-item.js +292 -0
  118. package/src/ds-list-item/ds-list-item.stories.js +101 -0
  119. package/src/ds-list-item/ds-list-item.test.js +63 -0
  120. package/src/ds-menu/ds-menu.js +30 -0
  121. package/src/ds-menu/ds-menu.stories.js +120 -0
  122. package/src/ds-menu/ds-menu.test.js +123 -0
  123. package/src/ds-menu-group/ds-menu-group.js +101 -0
  124. package/src/ds-menu-group/ds-menu-group.stories.js +99 -0
  125. package/src/ds-nav-item/ds-nav-item.a11y.test.js +91 -0
  126. package/src/ds-nav-item/ds-nav-item.js +307 -0
  127. package/src/ds-nav-item/ds-nav-item.stories.js +99 -0
  128. package/src/ds-nav-item/ds-nav-item.test.js +169 -0
  129. package/src/ds-nav-item/index.js +1 -0
  130. package/src/ds-nav-vertical/ds-nav-vertical.a11y.test.js +69 -0
  131. package/src/ds-nav-vertical/ds-nav-vertical.js +173 -0
  132. package/src/ds-nav-vertical/ds-nav-vertical.stories.js +124 -0
  133. package/src/ds-nav-vertical/ds-nav-vertical.test.js +176 -0
  134. package/src/ds-nav-vertical/index.js +1 -0
  135. package/src/ds-pagination/ds-pagination.a11y.test.js +50 -0
  136. package/src/ds-pagination/ds-pagination.js +232 -0
  137. package/src/ds-pagination/ds-pagination.stories.js +63 -0
  138. package/src/ds-pagination/ds-pagination.test.js +141 -0
  139. package/src/ds-pagination/index.js +1 -0
  140. package/src/ds-progress-bar/ds-progress-bar.a11y.test.js +25 -0
  141. package/src/ds-progress-bar/ds-progress-bar.js +81 -0
  142. package/src/ds-progress-bar/ds-progress-bar.stories.js +69 -0
  143. package/src/ds-progress-bar/ds-progress-bar.test.js +60 -0
  144. package/src/ds-radio/ds-radio.a11y.test.js +69 -0
  145. package/src/ds-radio/ds-radio.js +240 -0
  146. package/src/ds-radio/ds-radio.stories.js +102 -0
  147. package/src/ds-radio/ds-radio.test.js +114 -0
  148. package/src/ds-radio/index.js +1 -0
  149. package/src/ds-radio-group/ds-radio-group.a11y.test.js +164 -0
  150. package/src/ds-radio-group/ds-radio-group.js +257 -0
  151. package/src/ds-radio-group/ds-radio-group.stories.js +247 -0
  152. package/src/ds-radio-group/ds-radio-group.test.js +194 -0
  153. package/src/ds-radio-group/index.js +1 -0
  154. package/src/ds-rich-list/ds-rich-list.js +246 -0
  155. package/src/ds-rich-list/ds-rich-list.stories.js +368 -0
  156. package/src/ds-rich-list/ds-rich-list.test.js +293 -0
  157. package/src/ds-rich-list-item/ds-rich-list-item.js +579 -0
  158. package/src/ds-rich-list-item/ds-rich-list-item.stories.js +197 -0
  159. package/src/ds-rich-list-item/ds-rich-list-item.test.js +434 -0
  160. package/src/ds-slider/ds-slider.js +399 -0
  161. package/src/ds-slider/ds-slider.stories.js +107 -0
  162. package/src/ds-slider/ds-slider.test.js +308 -0
  163. package/src/ds-spinner/ds-spinner.js +173 -0
  164. package/src/ds-spinner/ds-spinner.stories.js +52 -0
  165. package/src/ds-spinner/ds-spinner.test.js +50 -0
  166. package/src/ds-status-border/ds-status-border.js +88 -0
  167. package/src/ds-status-border/ds-status-border.stories.js +242 -0
  168. package/src/ds-status-border/ds-status-border.test.js +168 -0
  169. package/src/ds-stepper/ds-stepper.a11y.test.js +198 -0
  170. package/src/ds-stepper/ds-stepper.js +207 -0
  171. package/src/ds-stepper/ds-stepper.stories.js +530 -0
  172. package/src/ds-stepper/ds-stepper.test.js +311 -0
  173. package/src/ds-stepper-item/ds-stepper-item.js +485 -0
  174. package/src/ds-stepper-item/ds-stepper-item.stories.js +288 -0
  175. package/src/ds-switch/ds-switch.js +348 -0
  176. package/src/ds-switch/ds-switch.stories.js +145 -0
  177. package/src/ds-switch/ds-switch.test.js +226 -0
  178. package/src/ds-switch/index.js +1 -0
  179. package/src/ds-tab-item/ds-tab-item.js +341 -0
  180. package/src/ds-tab-item/ds-tab-item.stories.js +69 -0
  181. package/src/ds-tabs/ds-tab-panel.js +48 -0
  182. package/src/ds-tabs/ds-tabs.a11y.test.js +56 -0
  183. package/src/ds-tabs/ds-tabs.js +180 -0
  184. package/src/ds-tabs/ds-tabs.stories.js +152 -0
  185. package/src/ds-tabs/ds-tabs.test.js +306 -0
  186. package/src/ds-tabs/index.js +3 -0
  187. package/src/ds-tag-action/ds-tag-action.a11y.test.js +32 -0
  188. package/src/ds-tag-action/ds-tag-action.js +185 -0
  189. package/src/ds-tag-action/ds-tag-action.stories.js +55 -0
  190. package/src/ds-tag-action/ds-tag-action.test.js +44 -0
  191. package/src/ds-tag-removable/ds-tag-removable.a11y.test.js +24 -0
  192. package/src/ds-tag-removable/ds-tag-removable.js +146 -0
  193. package/src/ds-tag-removable/ds-tag-removable.stories.js +52 -0
  194. package/src/ds-tag-removable/ds-tag-removable.test.js +46 -0
  195. package/src/ds-tag-status/ds-tag-status.a11y.test.js +93 -0
  196. package/src/ds-tag-status/ds-tag-status.js +164 -0
  197. package/src/ds-tag-status/ds-tag-status.stories.js +200 -0
  198. package/src/ds-tag-status/ds-tag-status.test.js +140 -0
  199. package/src/ds-tag-status/index.js +1 -0
  200. package/src/ds-textarea/ds-textarea-clearable.test.js +89 -0
  201. package/src/ds-textarea/ds-textarea.a11y.test.js +66 -0
  202. package/src/ds-textarea/ds-textarea.js +505 -0
  203. package/src/ds-textarea/ds-textarea.stories.js +335 -0
  204. package/src/ds-textarea/ds-textarea.test.js +218 -0
  205. package/src/ds-textarea/index.js +1 -0
  206. package/src/ds-thumbnail/ds-thumbnail.js +207 -0
  207. package/src/ds-thumbnail/ds-thumbnail.stories.js +217 -0
  208. package/src/ds-thumbnail/ds-thumbnail.test.js +220 -0
  209. package/src/ds-toast/ds-toast-provider.js +110 -0
  210. package/src/ds-toast/ds-toast.a11y.test.js +34 -0
  211. package/src/ds-toast/ds-toast.js +243 -0
  212. package/src/ds-toast/ds-toast.stories.js +143 -0
  213. package/src/ds-toast/ds-toast.test.js +93 -0
  214. package/src/ds-toast/index.js +2 -0
  215. package/src/ds-tooltip/ds-tooltip.a11y.test.js +110 -0
  216. package/src/ds-tooltip/ds-tooltip.js +217 -0
  217. package/src/ds-tooltip/ds-tooltip.mdx +75 -0
  218. package/src/ds-tooltip/ds-tooltip.stories.js +72 -0
  219. package/src/ds-tooltip/ds-tooltip.test.js +191 -0
  220. package/src/ds-tooltip/index.js +1 -0
  221. package/src/ds-tooltip/positioner.js +117 -0
  222. package/src/index.js +50 -0
  223. package/src/mixins/field-label.mixin.js +113 -0
  224. package/src/mixins/field-message.mixin.js +66 -0
  225. package/src/token-provider/index.js +1 -0
  226. package/src/token-provider/token-provider.a11y.test.js +44 -0
  227. package/src/token-provider/token-provider.js +85 -0
  228. package/src/token-provider/token-provider.stories.js +105 -0
  229. package/src/token-provider/token-provider.test.js +134 -0
  230. 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';