Skip to content

Instantly share code, notes, and snippets.

@samdroid-apps
Last active April 12, 2023 01:12
Show Gist options
  • Save samdroid-apps/180ffa263268e08760486563d0dc1e9c to your computer and use it in GitHub Desktop.
Save samdroid-apps/180ffa263268e08760486563d0dc1e9c to your computer and use it in GitHub Desktop.
Relume Tooltip Powerup

Relume Tooltips Script

This script is included in all of the tooltip components from Relume Library's UI Elements. It adds 2 features:

  1. Flip the tooltip to the opposite direction, to keep it within the viewport:
Screencast.from.2023-04-06.10-14-10.webm
  1. Slide the tooltip along the perpendicular axis, to keep it within the viewport:
Screencast.from.2023-04-06.10-16-40.webm

Below is the code snippet to implement those behaviours. You don't need to add the code manually; Relume Library uses a minified version of the code to improve page load performance. If you need to make changes, you can use the original code below:

// ==ClosureCompiler==
// @output_file_name default.js
// @compilation_level ADVANCED_OPTIMIZATIONS
// @js_externs var iconWrapperClass
// @js_externs var tooltipWrapperClass
// @js_externs var arrowClass
// @language_out ECMASCRIPT_2015
// ==/ClosureCompiler==
//
//
// Use https://closure-compiler.appspot.com/home
// Move the *Class variables inside the IIFE. They're outside so the names are not
// mangled by ClosureCompiler:
iconWrapperClass = "tooltip2_element-wrapper";
tooltipWrapperClass = "tooltip2_tooltip-wrapper";
arrowClass = "tooltip2_arrow";
(() => {
//////////////////////
// Helper functions //
//////////////////////
function titleCase(d) {
return d[0].toUpperCase() + d.slice(1);
}
function paddingPropertyName(d) {
return "padding" + titleCase(d);
}
/** The opposite direction of each direction */
const oppositeOf = {
["bottom"]: "top",
["left"]: "right",
["right"]: "left",
["top"]: "bottom",
};
const arrowRotation = {
["bottom"]: 180,
["left"]: -90,
["right"]: 90,
["top"]: 0,
};
/* The css property names for the horizontal axis */
const horizontalAxis = {
start: "left",
end: "right",
len: "width",
translate: "translateX",
};
/* The css property names for the vertical axis */
const verticalAxis = {
start: "top",
end: "bottom",
len: "height",
translate: "translateY",
};
/** The axis perpendicular to each direction */
const perpendicularAxisTo = {
["bottom"]: horizontalAxis,
["left"]: verticalAxis,
["right"]: verticalAxis,
["top"]: horizontalAxis,
};
/** Returns the style properties of the tooltip. Users might customize padding values in Webflow */
function getTooltipPadding(tooltip) {
const tooltipStyle = window.getComputedStyle(tooltip);
return (
parseInt(tooltipStyle.paddingTop, 10) ||
parseInt(tooltipStyle.paddingBottom, 10) ||
parseInt(tooltipStyle.paddingLeft, 10) ||
parseInt(tooltipStyle.paddingRight, 10) ||
0
);
}
function setupTooltip(icon) {
// Ensure setupTooltip is idempotent:
const isSetupKey = "relumeTooltipSetup";
if (icon.dataset[isSetupKey]) return;
icon.dataset[isSetupKey] = 1;
const tooltip = icon.parentElement.querySelector("." + tooltipWrapperClass);
const arrow = icon.parentElement.querySelector("." + arrowClass);
const naturalDirection = arrow.classList.contains("is-left")
? "left"
: arrow.classList.contains("is-right")
? "right"
: arrow.classList.contains("is-bottom")
? "bottom"
: "top";
const oppositeDirection = oppositeOf[naturalDirection];
const tooltipPadding = getTooltipPadding(tooltip);
/** Updates the tooltip's style to position it in direction `d` */
function updateTooltipStyle(d, slideAxis, slidePx) {
const o = oppositeOf[d];
tooltip.style[o] = "100%";
tooltip.style[d] = "auto";
tooltip.style[paddingPropertyName(o)] = tooltipPadding + "px";
tooltip.style[paddingPropertyName(d)] = "0";
tooltip.style.transform = slideAxis.translate + "(" + slidePx + "px)";
arrow.style.transform =
slideAxis.translate +
"(" +
-slidePx +
"px) " +
"rotate(" +
arrowRotation[d] +
"deg) ";
arrow.style[d] = "auto";
arrow.style[o] = d === "top" || d === "bottom" ? "0.25rem" : "0";
}
let open = false;
/** Runs every frame that the tooltip is open */
function keepInViewport() {
if (!open) return;
window.requestAnimationFrame(keepInViewport);
const iconBox = icon.getBoundingClientRect();
const tooltipBox = tooltip.getBoundingClientRect();
// Step 1 - on the perpendicular axis to naturalDirection, slide the
// tooltip to keep it in the viewport.
const slideAxis = perpendicularAxisTo[naturalDirection];
const desiredStart =
(iconBox[slideAxis.start] +
iconBox[slideAxis.end] -
tooltipBox[slideAxis.len]) /
2;
const desiredEnd =
(iconBox[slideAxis.start] +
iconBox[slideAxis.end] +
tooltipBox[slideAxis.len]) /
2;
let slidePx = 0;
const windowEnd = window["inner" + titleCase(slideAxis.len)];
if (desiredStart < 0) {
slidePx = -desiredStart;
} else if (desiredEnd > windowEnd) {
slidePx = windowEnd - desiredEnd;
}
// Step 2 - set the direction of the tooltip to either naturalDirection or
// oppositeDirection direction, whichever fits best.
const fits = {
["bottom"]: iconBox.bottom + tooltipBox.height < window.innerHeight,
["left"]: iconBox.left - tooltipBox.width > 0,
["right"]: iconBox.right + tooltipBox.width < window.innerWidth,
["top"]: iconBox.top - tooltipBox.height > 0,
};
const newDirection =
fits[naturalDirection] || !fits[oppositeDirection]
? naturalDirection
: oppositeDirection;
updateTooltipStyle(newDirection, slideAxis, slidePx);
}
icon.parentElement.addEventListener("mouseenter", () => {
open = true;
keepInViewport();
});
icon.parentElement.addEventListener("mouseleave", () => {
open = false;
});
}
function setupAllTooltips() {
document.querySelectorAll("." + iconWrapperClass).forEach(setupTooltip);
}
// Setup tooltips when the DOM loads:
window.addEventListener("DOMContentLoaded", setupAllTooltips);
// Also setup the tooltips now, in case the dom has already loaded. Setting
// up a tooltip is idempotent so this is safe:
setupAllTooltips();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment