SVE
SVE

Reputation: 1655

react horizontal scroll to section with mouse wheel

I have a simple project by react:

Edit React Scroll Nav To

There is a horizontal menu and content.
By clicking on a menu item - scroll to the current section.
When scrolling content, the active menu item is switched.

Navigation:

 <ul
    className="nav list-unstyled d-flex flex-nowrap fixed-top"
    ref={scrollNavRefs}
    onScroll={handleScroll}
  >
    {list.map((item, i) => (
      <li className="nav-item" key={i}>
        <a
          href={`#s-${i}`}
          className={`nav-link text-nowrap ${
            active === i ? "text-danger" : ""
          }`}
          onClick={scrollTo(i)}
        >
          {item}
        </a>
      </li>
    ))}
  </ul>

Sections:

  <ul className="mb-100 list-unstyled">
    {list.map((item, i) => (
      <li id={`s-${i}`} ref={scrollRefs.current[i]} className="py-100 px-3">
        <h3>{item}</h3>
        <p>
          Lorem ipsum dolor sit amet consectetur adipisicing elit. Nisi,
          dicta.
        </p>
      </li>
    ))}
  </ul>

>>>:

  const scrollRefs = useRef([]);
  const scrollNavRefs = useRef();

  const [active, setActive] = useState(0);

  const list = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5"];

  scrollRefs.current = [...Array(list.length).keys()].map(
    (_, i) => scrollRefs.current[i] ?? createRef()
  );

  const scrollTo = (index) => () => {
    scrollRefs.current[index].current.scrollIntoView({ behavior: "smooth" });
    setActive(index);
  };


  const scrollHandler = () => {
    const scrollRefsElements = scrollRefs.current;

    scrollRefsElements.forEach((el, i) => {
      const rect = el.current.getBoundingClientRect();

      const elemTop = rect.top;
      const elemBottom = rect.bottom;

      const isVisible = elemTop >= 0 && elemBottom <= window.innerHeight;

      if (isVisible) {
        setActive(i);
      }
    });
  };

  const onWheel = (e) => {
    if (e.deltaY === 0) return;
    e.preventDefault();

    const scrollNavRefsElement = scrollNavRefs.current;
    const scrollNavRefsElementLeft = scrollNavRefs.current.scrollLeft;

    scrollNavRefsElement.scrollTo({
      left: scrollNavRefsElementLeft + e.deltaY,
      behavior: "smooth"
    });
  };

  useEffect(() => {
    window.addEventListener("scroll", scrollHandler, true);
    return () => {
      window.removeEventListener("scroll", scrollHandler, true);
    };
  }, []);

  useEffect(() => {
    scrollNavRefs.current.addEventListener("wheel", onWheel, true);
    return () => {
      scrollNavRefs.current.removeEventListener("wheel", onWheel, true);
    };
  }, []);

Question: How to link the menu and content so that when scrolling the consumed content, when the active class of menu items changes, the menu scrolls so that the active menu item is visible? By analogy with example.

P.S:

Upvotes: 2

Views: 3583

Answers (2)

the Hutt
the Hutt

Reputation: 18418

I've modified your sandbox Working Demo.

Did following changes:

  • Created navRefs for each menu item. And did the same thing with navRefs as you did with scrollRefs.
// called it when user scrolls vertically, or clicks on a menu item
navRefs.current[i].current.scrollIntoView({
          behavior: "smooth",
          block: "start",
          inline: "center"
        });
  • Changed logic to find currently focused content.
const isVisible =
        elemTop < window.innerHeight / 2 && elemBottom > window.innerHeight / 2;

In short if content is on viewport's horizontal center then select it. This avoids selecting multiple contents if many of them are in viewport at the same time.

  • Used a flag to avoid executing scrollHandler when user clicks on a menu item. Also avoided the horizontal scroll events on navbar.
  • In CSS hide horizontal scrollbar on navbar. And added red underline to selected menu item.

Code for ready reference:
import "./styles.css";

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

var scrolling = false;

export default function App() {
  const scrollRefs = useRef([]);
  const navRefs = useRef([]);

  const [active, setActive] = useState(0);

  const list = [
    "Item 1",
    "Item 2",
    "Item 3",
    "Item 4",
    "Item 5",
    "Item 6",
    "Item 7",
    "Item 8",
    "Item 9",
    "Item 10"
  ];

  scrollRefs.current = [...Array(list.length).keys()].map(
    (_, i) => scrollRefs.current[i] ?? createRef()
  );

  navRefs.current = [...Array(list.length).keys()].map(
    (_, i) => navRefs.current[i] ?? createRef()
  );

  const scrollTo = (index) => {
    console.log("setting scrolling" + scrolling);
    scrolling = true;
    scrollRefs.current[index].current.scrollIntoView({ behavior: "smooth" });
    setActive(index);
    setTimeout(() => {
      scrolling = false;
      navRefs.current[index].current.scrollIntoView({
        behavior: "smooth",
        block: "start",
        inline: "center"
      });
    }, 1000);
  };

  const scrollHandler = (e) => {
    // handle scroll only on body
    // we don't want to handle horizontal scroll on nav bar
    if (e.target !== document) return;

    if (scrolling === true) return;

    const scrollRefsElements = scrollRefs.current;

    scrollRefsElements.forEach((el, i) => {
      const rect = el.current.getBoundingClientRect();
      const elemTop = rect.top;
      const elemBottom = rect.bottom;
      const isVisible =
        elemTop < window.innerHeight / 2 && elemBottom > window.innerHeight / 2;

      if (isVisible) {
        navRefs.current[i].current.scrollIntoView({
          behavior: "smooth",
          block: "start",
          inline: "center"
        });
        setActive(i);
      }
    });
  };

  useEffect(() => {
    window.addEventListener("scroll", scrollHandler, true);
    return () => {
      window.removeEventListener("scroll", scrollHandler, true);
    };
  }, []);

  return (
    <div className="container">
      <ul className="myMenu nav list-unstyled d-flex flex-nowrap fixed-top">
        {list.map((item, i) => (
          <li className="nav-item " key={i} ref={navRefs.current[i]}>
            <a
              href={`#s-${i}`}
              className={`nav-link text-nowrap ${
                active === i ? "text-danger" : ""
              }`}
              onClick={(e) => {
                scrollTo(i);
              }}
            >
              {item}
            </a>
          </li>
        ))}
      </ul>

      <ul className="mb-100 list-unstyled">
        {list.map((item, i) => (
          <li id={`s-${i}`} ref={scrollRefs.current[i]} className="py-100 px-3">
            <h3>{item}</h3>
            <p>
              Lorem ipsum dolor sit amet consectetur adipisicing elit. Nisi,
              dicta.
            </p>
          </li>
        ))}
      </ul>
    </div>
  );
}

Upvotes: 2

Anh Le
Anh Le

Reputation: 290

Yep. You can use IntersectionObserver. like: https://codesandbox.io/s/react-scroll-nav-to-forked-ohci4c

Upvotes: 2

Related Questions