Files
web/apps/scandic-web/hooks/useScrollSpy.ts
Anton Gunnarsson 1bd8fe6821 Merged in feat/sw-2879-booking-widget-to-booking-flow-package (pull request #2532)
feat(SW-2879): Move BookingWidget to booking-flow package

* Fix lockfile

* Fix styling

* a tiny little booking widget test

* Tiny fixes

* Merge branch 'master' into feat/sw-2879-booking-widget-to-booking-flow-package

* Remove unused scripts

* lint:fix

* Merge branch 'master' into feat/sw-2879-booking-widget-to-booking-flow-package

* Tiny lint fixes

* update test

* Update Input in booking-flow

* Clean up comments etc

* Merge branch 'master' into feat/sw-2879-booking-widget-to-booking-flow-package

* Setup tracking context for booking-flow

* Add missing use client

* Fix temp tracking function

* Pass booking to booking-widget

* Remove comment

* Add use client to booking widget tracking provider

* Add use client to tracking functions

* Merge branch 'master' into feat/sw-2879-booking-widget-to-booking-flow-package

* Move debug page

* Merge branch 'master' into feat/sw-2879-booking-widget-to-booking-flow-package

* Merge branch 'master' into feat/sw-2879-booking-widget-to-booking-flow-package

* Merge branch 'master' into feat/sw-2879-booking-widget-to-booking-flow-package


Approved-by: Bianca Widstam
2025-08-05 09:20:20 +00:00

93 lines
2.7 KiB
TypeScript

import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { debounce } from "@scandic-hotels/common/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 }
}