DEV Community

Miki Stanger
Miki Stanger

Posted on

Tooltips! A Nice Singleton-y Way To Only Show One At A Time

Tooltips are trickier than they look; For the optimal experience, you have to handle many intricacies and edge cases.

Positioning is complex, with its many edge cases (screen edges, scrolling). Hover behavior could be more intricate, so the tooltip remains open while you hover over it, and not just its target. Appearance and disappearance should both have delays and maybe animation. AND you don't want multiple tooltips appearing together and hiding each other.

Today I'll write about this last part. Let me know if you'd like me to elaborate on the other parts :)

===

Who am I?

I'm Miki Stanger, a front-end architect and advisor. I've been to the industry for 12 years. I currently accompany companies and help with architecture, decisions, mentoring and problem-solving.

I'm always looking for cool people and projects to help with! You can read more about me and find my up-to-date contact information here: https://about.me/mikistanger/

===

Tooltips (And When To Use Them)

Tooltips are a way to show additional information about something. They are usually triggered by hovering over an element, and should show an explanation about how to read or use a part of your system.

Because it is sensitive to mouse movement and positioned next to other elements, it is best used for short texts; Large bodies of text will require the user to not touch their mouse for a while (even though some people move the mouse, click or select texts while reading or while multitasking), and might create a huge box that overshadows or even hides the parts it explains.

If your explanation is more than a few sentences long, you could use a modal instead; A modal is more persistent and allows the use of photos and videos, and are thus a better medium for larger amounts of content.

The Problem

The premise of a tooltip is supposedly basic - Hover over something and some element should appear in the right position. However, if you have several available tooltips next to each others, this could happen:

Multiple tooltips appearing over each other

See how the tooltips hide each other? This kind of clutter not only looks messy, it could also steal the user's focus from the one tooltip they're looking for.

The Solution

When we show a tooltip, we could simply hide the other ones.
As with a lot of tooltip-related things, this is a bit more complex than it sounds, especially when working with React. There are two approaches here:

  1. Have a single tooltip component and control it through props. You could pass the method through a store or using context, so they're available globally. This is not a bad way, except that it might unnecessarily trigger a full re-render. We'll look at a different approach that doesn't require all of those additional mechanisms.
  2. Have multiple tooltip instances that live where they are used, and some way to turn the others off. This is also achievable with a store or a context, but there's a better way (imo) to do it - manage hiding tooltips with a singleton.

What Is A Singleton?

A singleton is a single instance of an object that's exposed to the whole system (as opposed to creating multiple objects or instances of a class).

A singleton is useful when one needs to guarantee the same options and methods will be available and persistent throughout the project. A common example for a singleton is a logging instance - a single instance of a class, with its configuration and options, which guarantee that all logging will be done with the same options and policies everywhere.

The PreventMultipleTooltipsManager Singleton

Our tooltip is a component that wraps another component and surrounds it with a tooltip-triggering component:

export const WithTooltip({ children }) => {
    const [isVisible, setIsVisible] = useState(false);

    const setIsVisibleToFalse = useCallback(() => {
        setIsVisible(false);
    }, [setIsVisible]);

    const setIsVisibleToTrue = useCallback(() => {
        setIsVisible(true);
    }, [setIsVisible]);

    // Rest of logic

    return <TooltipWithStylesAndStuff
        onMouseEnter={setIsVisibleToTrue} 
        onMouseLeave={setIsVisibleToFalse}
    >
        {children}
    </TooltipWithStylesAndStuff>
}
Enter fullscreen mode Exit fullscreen mode

OUTSIDE of the component, we'll create a class and initiate a single instance of it - a singleton:

class PreventMultipleTooltipsManager {}

const preventMultipleTooltipsManager = new PreventMultipleTooltipsManager();
Enter fullscreen mode Exit fullscreen mode

This singleton is going to hold a function that hides the currently visible tooltip (its setIsVisibleToFalse function). When a new tooltip is shown, the singleton will allow us to use the now-previous tooltip's function and replace it with the current one's. We can do both operations in a single method, as they're always going to be called together in the same order:

class PreventMultipleTooltipsManager {
    private currentHideFunc: (() => void) | null = null;

    public changeTooltip(hideFunc) {
        this.currentHideFunc?.();
        this.currentHideFunc = hideFunc;
    }
}

const preventMultipleTooltipsManager = new PreventMultipleTooltipsManager();
Enter fullscreen mode Exit fullscreen mode

We're also going to add an unmount method. This method will be used in a tooltip's teardown, in case it is removed from the DOM while visible. We'll later use it in a teardown function that's the return value of a useEffect:

class PreventMultipleTooltipsManager {
    private currentHideFunc: (() => void) | null = null;

    public changeTooltip(hideFunc) {
        this.currentHideFunc?.();
        this.currentHideFunc = hideFunc;
    }

    public unmount(hideFunc) {
        if (hideFunc === this.currentHideFunc) {
            this.currentHideFunc = null;
        }
    }
}

const preventMultipleTooltipsManager = new PreventMultipleTooltipsManager();
Enter fullscreen mode Exit fullscreen mode

The singleton is done. Now we can call it inside the component. We'll use its changeTooltip method in isSetVisibleToTrue, and its unmount method in a useEffect:

export const WithTooltip({ children }) => {
    const [isVisible, setIsVisible] = useState(false);

    const setIsVisibleToFalse = useCallback(() => {
        setIsVisible(false);
    }, [setIsVisible]);

    const setIsVisibleToTrue = useCallback(() => {
        preventMultipleTooltipsManager.changeTooltip(setIsVisibleToFalse);
        setIsVisible(true);
    }, [setIsVisible, setIsVisibleToFalse]);

    useEffect(() => (
        () => {
            preventMultipleTooltipsManager.unmount(setIsVisibleToFalse);
        }
    ), [setIsVisibleToFalse])

    // Rest of logic

    return <TooltipWithStylesAndStuff
        onMouseEnter={setIsVisibleToTrue} 
        onMouseLeave={setIsVisibleToFalse}
    >
        {children}
    </TooltipWithStylesAndStuff>
}
Enter fullscreen mode Exit fullscreen mode

And that's it! Look at it behaving nicely:

Now, only the required tooltip shows, while the others disappear

Conclusion

Tooltips are a wonderful case of how thinking about the small details makes a difference between buggy, unfriendly component and a delightful one. Today, we dug into one of several such details, learned what a singleton is, and use this knowledge to make our tooltips better.

What do you think? How would you implement a solution to this same problem?

Top comments (1)

Collapse
 
pjwalker profile image
PJ Walker

You could put this behaviour into a hook: