@bongione/react-element-scroll-hook
Version:
A react hook to use the scroll information of an element
201 lines (177 loc) • 5.37 kB
text/typescript
import {
useEffect,
useRef,
useState,
useCallback,
RefObject,
MutableRefObject,
} from "react";
declare const navigator: {
userAgent: string;
};
// Edge has a bug where scrollHeight is 1px bigger than clientHeight when there's no scroll.
const isEdge = /Edge\/\d./i.test(typeof navigator !== 'undefined' ? navigator.userAgent : '');
// Small hook to use ResizeOberver if available. This fixes some issues when the component is resized.
// This needs a polyfill to work on all browsers. The polyfill is not included in order to keep the package light.
function useResizeObserver(
ref: RefObject<HTMLElement>,
callback: (element: RefObject<HTMLElement> | DOMRectReadOnly) => any
) {
useEffect(() => {
if (typeof window !== "undefined" && window.ResizeObserver) {
const resizeObserver = new ResizeObserver((entries) => {
callback(entries[0].contentRect);
});
if (ref.current) {
resizeObserver.observe(ref.current);
return () => {
resizeObserver.disconnect();
};
}
}
}, [ref]);
}
type VoidNoPrmsFn = (this: any) => void;
function throttle(func: VoidNoPrmsFn, wait: number): VoidNoPrmsFn {
let a: IArguments;
let context: any | null, args: null | [], result: any;
let timeout: number | null = null;
let previous = 0;
const later = function () {
timeout = null;
result = func.apply(context, args || []);
if (!timeout) {
context = args = null;
}
};
return function () {
const now = Date.now();
const remaining = wait - (now - previous);
context = this;
args = Array.prototype.slice.call(arguments) as [];
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) {
context = args = null;
}
} else if (!timeout) {
timeout = setTimeout(later, remaining);
}
return result;
};
}
interface IDimensionScrollInfo {
percentage: number | null;
value: number;
total: number;
className: string;
direction: number;
}
export interface IScrollInfo {
x: Partial<IDimensionScrollInfo>;
y: Partial<IDimensionScrollInfo>;
}
export type UseScrollInfoProps = [
IScrollInfo,
(element: HTMLElement) => void,
RefObject<HTMLElement>
];
function useScrollInfo(): UseScrollInfoProps {
const [scroll, setScroll] = useState<IScrollInfo>({ x: {}, y: {} });
const ref: MutableRefObject<HTMLElement | null> = useRef(null);
const previousScroll: MutableRefObject<IScrollInfo | null> = useRef(null);
useResizeObserver(ref, () => {
update();
});
const throttleTime = 50;
function update() {
const element = ref.current!;
let maxY = element.scrollHeight - element.clientHeight;
const maxX = element.scrollWidth - element.clientWidth;
// Edge has a bug where scrollHeight is 1px bigger than clientHeight when there's no scroll.
if (isEdge && maxY === 1 && element.scrollTop === 0) {
maxY = 0;
}
const percentageY = maxY !== 0 ? element.scrollTop / maxY : null;
const percentageX = maxX !== 0 ? element.scrollLeft / maxX : null;
let classNameY = "no-scroll-y";
if (percentageY === 0) {
classNameY = "scroll-top";
} else if (percentageY === 1) {
classNameY = "scroll-bottom";
} else if (percentageY) {
classNameY = "scroll-middle-y";
}
let classNameX = "no-scroll-x";
if (percentageX === 0) {
classNameX = "scroll-left";
} else if (percentageX === 1) {
classNameX = "scroll-right";
} else if (percentageX) {
classNameX = "scroll-middle-x";
}
const previous = previousScroll.current || {
x: {
className: "",
direction: 0,
percentage: 0,
total: 0,
value: 0,
},
y: {
className: "",
direction: 0,
percentage: 0,
total: 0,
value: 0,
},
};
const scrollInfo = {
x: {
percentage: percentageX,
value: element.scrollLeft,
total: maxX,
className: classNameX,
direction: previous
? Math.sign(element.scrollLeft - previous.x.value!)
: 0,
},
y: {
percentage: percentageY,
value: element.scrollTop,
total: maxY,
className: classNameY,
direction: previous
? Math.sign(element.scrollTop - previous.y.value!)
: 0,
},
};
previousScroll.current = scrollInfo;
setScroll(scrollInfo);
}
const throttledUpdate = throttle(update, throttleTime);
const setRef = useCallback((node: HTMLElement) => {
if (node) {
// When the ref is first set (after mounting)
node.addEventListener("scroll", throttledUpdate);
if (!window.ResizeObserver) {
window.addEventListener("resize", throttledUpdate); // Fallback if ResizeObserver is not available
}
ref.current = node;
throttledUpdate(); // initialization
} else if (ref.current) {
// When unmounting
ref.current.removeEventListener("scroll", throttledUpdate);
if (!window.ResizeObserver) {
window.removeEventListener("resize", throttledUpdate);
}
}
}, []);
return [scroll, setRef, ref];
}
export default useScrollInfo;