Files
web/apps/scandic-web/hooks/useScrollSpy.ts
Erik Tiekstra d73f8d844e feat(SW-1960): Improved scrolling and tabnavigation on hotel pages
Approved-by: Matilda Landström
2025-05-26 12:32:18 +00:00

93 lines
2.7 KiB
TypeScript

import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { debounce } from "../utils/debounce"
export default function useScrollSpy(
sectionIds: string[],
options: IntersectionObserverInit = {}
): {
activeSectionId: string
pauseScrollSpy: () => void
} {
const [activeSectionId, setActiveSectionId] = useState("")
const observerIsInactive = useRef(false)
const safetyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
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)
}
},
[]
)
// Scroll event listener to reset the observerIsInactive state
// This is to support the pauseScrollSpy function to prevent the observer from firing when scrolling.
useEffect(() => {
const handleScroll = debounce(() => {
if (observerIsInactive.current) {
observerIsInactive.current = false
}
}, 200)
window.addEventListener("scroll", handleScroll, { passive: true })
return () => {
window.removeEventListener("scroll", handleScroll)
}
}, [])
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])
function pauseScrollSpy() {
observerIsInactive.current = true
if (safetyTimeoutRef.current) {
clearTimeout(safetyTimeoutRef.current)
}
// Safety timeout - maximum time to wait before re-enabling
safetyTimeoutRef.current = setTimeout(() => {
observerIsInactive.current = false
safetyTimeoutRef.current = null
}, 1500)
}
return { activeSectionId, pauseScrollSpy }
}