<!-- html structure
following a heading include paragraph elements with the custom element instead of the default anchor link
-->
<main>
<h1>Custom Tooltip <span role="img">💬</span></h1>
<!--
each anchor link benefits from the following attributes
- href for the referenced URL
- tip for the text shown on hover
- corner for the position of the tooltip (starting at 0 with the top left corner and going clockwise)
<custom-link
href="#"
tip="You really should"
corner="0">
Go to page
<custom-link>
-->
<p>
Hello there. This is meant to be an entry for the
<custom-link
href="https://www.florin-pop.com/blog/2019/03/weekly-coding-challenge/"
tip="Check it out!"
corner="0">
weekly coding challenge
</custom-link>.
It is also a perfect excuse to practice with the
<custom-link
href="https://developer.mozilla.org/en-US/docs/Web/Web_Components"
tip="Plenty to learn"
corner="0">
Web Component API
</custom-link>.
</p>
<p>
What's so special about it? Well, turns out anchor links on this page are actually
<custom-link
href="https://developers.google.com/web/fundamentals/web-components/customelements"
tip="Handcrafted with care"
corner="0">
custom elements
</custom-link>,
equipped with a rather
<custom-link
href="https://www.merriam-webster.com/dictionary/cutesy"
tip="Hope that's a word"
corner="3">
cutsy
</custom-link>
tooltip.
</p>
<p>
Hover or focus on them at will.
</p>
<p>
Here's
<custom-link
href="#"
tip="Right here!"
corner="1">
one
</custom-link>
, here's
<custom-link
href="#"
tip="Oh well..."
corner="1">
another
</custom-link>.
Getting old quickly?
<custom-link
href="#"
tip="Hope not"
corner="2">
Nah
</custom-link>.
</p>
<!--
reference the pen showcasing a single custom tooltip,
built without the web components API
-->
<p>
If you are on Edge, you might enjoy
<custom-link
href="https://codepen.io/borntofrappe/full/mYaPwg"
tip="Not supported yet..."
corner="0">
this proof of concept
</custom-link>.
</p>
</main>
@import url("https://fonts.googleapis.com/css?family=Lato|Poppins:300&display=swap");
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
body {
font-family: "Open Sans", sans-serif;
background: #fff;
color: #1d1e22;
background: #111214;
font-family: "Lato", sans-serif;
}
/* horizontally center the main container in the viewport */
main {
max-width: 400px;
margin: 0 auto;
padding: 2rem 1.5rem;
line-height: 1.5;
min-height: 100vh;
background: #fff;
box-shadow: 0 0 5px -4px currentColor;
}
/* create noticeable whitespace around the elements of the page */
h1 {
font-family: "Poppins", sans-serif;
font-weight: 300;
margin-bottom: 1.25rem;
}
p {
margin: 1rem 0;
line-height: 2.5;
}
/* reduce the size of the last paragraph, used as a final note */
p:last-of-type {
margin-top: 3rem;
font-size: 0.8rem;
text-align: right;
}
// create a reusable element through a template tag
const template = document.createElement('template');
/* element structure
<a>
text shown on page
<div>text shown on hover/focus</div>
</a>
by default the div container includes three dots
*/
template.innerHTML = `
<style>
a {
text-decoration: none;
color: inherit;
font-weight: bold;
position: relative;
line-height: 1.5;
font-size: 1em;
}
a .tooltip {
position: absolute;
font-size: 0.9em;
font-weight: normal;
padding: 0.25rem 0.75rem;
white-space: nowrap;
background: #0580ed;
color: #fff;
box-shadow: 0 0 5px -3px #000;
transition: all 0.2s ease-out;
transform: scale(0);
}
a:hover .tooltip,
a:focus .tooltip {
transform: scale(1);
}
a .tooltip span.tool {
display: inline-block;
}
a:hover .tooltip span.tool,
a:focus .tooltip span.tool {
animation: wait 0.75s 0.3s ease-out 2;
}
a .tooltip span.tool:nth-of-type(2) {
animation-delay: 0.45s;
}
a .tooltip span.tool:nth-of-type(3) {
animation-delay: 0.6s;
}
@keyframes wait {
25% {
transform: translateY(-3px);
}
50% {
transform: translateY(3px);
}
75% {
transform: translateY(0);
}
}
</style>
<a>
<slot></slot>
<div class="tooltip"><span class="tool">•</span><span class="tool">•</span><span class="tool">•</span></div>
</a>
`;
// class describing the custom element
class CustomLinkComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
// boolean to manage the state of the tooltip
this.isShowing = false;
// functions updating the tooltip's appearance and markup
this.updateTooltip = this.updateTooltip.bind(this);
this.showTooltip = this.showTooltip.bind(this);
this.hideTooltip = this.hideTooltip.bind(this);
}
/* function called to update the tooltip
- when the three dots have done animating the function updates the tooltip with the informative text and scales the tooltip up
- when the tooltip is scaled down following the blur/mouseout event the function resets the tooltip's html and removes the inline transform property
*/
updateTooltip() {
const { isShowing } = this;
const tooltip = this.shadowRoot.querySelector('a .tooltip');
tooltip.innerHTML = isShowing ? this.getAttribute('tip') : Array(3).fill('').map(dot => '<span class="tool">•</span>').join('');
tooltip.style.transform = isShowing ? 'scale(1)' : '';
// remove the event listener to be able to fire it once more through the showTooltip/hideTooltip functions
tooltip.removeEventListener('transitionend', this.updateTooltip);
}
// function called following the mouseenter/focus events
showTooltip() {
// proceed only if the tooltip is not in the process of being shown
const { isShowing } = this;
if (!isShowing) {
this.isShowing = true;
// target the .tooltip container, the nested span elements
const tooltip = this.shadowRoot.querySelector('a .tooltip');
const spans = tooltip.querySelectorAll('span');
// identify the last span (last to be animated)
const lastSpan = spans[spans.length - 1];
// when the last span has done animating, scale the tooltip back to 0
lastSpan.addEventListener('animationend', () => {
tooltip.style.transform = 'scale(0)';
// as the tooltip finishes its transition, call the update function to show the tooltip's text
tooltip.addEventListener('transitionend', this.updateTooltip);
});
}
}
// function called following the mouseout/blur events
hideTooltip() {
// ! proceed only if the tooltip is in the process of being shown
const { isShowing } = this;
if (isShowing) {
this.isShowing = false;
// target the tooltip
const tooltip = this.shadowRoot.querySelector('a .tooltip');
// scale the tooltip to 0
tooltip.style.transform = 'scale(0)';
// as the tooltip disappears from sight call the update function to reset the tooltip's markup
tooltip.addEventListener('transitionend', this.updateTooltip);
}
}
// when instantiated create a copy of the template
connectedCallback() {
this.shadowRoot.appendChild(template.content.cloneNode(true));
// target the elements in the template which need updating
const link = this.shadowRoot.querySelector('a');
const tooltip = link.querySelector('.tooltip');
// set the href attribute through the href attribute of the custom element itself
const href = this.getAttribute('href');
link.setAttribute('href', href);
// if the link references an actual page open the page in another tab
if (href !== '#') {
link.setAttribute('target', '_blank');
}
// retrieve the corner to modify the appearance of the tooltip box
const corner = Number.parseInt(this.getAttribute('corner'), 10);
/* based on the corner value update the following properties
-
-
*/
// border-radius to have 20px for every corner except the one closes to the anchor link element
const borderZero = (corner + 2) % 4;
const borderRadius = Array(4).fill('20px');
borderRadius[borderZero] = '0';
tooltip.style.borderRadius = borderRadius.join(' ');
// top, right, bottom, left to position the tooltip
if (corner < 2) {
tooltip.style.bottom = '100%';
} else {
tooltip.style.top = '100%';
}
if (corner % 3 === 0) {
tooltip.style.right = '100%';
} else {
tooltip.style.left = '100%';
}
// transform-origin to determine from where the tooltip should spawn
const originX = corner % 3 === 0 ? '100%' : '0%';
const originY = corner < 2 ? '100%' : '0%';
tooltip.style.transformOrigin = `${originX} ${originY}`;
// attach event listeners on the anchor link to show/hide the tooltip as needed
link.addEventListener('focus', this.showTooltip);
link.addEventListener('mouseenter', this.showTooltip);
link.addEventListener('blur', this.hideTooltip);
link.addEventListener('mouseout', this.hideTooltip);
}
}
// define the custom-link element referencing the custom element
window.customElements.define('custom-link', CustomLinkComponent);
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.