Search code examples
javascripttooltipbootstrap-5

Bootstrap 5.2, prevent closing tooltip if cursor is back on triggering element


TLDR: moving the cursor from tooltip back to triggering element closes, shows and closes the tooltip (flickers).

I need to make the tooltips open on hover and make their content clickable. I have found a working example here on SO.

As you hover over the element it shows you a tooltip which can be interacted with, once you move the cursor away from the tooltip it closes.

There is a problem though.

If you leave the tooltip and move the cursor back on the element which triggered the tooltip, the tooltip pops back up, but dissapears after a moment ("flickering"). You need to move the cursor away from the element and back on the element for the tooltip to show again.

What I am trying to do, is check if the cursor is back on the triggering element and if that is the case not run the closing function (tooltip.hide()).

I have tried to do this by imitating the existing process from the example found on SO. That is, check if the tooltip has lost :hover, setTimout (300ms) and check if cursor is now positioned on the triggering element or back on the tooltip.

Here is a jsFiddle example.

This is the code. The problematic code is between the two looong comment lines.

Note: Moving the cursor away from the triggering element and back on the triggering element also triggers the flickering.


//https://stackoverflow.com/questions/67993080/bootstrap-5-make-tooltip-hoverable-and-link-clickable
var tooltipTriggerList = [].slice.call(document.querySelectorAll('button'))
for (let tt of tooltipTriggerList){
    tt.setAttribute("data-bs-placement","top")
}

var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
    const tooltip =  new bootstrap.Tooltip(tooltipTriggerEl, {
        trigger: "manual",
        'customClass': 'custom-tooltip'
    })

    let tooltipElTimeout;
    let currentToolTip;
    
    let currentTooltipTimeout;

    tooltipTriggerEl.addEventListener("mouseenter", function () {
        let toolTipID;
        
        // Clear Set Timeout
        clearTimeout(currentTooltipTimeout);
        
        // Show Tooltip
        tooltip.show();

        
        // Assign current tooltip ID to toolTipID variable
        toolTipID = tooltipTriggerEl.getAttribute("aria-describedby");

        
        // Assign current tooltip to currentToolTip variable
        currentToolTip = document.querySelector(`#${toolTipID}`);

        /*******************************************************************/
        // Hide tooltip on tooltip mouse leave
        currentToolTip.addEventListener("mouseleave", function () {
            currentTooltipTimeout = setTimeout(()=>{
                    console.log("!currentToolTip.matches(':hover')");
                    console.log(!currentToolTip.matches(":hover"));
                    if(!tooltipTriggerEl.matches(":hover")){
                        console.log("!tooltipTriggerEl.matches(':hover')");
                        console.log(!tooltipTriggerEl.matches(":hover"));
                        if (!currentToolTip.matches(":hover")) {
                            tooltip.hide();
                        }
                    }
            }, 300)
        });
  
    /***********************************************************************/

    });

    tooltipTriggerEl.addEventListener("mouseleave", function () {
      // SetTimeout before tooltip disappears
      tooltipTimeout = setTimeout(function () {
        // Hide tooltip if not hovered.
        if (!currentToolTip.matches(":hover")) {
          tooltip.hide();
        }
      }, 100);
    });

    return tooltip;

})

Thank you

Edit: Amine Ramouls answer is correct. isHidden also needs to bet set to false on the 2cnd eventListener, otherwise the tooltips no longer work (problem with aria-describedby).


Solution

  • In your code, you have an event listener that adds another event listener, which is a big mistake because it adds an infinite number of event listeners to your element.

    To fix this, organize your code like this:

    //https://stackoverflow.com/questions/67993080/bootstrap-5-make-tooltip-hoverable-and-link-clickable
    var tooltipTriggerList = [].slice.call(document.querySelectorAll('button'))
    for (let tt of tooltipTriggerList){
        tt.setAttribute("data-bs-placement","top")
    }
    
    var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
        const tooltip =  new bootstrap.Tooltip(tooltipTriggerEl, {
            trigger: "manual",
            'customClass': 'custom-tooltip'
        })
        let isHidden = true;        
        let currentTooltipTimeout;
        tooltipTriggerEl.addEventListener("mouseenter", function () {
    
            let toolTipID;
            // Clear Set Timeout
            clearTimeout(tooltipElTimeout);
            clearTimeout(currentTooltipTimeout);
            
            if (isHidden)
            {
                    tooltip.show();
                isHidden=false;
            }
                
            
        });
            // Hide tooltip on tooltip mouse leave
        tooltipTriggerEl.addEventListener("mouseleave", function () {
                                               
                        console.log("!currentToolTip.matches(':hover')");
                        if(!tooltipTriggerEl.matches(":hover")){
                                currentTooltipTimeout=setTimeout(()=>{
                                if (!isHidden && !tooltipTriggerEl.matches(":hover")){
                                    tooltip.hide();
                                  isHidden=true;
                              }
                                
                                console.log("!tooltipTriggerEl.matches(':hover')");
                                console.log(!tooltipTriggerEl.matches(":hover"));
                            }, 3000)
                        }                
            });
      
        return tooltip;
    
    })
    

    I've just added the isHidden variable to check if the popup info is hidden or not. You can do this with the element if you can get it by a query selector request. That's it! Enjoy your life.

    Edit: I forgot to mention that I've added a 3-second delay before checking if the popup is hidden or not.