3
\$\begingroup\$

I'm showing a tooltip when you hover an element with the tooltip attribute. E.g. <div class="example" tooltip="Hello World">Hover me.</div>

I also have to set the position of the tooltip based on its width, and because I use width: max-content for the element, I have to do something like shown in the code example.

Is there any way I can make my code more efficient? I feel like I'm making it unnecessarily complicated.

(function() {
    var tooltip = document.querySelectorAll("[tooltip]");

    callback = (element, index, array) => {
        tooltip[index].onmouseenter = () => {
            let data = element.getAttribute("tooltip");
            let child = document.createElement("div");

            child.className = "tooltip";
            child.innerHTML = data;

            element.appendChild(child);

            child.style.left = "50%";
            child.style.marginLeft = - (child.offsetWidth / 2) + "px";

        }

        tooltip[index].onmouseleave = () => {
            if(element.getElementsByClassName("tooltip")[0] != undefined) {
                element.removeChild(element.getElementsByClassName("tooltip")[0]);
            }
        }
    }

    tooltip.forEach(callback);
})();
/* Don't review this CSS; it was added just for the demo. */
div[tooltip] { width: max-content; margin: auto; }
<div class="example" tooltip="Hello World">Hover me.</div>

\$\endgroup\$
2
\$\begingroup\$

For one, it's a bit wasteful to always create a new element for each tooltip; you could create one when the page loads, and just reuse that (since the user will only ever see one tooltip at the time).

The biggest pitfall, though, is that if you add new elements to the page programmatically, the event handlers won't be applied to them. E.g. if you do something like this:

let newButton = document.createElement('button');
newButton.textContent = 'click me!';
newButton.setAttribute('tooltip', 'Click here to do stuff');
newButton.onclick = () => {
    alert('You did stuff');
}
document.body.appendChild(newButton);

This button, although it has a tooltip attribute, will not show the tooltip when hovered.

You can solve this by taking advantage of event propagation: instead of setting the event handler on each individual element, you can just set one event handler on the window object, and handle it there.

(function() {
    // Create the tooltip element we will be reusing
    let tooltip = document.createElement('div');
    tooltip.className = 'tooltip';
    tooltip.style.left = '50%';
    tooltip.style.transform = 'translateX(-50%)';

    // Prepare the mouseout event handler
    let mouseoutHandler = (event) => {
        event.target.removeChild(tooltip);
    };

    // Set the mouseover event handler
    window.addEventListener('mouseover', event => {
        if (!event.target.dataset.tooltip) return true;

        tooltip.textContent = event.target.dataset.tooltip;

        event.target.appendChild(tooltip);
        event.target.addEventListener(
            'mouseout',
            mouseoutHandler,
            { once: true }
        );
    });
})();

In this version, we capture the mouseenter event on the window level, and only set the mouseleave event handler to fire once (by passing the option { once: true }). This also means you don't have to check if there is a tooltip inside the target element, since it is guaranteed that there will be, and the event handler won't get invoked after that until the next time we show the tooltip.

Also, instead of getAttribute() I used the dataset property. This also means you'll have to use <button data-tooltip="click here"> instead of <button tooltip="click here">. It's a bit longer, but it is recommended to always use the data- prefix for custom attributes; for one, because this way you can access the attribute directly in the dataset property as seen above, and also to make sure your custom attribute doesn't end up colliding with any standard attribute that might be introduced later (especially for something as common as "tooltip").

I also used pure CSS to center the tooltip.

\$\endgroup\$
  • \$\begingroup\$ According to this, mouseenter does not bubble. Use mouseover instead. \$\endgroup\$ – Kruga Aug 15 '18 at 15:22
  • \$\begingroup\$ @Kruga Oh! Well spotted, thanks. Editing my answer now. \$\endgroup\$ – Máté Safranka Aug 15 '18 at 15:24
5
\$\begingroup\$

Don't do anything in JavaScript that can be done with just CSS! The :hover pseudo-class is perfect for tooltips. Use ::after to append a pseudo-element with the tooltip content, where the content is specified by the content property.

div[tooltip]:hover::after {
    content: attr(tooltip);
    display: block;
    position: absolute;
    transform: translateX(-50%);
}

/* Don't review this CSS; it was added just for the demo. */
div[tooltip] { width: max-content; margin: auto; }
<div class="example" tooltip="Hello World">Hover me.</div>

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.