Skip to content

Commit

Permalink
Merge pull request #21 from svelte-plugins/absolute-tooltips
Browse files Browse the repository at this point in the history
refactor(tooltips): use absolute positioning with tooltips
  • Loading branch information
dysfunc authored Nov 14, 2023
2 parents 0707213 + b1a57e7 commit d974e01
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 51 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@svelte-plugins/tooltips",
"version": "0.1.9",
"version": "1.0.0",
"license": "MIT",
"description": "A simple tooltip action and component designed for Svelte.",
"author": "Kieran Boyle (https://github.com/dysfunc)",
Expand Down
2 changes: 1 addition & 1 deletion src/action-tooltip.snap.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ exports[`Components: Tooltip should render the component 1`] = `
<div>
<div
class="tooltip animation-null top"
style="min-width: 200px; max-width: 200px; text-align: left;"
style="left: 0px; min-width: 200px; max-width: 200px; text-align: left; top: 0px;"
>
Hello World!
Expand Down
23 changes: 19 additions & 4 deletions src/action-tooltip.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
// @ts-check
import { onMount, onDestroy } from 'svelte';
import { formatVariableKey, getMinWidth, isInViewport } from './helpers';
import { computeTooltipPosition, formatVariableKey, getMinWidth, isElementInViewport } from './helpers';
import { inverse } from './constants';
/** @type {HTMLElement | null} */
export let targetElement = null;
/** @type {'hover' | 'click' | 'prop' | string} */
export let action = 'hover';
Expand Down Expand Up @@ -53,6 +56,14 @@
/** @type {boolean} */
let visible = false;
/** @type {{ bottom: number, top: number, right: number, left: number }} */
let coords = {
bottom: 0,
top: 0,
right: 0,
left: 0
};
const delay = animation ? 200 : 0;
onMount(() => {
Expand All @@ -74,11 +85,14 @@
}
}
if (autoPosition && !isInViewport(tooltipRef)) {
// @ts-ignore
if (autoPosition && !isElementInViewport(tooltipRef, targetElement, position)) {
// @ts-ignore
position = inverse[position];
}
coords = computeTooltipPosition(targetElement, tooltipRef, position, coords);
if (animation) {
animationEffect = animation;
}
Expand All @@ -103,8 +117,7 @@
class="tooltip animation-{animationEffect} {position} {theme}"
class:show={visible}
class:arrowless={!arrow}
style="min-width: {minWidth}px; max-width: {maxWidth}px; text-align: {align};"
>
style="bottom: auto; right: auto; left: {coords.left}px; min-width: {minWidth}px; max-width: {maxWidth}px; text-align: {align}; top: {coords.top}px;">
{#if !isComponent}
{@html content}
{/if}
Expand All @@ -130,6 +143,7 @@
--tooltip-offset-x: 12px;
--tooltip-offset-y: 12px;
--tooltip-padding: 12px;
--tooltip-pointer-events: none;
--tooltip-white-space-hidden: nowrap;
--tooltip-white-space-shown: normal;
--tooltip-z-index: 100;
Expand All @@ -151,6 +165,7 @@
font-weight: var(--tooltip-font-weight);
line-height: var(--tooltip-line-height);
padding: var(--tooltip-padding);
pointer-events: var(---tooltip-pointer-events);
position: absolute;
text-align: left;
visibility: hidden;
Expand Down
6 changes: 6 additions & 0 deletions src/action-tooltip.svelte.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ export interface ComponentProps {
*/
style?: undefined;

/**
* The target element to bind the tooltip to.
* @default null
*/
targetElement?: HTMLElement | null,

/**
* The theme of the tooltip.
* @default ''
Expand Down
23 changes: 11 additions & 12 deletions src/action.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ export const tooltip = (element, props) => {
let title = element.getAttribute('title');
let action = props?.action || element.getAttribute('action') || 'hover';

const config = {
...props,
targetElement: element
};

if (title) {
element.removeAttribute('title');

props = {
content: title,
...props
}
config.content = title;
}

const onClick = () => {
Expand All @@ -26,7 +27,7 @@ export const tooltip = (element, props) => {
if (!component) {
component = new Tooltip({
target: element,
props
props: config
});
}
};
Expand All @@ -42,6 +43,10 @@ export const tooltip = (element, props) => {
if (element !== null) {
removeListeners();

if (config.show) {
onShow();
}

if (action === 'click') {
element.addEventListener('click', onClick);
}
Expand All @@ -63,12 +68,6 @@ export const tooltip = (element, props) => {

addListeners();

element.style.position = 'relative';

if (props.show) {
onShow();
}

return {
destroy() {
removeListeners();
Expand Down
67 changes: 61 additions & 6 deletions src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,68 @@ export const getMinWidth = (element, maxWidth) => {
return Math.round(Math.min(maxWidth, contentWidth || maxWidth));
};

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

return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
let isInsideViewport = (
rect.bottom > 0 &&
rect.top < viewportHeight &&
rect.right > 0 &&
rect.left < viewportWidth
);

if (container) {
const containerRect = container.getBoundingClientRect();

if (position === 'top' || position === 'bottom') {
isInsideViewport = (
(containerRect.bottom + containerRect.height) < viewportHeight &&
containerRect.top < viewportHeight
);
} else {
isInsideViewport = (
(containerRect.right + containerRect.width) < viewportWidth &&
containerRect.left < viewportWidth
);
}

return isInsideViewport;
}

return isInsideViewport;
};

export const computeTooltipPosition = (containerRef, tooltipRef, position, coords) => {
if (!containerRef || !tooltipRef) {
return coords;
}

const containerRect = containerRef.getBoundingClientRect();
const tooltipRect = tooltipRef.getBoundingClientRect();

switch (position) {
case 'top':
coords.top = containerRect.top;
coords.left = containerRect.left + (containerRect.width / 2);
break;
case 'bottom':
coords.top = containerRect.top - tooltipRect.height;
coords.left = containerRect.left + (containerRect.width / 2);
break;
case 'left':
coords.left = containerRect.left;
coords.top = containerRect.top + (containerRect.height / 2);
break;
case 'right':
coords.left = containerRect.right - tooltipRect.width;
coords.top = containerRect.top + (containerRect.height / 2);
break;
}

coords.top += window.scrollY;
coords.left += window.scrollX;

return coords;
};
17 changes: 8 additions & 9 deletions src/tooltip.snap.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@ exports[`Components: Tooltip should render the component 1`] = `
<div>
<span
class="tooltip-container"
/>
<div
class="tooltip animation-null top"
style="left: 0px; min-width: 200px; max-width: 200px; text-align: left; top: 0px;"
>
<div
class="tooltip animation-null top"
style="min-width: 200px; max-width: 200px; text-align: left;"
>
Hello World!
</div>
</span>
Hello World!
</div>
</div>
</body>
Expand Down
46 changes: 28 additions & 18 deletions src/tooltip.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// @ts-check
import { onMount, onDestroy } from 'svelte';
import { formatVariableKey, getMinWidth, isInViewport } from './helpers';
import { computeTooltipPosition, formatVariableKey, getMinWidth, isElementInViewport } from './helpers';
import { inverse } from './constants';
/** @type {'hover' | 'click' | 'prop' | string} */
Expand Down Expand Up @@ -62,6 +62,14 @@
/** @type {boolean} */
let visible = false;
/** @type {{ bottom: number, top: number, right: number, left: number }} */
let coords = {
bottom: 0,
top: 0,
right: 0,
left: 0
};
const onClick = () => {
if (visible) {
onHide();
Expand All @@ -73,11 +81,14 @@
const onShow = () => {
const delay = animation ? 200 : 0;
if (autoPosition && !isInViewport(tooltipRef)) {
// @ts-ignore
if (autoPosition && !isElementInViewport(containerRef, tooltipRef, position)) {
// @ts-ignore
position = inverse[position];
}
coords = computeTooltipPosition(containerRef, tooltipRef, position, coords);
if (animation) {
animationEffect = animation;
}
Expand Down Expand Up @@ -122,6 +133,8 @@
onMount(() => {
addListeners();
computeTooltipPosition();
if (tooltipRef !== null) {
if (isComponent && !component) {
// @ts-ignore
Expand Down Expand Up @@ -162,18 +175,17 @@
{#if content}
<span bind:this={containerRef} class="tooltip-container">
<slot />
<div
bind:this={tooltipRef}
class="tooltip animation-{animationEffect} {position} {theme}"
class:arrowless={!arrow}
class:show={visible}
style="min-width: {minWidth}px; max-width: {maxWidth}px; text-align: {align};"
>
{#if !isComponent}
{@html content}
{/if}
</div>
</span>
</span>
<div
bind:this={tooltipRef}
class="tooltip animation-{animationEffect} {position} {theme}"
class:arrowless={!arrow}
class:show={visible}
style="bottom: auto; right: auto; left: {coords.left}px; min-width: {minWidth}px; max-width: {maxWidth}px; text-align: {align}; top: {coords.top}px;">
{#if !isComponent}
{@html content}
{/if}
</div>
{:else}
<slot />
{/if}
Expand All @@ -197,6 +209,7 @@
--tooltip-offset-x: 0px;
--tooltip-offset-y: 0px;
--tooltip-padding: 12px;
--tooltip-pointer-events: none;
--tooltip-white-space-hidden: nowrap;
--tooltip-white-space-shown: normal;
--tooltip-z-index: 100;
Expand All @@ -206,10 +219,6 @@
* Tooltip Styling
*--------------------------*/
.tooltip-container {
position: relative;
}
.tooltip {
background-color: var(--tooltip-background-color);
box-shadow: var(--tooltip-box-shadow);
Expand All @@ -222,6 +231,7 @@
font-weight: var(--tooltip-font-weight);
line-height: var(--tooltip-line-height);
padding: var(--tooltip-padding);
pointer-events: var(--tooltip-pointer-events);
position: absolute;
text-align: left;
visibility: hidden;
Expand Down

0 comments on commit d974e01

Please sign in to comment.