@aquera/nile-elements 0.1.32-beta-1.2 → 0.1.32-beta-1.4

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 (192) hide show
  1. package/README.md +9 -23
  2. package/demo/index.html +263 -24
  3. package/dist/axe.min-2b379f29.cjs.js +12 -0
  4. package/dist/axe.min-2b379f29.cjs.js.map +1 -0
  5. package/dist/axe.min-c2cd8733.esm.js +12 -0
  6. package/dist/{fixture-3acb409b.cjs.js → fixture-d5b55278.cjs.js} +3 -3
  7. package/dist/fixture-d5b55278.cjs.js.map +1 -0
  8. package/dist/{fixture-db35a8ae.esm.js → fixture-df8b52d7.esm.js} +1 -1
  9. package/dist/index.cjs.js +1 -1
  10. package/dist/index.esm.js +1 -1
  11. package/dist/nile-accordion/nile-accordian.test.cjs.js +1 -1
  12. package/dist/nile-accordion/nile-accordian.test.esm.js +1 -1
  13. package/dist/nile-accordion/nile-accordion.cjs.js +1 -1
  14. package/dist/nile-accordion/nile-accordion.cjs.js.map +1 -1
  15. package/dist/nile-accordion/nile-accordion.css.cjs.js +1 -1
  16. package/dist/nile-accordion/nile-accordion.css.cjs.js.map +1 -1
  17. package/dist/nile-accordion/nile-accordion.css.esm.js +1 -1
  18. package/dist/nile-accordion/nile-accordion.esm.js +4 -4
  19. package/dist/nile-auto-complete/nile-auto-complete.test.cjs.js +1 -1
  20. package/dist/nile-auto-complete/nile-auto-complete.test.esm.js +1 -1
  21. package/dist/nile-avatar/nile-avatar.test.cjs.js +1 -1
  22. package/dist/nile-avatar/nile-avatar.test.esm.js +1 -1
  23. package/dist/nile-badge/nile-badge.test.cjs.js +1 -1
  24. package/dist/nile-badge/nile-badge.test.esm.js +1 -1
  25. package/dist/nile-button/nile-button.test.cjs.js +1 -1
  26. package/dist/nile-button/nile-button.test.esm.js +1 -1
  27. package/dist/nile-button-toggle-group/nile-button-toggle-group.test.cjs.js +1 -1
  28. package/dist/nile-button-toggle-group/nile-button-toggle-group.test.esm.js +1 -1
  29. package/dist/nile-calendar/nile-calendar.test.cjs.js +1 -1
  30. package/dist/nile-calendar/nile-calendar.test.esm.js +1 -1
  31. package/dist/nile-card/nile-card.test.cjs.js +1 -1
  32. package/dist/nile-card/nile-card.test.esm.js +1 -1
  33. package/dist/nile-checkbox/nile-checkbox.test.cjs.js +1 -1
  34. package/dist/nile-checkbox/nile-checkbox.test.esm.js +1 -1
  35. package/dist/nile-chip/nile-chip.test.cjs.js +1 -1
  36. package/dist/nile-chip/nile-chip.test.esm.js +1 -1
  37. package/dist/nile-circular-progressbar/nile-circular-progressbar.css.cjs.js +1 -1
  38. package/dist/nile-circular-progressbar/nile-circular-progressbar.css.cjs.js.map +1 -1
  39. package/dist/nile-circular-progressbar/nile-circular-progressbar.css.esm.js +49 -49
  40. package/dist/nile-code-editor/extensionSetup.cjs.js +7 -7
  41. package/dist/nile-code-editor/extensionSetup.cjs.js.map +1 -1
  42. package/dist/nile-code-editor/extensionSetup.esm.js +1 -1
  43. package/dist/nile-code-editor/nile-code-editor.cjs.js +2 -2
  44. package/dist/nile-code-editor/nile-code-editor.cjs.js.map +1 -1
  45. package/dist/nile-code-editor/nile-code-editor.esm.js +2 -2
  46. package/dist/nile-dialog/nile-dialog.test.cjs.js +1 -1
  47. package/dist/nile-dialog/nile-dialog.test.esm.js +1 -1
  48. package/dist/nile-drawer/nile-drawer.test.cjs.js +1 -1
  49. package/dist/nile-drawer/nile-drawer.test.esm.js +1 -1
  50. package/dist/nile-dropdown/nile-dropdown.test.cjs.js +1 -1
  51. package/dist/nile-dropdown/nile-dropdown.test.esm.js +1 -1
  52. package/dist/nile-empty-state/nile-empty-state.test.cjs.js +1 -1
  53. package/dist/nile-empty-state/nile-empty-state.test.esm.js +1 -1
  54. package/dist/nile-error-message/nile-error-message.test.cjs.js +1 -1
  55. package/dist/nile-error-message/nile-error-message.test.esm.js +1 -1
  56. package/dist/nile-form-group/nile-form-group.test.cjs.js +1 -1
  57. package/dist/nile-form-group/nile-form-group.test.esm.js +1 -1
  58. package/dist/nile-form-help-text/nile-form-help-text.test.cjs.js +1 -1
  59. package/dist/nile-form-help-text/nile-form-help-text.test.esm.js +1 -1
  60. package/dist/nile-hero/nile-hero.test.cjs.js +1 -1
  61. package/dist/nile-hero/nile-hero.test.esm.js +1 -1
  62. package/dist/nile-icon/nile-icon.test.cjs.js +1 -1
  63. package/dist/nile-icon/nile-icon.test.esm.js +1 -1
  64. package/dist/nile-input/nile-input.css.cjs.js +1 -1
  65. package/dist/nile-input/nile-input.css.cjs.js.map +1 -1
  66. package/dist/nile-input/nile-input.css.esm.js +8 -0
  67. package/dist/nile-input/nile-input.test.cjs.js +1 -1
  68. package/dist/nile-input/nile-input.test.esm.js +1 -1
  69. package/dist/nile-link/nile-link.test.cjs.js +1 -1
  70. package/dist/nile-link/nile-link.test.esm.js +1 -1
  71. package/dist/nile-loader/nile-loader.test.cjs.js +1 -1
  72. package/dist/nile-loader/nile-loader.test.esm.js +1 -1
  73. package/dist/nile-popover/nile-popover.test.cjs.js +1 -1
  74. package/dist/nile-popover/nile-popover.test.esm.js +1 -1
  75. package/dist/nile-popup/nile-popup.test.cjs.js +1 -1
  76. package/dist/nile-popup/nile-popup.test.esm.js +1 -1
  77. package/dist/nile-progress-bar/nile-progress-bar.test.cjs.js +1 -1
  78. package/dist/nile-progress-bar/nile-progress-bar.test.esm.js +1 -1
  79. package/dist/nile-radio/nile-radio.test.cjs.js +1 -1
  80. package/dist/nile-radio/nile-radio.test.esm.js +1 -1
  81. package/dist/nile-radio-group/nile-radio-group.test.cjs.js +1 -1
  82. package/dist/nile-radio-group/nile-radio-group.test.esm.js +1 -1
  83. package/dist/nile-select/nile-select.test.cjs.js +1 -1
  84. package/dist/nile-select/nile-select.test.esm.js +1 -1
  85. package/dist/nile-slide-toggle/nile-slide-toggle.test.cjs.js +1 -1
  86. package/dist/nile-slide-toggle/nile-slide-toggle.test.esm.js +1 -1
  87. package/dist/nile-tab-group/nile-tab-group.test.cjs.js +1 -1
  88. package/dist/nile-tab-group/nile-tab-group.test.esm.js +1 -1
  89. package/dist/nile-table-body/nile-table-body.cjs.js +1 -1
  90. package/dist/nile-table-body/nile-table-body.cjs.js.map +1 -1
  91. package/dist/nile-table-body/nile-table-body.esm.js +2 -2
  92. package/dist/nile-table-cell-item/nile-table-cell-item.css.cjs.js +1 -1
  93. package/dist/nile-table-cell-item/nile-table-cell-item.css.cjs.js.map +1 -1
  94. package/dist/nile-table-cell-item/nile-table-cell-item.css.esm.js +4 -8
  95. package/dist/nile-table-header-item/nile-table-header-item.cjs.js +1 -1
  96. package/dist/nile-table-header-item/nile-table-header-item.cjs.js.map +1 -1
  97. package/dist/nile-table-header-item/nile-table-header-item.css.cjs.js +1 -1
  98. package/dist/nile-table-header-item/nile-table-header-item.css.cjs.js.map +1 -1
  99. package/dist/nile-table-header-item/nile-table-header-item.css.esm.js +0 -5
  100. package/dist/nile-table-header-item/nile-table-header-item.esm.js +9 -9
  101. package/dist/nile-table-row/nile-table-row.cjs.js.map +1 -1
  102. package/dist/nile-textarea/nile-textarea.test.cjs.js +1 -1
  103. package/dist/nile-textarea/nile-textarea.test.esm.js +1 -1
  104. package/dist/nile-tooltip/index.cjs.js +1 -1
  105. package/dist/nile-tooltip/index.esm.js +1 -1
  106. package/dist/nile-tooltip/nile-tooltip-utils.cjs.js +2 -0
  107. package/dist/nile-tooltip/nile-tooltip-utils.cjs.js.map +1 -0
  108. package/dist/nile-tooltip/nile-tooltip-utils.esm.js +1 -0
  109. package/dist/nile-tooltip/nile-tooltip.cjs.js +1 -1
  110. package/dist/nile-tooltip/nile-tooltip.cjs.js.map +1 -1
  111. package/dist/nile-tooltip/nile-tooltip.css.cjs.js +1 -1
  112. package/dist/nile-tooltip/nile-tooltip.css.cjs.js.map +1 -1
  113. package/dist/nile-tooltip/nile-tooltip.css.esm.js +20 -76
  114. package/dist/nile-tooltip/nile-tooltip.esm.js +20 -8
  115. package/dist/nile-tooltip/nile-tooltip.test.cjs.js +2 -0
  116. package/dist/nile-tooltip/nile-tooltip.test.cjs.js.map +1 -0
  117. package/dist/nile-tooltip/nile-tooltip.test.esm.js +51 -0
  118. package/dist/src/index.d.ts +0 -1
  119. package/dist/src/index.js +0 -1
  120. package/dist/src/index.js.map +1 -1
  121. package/dist/src/nile-accordion/nile-accordion.css.js +1 -1
  122. package/dist/src/nile-accordion/nile-accordion.css.js.map +1 -1
  123. package/dist/src/nile-accordion/nile-accordion.d.ts +5 -6
  124. package/dist/src/nile-accordion/nile-accordion.js +8 -21
  125. package/dist/src/nile-accordion/nile-accordion.js.map +1 -1
  126. package/dist/src/nile-circular-progressbar/nile-circular-progressbar.css.js +49 -49
  127. package/dist/src/nile-circular-progressbar/nile-circular-progressbar.css.js.map +1 -1
  128. package/dist/src/nile-input/nile-input.css.js +8 -0
  129. package/dist/src/nile-input/nile-input.css.js.map +1 -1
  130. package/dist/src/nile-table-body/nile-table-body.d.ts +1 -3
  131. package/dist/src/nile-table-body/nile-table-body.js +1 -9
  132. package/dist/src/nile-table-body/nile-table-body.js.map +1 -1
  133. package/dist/src/nile-table-cell-item/nile-table-cell-item.css.js +2 -6
  134. package/dist/src/nile-table-cell-item/nile-table-cell-item.css.js.map +1 -1
  135. package/dist/src/nile-table-header-item/nile-table-header-item.css.js +0 -5
  136. package/dist/src/nile-table-header-item/nile-table-header-item.css.js.map +1 -1
  137. package/dist/src/nile-table-header-item/nile-table-header-item.js +1 -1
  138. package/dist/src/nile-table-header-item/nile-table-header-item.js.map +1 -1
  139. package/dist/src/nile-table-row/nile-table-row.js.map +1 -1
  140. package/dist/src/nile-tooltip/nile-tooltip-utils.d.ts +18 -0
  141. package/dist/src/nile-tooltip/nile-tooltip-utils.js +151 -0
  142. package/dist/src/nile-tooltip/nile-tooltip-utils.js.map +1 -0
  143. package/dist/src/nile-tooltip/nile-tooltip.css.js +19 -75
  144. package/dist/src/nile-tooltip/nile-tooltip.css.js.map +1 -1
  145. package/dist/src/nile-tooltip/nile-tooltip.d.ts +28 -9
  146. package/dist/src/nile-tooltip/nile-tooltip.js +111 -117
  147. package/dist/src/nile-tooltip/nile-tooltip.js.map +1 -1
  148. package/dist/src/nile-tooltip/nile-tooltip.test.d.ts +1 -0
  149. package/dist/src/nile-tooltip/nile-tooltip.test.js +158 -0
  150. package/dist/src/nile-tooltip/nile-tooltip.test.js.map +1 -0
  151. package/dist/tsconfig.tsbuildinfo +1 -1
  152. package/package.json +3 -3
  153. package/src/index.ts +1 -2
  154. package/src/nile-accordion/nile-accordion.css.ts +1 -1
  155. package/src/nile-accordion/nile-accordion.ts +4 -15
  156. package/src/nile-circular-progressbar/nile-circular-progressbar.css.ts +56 -55
  157. package/src/nile-input/nile-input.css.ts +8 -0
  158. package/src/nile-table-body/nile-table-body.ts +2 -8
  159. package/src/nile-table-cell-item/nile-table-cell-item.css.ts +2 -6
  160. package/src/nile-table-header-item/nile-table-header-item.css.ts +0 -5
  161. package/src/nile-table-header-item/nile-table-header-item.ts +1 -1
  162. package/src/nile-table-row/nile-table-row.ts +2 -1
  163. package/src/nile-tooltip/nile-tooltip-utils.ts +190 -0
  164. package/src/nile-tooltip/nile-tooltip.css.ts +20 -76
  165. package/src/nile-tooltip/nile-tooltip.test.ts +178 -0
  166. package/src/nile-tooltip/nile-tooltip.ts +142 -162
  167. package/vscode-html-custom-data.json +72 -90
  168. package/dist/axe.min-5bf06036.esm.js +0 -12
  169. package/dist/axe.min-ff35bfba.cjs.js +0 -12
  170. package/dist/axe.min-ff35bfba.cjs.js.map +0 -1
  171. package/dist/fixture-3acb409b.cjs.js.map +0 -1
  172. package/dist/nile-table/index.cjs.js +0 -2
  173. package/dist/nile-table/index.cjs.js.map +0 -1
  174. package/dist/nile-table/index.esm.js +0 -1
  175. package/dist/nile-table/nile-table.cjs.js +0 -2
  176. package/dist/nile-table/nile-table.cjs.js.map +0 -1
  177. package/dist/nile-table/nile-table.css.cjs.js +0 -2
  178. package/dist/nile-table/nile-table.css.cjs.js.map +0 -1
  179. package/dist/nile-table/nile-table.css.esm.js +0 -6
  180. package/dist/nile-table/nile-table.esm.js +0 -3
  181. package/dist/src/nile-table/index.d.ts +0 -1
  182. package/dist/src/nile-table/index.js +0 -2
  183. package/dist/src/nile-table/index.js.map +0 -1
  184. package/dist/src/nile-table/nile-table.css.d.ts +0 -12
  185. package/dist/src/nile-table/nile-table.css.js +0 -18
  186. package/dist/src/nile-table/nile-table.css.js.map +0 -1
  187. package/dist/src/nile-table/nile-table.d.ts +0 -48
  188. package/dist/src/nile-table/nile-table.js +0 -143
  189. package/dist/src/nile-table/nile-table.js.map +0 -1
  190. package/src/nile-table/index.ts +0 -1
  191. package/src/nile-table/nile-table.css.ts +0 -20
  192. package/src/nile-table/nile-table.ts +0 -161
@@ -0,0 +1,178 @@
1
+ import { fixture, html, assert, oneEvent, waitUntil } from '@open-wc/testing';
2
+ import './nile-tooltip';
3
+ import { NileTooltip } from './nile-tooltip';
4
+
5
+ describe('NileTooltip', () => {
6
+
7
+ it('renders with default properties', async () => {
8
+ const el = await fixture<NileTooltip>(html`<nile-tooltip content="Hello"><button>Hover me</button></nile-tooltip>`);
9
+ assert.equal(el.content, 'Hello');
10
+ assert.equal(el.placement, 'bottom');
11
+ assert.equal(el.size, 'small');
12
+ assert.equal(el.disabled, false);
13
+ assert.equal(el.open, false);
14
+ });
15
+
16
+ it('renders text content when no slot is used', async () => {
17
+ const el = await fixture<NileTooltip>(html`<nile-tooltip content="Fallback"><button>Trigger</button></nile-tooltip>`);
18
+ const content = el.shadowRoot!.querySelector('.tooltip-content')!;
19
+ assert.include(content.textContent!, 'Fallback');
20
+ });
21
+
22
+ it('uses slotted content when provided', async () => {
23
+ const el = await fixture<NileTooltip>(html`
24
+ <nile-tooltip>
25
+ <div slot="content">Slot Content</div>
26
+ <button>Trigger</button>
27
+ </nile-tooltip>
28
+ `);
29
+
30
+ const slot = el.shadowRoot!.querySelector('slot[name="content"]') as HTMLSlotElement;
31
+ const assigned = slot.assignedNodes({ flatten: true });
32
+ assert.isAbove(assigned.length, 0);
33
+ assert.include(assigned[0].textContent ?? '', 'Slot Content');
34
+ });
35
+
36
+ it('shows tooltip on hover when trigger includes "hover"', async () => {
37
+ const el = await fixture<NileTooltip>(html`
38
+ <nile-tooltip content="Tooltip" trigger="hover">
39
+ <button>Hover me</button>
40
+ </nile-tooltip>
41
+ `);
42
+ const trigger = el.shadowRoot!.querySelector('.trigger-container')!;
43
+ trigger.dispatchEvent(new Event('mouseover', { bubbles: true }));
44
+ await waitUntil(() => el.open === true);
45
+ assert.isTrue(el.open);
46
+ });
47
+
48
+ it('hides tooltip on mouseout when trigger includes "hover"', async () => {
49
+ const el = await fixture<NileTooltip>(html`
50
+ <nile-tooltip content="Tooltip" trigger="hover">
51
+ <button>Hover me</button>
52
+ </nile-tooltip>
53
+ `);
54
+ const trigger = el.shadowRoot!.querySelector('.trigger-container')!;
55
+ trigger.dispatchEvent(new Event('mouseover', { bubbles: true }));
56
+ await waitUntil(() => el.open === true);
57
+ trigger.dispatchEvent(new Event('mouseout', { bubbles: true }));
58
+ await waitUntil(() => el.open === false);
59
+ });
60
+
61
+ it('toggles tooltip on click when trigger includes "click"', async () => {
62
+ const el = await fixture<NileTooltip>(html`
63
+ <nile-tooltip content="Click me" trigger="click">
64
+ <button>Click</button>
65
+ </nile-tooltip>
66
+ `);
67
+ const trigger = el.shadowRoot!.querySelector('.trigger-container') as HTMLElement;
68
+ trigger.click();
69
+ await waitUntil(() => el.open === true);
70
+ trigger.click();
71
+ await waitUntil(() => el.open === false);
72
+ });
73
+
74
+ it('shows and hides tooltip on focus/blur when trigger includes "focus"', async () => {
75
+ const el = await fixture<NileTooltip>(html`
76
+ <nile-tooltip content="Focus" trigger="focus">
77
+ <button>Focus</button>
78
+ </nile-tooltip>
79
+ `);
80
+ const trigger = el.shadowRoot!.querySelector('.trigger-container')!;
81
+ trigger.dispatchEvent(new Event('focusin'));
82
+ await waitUntil(() => el.open === true);
83
+ trigger.dispatchEvent(new Event('focusout'));
84
+ await waitUntil(() => el.open === false);
85
+ });
86
+
87
+ it('does not show tooltip when disabled', async () => {
88
+ const el = await fixture<NileTooltip>(html`
89
+ <nile-tooltip content="Disabled" disabled trigger="hover">
90
+ <button>Hover me</button>
91
+ </nile-tooltip>
92
+ `);
93
+ const trigger = el.shadowRoot!.querySelector('.trigger-container')!;
94
+ trigger.dispatchEvent(new Event('mouseover'));
95
+ await new Promise(resolve => setTimeout(resolve, 200));
96
+ assert.isFalse(el.open);
97
+ });
98
+
99
+ it('applies size class correctly', async () => {
100
+ const el = await fixture<NileTooltip>(html`
101
+ <nile-tooltip content="Tooltip" size="large">
102
+ <button>Trigger</button>
103
+ </nile-tooltip>
104
+ `);
105
+ const tooltip = el.shadowRoot!.querySelector('.tooltip')!;
106
+ assert.isTrue(tooltip.classList.contains('tooltip__body--large'));
107
+ });
108
+
109
+ it('emits nile-show and nile-after-show events', async () => {
110
+ const el = await fixture<NileTooltip>(html`
111
+ <nile-tooltip content="Event test" trigger="click">
112
+ <button>Click</button>
113
+ </nile-tooltip>
114
+ `);
115
+ const trigger = el.shadowRoot!.querySelector('.trigger-container') as HTMLElement;
116
+ setTimeout(() => trigger.click());
117
+ const showEvent = await oneEvent(el, 'nile-show');
118
+ assert.ok(showEvent);
119
+ const afterShow = await oneEvent(el, 'nile-after-show');
120
+ assert.ok(afterShow);
121
+ });
122
+
123
+ it('emits nile-hide and nile-after-hide events', async () => {
124
+ const el = await fixture<NileTooltip>(html`
125
+ <nile-tooltip content="Event test" trigger="click">
126
+ <button>Click</button>
127
+ </nile-tooltip>
128
+ `);
129
+ const trigger = el.shadowRoot!.querySelector('.trigger-container') as HTMLElement;
130
+ trigger.click();
131
+ await waitUntil(() => el.open === true);
132
+ setTimeout(() => trigger.click());
133
+ const hideEvent = await oneEvent(el, 'nile-hide');
134
+ assert.ok(hideEvent);
135
+ const afterHide = await oneEvent(el, 'nile-after-hide');
136
+ assert.ok(afterHide);
137
+ });
138
+
139
+ it('defaults to "bottom" placement when given invalid value', async () => {
140
+ const el = await fixture<NileTooltip>(html`
141
+ <nile-tooltip content="Fallback" placement=${'invalid' as unknown as NileTooltip['placement']} >
142
+ <button>Trigger</button>
143
+ </nile-tooltip>
144
+ `);
145
+ await el.updateComplete;
146
+ assert.equal(el.placement, 'bottom');
147
+ });
148
+
149
+
150
+ it('updates hasTooltipSlot on slot change', async () => {
151
+ const el = await fixture<NileTooltip>(html`
152
+ <nile-tooltip>
153
+ <div slot="content">Initial slot</div>
154
+ <button>Trigger</button>
155
+ </nile-tooltip>
156
+ `);
157
+
158
+ const slot = el.shadowRoot!.querySelector('slot[name="content"]') as HTMLSlotElement;
159
+ const newSlotNode = document.createElement('div');
160
+ newSlotNode.slot = 'content';
161
+ newSlotNode.textContent = 'Updated slot';
162
+ el.appendChild(newSlotNode);
163
+
164
+ slot.dispatchEvent(new Event('slotchange'));
165
+ await el.updateComplete;
166
+
167
+ assert.isTrue((el as any).hasTooltipSlot);
168
+ });
169
+
170
+ it('respects hoist attribute', async () => {
171
+ const el = await fixture<NileTooltip>(html`
172
+ <nile-tooltip hoist content="Hoisted">
173
+ <button>Hoist</button>
174
+ </nile-tooltip>
175
+ `);
176
+ assert.isTrue(el.hoist);
177
+ });
178
+ });
@@ -1,198 +1,171 @@
1
- import { LitElement, html, css, CSSResultArray } from 'lit';
1
+ /**
2
+ * Copyright Aquera Inc 2023
3
+ *
4
+ * This source code is licensed under the BSD-3-Clause license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+ import { LitElement, html, css, CSSResultArray } from 'lit';
2
8
  import { customElement, property, query } from 'lit/decorators.js';
3
9
  import { classMap } from 'lit/directives/class-map.js';
4
10
  import { styles } from './nile-tooltip.css';
5
11
  import NileElement from '../internal/nile-element';
12
+ import { isInViewport, getValidTooltipPosition, getCaretPosition } from './nile-tooltip-utils';
13
+ /**
14
+ * Nile icon component.
15
+ *
16
+ * @tag nile-tooltip
17
+ *
18
+ */
6
19
 
7
- type TooltipPlacement =
8
- | 'top' | 'top-start' | 'top-end'
9
- | 'right' | 'right-start' | 'right-end'
10
- | 'bottom' | 'bottom-start' | 'bottom-end'
11
- | 'left' | 'left-start' | 'left-end';
12
-
13
- // CSS Anchor positiong
14
20
  @customElement('nile-tooltip')
15
21
  export class NileTooltip extends NileElement {
16
22
  @property({ type: String }) content = '';
17
23
  @property({ reflect: true }) size: 'small' | 'large' = 'small';
18
- @property({ type: String }) placement: TooltipPlacement = 'top';
24
+ @property({ type: String })
25
+ placement:
26
+ | 'top'
27
+ | 'top-start'
28
+ | 'top-end'
29
+ | 'right'
30
+ | 'right-start'
31
+ | 'right-end'
32
+ | 'bottom'
33
+ | 'bottom-start'
34
+ | 'bottom-end'
35
+ | 'left'
36
+ | 'left-start'
37
+ | 'left-end' = 'bottom';
19
38
  @property({ type: Boolean, reflect: true }) disabled = false;
20
39
  @property({ type: Boolean, reflect: true }) open = false;
21
- @property() trigger = 'hover focus';
40
+ /**
41
+ * Controls how the tooltip is activated. Possible options include `click`, `hover`, `focus`, and `manual`. Multiple
42
+ * options can be passed by separating them with a space. When manual is used, the tooltip must be activated
43
+ * programmatically.
44
+ */
45
+ @property() trigger = 'hover focus';
22
46
  @property({ type: Number }) distance = 8;
47
+ private readonly SHIFT_OFFSET = 16;
48
+ /** The distance in pixels from which to offset the tooltip along its target. */
23
49
  @property({ type: Number }) skidding = 0;
24
- @property({ type: Boolean }) hoist = false;
50
+ @property({ type: Boolean, reflect: true }) hoist = false;
25
51
 
26
52
  @query('.tooltip') tooltip!: HTMLElement;
27
- @query('slot') slotElement!: HTMLSlotElement;
53
+ @query('.trigger-container') triggerContainer!: HTMLElement;
54
+ @query('.tooltip-caret') caret!: HTMLElement;
55
+ @query('slot[name="content"]') tooltipSlot!: HTMLSlotElement;
28
56
 
29
- private hoverTimeout: number;
57
+ private hasTooltipSlot = false;
58
+ private hoverTimeout: number = 0;
59
+ private caretSize = 6;
60
+ private originalPlacement: string = this.placement;
30
61
 
31
- public static get styles(): CSSResultArray {
32
- return [styles];
33
- }
34
-
62
+ public static get styles(): CSSResultArray {
63
+ return [styles];
64
+ }
35
65
 
36
-
37
-
38
66
  connectedCallback() {
39
67
  super.connectedCallback();
68
+ this.originalPlacement = this.placement;
40
69
  window.addEventListener('resize', this.updateTooltipPosition);
41
70
  window.addEventListener('scroll', this.updateTooltipPosition, true);
42
71
  }
43
72
 
73
+ updated(changedProps: Map<string, unknown>) {
74
+ super.updated?.(changedProps);
75
+
76
+ const validPlacements = [
77
+ 'top', 'top-start', 'top-end',
78
+ 'right', 'right-start', 'right-end',
79
+ 'bottom', 'bottom-start', 'bottom-end',
80
+ 'left', 'left-start', 'left-end'
81
+ ];
82
+
83
+ if (!validPlacements.includes(this.placement)) {
84
+ console.warn(`[nile-tooltip] Invalid placement "${this.placement}", defaulting to "bottom".`);
85
+ this.placement = 'bottom';
86
+ }
87
+
88
+ if (!validPlacements.includes(this.originalPlacement)) {
89
+ this.originalPlacement = 'bottom';
90
+ }
91
+ }
92
+
44
93
  disconnectedCallback() {
45
94
  super.disconnectedCallback();
46
95
  window.removeEventListener('resize', this.updateTooltipPosition);
47
96
  window.removeEventListener('scroll', this.updateTooltipPosition, true);
48
97
  }
49
98
 
50
- private getBasePlacement(placement: TooltipPlacement): 'top' | 'right' | 'bottom' | 'left' {
51
- return placement.split('-')[0] as 'top' | 'right' | 'bottom' | 'left';
52
- }
53
-
54
- private getAlignmentModifier(placement: TooltipPlacement): 'start' | 'end' | null {
55
- const parts = placement.split('-');
56
- return parts.length > 1 ? parts[1] as 'start' | 'end' : null;
99
+ private handleTooltipSlotChange() {
100
+ const nodes = this.tooltipSlot.assignedNodes({ flatten: true });
101
+ this.hasTooltipSlot = nodes.length > 0;
102
+ this.requestUpdate();
57
103
  }
58
104
 
59
105
  private updateTooltipPosition = () => {
60
- if (!this.tooltip || !this.open) return;
61
-
62
- const triggerEl = this.slotElement.assignedElements()[0] as HTMLElement;
63
- if (!triggerEl) return;
106
+ if (!isInViewport(this.triggerContainer)) {
107
+ this.open = false;
108
+ return;
109
+ }
64
110
 
65
-
66
-
67
- const triggerRect = triggerEl.getBoundingClientRect();
111
+ const triggerRect = this.triggerContainer.getBoundingClientRect();
68
112
  const tooltipRect = this.tooltip.getBoundingClientRect();
69
113
  const viewportWidth = window.innerWidth;
70
114
  const viewportHeight = window.innerHeight;
71
-
72
- let top = 0, left = 0;
73
- let finalPlacement = this.placement;
74
- const basePlacement = this.getBasePlacement(this.placement);
75
- const alignment = this.getAlignmentModifier(this.placement);
76
-
77
-
78
- const fitsLeft = triggerRect.left - tooltipRect.width - this.distance > 0;
79
- const fitsRight = triggerRect.right + tooltipRect.width + this.distance < viewportWidth;
80
- const fitsBottom = triggerRect.bottom + tooltipRect.height + this.distance < viewportHeight;
81
- const fitsTop = triggerRect.top - tooltipRect.height - this.distance > 0;
82
-
83
-
84
- let finalBasePlacement = basePlacement;
85
-
86
-
87
- if ((basePlacement === 'left' && !fitsLeft) || (basePlacement === 'right' && !fitsRight)) {
88
- finalBasePlacement = fitsBottom ? 'bottom' : (fitsTop ? 'top' : 'bottom');
89
- }
90
-
91
- else if (basePlacement === 'top' && !fitsTop) {
92
- finalBasePlacement = fitsBottom ? 'bottom' : (fitsLeft ? 'left' : 'right');
93
- } else if (basePlacement === 'bottom' && !fitsBottom) {
94
- finalBasePlacement = fitsTop ? 'top' : (fitsLeft ? 'left' : 'right');
95
- }
96
-
97
-
98
- let finalAlignment = alignment;
99
-
100
-
101
- if ((basePlacement === 'left' || basePlacement === 'right') &&
102
- (finalBasePlacement === 'top' || finalBasePlacement === 'bottom')) {
103
-
104
- if (alignment === 'start') {
105
- finalAlignment = 'start';
106
- } else if (alignment === 'end') {
107
- finalAlignment = 'end';
108
- }
109
- }
110
-
111
-
112
- switch (finalBasePlacement) {
113
- case 'left':
114
- top = triggerRect.top + (triggerRect.height - tooltipRect.height) / 2 + this.skidding;
115
- left = triggerRect.left - tooltipRect.width - this.distance;
116
- break;
117
- case 'right':
118
- top = triggerRect.top + (triggerRect.height - tooltipRect.height) / 2 + this.skidding;
119
- left = triggerRect.right + this.distance;
120
- break;
121
- case 'top':
122
- top = triggerRect.top - tooltipRect.height - this.distance;
123
- left = triggerRect.left + (triggerRect.width - tooltipRect.width) / 2 + this.skidding;
124
- break;
125
- case 'bottom':
126
- top = triggerRect.bottom + this.distance;
127
- left = triggerRect.left + (triggerRect.width - tooltipRect.width) / 2 + this.skidding;
128
- break;
129
- }
130
-
131
-
132
- if ((finalBasePlacement === 'top' || finalBasePlacement === 'bottom') && finalAlignment) {
133
- if (finalAlignment === 'start') {
134
- left = triggerRect.left + this.skidding;
135
- } else if (finalAlignment === 'end') {
136
- left = triggerRect.right - tooltipRect.width + this.skidding;
137
- }
138
- }
139
-
140
-
141
- if ((finalBasePlacement === 'left' || finalBasePlacement === 'right') && finalAlignment) {
142
- if (finalAlignment === 'start') {
143
- top = triggerRect.top + this.skidding;
144
- } else if (finalAlignment === 'end') {
145
- top = triggerRect.bottom - tooltipRect.height + this.skidding;
146
- }
147
- }
148
-
149
-
150
- finalPlacement = finalAlignment ? `${finalBasePlacement}-${finalAlignment}` : finalBasePlacement;
151
-
152
-
153
- if (left < 0) left = 5;
154
- if (left + tooltipRect.width > viewportWidth) left = viewportWidth - tooltipRect.width - 5;
155
- if (top < 0) top = 5;
156
- if (top + tooltipRect.height > viewportHeight) top = viewportHeight - tooltipRect.height - 5;
157
115
 
158
-
116
+ const { top, left, placement } = getValidTooltipPosition(
117
+ triggerRect,
118
+ tooltipRect,
119
+ this.originalPlacement,
120
+ this.distance,
121
+ this.skidding,
122
+ this.caretSize,
123
+ viewportWidth,
124
+ viewportHeight
125
+ );
126
+
127
+ this.setAttribute('placement', placement);
159
128
  this.tooltip.style.top = `${top}px`;
160
129
  this.tooltip.style.left = `${left}px`;
161
- this.tooltip.setAttribute('data-placement', finalPlacement as string);
130
+
131
+ const { caretLeft, caretTop } = getCaretPosition({
132
+ placement,
133
+ tooltipRect,
134
+ triggerRect,
135
+ caretSize: this.caretSize,
136
+ left,
137
+ top
138
+ });
139
+
140
+ this.caret.style.left = `${caretLeft}px`;
141
+ this.caret.style.top = `${caretTop}px`;
162
142
  };
163
143
 
164
144
  private showTooltip = () => {
165
- if (!this.disabled &&
166
- this.content.trim() !== '') {
167
- this.open = true;
145
+ const trimmedContent = this.content.trim();
146
+ if (!trimmedContent && !this.hasTooltipSlot) {
147
+ return;
148
+ }
149
+ if (!this.disabled && isInViewport(this.triggerContainer)) {
168
150
  this.emit('nile-show');
151
+ this.open = true;
169
152
  this.updateComplete.then(() => {
170
153
  this.updateTooltipPosition();
171
-
172
-
173
- this.tooltip.addEventListener('transitionend', this.handleAfterShow, { once: true });
154
+ this.emit('nile-after-show');
174
155
  });
156
+ } else {
157
+ this.open = false;
175
158
  }
176
159
  };
177
-
178
- private handleAfterShow = () => {
179
- this.emit('nile-after-show');
180
- };
181
-
182
160
 
183
161
  private hideTooltip = () => {
184
- if (!this.open) return;
185
-
186
162
  this.emit('nile-hide');
187
163
  this.open = false;
188
-
189
- this.tooltip.addEventListener('transitionend', this.handleAfterHide, { once: true });
190
- };
191
-
192
- private handleAfterHide = () => {
193
- this.emit('nile-after-hide');
164
+ setTimeout(() => {
165
+ this.emit('nile-after-hide');
166
+ }, 200);
194
167
  };
195
-
168
+
196
169
  private handleMouseOver = () => {
197
170
  if (this.trigger.includes('hover')) {
198
171
  clearTimeout(this.hoverTimeout);
@@ -209,22 +182,13 @@ export class NileTooltip extends NileElement {
209
182
 
210
183
  private handleClick = () => {
211
184
  if (this.trigger.includes('click')) {
212
- this.open = !this.open;
213
- this.emit('nile-show');
214
- this.updateComplete.then(() => this.updateTooltipPosition());
215
- }
216
- };
217
-
218
- updated(changedProperties: Map<string, any>) {
219
- super.updated(changedProperties);
220
-
221
-
222
- if (changedProperties.has('open') || changedProperties.has('hoist')) {
223
- if (this.open && this.hoist) {
224
-
185
+ if (!this.open && isInViewport(this.triggerContainer)) {
186
+ this.showTooltip();
187
+ } else {
188
+ this.hideTooltip();
225
189
  }
226
190
  }
227
- }
191
+ };
228
192
 
229
193
  private handleFocus = () => {
230
194
  if (this.trigger.includes('focus')) {
@@ -240,17 +204,32 @@ export class NileTooltip extends NileElement {
240
204
 
241
205
  render() {
242
206
  return html`
243
- <div class=${classMap({ tooltip: true, 'tooltip__body--large': this.size === 'large' })}>
244
- ${this.content}
207
+ <div
208
+ class=${classMap({
209
+ tooltip: true,
210
+ 'tooltip__body--large': this.size === 'large',
211
+ })}
212
+ id="tooltip"
213
+ >
214
+ <div class="tooltip-content" part="content">
215
+ <slot name="content" @slotchange=${this.handleTooltipSlotChange}></slot>
216
+ ${!this.hasTooltipSlot ? html`${this.content}` : null}
217
+ </div>
218
+ <div class="tooltip-caret" style="--caret-size: ${this.caretSize}px;"></div>
245
219
  </div>
246
- <slot
220
+
221
+ <div
222
+ class="trigger-container"
223
+ tabindex="0"
247
224
  @mouseover=${this.handleMouseOver}
248
225
  @mouseout=${this.handleMouseOut}
249
226
  @click=${this.handleClick}
250
- @focus=${this.handleFocus}
251
- @blur=${this.handleBlur}
227
+ @focusin=${this.handleFocus}
228
+ @focusout=${this.handleBlur}
252
229
  aria-describedby="tooltip"
253
- ></slot>
230
+ >
231
+ <slot></slot>
232
+ </div>
254
233
  `;
255
234
  }
256
235
  }
@@ -259,4 +238,5 @@ declare global {
259
238
  interface HTMLElementTagNameMap {
260
239
  'nile-tooltip': NileTooltip;
261
240
  }
262
- }
241
+ }
242
+