Preventing flicker when dynamically repositioning a popover in react

1k views Asked by At

I'm trying to dynamically reposition a popover in react like the example shown in react-laag docs.

Here's what happens in that example: When scrolling, if the popover is about to go out of view, it moves so that it stays inside the viewport as long as the button that toggles the popover is visible.

I want to accomplish the same using the IntersectionObserverAPI and so far this is what I have done,

import React, { useState, useEffect, useRef, useLayoutEffect } from "react";

function CustomPopOver(props) {
  const menu = useRef(null);
  const scrollRef = useRef(null);
  const btn = useRef(null);
  const [show, setShow] = useState(false);

  useEffect(() => {
    scrollRef.current.scrollLeft = 300;
    scrollRef.current.scrollTop = 300;
  }, []);

  useLayoutEffect(() => {
    if (menu.current) {
      createObserver();
    }
  });

  function createObserver() {
    let options = {
      root: null,
      rootMargin: "0px",
      threshold: [0.99,0]
    };
    let observer = new IntersectionObserver(handleIntersect, options);
    observer.observe(menu.current);
  }

  function handleIntersect(entries, observer) {
    entries.forEach((entry) => {
      if (entry.intersectionRatio < 1) {
        if (menu.current) {
          const { height: iht, y: iy } = entry.intersectionRect;
          const { height: bht, y: by } = entry.boundingClientRect;
          if (iht < bht) {
            menu.current.style.top =
              640 -
              (bht + btn.current.getBoundingClientRect().height) -
              20 +
              "px";
          }
          if (iy > by) {
            menu.current.style.top = 640 + "px";
          }
          // console.log(entry.intersectionRect, entry.boundingClientRect);
        }
      }
    });
  }

  return (
    <div
      ref={scrollRef}
      style={{
        position: "relative",
        height: "600px",
        width: "600px",
        overflow: "scroll",
        backgroundColor: "#f9fafb"
      }}
    >
      <button
        ref={btn}
        className="btn btn-primary"
        style={{
          position: "relative",
          width: "120px",
          top: "600px",
          left: "650px"
        }}
        onClick={() => {
          setShow(!show);
        }}
      >
        {!show ? "Show" : "Hide"}
      </button>
      {show ? (
        <ul
          ref={menu}
          className="menu"
          style={{
            position: "absolute",
            left: "670px",
            top: "650px"
          }}
        >
          <li className="popup-list">item 1</li>
          <li className="popup-list">item 2</li>
          <li className="popup-list">item 3</li>
          <li className="popup-list">item 4</li>
        </ul>
      ) : null}
      <div style={{ width: "1500px", height: "1500px" }}></div>
    </div>
  );
}

export default CustomPopOver;

I have only covered the cases for top and bottom boundaries. It works as expected when I scroll up or down after the popover is toggled to be in view. The popover moves up or down when it hits the top or bottom boundaries. Here's the issue for me, when the button is scrolled down before the popover is toggled so that the popover can't be fully in view, the popover does move up when it is toggled from that position but that creates an unpleasant flickering effect. It is probably because the intersectionObserver observes it only after the popover is in the DOM and hence the reason for that flicker when the popover is positioned correctly.

I thought it might be because I was using the observer from useEffect() and so I tried the same with useLayoutEffect() instead but there were no changes in the result. I have created a CodeSandBox for the above code. I'd really appreciate any inputs on how to move from here. Thanks in advance!

0

There are 0 answers