@supunlakmal/hooks
Version:
A collection of reusable React hooks
85 lines • 3.93 kB
JavaScript
import { useState, useEffect, useRef, useCallback } from 'react';
/**
* Monitors scroll position to determine which element/section is currently active in the viewport.
*
* @param sectionRefs An array of React refs attached to the section elements to be monitored.
* @param options Configuration options for offset and container.
* @returns The id of the currently active section element, or null if none are active.
*/
export const useScrollSpy = (sectionRefs, options = {}) => {
const { containerRef, offset = 0, throttleMs = 100 } = options;
const [activeSectionId, setActiveSectionId] = useState(null);
const throttleTimeoutRef = useRef(null);
const getScrollContainer = useCallback(() => {
return (containerRef === null || containerRef === void 0 ? void 0 : containerRef.current) || window;
}, [containerRef]);
const getContainerScrollTop = useCallback(() => {
const container = getScrollContainer();
if (container instanceof Window) {
return container.scrollY;
}
else {
return container.scrollTop;
}
}, [getScrollContainer]);
const handleScroll = useCallback(() => {
var _a;
const scrollTop = getContainerScrollTop() + offset;
let currentActiveId = null;
for (let i = sectionRefs.length - 1; i >= 0; i--) {
const section = (_a = sectionRefs[i]) === null || _a === void 0 ? void 0 : _a.current;
if (section) {
// Calculate section top relative to the scroll container
let sectionTop = section.offsetTop;
if (!(getScrollContainer() instanceof Window) &&
(containerRef === null || containerRef === void 0 ? void 0 : containerRef.current)) {
// Adjust if container is not window, offsetTop is relative to offsetParent
// This simple calculation might need refinement depending on complex layouts
sectionTop =
section.getBoundingClientRect().top -
containerRef.current.getBoundingClientRect().top +
containerRef.current.scrollTop;
}
if (scrollTop >= sectionTop) {
currentActiveId = section.id;
break; // Found the topmost active section
}
}
}
setActiveSectionId(currentActiveId);
}, [
sectionRefs,
offset,
getContainerScrollTop,
containerRef,
getScrollContainer,
]);
// Throttled scroll handler
const throttledHandleScroll = useCallback(() => {
if (!throttleTimeoutRef.current) {
handleScroll(); // Run immediately the first time
throttleTimeoutRef.current = setTimeout(() => {
throttleTimeoutRef.current = null;
// Optional: Could re-run handleScroll here if needed after timeout,
// but usually running on leading edge is sufficient.
// handleScroll();
}, throttleMs);
}
}, [handleScroll, throttleMs]);
useEffect(() => {
const container = getScrollContainer();
// Initial check
handleScroll();
container.addEventListener('scroll', throttledHandleScroll);
window.addEventListener('resize', throttledHandleScroll); // Also check on resize
return () => {
container.removeEventListener('scroll', throttledHandleScroll);
window.removeEventListener('resize', throttledHandleScroll);
if (throttleTimeoutRef.current) {
clearTimeout(throttleTimeoutRef.current);
}
};
}, [getScrollContainer, throttledHandleScroll, handleScroll]); // Rerun if container or handler changes
return activeSectionId;
};
//# sourceMappingURL=useScrollSpy.js.map