import React, { PropsWithChildren, useCallback, useEffect, useRef, } from 'react'; /** * Prevents Chrome hangups * See: https://stackoverflow.com/questions/47524205/random-high-content-download-time-in-chrome/47684257#47684257 */ const mousewheelListener = (event: Event) => { if (event instanceof WheelEvent && event.deltaY === 1) { event.preventDefault(); } }; export interface InfiniteScrollProps { className?: string; element?: React.ElementType; hasMore?: boolean; hasMoreNewer?: boolean; initialLoad?: boolean; isLoading?: boolean; listenToScroll?: (offset: number, reverseOffset: number, threshold: number) => void; loader?: React.ReactNode; loading?: React.ReactNode; loadMore?: () => void; loadMoreNewer?: () => void; pageStart?: number; threshold?: number; useCapture?: boolean; } export function InfiniteScroll(props: PropsWithChildren) { const { children, element = 'div', hasMore = false, hasMoreNewer = false, initialLoad = true, isLoading = false, listenToScroll, loader, loadMore, loadMoreNewer, threshold = 150, useCapture = false, ...elementProps } = props; const scrollComponent = useRef(); const scrollListener = useCallback(() => { const ele = scrollComponent.current; if (!ele || ele.offsetParent === null) { return; } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const parentElement = ele.parentElement!; const offset = ele.scrollHeight - parentElement.scrollTop - parentElement.clientHeight; const reverseOffset = parentElement.scrollTop; if (listenToScroll) { listenToScroll(offset, reverseOffset, threshold); } if (reverseOffset < Number(threshold) && typeof loadMore === 'function' && hasMore) { loadMore(); } if (offset < Number(threshold) && typeof loadMoreNewer === 'function' && hasMoreNewer) { loadMoreNewer(); } }, [hasMore, hasMoreNewer, threshold, listenToScroll, loadMore, loadMoreNewer]); useEffect(() => { const scrollElement = scrollComponent.current?.parentNode; if (isLoading || !scrollElement) { return () => undefined; } scrollElement.addEventListener('scroll', scrollListener, useCapture); scrollElement.addEventListener('resize', scrollListener, useCapture); return () => { scrollElement.removeEventListener('scroll', scrollListener, useCapture); scrollElement.removeEventListener('resize', scrollListener, useCapture); }; }, [initialLoad, isLoading, scrollListener, useCapture]); useEffect(() => { const scrollElement = scrollComponent.current?.parentNode; if (scrollElement) { scrollElement.addEventListener('wheel', mousewheelListener, { passive: false }); } return () => { if (scrollElement) { scrollElement.removeEventListener('wheel', mousewheelListener, useCapture); } }; }, [useCapture]); const attributes = { ...elementProps, ref: (ele: HTMLElement) => { scrollComponent.current = ele; }, }; const childrenArray = [loader, children]; return React.createElement(element, attributes, childrenArray); }