Skip to content

Commit

Permalink
Port InfiniteScroller
Browse files Browse the repository at this point in the history
Signed-off-by: Radoslaw Szwajkowski <rszwajko@redhat.com>
  • Loading branch information
rszwajko committed Aug 5, 2024
1 parent 45f6dd7 commit 5e72edd
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.infinite-scroll-sentinel {
width: 100%;
text-align: center;
font-style: italic;
font-weight: bold;
}
72 changes: 72 additions & 0 deletions client/src/app/components/InfiniteScroller/InfiniteScroller.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, { ReactNode, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useInfiniteScroll } from "./useInfiniteScroller";
import "./InfiniteScroller.css";

export interface InfiniteScrollerProps<T> {
children: ReactNode;
className: string;
fetchMore: () => undefined;
entities: T[];
hasMore: boolean;
}

export const InfiniteScroller = <T,>({
children,
className,
fetchMore,
entities,
hasMore,
}: InfiniteScrollerProps<T>) => {
const { t } = useTranslation();
// Handle the infinite scroll and pagination
const [page, sentinelRef, scrollerRef] = useInfiniteScroll({
hasMore,
distance: 0,
});

useEffect(() => {
// parent will not display this component until the first page of data is loaded
if (page > 0) {
fetchMore();
}
}, [page, fetchMore]);

useEffect(() => {
if (!scrollerRef.current || !sentinelRef.current) {
return;
}

//
// If a page fetch doesn't pull enough entities to push the sentinel out of view
// underlying IntersectionObserver doesn't fire another event, and the scroller
// gets stuck. Manually check if the sentinel is in view, and if it is, fetch
// more data. The effect is only run when the `vms` part of the redux store is
// updated.
//
const scrollRect = scrollerRef.current.getBoundingClientRect();
const scrollVisibleTop = scrollRect.y;
const scrollVisibleBottom = scrollRect.y + scrollRect.height;

const sentinelRect = sentinelRef.current.getBoundingClientRect();
const sentinelTop = sentinelRect.y;
const sentinelBottom = sentinelRect.y + sentinelRect.height;

const sentinelStillInView =
sentinelBottom >= scrollVisibleTop && sentinelTop <= scrollVisibleBottom;
if (sentinelStillInView) {
fetchMore();
}
}, [entities, scrollerRef, sentinelRef, fetchMore]);

return (
<div ref={scrollerRef} className={className}>
{children}
{hasMore && (
<div ref={sentinelRef} className={"infinite-scroll-sentinel"}>
{t("loadingTripleDot")}
</div>
)}
</div>
);
};
1 change: 1 addition & 0 deletions client/src/app/components/InfiniteScroller/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./InfiniteScroller";
61 changes: 61 additions & 0 deletions client/src/app/components/InfiniteScroller/useInfiniteScroller.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { RefObject, useLayoutEffect, useRef, useState } from "react";

export function useInfiniteScroll({
hasMore,
reset = false,
distance = 250,
}: {
hasMore: boolean;
reset?: boolean;
distance?: number;
}): [number, RefObject<HTMLDivElement>, RefObject<HTMLDivElement>] {
const scrollContainerRef = useRef<HTMLDivElement>(null);
const loaderRef = useRef<HTMLDivElement>(null);
const [page, setPage] = useState(0);

if (reset && page !== 0) {
setPage(0);
}

useLayoutEffect(() => {
const loaderNode = loaderRef.current;
const scrollContainerNode = scrollContainerRef.current;
if (!scrollContainerNode || !loaderNode || !hasMore) return;

const options = {
root: scrollContainerNode,
rootMargin: `0px 0px ${distance}px 0px`,
};

let previousY: number | undefined = 0;
let previousRatio = 0;

const listener = (entries: IntersectionObserverEntry[]) => {
entries.forEach(
({
isIntersecting,
intersectionRatio,
boundingClientRect = { y: 0 },
}) => {
const { y = 0 } = boundingClientRect;
if (
isIntersecting &&
intersectionRatio >= previousRatio &&
(!previousY || y < previousY)
) {
setPage((page) => page + 1);
}
previousY = y;
previousRatio = intersectionRatio;
}
);
};

const observer = new IntersectionObserver(listener, options);
observer.observe(loaderNode);

return () => observer.disconnect();
}, [hasMore, distance]);

return [page, loaderRef, scrollContainerRef];
}

0 comments on commit 5e72edd

Please sign in to comment.