68 lines
1.8 KiB
TypeScript
68 lines
1.8 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
|
|
export default function useScrollSpy(
|
|
sectionIds: string[],
|
|
options: IntersectionObserverInit = {}
|
|
): {
|
|
activeSectionId: string
|
|
pauseScrollSpy: () => void
|
|
} {
|
|
const [activeSectionId, setActiveSectionId] = useState("")
|
|
const observerIsInactive = useRef(false)
|
|
|
|
const mergedOptions = useMemo(
|
|
() => ({
|
|
root: null,
|
|
// Make sure only to activate the section when it reaches the top of the viewport.
|
|
// A negative value for rootMargin shrinks the root bounding box inward,
|
|
// meaning elements will only be considered intersecting when they are further inside the viewport.
|
|
rootMargin: "-15% 0% -85% 0%",
|
|
threshold: 0,
|
|
...options,
|
|
}),
|
|
[options]
|
|
)
|
|
|
|
const handleIntersection = useCallback(
|
|
(entries: IntersectionObserverEntry[]) => {
|
|
if (observerIsInactive.current) {
|
|
return
|
|
}
|
|
|
|
const intersectingEntries: IntersectionObserverEntry[] = []
|
|
entries.forEach((e) => {
|
|
if (e.isIntersecting) {
|
|
intersectingEntries.push(e)
|
|
}
|
|
})
|
|
if (intersectingEntries.length) {
|
|
setActiveSectionId(intersectingEntries[0].target.id)
|
|
}
|
|
},
|
|
[]
|
|
)
|
|
|
|
useEffect(() => {
|
|
const observer = new IntersectionObserver(handleIntersection, mergedOptions)
|
|
const elements = sectionIds
|
|
.map((id) => document.getElementById(id))
|
|
.filter((el): el is HTMLElement => !!el)
|
|
|
|
elements.forEach((element) => {
|
|
observer.observe(element)
|
|
})
|
|
|
|
return () => elements.forEach((el) => el && observer.unobserve(el))
|
|
}, [sectionIds, mergedOptions, handleIntersection])
|
|
|
|
const pauseScrollSpy = () => {
|
|
observerIsInactive.current = true
|
|
|
|
setTimeout(() => {
|
|
observerIsInactive.current = false
|
|
}, 500)
|
|
}
|
|
|
|
return { activeSectionId, pauseScrollSpy }
|
|
}
|