@faceless-ui/scroll-info
Version:
A React library for subscribing to scroll events
82 lines • 3.95 kB
JavaScript
import React, { useCallback, useEffect, useReducer, useRef, } from 'react';
import { ScrollInfoContext } from '../ScrollInfoProvider/context.js';
const reducer = (state, payload) => {
const { timestamp, animationRef, } = payload;
animationRef.current = null;
const { x: prevScrollX, y: prevScrollY, xDirection: prevXDirection, yDirection: prevYDirection, eventsFired: prevEventsFired, hasScrolled: prevHasScrolled, } = state;
// Set currentScroll to zero on mount for cross-browser compatibility --
// Some browsers populate the cached window.pageOffset at different points of the component lifecycle.
// Chrome mounts with cached window.pageOffset coordinates even before firing the respective cached scroll event.
// Neither Safari and FireFox populate these coordinates until this cached scroll (natively triggered by the browser).
// The presence of a timestamp indicates that the caller of this method was not componentDidMount,
// but rather a true scroll event via requestAnimationFrame. Keep at zero otherwise.
const hasScrolled = prevHasScrolled || Boolean(timestamp);
const currentScrollX = hasScrolled ? window.pageXOffset : 0;
const currentScrollY = hasScrolled ? window.pageYOffset : 0;
// Only increment the eventsFired state after the first true scroll event.
const eventsFired = hasScrolled ? prevEventsFired + 1 : prevEventsFired;
const xDifference = currentScrollX - prevScrollX;
const yDifference = currentScrollY - prevScrollY;
const xPercentage = (currentScrollX / (document.body.scrollWidth - window.innerWidth)) * 100;
const yPercentage = (currentScrollY / (document.body.scrollHeight - window.innerHeight)) * 100;
const totalPercentage = (xPercentage + yPercentage) / 2;
const xDirection = xDifference > 0 ? 'right' : xDifference < 0 ? 'left' : prevXDirection; // eslint-disable-line no-nested-ternary
const yDirection = yDifference > 0 ? 'down' : yDifference < 0 ? 'up' : prevYDirection; // eslint-disable-line no-nested-ternary
return {
x: currentScrollX,
y: currentScrollY,
xDifference,
yDifference,
xDirection,
yDirection,
xPercentage,
yPercentage,
totalPercentage,
eventsFired,
hasScrolled,
};
};
export const ScrollInfoProvider = (props) => {
const { children, } = props;
const animationRef = useRef(null);
const [state, dispatch] = useReducer(reducer, {
x: 0,
y: 0,
xDifference: 0,
yDifference: 0,
xDirection: undefined,
yDirection: undefined,
xPercentage: 0,
yPercentage: 0,
totalPercentage: 0,
eventsFired: 0,
hasScrolled: false,
});
const requestAnimation = useCallback((e) => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
animationRef.current = requestAnimationFrame((timestamp) => {
dispatch({
e,
timestamp,
animationRef,
});
});
}, []);
useEffect(() => {
window.addEventListener('scroll', requestAnimation);
return () => {
window.removeEventListener('scroll', requestAnimation);
};
}, [requestAnimation]);
// use this effect to test rAF debounce by requesting animation every 1ms, for a total 120ms
// count the number of times 'requestAnimation' callback is fired compared to the reducer
// results: ~23 requests will be made, ~17 requests will be canceled, so only ~8 will truly dispatch
// useEffect(() => {
// const firstID = setInterval(requestAnimation, 1);
// setInterval(() => clearInterval(firstID), 120);
// }, [requestAnimation]);
return (React.createElement(ScrollInfoContext.Provider, { value: Object.assign({}, state) }, children && children));
};
//# sourceMappingURL=index.js.map