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
93 lines
2.7 KiB
TypeScript
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 }
|
|
}
|