Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(InfiniteScroll): added InfiniteScroll component #35

Merged
merged 9 commits into from
Apr 25, 2024
91 changes: 91 additions & 0 deletions packages/apsara-ui/src/Listing/InfiniteScroll.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { debounce } from "lodash";
StewartJingga marked this conversation as resolved.
Show resolved Hide resolved
import React, { useState, useEffect } from "react";

interface InfiniteScrollProps<T> {
StewartJingga marked this conversation as resolved.
Show resolved Hide resolved
fetchMoreData: (page: number, pageSize: number, filters: any) => Promise<T[]>;
contentRef: React.RefObject<HTMLDivElement>;
page: number;
pageSize?: number;
filters?: any;
threshold?: number;
renderItem: (item: T) => React.ReactNode;
loadingComponent?: React.ReactNode;
noMoreDataComponent?: React.ReactNode;
}

const InfiniteScroll: React.FC<InfiniteScrollProps<any>> = ({
fetchMoreData,
contentRef,
page,
pageSize = 10,
threshold = 0,
filters,
renderItem,
loadingComponent,
noMoreDataComponent,
}) => {
const [data, setData] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [hasMoreData, setHasMoreData] = useState(true);

useEffect(() => {
setData([]);
setHasMoreData(true);
}, [filters]);

const fetchMore = debounce(async () => {
StewartJingga marked this conversation as resolved.
Show resolved Hide resolved
if (!isLoading && hasMoreData && isBottom(contentRef)) {
setIsLoading(true);
try {
const newData = await fetchMoreData(page, pageSize, filters);
setData((prevData) => prevData.concat(newData));
setHasMoreData(newData.length < pageSize ? false : true);
} finally {
setIsLoading(false);
}
}
}, 200);

useEffect(() => {
ayushi0014 marked this conversation as resolved.
Show resolved Hide resolved
if (page == 1 && !data.length) {
fetchMore();
}

const onScroll = () => {
if (page != 1) {
fetchMore();
}
};

const currentContentRef = contentRef.current;
if (currentContentRef) {
currentContentRef.addEventListener("scroll", onScroll);
}

return () => {
if (currentContentRef) {
currentContentRef.removeEventListener("scroll", onScroll);
}
};
}, [contentRef, fetchMore, page, filters]);

const isBottom = (ref: React.RefObject<HTMLDivElement>) => {
if (!ref.current) {
return false;
}

const { scrollTop, scrollHeight, clientHeight } = ref.current;

return scrollTop + clientHeight >= scrollHeight - threshold;
};

return (
<div className="infinite-list">
StewartJingga marked this conversation as resolved.
Show resolved Hide resolved
{data.map(renderItem)}
{isLoading ? loadingComponent : null}
{!hasMoreData && noMoreDataComponent}
</div>
);
};

export default InfiniteScroll;
78 changes: 76 additions & 2 deletions packages/apsara-ui/src/Listing/Listing.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import React, { useState } from "react";
import React, { FormEvent, useRef, useState } from "react";
import Listing from "./Listing";
import InfiniteListing from "./InfiniteListing";
import { ScrollableList, UserCard } from "./Listing.styles";
import InfiniteScroll from "./InfiniteScroll";
import Search from "../Search";

export default {
title: "Data Display/Listing",
Expand Down Expand Up @@ -28,7 +31,7 @@ function getData(page = 1): User[] {
}

export const listing = () => (
<div style={{ height: '720px' }}>
<div style={{ height: "720px" }}>
<h4>Listing</h4>
<Listing<User>
loading={false}
Expand Down Expand Up @@ -188,3 +191,74 @@ export const infiniteListingWithApply = () => {
/>
);
};

export const infiniteListWithCustomComponent = () => {
const pageSize = 10;
const contentRef = useRef<HTMLDivElement>(null);
const [searchTerm, setSearchTerm] = useState("");
const [filter, setFilter] = useState<string>();
const [page, setPage] = useState<number>(1);

const fetchMore = async (page: number, pageSize: number, filter: string) => {
if (page === 4) return [];

let records = getData(page);
if (filter) {
records = records.filter((record) => record.name.toLowerCase().includes(filter.toLowerCase()));
}

setPage((prevPage) => prevPage + 1);
records = records.slice(0, pageSize);
return records;
};

const Card = ({ user }: { user: User }) => {
return (
<UserCard>
<div className="body">
<div className="body-left">
<div className="description">{user.name}</div>
</div>
<div className="body-right">
<div>Age {user.age}</div>
</div>
</div>
</UserCard>
);
};

const onSubmit = (e: FormEvent) => {
e.preventDefault();
setPage(1);
setFilter(searchTerm);
};

return (
<div style={{ display: "flex", justifyContent: "center" }}>
<form onSubmit={onSubmit}>
<Search
style={{ width: "90vw", height: "100%" }}
value={searchTerm}
onChange={(e) => {
return setSearchTerm(e.target.value);
}}
placeholder="Type your search query here.."
// @ts-ignore
secondary={true}
/>
</form>
<ScrollableList className="results-list" ref={contentRef}>
<InfiniteScroll
page={page}
fetchMoreData={fetchMore}
filters={filter}
pageSize={pageSize}
contentRef={contentRef}
renderItem={(user: User) => <Card user={user} />}
loadingComponent={<div>Loading...</div>}
noMoreDataComponent={<div>No more data to fetch!</div>}
></InfiniteScroll>
</ScrollableList>
</div>
);
};
86 changes: 86 additions & 0 deletions packages/apsara-ui/src/Listing/Listing.styles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,89 @@ export const FilterActions = styled.span`
display: flex;
align-items: center;
`;

export const ScrollableList = styled.div`
position: absolute;
ayushi0014 marked this conversation as resolved.
Show resolved Hide resolved
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
height: 70%;
background-color: ${({ theme }) => theme.backgroundColor || "white"};
overflow-y: auto;
box-shadow: rgb(179 179 179 / 31%) 0px 1px 5px;
`;

export const UserCard = styled.div`
ayushi0014 marked this conversation as resolved.
Show resolved Hide resolved
margin-bottom: 16px;
border-radius: 2px;
box-shadow: rgb(179 179 179 / 31%) 0px 1px 5px;

.title {
color: #4d85f4;
font-size: 16px;
}

.description {
margin-top: 20px;
word-break: break-all;
}

.label-box {
display: flex;
flex-direction: row;
position: relative;

> :first-child {
font-weight: bold;
::after {
content: ":";
margin-right: 3px;
}
}
}

.header {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 12px 20px 0px 20px;
.title {
font-size: 16px;
font-weight: 500;
line-height: 1;
letter-spacing: 0.4px;
word-break: break-word;
}
}

.body {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 0px 20px 12px 20px;

> .body-left {
margin-top: 16px;
flex: 1;
}

> .body-right {
width: 240px;
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: space-between;

> div {
display: flex;
align-items: flex-end;
flex-direction: column;
}

.tag_content {
text-transform: uppercase;
}
}
}
`;
1 change: 1 addition & 0 deletions packages/apsara-ui/src/Listing/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Listing from "./Listing";
export { default as InfiniteScroll } from "./InfiniteScroll";
export { default as InfiniteListing } from "./InfiniteListing";
export { default as useSearchFilter } from "./hooks/useSearchFilter";

Expand Down
3 changes: 2 additions & 1 deletion packages/apsara-ui/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Button from "./Button";
import Icon, { IconImage } from "./Icon";
import Listing, { useSearchFilter, InfiniteListing } from "./Listing";
import Listing, { useSearchFilter, InfiniteListing, InfiniteScroll } from "./Listing";
import Search from "./Search";
import Table from "./Table";
import VTable from "./Table/VirtualisedTable";
Expand Down Expand Up @@ -103,6 +103,7 @@ export {
Slider,
Themes,
InfoModal,
InfiniteScroll,
DiffTimeline,
InputNumber,
DatePicker,
Expand Down