Skip to content

Commit b19c100

Browse files
fix: display loading when children are present (#5)
* fix: display loading when children are present * fix: condition to display button load more * chore: remove status for request * chore: indent code * chore: add comment * fix: has items left condition * fix: imports of other react ck components
1 parent 4f099bf commit b19c100

File tree

3 files changed

+84
-48
lines changed

3 files changed

+84
-48
lines changed
Lines changed: 82 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1-
import React, { useEffect, useMemo, useRef, useState } from "react";
1+
import React, { useEffect, useMemo, useReducer, useRef } from "react";
22
import { Skeleton } from "../skeleton";
33
import { Spinner } from "../spinner";
44
import { Button } from "../button";
55

6-
const defaultContentWrapper: React.FC<React.PropsWithChildren> = ({ children }) => <>{children}</>;
6+
const DefaultContentWrapper: React.FC<React.PropsWithChildren> = ({ children }) => children;
77

88
/**
99
* Props for the InfiniteScroll component that handles automatic loading of content
1010
* as the user scrolls or clicks a load more button.
1111
*/
1212
export interface InfiniteScrollProps {
13-
/** Direction of infinite scroll - adapts the behaviour according to the scroll direction */
13+
/**
14+
* Direction of infinite scroll - adapts the behaviour according to the scroll direction
15+
* @default "bottom"
16+
*/
1417
direction?: "bottom" | "top";
1518
/** Number of items currently loaded - used to determine if more items are available */
1619
loaded?: number;
@@ -24,19 +27,30 @@ export interface InfiniteScrollProps {
2427
loadingMore: boolean;
2528
/** Custom element to display while loading more content (defaults to Spinner) */
2629
loadingMoreElement?: React.ReactNode;
27-
/** Whether to show a "Load more" button instead of automatic infinite scroll */
30+
/**
31+
* Whether to show a "Load more" button instead of automatic infinite scroll
32+
* @default true
33+
*/
2834
displayLoadMore?: boolean;
2935
/** Function called when more content should be loaded */
3036
onLoadMore: (() => void) | (() => Promise<void>);
3137
/** Custom button element or render function for the load more button */
3238
loadMoreButton?: React.ReactNode | ((props: { loadMore: () => void }) => React.ReactNode);
3339
/** Custom wrapper component for the content area */
34-
ContentWrapper?: React.FC<React.PropsWithChildren>;
40+
ContentWrapper?: typeof DefaultContentWrapper;
3541
/** Additional props passed to the intersection observer element */
3642
intersectionElementProps?: React.ComponentPropsWithoutRef<"div">;
43+
/** Mode of the infinite scroll */
3744
mode?: "infinite" | "enable-once" | "pagination";
3845
}
3946

47+
interface InfiniteScrollStatus {
48+
/** Whether the infinite scroll is enabled */
49+
infiniteScrollEnabled: boolean;
50+
/** Whether the intersection observer is intersecting */
51+
isIntersecting: boolean;
52+
}
53+
4054
export const InfiniteScroll: React.FC<React.PropsWithChildren<InfiniteScrollProps>> = ({
4155
direction = "bottom",
4256
children,
@@ -49,26 +63,37 @@ export const InfiniteScroll: React.FC<React.PropsWithChildren<InfiniteScrollProp
4963
loadingMoreElement,
5064
onLoadMore,
5165
loadMoreButton = "Load more",
52-
ContentWrapper = defaultContentWrapper,
66+
ContentWrapper = DefaultContentWrapper,
5367
intersectionElementProps,
5468
mode = loadMoreButton ? "enable-once" : "infinite",
5569
}) => {
70+
// Intersection ref
5671
const intersectionRef = useRef<HTMLDivElement>(null);
5772

58-
const [infiniteScrollEnabled, setInfiniteScrollEnabled] = useState(mode === "infinite");
59-
const [isIntersecting, setIsIntersecting] = useState(false);
73+
// Status
74+
const [{ infiniteScrollEnabled, isIntersecting }, setStatus] = useReducer(
75+
(prev: InfiniteScrollStatus, next: Partial<InfiniteScrollStatus>) => {
76+
return { ...prev, ...next };
77+
},
78+
{
79+
infiniteScrollEnabled: mode === "infinite",
80+
isIntersecting: false,
81+
},
82+
);
6083

84+
// Has items left
6185
const hasItemsLeft = useMemo(
6286
() => (loaded === undefined && total === undefined) || (loaded ?? 0) < (total ?? 0),
6387
[loaded, total],
6488
);
6589

90+
// Load more button
6691
const LoadMoreButton = useMemo(() => {
67-
if (!displayLoadMore) return null;
92+
if (!displayLoadMore) return;
6893

6994
const action = () => {
7095
if (mode === "enable-once") {
71-
setInfiniteScrollEnabled(true);
96+
setStatus({ infiniteScrollEnabled: true });
7297
} else {
7398
if (loading) return;
7499
void onLoadMore();
@@ -88,45 +113,14 @@ export const InfiniteScroll: React.FC<React.PropsWithChildren<InfiniteScrollProp
88113
);
89114
}, [displayLoadMore, loadMoreButton, loading, mode, onLoadMore]);
90115

91-
const elements = useMemo<React.ReactNode[]>(() => {
92-
const elements = [
93-
hasItemsLeft && loading ? (loadingElement ?? <Skeleton />) : null,
94-
children ? (
95-
<ContentWrapper>
96-
{direction === "top" && <div ref={intersectionRef} {...intersectionElementProps} />}
97-
{children}
98-
{direction === "bottom" && <div ref={intersectionRef} {...intersectionElementProps} />}
99-
</ContentWrapper>
100-
) : null,
101-
hasItemsLeft && loadingMore ? (loadingMoreElement ?? <Spinner />) : null,
102-
displayLoadMore && !infiniteScrollEnabled && hasItemsLeft && !loading && !loadingMore
103-
? LoadMoreButton
104-
: null,
105-
];
106-
107-
return direction === "bottom" ? elements : elements.reverse();
108-
}, [
109-
hasItemsLeft,
110-
loading,
111-
loadingElement,
112-
children,
113-
ContentWrapper,
114-
intersectionRef,
115-
loadingMore,
116-
loadingMoreElement,
117-
displayLoadMore,
118-
infiniteScrollEnabled,
119-
LoadMoreButton,
120-
direction,
121-
intersectionElementProps,
122-
]);
123-
116+
// Intersection observer
124117
useEffect(() => {
125118
if (!intersectionRef.current || !infiniteScrollEnabled || !children || !hasItemsLeft) return;
126119

120+
// eslint-disable-next-line compat/compat -- IntersectionObserver is not supported old browsers, but we target modern browsers
127121
const observer = new IntersectionObserver((entries) => {
128122
const isIntersecting = entries.some((entry) => entry.isIntersecting);
129-
setIsIntersecting(isIntersecting);
123+
setStatus({ isIntersecting });
130124
});
131125

132126
observer.observe(intersectionRef.current);
@@ -136,12 +130,54 @@ export const InfiniteScroll: React.FC<React.PropsWithChildren<InfiniteScrollProp
136130
};
137131
}, [children, hasItemsLeft, infiniteScrollEnabled]);
138132

133+
// Load more
139134
useEffect(() => {
140-
if (!isIntersecting || !infiniteScrollEnabled || loadingMore || loading || !hasItemsLeft)
135+
if (!isIntersecting || !infiniteScrollEnabled || loadingMore || loading || !hasItemsLeft) {
141136
return;
137+
}
142138

139+
// Load more items
143140
void onLoadMore();
144141
}, [hasItemsLeft, infiniteScrollEnabled, isIntersecting, loading, loadingMore, onLoadMore]);
145142

146-
return <>{elements}</>;
143+
// Load more controls
144+
const LoadMoreControls = useMemo(() => {
145+
if (loadingMore) {
146+
return loadingMoreElement ?? <Spinner />;
147+
}
148+
149+
return displayLoadMore && hasItemsLeft && !infiniteScrollEnabled && LoadMoreButton;
150+
}, [
151+
LoadMoreButton,
152+
displayLoadMore,
153+
hasItemsLeft,
154+
infiniteScrollEnabled,
155+
loadingMore,
156+
loadingMoreElement,
157+
]);
158+
159+
// Loading element
160+
if (loading) {
161+
return loadingElement ?? <Skeleton />;
162+
}
163+
164+
return (
165+
<ContentWrapper>
166+
{direction === "top" && (
167+
<>
168+
{LoadMoreControls}
169+
<div ref={intersectionRef} {...intersectionElementProps} />
170+
</>
171+
)}
172+
173+
{children}
174+
175+
{direction === "bottom" && (
176+
<>
177+
<div ref={intersectionRef} {...intersectionElementProps} />
178+
{LoadMoreControls}
179+
</>
180+
)}
181+
</ContentWrapper>
182+
);
147183
};

packages/utils/react/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export * from "./click-outside";
88

99
export * from "./children-without-fragments";
1010

11-
export * from "./merge-refs";
11+
export { mergeRefs, mergeRefs as megeRefs } from "./merge-refs";
1212

1313
export * from "./raf";
1414

packages/utils/react/src/merge-refs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type ForwardedRef, type RefObject, type RefCallback } from "react";
22

3-
export const megeRefs =
3+
export const mergeRefs =
44
<T>(
55
...refs: Array<undefined | null | ForwardedRef<T> | RefObject<T | null> | RefCallback<T>>
66
): RefCallback<T> =>

0 commit comments

Comments
 (0)