@@ -3,26 +3,41 @@ import { Skeleton } from "../skeleton";
33import { Spinner } from "../spinner" ;
44import { Button } from "../button" ;
55
6- const defaultContentWrapper : React . FC < React . PropsWithChildren > = ( { children } ) => (
7- < div > { children } </ div >
8- ) ;
9-
10- export const InfiniteScroll : React . FC <
11- React . PropsWithChildren < {
12- readonly loaded ?: number ;
13- readonly loading : boolean ;
14- readonly loadingElement ?: React . ReactNode ;
15- readonly loadingMore : boolean ;
16- readonly loadingMoreElement ?: React . ReactNode ;
17- readonly total ?: number ;
18- readonly displayLoadMore ?: boolean ;
19- readonly onLoadMore : ( ) => void ;
20- readonly loadMoreButton ?:
21- | React . ReactNode
22- | ( ( props : { loadMore : ( ) => void } ) => React . ReactNode ) ;
23- readonly ContentWrapper ?: React . FC < React . PropsWithChildren > ;
24- } >
25- > = ( {
6+ const defaultContentWrapper : React . FC < React . PropsWithChildren > = ( { children } ) => < > { children } </ > ;
7+
8+ /**
9+ * Props for the InfiniteScroll component that handles automatic loading of content
10+ * as the user scrolls or clicks a load more button.
11+ */
12+ export interface InfiniteScrollProps {
13+ /** Direction of infinite scroll - adapts the behaviour according to the scroll direction */
14+ direction ?: "bottom" | "top" ;
15+ /** Number of items currently loaded - used to determine if more items are available */
16+ loaded ?: number ;
17+ /** Total number of items available - used with loaded to determine remaining items */
18+ total ?: number ;
19+ /** Whether initial content is currently loading */
20+ loading : boolean ;
21+ /** Custom element to display during initial loading (defaults to Skeleton) */
22+ loadingElement ?: React . ReactNode ;
23+ /** Whether additional content is currently being loaded */
24+ loadingMore : boolean ;
25+ /** Custom element to display while loading more content (defaults to Spinner) */
26+ loadingMoreElement ?: React . ReactNode ;
27+ /** Whether to show a "Load more" button instead of automatic infinite scroll */
28+ displayLoadMore ?: boolean ;
29+ /** Function called when more content should be loaded */
30+ onLoadMore : ( ( ) => void ) | ( ( ) => Promise < void > ) ;
31+ /** Custom button element or render function for the load more button */
32+ loadMoreButton ?: React . ReactNode | ( ( props : { loadMore : ( ) => void } ) => React . ReactNode ) ;
33+ /** Custom wrapper component for the content area */
34+ ContentWrapper ?: React . FC < React . PropsWithChildren > ;
35+ /** Additional props passed to the intersection observer element */
36+ intersectionElementProps ?: React . ComponentPropsWithoutRef < "div" > ;
37+ }
38+
39+ export const InfiniteScroll : React . FC < React . PropsWithChildren < InfiniteScrollProps > > = ( {
40+ direction = "bottom" ,
2641 children,
2742 displayLoadMore = true ,
2843 loaded,
@@ -34,6 +49,7 @@ export const InfiniteScroll: React.FC<
3449 onLoadMore,
3550 loadMoreButton = "Load more" ,
3651 ContentWrapper = defaultContentWrapper ,
52+ intersectionElementProps,
3753} ) => {
3854 const intersectionRef = useRef < HTMLDivElement > ( null ) ;
3955
@@ -67,6 +83,37 @@ export const InfiniteScroll: React.FC<
6783 ) ;
6884 } , [ loadMoreButton , displayLoadMore ] ) ;
6985
86+ const elements = useMemo < React . ReactNode [ ] > ( ( ) => {
87+ const elements = [
88+ hasItemsLeft && loading ? ( loadingElement ?? < Skeleton /> ) : null ,
89+ children ? (
90+ < ContentWrapper >
91+ { direction === "top" && < div ref = { intersectionRef } { ...intersectionElementProps } /> }
92+ { children }
93+ { direction === "bottom" && < div ref = { intersectionRef } { ...intersectionElementProps } /> }
94+ </ ContentWrapper >
95+ ) : null ,
96+ hasItemsLeft && loadingMore ? ( loadingMoreElement ?? < Spinner /> ) : null ,
97+ displayLoadMore && ! infiniteScrollEnabled && hasItemsLeft ? LoadMoreButton : null ,
98+ ] ;
99+
100+ return direction === "bottom" ? elements : elements . reverse ( ) ;
101+ } , [
102+ hasItemsLeft ,
103+ loading ,
104+ loadingElement ,
105+ children ,
106+ ContentWrapper ,
107+ intersectionRef ,
108+ loadingMore ,
109+ loadingMoreElement ,
110+ displayLoadMore ,
111+ infiniteScrollEnabled ,
112+ LoadMoreButton ,
113+ direction ,
114+ intersectionElementProps ,
115+ ] ) ;
116+
70117 useEffect ( ( ) => {
71118 if ( ! intersectionRef . current || ! infiniteScrollEnabled || ! children || ! hasItemsLeft ) return ;
72119
@@ -80,29 +127,14 @@ export const InfiniteScroll: React.FC<
80127 return ( ) => {
81128 observer . disconnect ( ) ;
82129 } ;
83- } , [ onLoadMore , infiniteScrollEnabled , children , hasItemsLeft ] ) ;
130+ } , [ children , hasItemsLeft , infiniteScrollEnabled ] ) ;
84131
85132 useEffect ( ( ) => {
86133 if ( ! isIntersecting || ! infiniteScrollEnabled || loadingMore || loading || ! hasItemsLeft )
87134 return ;
88135
89- onLoadMore ( ) ;
90- } , [ isIntersecting , onLoadMore , infiniteScrollEnabled , loadingMore , loading , hasItemsLeft ] ) ;
91-
92- return (
93- < >
94- { hasItemsLeft && loading ? ( loadingElement ?? < Skeleton /> ) : null }
136+ void onLoadMore ( ) ;
137+ } , [ hasItemsLeft , infiniteScrollEnabled , isIntersecting , loading , loadingMore , onLoadMore ] ) ;
95138
96- { children ? (
97- < ContentWrapper >
98- { children }
99- < div ref = { intersectionRef } />
100- </ ContentWrapper >
101- ) : null }
102-
103- { hasItemsLeft && loadingMore ? ( loadingMoreElement ?? < Spinner /> ) : null }
104-
105- { displayLoadMore && ! infiniteScrollEnabled && hasItemsLeft ? LoadMoreButton : null }
106- </ >
107- ) ;
139+ return < > { elements } </ > ;
108140} ;
0 commit comments