-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Radoslaw Szwajkowski <rszwajko@redhat.com>
- Loading branch information
Showing
4 changed files
with
140 additions
and
0 deletions.
There are no files selected for viewing
6 changes: 6 additions & 0 deletions
6
client/src/app/components/InfiniteScroller/InfiniteScroller.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
72
client/src/app/components/InfiniteScroller/InfiniteScroller.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./InfiniteScroller"; |
61 changes: 61 additions & 0 deletions
61
client/src/app/components/InfiniteScroller/useInfiniteScroller.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]; | ||
} |