/ shared / components / src / actions / intersection-observer.ts
intersection-observer.ts
  1  import { getRafQueue } from '@amp/web-app-components/src/utils/rafQueue';
  2  // TODO: rdar://91082022 (JMOTW: Performance - Refactor IntersectionObserver Admin Locally)
  3  import IntersectionObserverAdmin from 'intersection-observer-admin';
  4  
  5  // Threshold is how much of the target element is currently visible within the
  6  // root's intersection ratio, as a value between 0.0 and 1.0.
  7  // https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry/intersectionRatio
  8  //
  9  // Examples:
 10  // 0 = a single visible pixel counts as the target being "visible"
 11  // 1 = a single non-visible pixel counts as the target being "not visible""
 12  const DEFAULT_VIEWPORT_THRESHOLD = 0.6;
 13  
 14  // https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver#properties
 15  // Adding `callback` to the type since you can only pass an array or object into actions
 16  type configObject = {
 17      root?: Element | null;
 18      rootMargin?: string;
 19      threshold?: number;
 20      callback?: Function;
 21  };
 22  
 23  let intersectionObserverAdmin;
 24  
 25  /**
 26   * IntersectionObserver action to track when an element comes in to/goes out of the visible viewport.
 27   * Useful for stopping animations of elements no longer visible, starting animations when
 28   * they appear/reappear, applying/removing styles, etc.
 29   *
 30   * Callbacks will be called with a boolean depending on if the item is intersecting (true) or not (false).
 31   *
 32   * Utilizes Intersection Observer Admin (https://github.com/snewcomer/intersection-observer-admin) to allow
 33   * the setup of a single Intersection Observer queue that handles observations in a way that allows each
 34   * element to have it's own callback and IntersectionObserver configuration.
 35   *
 36   * @function intersectionObserver
 37   * @param {Element} target Element to track (DOM element, Document, or null for top-level document viewport)
 38   * @param {configObject} options callback function for handling viewport visiblity changes
 39   *
 40   * @example `<div use:intersectionObserver={{ callback: handleIntersectionUpdate }}></div>`
 41   * @example `<div use:intersectionObserver={{
 42   *              callback: handleIntersectionUpdate,
 43   *              root: document.querySelector('some-element')
 44   *          }}></div>`
 45   * @example `<div use:intersectionObserver={{
 46   *              callback: handleIntersectionUpdate,
 47   *              root: document.querySelector('some-element'),
 48   *              threshold: 1
 49   *          }}></div>`
 50   * @example `<div use:intersectionObserver={{
 51   *              callback: handleIntersectionUpdate,
 52   *              root: document.querySelector('some-element'),
 53   *              rootMargin: '0px 0px 0px 0px',
 54   *              threshold: 1
 55   *          }}></div>`
 56   */
 57  export function intersectionObserver(
 58      target: Element,
 59      options: configObject = {},
 60  ): { destroy: () => void } {
 61      if (!('IntersectionObserver' in window)) return;
 62  
 63      if (!options.callback) {
 64          console.warn(
 65              'Use of intersectionObserver action requires passing in a callback function',
 66          );
 67          return;
 68      }
 69  
 70      const rafQueue = getRafQueue();
 71      const customCallback = options.callback;
 72  
 73      // Clone options to manipulate object without side effects
 74      // Assign initial default threshold, overridden by any settings in `options`
 75      const optionsObj = Object.assign(
 76          { threshold: DEFAULT_VIEWPORT_THRESHOLD },
 77          options,
 78      );
 79      delete optionsObj.callback;
 80  
 81      const callback = (ioEntry) => {
 82          rafQueue.add(() => customCallback(ioEntry.isIntersecting));
 83      };
 84  
 85      if (!intersectionObserverAdmin) {
 86          intersectionObserverAdmin = new IntersectionObserverAdmin();
 87      }
 88  
 89      // Add callbacks that will be called when observer detects entering and leaving viewport
 90      intersectionObserverAdmin.addEnterCallback(target, callback);
 91      intersectionObserverAdmin.addExitCallback(target, callback);
 92  
 93      intersectionObserverAdmin.observe(target, optionsObj);
 94  
 95      return {
 96          destroy() {
 97              intersectionObserverAdmin.unobserve(target, optionsObj);
 98          },
 99      };
100  }