Reputation: 1655
I have a simple project by react:
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
Reputation: 18418
I've modified your sandbox Working Demo.
Did following changes:
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"
});
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.
scrollHandler
when user clicks on a menu item. Also avoided the horizontal scroll events on navbar.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
Reputation: 290
Yep. You can use IntersectionObserver. like: https://codesandbox.io/s/react-scroll-nav-to-forked-ohci4c
Upvotes: 2