Jezza
Jezza

Reputation: 21

Using setTimeout on hover interactions with a tooltip

I've got an SVG map made up of hexagons which are divided up into groups. When the user hovers over a group, I'd like to have a tooltip appear. Thing is, I want there to be like a 3 second delay to this tooltip. So if the user decides to hover off during that delay, I'd like to clear that delay to stop the tooltip from appearing as they are no longer hovering over it. There is also a delay for when the tooltip disappears as well just in case you hover over another element after quickly moving off the previous one.

I'm using setTimeout for this. What I've done here works 50% of the time but I'm finding that if I hover over and let the tooltip display, and then quickly hover over a bunch of other different elements before hovering off, the tooltip will disappear but then quickly reappear again.

Here's my code, am happy to explain anything further if needed. Cheers!

// Setting the tooltip to appear below the tooltip wrapper
gsap.set(tip, {
    yPercent: 100
});

// Tooltip Hovering Functionality with Timeouts
let hoverOutTimeout; // Timeout for hovering out

// GO through grouped elements with an event listener on mouse move, and use timeouts to delay hover
for (i = 0; i < lgas.length; i++) {
    lgas[i].onmouseover = function () {
        if (hoverOutTimeout) { // Check to see if the delay for the mouseleave function is running
            clearTimeout(hoverOutTimeout);
            //console.log("Hover back in");
        } else {
            //console.log("Hovering in");
        }

        // Set 3s delay to display tooltip
        hoverTimeout = setTimeout(function () {
            //console.log("Hovered in");

            gsap.to(tip, {
                yPercent: 0,
                ease: 'bounce.out'
            });
        }, 3000);
    }

    lgas[i].onmouseleave = function () {
        if (hoverTimeout) { // If delay to show tooltip is running, clear it
            clearTimeout(hoverTimeout);
            //console.log("Hovering back out")

            hoverOutTimeout = setTimeout(() => { // Start new delay to hide tooltip
                //console.log("Hovered out");

                gsap.to(tip, {
                    yPercent: 100,
                    ease: 'back.in'
                });
            }, 2000);

        }
        clearTimeout(hoverTimeout);
    }
}

Upvotes: 1

Views: 1245

Answers (2)

jsejcksn
jsejcksn

Reputation: 33816

Some of your code is missing your post, so I took the liberty of supplying example DOM structure and generic functions for showing/hiding the tooltips. In the code below, the background color of each element will change based on whether or not the tooltip is visible. The key concept is that you need to store some state for each element (its timerId).

const SHOW_DELAY = 1500;
const HIDE_DELAY = 1200;

function showTooltip(element) {
  element.classList.add('hovered');
}

function hideTooltip(element) {
  element.classList.remove('hovered');
}

function setTooltips(elements) {
  function getMouseEventHandler(elementHandler, delay) {
    return ({target: element}) => {
      let timerId = timerIdMap.get(element) ?? 0;
      clearTimeout(timerId);
      timerId = setTimeout(() => elementHandler(element), delay);
      timerIdMap.set(element, timerId);
    };
  }

  const handleMouseEnter = getMouseEventHandler(showTooltip, SHOW_DELAY);
  const handleMouseLeave = getMouseEventHandler(hideTooltip, HIDE_DELAY);

  const timerIdMap = new WeakMap();

  for (const element of elements) {
    timerIdMap.set(element, 0);
    element.addEventListener('mouseenter', handleMouseEnter);
    element.addEventListener('mouseleave', handleMouseLeave);
  }
}

const elements = [...document.querySelectorAll('div.item')];
setTooltips(elements);
.container {
  display: flex;
  flex-wrap: wrap;
  width: 14rem;
}

.item {
  border: 1px solid;
  height: 3rem;
  width: 3rem;
}

.hovered {
  background-color: orangered;
}
<div class="container">
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
</div>

Upvotes: 1

jet_24
jet_24

Reputation: 656

I was able to accomplish this by binding events to an associated ID. The idea is the setTimeout will check to see if it's ID is the current generated ID. The ID is generated at the time of the event binding.

On mouseenter, the event associates to the current generated ID. On mouseleave, the events are un-binded and then re-binded with a new generated ID. This approach is what prevents hovers from stacking.

I hope it sort of makes sense. I developed a snippet to experiment with.

let bindCurID;

let hoverBinder = function(){
  bindCurID = new Date() * Math.random();
  
  $('div').off('mouseenter').on('mouseenter', function(){
    let sMsg = $(this).find('h2').text();
    let bindID = bindCurID;
    
    setTimeout(function(){
      if(bindID != bindCurID)
        return
      $('body').append('<span class="hov">' + sMsg + '</span>');
    }, 2000)
    
  });
  
  $('div').off('mouseleave').on('mouseleave', function(){
    $('.hov').remove();
    hoverBinder()
  });
}

hoverBinder()
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="div1" style='background-color:lightblue'>
<h2>Hello</h2>
</div><br /><br />
<div id="div2" style='background-color:lightblue'>
<h2>World</h2>
</div>

I'm sure there are others out there who can do all of this in one line with some sort of wizardly tricks, but this will get the job done.

Upvotes: 0

Related Questions