Skip to content

Commit d974e01

Browse files
authored
Merge pull request #21 from svelte-plugins/absolute-tooltips
refactor(tooltips): use absolute positioning with tooltips
2 parents 0707213 + b1a57e7 commit d974e01

8 files changed

+135
-51
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@svelte-plugins/tooltips",
3-
"version": "0.1.9",
3+
"version": "1.0.0",
44
"license": "MIT",
55
"description": "A simple tooltip action and component designed for Svelte.",
66
"author": "Kieran Boyle (https://github.com/dysfunc)",

src/action-tooltip.snap.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ exports[`Components: Tooltip should render the component 1`] = `
55
<div>
66
<div
77
class="tooltip animation-null top"
8-
style="min-width: 200px; max-width: 200px; text-align: left;"
8+
style="left: 0px; min-width: 200px; max-width: 200px; text-align: left; top: 0px;"
99
>
1010
Hello World!
1111

src/action-tooltip.svelte

+19-4
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
// @ts-check
33
44
import { onMount, onDestroy } from 'svelte';
5-
import { formatVariableKey, getMinWidth, isInViewport } from './helpers';
5+
import { computeTooltipPosition, formatVariableKey, getMinWidth, isElementInViewport } from './helpers';
66
import { inverse } from './constants';
77
8+
/** @type {HTMLElement | null} */
9+
export let targetElement = null;
10+
811
/** @type {'hover' | 'click' | 'prop' | string} */
912
export let action = 'hover';
1013
@@ -53,6 +56,14 @@
5356
/** @type {boolean} */
5457
let visible = false;
5558
59+
/** @type {{ bottom: number, top: number, right: number, left: number }} */
60+
let coords = {
61+
bottom: 0,
62+
top: 0,
63+
right: 0,
64+
left: 0
65+
};
66+
5667
const delay = animation ? 200 : 0;
5768
5869
onMount(() => {
@@ -74,11 +85,14 @@
7485
}
7586
}
7687
77-
if (autoPosition && !isInViewport(tooltipRef)) {
88+
// @ts-ignore
89+
if (autoPosition && !isElementInViewport(tooltipRef, targetElement, position)) {
7890
// @ts-ignore
7991
position = inverse[position];
8092
}
8193
94+
coords = computeTooltipPosition(targetElement, tooltipRef, position, coords);
95+
8296
if (animation) {
8397
animationEffect = animation;
8498
}
@@ -103,8 +117,7 @@
103117
class="tooltip animation-{animationEffect} {position} {theme}"
104118
class:show={visible}
105119
class:arrowless={!arrow}
106-
style="min-width: {minWidth}px; max-width: {maxWidth}px; text-align: {align};"
107-
>
120+
style="bottom: auto; right: auto; left: {coords.left}px; min-width: {minWidth}px; max-width: {maxWidth}px; text-align: {align}; top: {coords.top}px;">
108121
{#if !isComponent}
109122
{@html content}
110123
{/if}
@@ -130,6 +143,7 @@
130143
--tooltip-offset-x: 12px;
131144
--tooltip-offset-y: 12px;
132145
--tooltip-padding: 12px;
146+
--tooltip-pointer-events: none;
133147
--tooltip-white-space-hidden: nowrap;
134148
--tooltip-white-space-shown: normal;
135149
--tooltip-z-index: 100;
@@ -151,6 +165,7 @@
151165
font-weight: var(--tooltip-font-weight);
152166
line-height: var(--tooltip-line-height);
153167
padding: var(--tooltip-padding);
168+
pointer-events: var(---tooltip-pointer-events);
154169
position: absolute;
155170
text-align: left;
156171
visibility: hidden;

src/action-tooltip.svelte.d.ts

+6
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ export interface ComponentProps {
6161
*/
6262
style?: undefined;
6363

64+
/**
65+
* The target element to bind the tooltip to.
66+
* @default null
67+
*/
68+
targetElement?: HTMLElement | null,
69+
6470
/**
6571
* The theme of the tooltip.
6672
* @default ''

src/action.js

+11-12
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ export const tooltip = (element, props) => {
55
let title = element.getAttribute('title');
66
let action = props?.action || element.getAttribute('action') || 'hover';
77

8+
const config = {
9+
...props,
10+
targetElement: element
11+
};
12+
813
if (title) {
914
element.removeAttribute('title');
10-
11-
props = {
12-
content: title,
13-
...props
14-
}
15+
config.content = title;
1516
}
1617

1718
const onClick = () => {
@@ -26,7 +27,7 @@ export const tooltip = (element, props) => {
2627
if (!component) {
2728
component = new Tooltip({
2829
target: element,
29-
props
30+
props: config
3031
});
3132
}
3233
};
@@ -42,6 +43,10 @@ export const tooltip = (element, props) => {
4243
if (element !== null) {
4344
removeListeners();
4445

46+
if (config.show) {
47+
onShow();
48+
}
49+
4550
if (action === 'click') {
4651
element.addEventListener('click', onClick);
4752
}
@@ -63,12 +68,6 @@ export const tooltip = (element, props) => {
6368

6469
addListeners();
6570

66-
element.style.position = 'relative';
67-
68-
if (props.show) {
69-
onShow();
70-
}
71-
7271
return {
7372
destroy() {
7473
removeListeners();

src/helpers.js

+61-6
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,68 @@ export const getMinWidth = (element, maxWidth) => {
1818
return Math.round(Math.min(maxWidth, contentWidth || maxWidth));
1919
};
2020

21-
export const isInViewport = (element) => {
21+
export const isElementInViewport = (element, container = null, position) => {
2222
const rect = element.getBoundingClientRect();
23+
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
24+
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
2325

24-
return (
25-
rect.top >= 0 &&
26-
rect.left >= 0 &&
27-
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
28-
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
26+
let isInsideViewport = (
27+
rect.bottom > 0 &&
28+
rect.top < viewportHeight &&
29+
rect.right > 0 &&
30+
rect.left < viewportWidth
2931
);
32+
33+
if (container) {
34+
const containerRect = container.getBoundingClientRect();
35+
36+
if (position === 'top' || position === 'bottom') {
37+
isInsideViewport = (
38+
(containerRect.bottom + containerRect.height) < viewportHeight &&
39+
containerRect.top < viewportHeight
40+
);
41+
} else {
42+
isInsideViewport = (
43+
(containerRect.right + containerRect.width) < viewportWidth &&
44+
containerRect.left < viewportWidth
45+
);
46+
}
47+
48+
return isInsideViewport;
49+
}
50+
51+
return isInsideViewport;
52+
};
53+
54+
export const computeTooltipPosition = (containerRef, tooltipRef, position, coords) => {
55+
if (!containerRef || !tooltipRef) {
56+
return coords;
57+
}
58+
59+
const containerRect = containerRef.getBoundingClientRect();
60+
const tooltipRect = tooltipRef.getBoundingClientRect();
61+
62+
switch (position) {
63+
case 'top':
64+
coords.top = containerRect.top;
65+
coords.left = containerRect.left + (containerRect.width / 2);
66+
break;
67+
case 'bottom':
68+
coords.top = containerRect.top - tooltipRect.height;
69+
coords.left = containerRect.left + (containerRect.width / 2);
70+
break;
71+
case 'left':
72+
coords.left = containerRect.left;
73+
coords.top = containerRect.top + (containerRect.height / 2);
74+
break;
75+
case 'right':
76+
coords.left = containerRect.right - tooltipRect.width;
77+
coords.top = containerRect.top + (containerRect.height / 2);
78+
break;
79+
}
80+
81+
coords.top += window.scrollY;
82+
coords.left += window.scrollX;
83+
84+
return coords;
3085
};

src/tooltip.snap.js

+8-9
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,15 @@ exports[`Components: Tooltip should render the component 1`] = `
55
<div>
66
<span
77
class="tooltip-container"
8+
/>
9+
10+
<div
11+
class="tooltip animation-null top"
12+
style="left: 0px; min-width: 200px; max-width: 200px; text-align: left; top: 0px;"
813
>
9-
10-
<div
11-
class="tooltip animation-null top"
12-
style="min-width: 200px; max-width: 200px; text-align: left;"
13-
>
14-
Hello World!
15-
16-
</div>
17-
</span>
14+
Hello World!
15+
16+
</div>
1817
1918
</div>
2019
</body>

src/tooltip.svelte

+28-18
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// @ts-check
33
44
import { onMount, onDestroy } from 'svelte';
5-
import { formatVariableKey, getMinWidth, isInViewport } from './helpers';
5+
import { computeTooltipPosition, formatVariableKey, getMinWidth, isElementInViewport } from './helpers';
66
import { inverse } from './constants';
77
88
/** @type {'hover' | 'click' | 'prop' | string} */
@@ -62,6 +62,14 @@
6262
/** @type {boolean} */
6363
let visible = false;
6464
65+
/** @type {{ bottom: number, top: number, right: number, left: number }} */
66+
let coords = {
67+
bottom: 0,
68+
top: 0,
69+
right: 0,
70+
left: 0
71+
};
72+
6573
const onClick = () => {
6674
if (visible) {
6775
onHide();
@@ -73,11 +81,14 @@
7381
const onShow = () => {
7482
const delay = animation ? 200 : 0;
7583
76-
if (autoPosition && !isInViewport(tooltipRef)) {
84+
// @ts-ignore
85+
if (autoPosition && !isElementInViewport(containerRef, tooltipRef, position)) {
7786
// @ts-ignore
7887
position = inverse[position];
7988
}
8089
90+
coords = computeTooltipPosition(containerRef, tooltipRef, position, coords);
91+
8192
if (animation) {
8293
animationEffect = animation;
8394
}
@@ -122,6 +133,8 @@
122133
onMount(() => {
123134
addListeners();
124135
136+
computeTooltipPosition();
137+
125138
if (tooltipRef !== null) {
126139
if (isComponent && !component) {
127140
// @ts-ignore
@@ -162,18 +175,17 @@
162175
{#if content}
163176
<span bind:this={containerRef} class="tooltip-container">
164177
<slot />
165-
<div
166-
bind:this={tooltipRef}
167-
class="tooltip animation-{animationEffect} {position} {theme}"
168-
class:arrowless={!arrow}
169-
class:show={visible}
170-
style="min-width: {minWidth}px; max-width: {maxWidth}px; text-align: {align};"
171-
>
172-
{#if !isComponent}
173-
{@html content}
174-
{/if}
175-
</div>
176-
</span>
178+
</span>
179+
<div
180+
bind:this={tooltipRef}
181+
class="tooltip animation-{animationEffect} {position} {theme}"
182+
class:arrowless={!arrow}
183+
class:show={visible}
184+
style="bottom: auto; right: auto; left: {coords.left}px; min-width: {minWidth}px; max-width: {maxWidth}px; text-align: {align}; top: {coords.top}px;">
185+
{#if !isComponent}
186+
{@html content}
187+
{/if}
188+
</div>
177189
{:else}
178190
<slot />
179191
{/if}
@@ -197,6 +209,7 @@
197209
--tooltip-offset-x: 0px;
198210
--tooltip-offset-y: 0px;
199211
--tooltip-padding: 12px;
212+
--tooltip-pointer-events: none;
200213
--tooltip-white-space-hidden: nowrap;
201214
--tooltip-white-space-shown: normal;
202215
--tooltip-z-index: 100;
@@ -206,10 +219,6 @@
206219
* Tooltip Styling
207220
*--------------------------*/
208221
209-
.tooltip-container {
210-
position: relative;
211-
}
212-
213222
.tooltip {
214223
background-color: var(--tooltip-background-color);
215224
box-shadow: var(--tooltip-box-shadow);
@@ -222,6 +231,7 @@
222231
font-weight: var(--tooltip-font-weight);
223232
line-height: var(--tooltip-line-height);
224233
padding: var(--tooltip-padding);
234+
pointer-events: var(--tooltip-pointer-events);
225235
position: absolute;
226236
text-align: left;
227237
visibility: hidden;

0 commit comments

Comments
 (0)