用鼠标滚轮对水平滚动做出反应

react horizontal scroll to section with mouse wheel

我有一个简单的反应项目:

有水平菜单和内容。
通过单击菜单项 - 滚动到当前部分。
滚动内容时,切换活动菜单项。

导航:

 <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>

栏目:

  <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);
    };
  }, []);

问题:如何link菜单和内容,以便在滚动消费内容时,当活动的class菜单项发生变化时,菜单滚动以便活动菜单项可见?类推example.

P.S:

是的。您可以使用 IntersectionObserver。像: https://codesandbox.io/s/react-scroll-nav-to-forked-ohci4c

我已经修改了你的沙箱 Working Demo

进行了以下更改:

  • 为每个菜单项创建 navRefs。并且对 navRefs 做了与 scrollRefs.
  • 相同的事情
// called it when user scrolls vertically, or clicks on a menu item
navRefs.current[i].current.scrollIntoView({
          behavior: "smooth",
          block: "start",
          inline: "center"
        });
  • 更改逻辑以查找当前聚焦的内容。
const isVisible =
        elemTop < window.innerHeight / 2 && elemBottom > window.innerHeight / 2;

简而言之,如果内容位于视口的水平中心,则 select 它。如果多个内容同时出现在视口中,这可以避免 select 编辑多个内容。

  • 使用标志来避免在用户单击菜单项时执行 scrollHandler。还避免了导航栏上的水平滚动事件。
  • 在 CSS 中隐藏导航栏上的水平滚动条。并在 selected 菜单项中添加了红色下划线。

可供参考的代码:
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>
  );
}