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
134 changes: 134 additions & 0 deletions packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import React, { useCallback, useEffect, useState } from "react";
import InfiniteScroll from "./InfiniteScroll";
import styled from "styled-components";

export default {
title: "Data Display/InfiniteScroll",
component: InfiniteScroll,
};

interface User {
key: number;
name: string;
status: "active" | "inactive";
age: number;
address: string;
}

function getData(page = 1, page_size = 10): User[] {
return new Array(page * page_size).fill(0).map((_, index) => {
return {
key: index,
name: `name ${index}`,
status: index % 2 ? "active" : "inactive",
age: index,
address: `A${index} Downing Street`,
};
});
}

function getDataAsync(page = 1, page_size = 10): Promise<User[]> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(getData(page, page_size));
}, 1000);
});
}

export const basic = () => {
const [data, setData] = useState<User[]>([]);
const [isFetching, setIsFetching] = useState<boolean>(false);

const fetchData = useCallback(() => {
setIsFetching(true);
getDataAsync()
.then((d) => setData((curr) => curr.concat(d)))
.finally(() => setIsFetching(false));
}, []);

useEffect(() => {
fetchData();
}, [fetchData]);

const onScroll = () => {
if (isFetching) return;

fetchData();
};

return (
<div style={{ height: "80vh" }}>
<InfiniteScroll<User>
items={data}
renderItem={(item) => <Card user={item} />}
onBottomScroll={onScroll}
isLoading={isFetching}
/>
</div>
);
};

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

const UserCard = styled.div`
margin-bottom: 16px;
border-radius: 2px;
box-shadow: rgb(179 179 179 / 31%) 0px 1px 5px;
.title {
margin: 10px;
color: #4d85f4;
font-size: 16px;
}
.description {
margin-top: 20px;
word-break: break-all;
}
.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;
}
}
}
`;
82 changes: 82 additions & 0 deletions packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React, { useEffect, useRef } from "react";

interface InfiniteScrollProps<T> {
renderItem: (item: T) => React.ReactNode;
onBottomScroll: () => void;
items: T[];
height?: string;
style?: React.CSSProperties;
isLoading?: boolean;
noMoreData?: boolean;
containerRef?: React.RefObject<HTMLElement>;
threshold?: number;
loadingComponent?: React.ReactNode;
noMoreDataComponent?: React.ReactNode;
}

const InfiniteScroll = <T,>({
renderItem,
onBottomScroll,
items,
style,
height,
isLoading,
noMoreData,
containerRef,
threshold = 1,
loadingComponent,
noMoreDataComponent,
}: InfiniteScrollProps<T>) => {
const defaultContainerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const containerElem = containerRef ? containerRef.current : defaultContainerRef.current;
if (!containerElem) return;

const onScroll = () => {
if (isBottom(containerElem, threshold)) {
onBottomScroll();
}
};

containerElem.addEventListener("scroll", onScroll);

return () => {
containerElem.removeEventListener("scroll", onScroll);
};
}, [containerRef, defaultContainerRef, onBottomScroll, threshold]);

const loadingComp = loadingComponent || <DefaultLoading />;

const scrollStyle: React.CSSProperties = !containerRef
? {
height: height || "calc(100%-0px)",
overflow: "scroll",
position: "relative",
...style,
}
: {};

return (
<div className="apsara-infinite-scroll-wrapper" style={scrollStyle} ref={defaultContainerRef}>
{items?.map(renderItem)}
{isLoading && loadingComp}
{!noMoreData && noMoreDataComponent}
</div>
);
};

const DefaultLoading = () => <div>Loading...</div>;

const isBottom = (elem: HTMLElement, threshold: number): boolean => {
if (!elem) return false;

const { scrollTop, scrollHeight, clientHeight } = elem;

const a = scrollTop + clientHeight;
const b = scrollHeight - threshold;

return a >= b;
};

export default InfiniteScroll;
1 change: 1 addition & 0 deletions packages/apsara-ui/src/InfiniteScroll/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default "./InfiniteScroll";
2 changes: 2 additions & 0 deletions packages/apsara-ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { TooltipPlacement } from "./Tooltip/Tooltip";
import Modal from "./Modal";
import Alert from "./Alert";
import { DotBadge } from "./Badge";
import InfiniteScroll from "./InfiniteScroll";
export { DynamicList } from "./DynamicList";

export * from "./Notification";
Expand Down Expand Up @@ -103,6 +104,7 @@ export {
Slider,
Themes,
InfoModal,
InfiniteScroll,
DiffTimeline,
InputNumber,
DatePicker,
Expand Down