Luke
Luke

Reputation: 343

How to delay intersection observer API

The Situation

I have a fixed nav bar at the top of the page. As you scroll down through different sections of the page the nav bar dynamically updates (underlines and highlights). You can also click a section on the nav bar and it will scroll down to that section.

This is done using the intersection observer API to detect which section it's on and scrollIntoView to scroll to each section.

The Problem

Lets say you are on section 1 and you click the last section, 5, and it scrolls the page down past all the other sections in-between. The scroll is fast and as it scrolls all the sections are detected by the intersection observer and therefore the nav is updated. You end up getting an effect of the nav quickly changing for each nav item as it goes past each corresponding section.

The Goal

How do you delay the intersection observer from triggering the menu change if the section is only in frame for a millisecond? When quickly scrolling the nav bar should only update once the scrolling has stopped on a section.

Code Setup

const sectionItemOptions = {
  threshold: 0.7,
};

const sectionItemObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach((entry) => {

    if (entry.isIntersecting) {
      // select navigation link corresponding to section

    } else {
      // deselect navigation link corresponding to section

    }
  });
}, sectionItemOptions);

// start observing all sections on page
sections.forEach((section) => {
  sectionItemObserver.observe(section);
});

Ideas

My first thought was to put a setTimeout so that the nav wouldn't change until the Timeout was finished, then cancel the Timeout if the section left the screen before the timeout finished. But as the timeout is in a forEach loop this didn't work.

const sectionItemObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach((entry) => {

    let selectNavTimeout

    if (entry.isIntersecting) {

      // Set timeout when section is scrolled past
      selectNavTimeout = setTimeout(() => {
        // select navigation link corresponding to section
      }, 1000)

    } else {
      // deselect navigation link corresponding to section
      // cancel timeout when section has left screen
      clearTimeout(selectNavTimeout)
    }
  });
}, sectionItemOptions);

Any other ideas would be greatly appreciated! Thanks :)

Upvotes: 5

Views: 10120

Answers (3)

Adam Chapman
Adam Chapman

Reputation: 93

Ran into same issue. Per this article: https://web.dev/intersectionobserver-v2/, observer v2 allows you to set a delay in the observer options. In my nav menu situation the delay works like a charm:

const observer = new IntersectionObserver((changes) => {
  for (const change of changes) {
    // ⚠️ Feature detection
    if (typeof change.isVisible === 'undefined') {
      // The browser doesn't support Intersection Observer v2, falling back to v1 behavior.
      change.isVisible = true;
    }
    if (change.isIntersecting && change.isVisible) {
      visibleSince = change.time;
    } else {
      visibleSince = 0;
    }
  }
}, {
  threshold: [1.0],
  // 🆕 Track the actual visibility of the element
  trackVisibility: true,
  // 🆕 =====ANSWER=====: Set a minimum delay between notifications
  delay: 100
}));

Upvotes: 5

Reci
Reci

Reputation: 4274

I had the same problem. I end up use the setTimeout approach. You need to associate the timeouts with the entry target, provided each entry target has some unique ID. For example, suppose we are intersecting nodes with id property:

    let timeouts = {};
    const observer = new IntersectionObserver((entries, ob) => {
        for (const e of entries) {
            if (e.isIntersecting) {
                timeouts[e.target.id] = setTimeout(() => {
                    ob.unobserve(e.target)

                    // handling

                }, 1000)  // delay for 1 second
            } else {
                clearTimeout(timeouts[e.target.id])
            }
        }
    }, options)

Upvotes: 6

Luke
Luke

Reputation: 343

After lots of brainstorming I came up with an idea that didn't exactly answer the question of delaying the Intersection Observer API but it did solve the problem of the nav bar flickering.

The highlighting of the nav item is done through adding an "is-active" class onto it and then applying CSS to it. Because the "is-active" class is only on the nav item for a split second you can use CSS keyframes to delay the application of CSS styles. By the time the delay has finished the "is-active" class isn't present on the nav item and no styles are changed.

Keeping the original JS the same this is the CSS used

.is-active {
  animation: navItemSelected;
  animation-duration: 0.3s;
  // delay longer than the time nav item is in frame
  animation-delay: 0.1s;
  // fill mode to hold animation at the end
  animation-fill-mode: forwards;
}

@keyframes navItemSelected {
  // default non-selected style of nav item
  from {
    font-style: normal;
    opacity: 0.5;
  }
  // highlighted style of nav item
  to {
    font-style: italic;
    opacity: 1;
  }
}

Upvotes: 0

Related Questions