Files
web/hooks/useScrollSpy.ts
2024-10-10 12:08:18 +02:00

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: "-8% 0% -90% 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 }
}