From 6ec1c855c67dba44bbe7edcc5adcc91aa50574d2 Mon Sep 17 00:00:00 2001 From: Ayushi Sharma Date: Fri, 19 Apr 2024 10:22:18 +0530 Subject: [PATCH 1/9] feat: added new component for infinite scroll --- .../apsara-ui/src/Listing/InfiniteScroll.tsx | 103 ++++++++++++++++++ .../apsara-ui/src/Listing/Listing.stories.tsx | 55 +++++++++- .../apsara-ui/src/Listing/Listing.styles.tsx | 86 +++++++++++++++ packages/apsara-ui/src/Listing/index.tsx | 1 + packages/apsara-ui/src/index.ts | 3 +- 5 files changed, 245 insertions(+), 3 deletions(-) create mode 100644 packages/apsara-ui/src/Listing/InfiniteScroll.tsx diff --git a/packages/apsara-ui/src/Listing/InfiniteScroll.tsx b/packages/apsara-ui/src/Listing/InfiniteScroll.tsx new file mode 100644 index 00000000..528a8deb --- /dev/null +++ b/packages/apsara-ui/src/Listing/InfiniteScroll.tsx @@ -0,0 +1,103 @@ +import { debounce } from "lodash"; +import React, { useState, useEffect } from "react"; + +interface InfiniteScrollProps { + fetchMoreData: (page: number, pageSize: number, filters: any) => Promise; + contentRef: React.RefObject; + pageSize?: number; + filters?: any; + renderItem: (item: T) => React.ReactNode; + loadingComponent?: React.ReactNode; + noMoreDataComponent?: React.ReactNode; +} + +const InfiniteScroll: React.FC> = ({ + fetchMoreData, + contentRef, + pageSize = 10, + filters, + renderItem, + loadingComponent, + noMoreDataComponent, +}) => { + const [data, setData] = useState([]); + const [page, setPage] = useState(1); + const [isLoading, setIsLoading] = useState(false); + const [hasMoreData, setHasMoreData] = useState(true); + const [isFirstFetch, setIsFirstFetch] = useState(true); + + const onBottomHit = debounce(async () => { + if (!isLoading && hasMoreData && isBottom(contentRef)) { + setIsLoading(true); + try { + const newData = await fetchMoreData(page, pageSize, filters); + setData((prevData) => prevData.concat(newData)); + setPage((prevPage) => prevPage + 1); + setHasMoreData(newData.length > 0); + } finally { + setIsLoading(false); + } + } + }, 200); + + useEffect(() => { + setPage(1); + setHasMoreData(true); + setData([]); + }, [filters]); + + useEffect(() => { + if (isFirstFetch) { + onBottomHit(); + } + setIsFirstFetch(false); + + const onScroll = () => { + onBottomHit(); + }; + + const currentContentRef = contentRef.current; + + if (currentContentRef) { + currentContentRef.addEventListener("scroll", onScroll); + } + + return () => { + if (currentContentRef) { + currentContentRef.removeEventListener("scroll", onScroll); + } + }; + }, [ + contentRef, + isLoading, + hasMoreData, + page, + filters, + fetchMoreData, + setData, + setPage, + setHasMoreData, + isFirstFetch, + onBottomHit, + ]); + + const isBottom = (ref: React.RefObject) => { + if (!ref.current) { + return false; + } + + const { scrollTop, scrollHeight, clientHeight } = ref.current; + + return scrollTop + clientHeight >= scrollHeight; + }; + + return ( +
+ {data.map(renderItem)} + {isLoading ? loadingComponent : null} + {!hasMoreData && noMoreDataComponent} +
+ ); +}; + +export default InfiniteScroll; diff --git a/packages/apsara-ui/src/Listing/Listing.stories.tsx b/packages/apsara-ui/src/Listing/Listing.stories.tsx index 88677439..1d4a3d68 100644 --- a/packages/apsara-ui/src/Listing/Listing.stories.tsx +++ b/packages/apsara-ui/src/Listing/Listing.stories.tsx @@ -1,6 +1,8 @@ -import React, { useState } from "react"; +import React, { useRef, useState } from "react"; import Listing from "./Listing"; import InfiniteListing from "./InfiniteListing"; +import { ScrollableList, UserCard } from "./Listing.styles"; +import InfiniteScroll from "./InfiniteScroll"; export default { title: "Data Display/Listing", @@ -28,7 +30,7 @@ function getData(page = 1): User[] { } export const listing = () => ( -
+

Listing

loading={false} @@ -188,3 +190,52 @@ export const infiniteListingWithApply = () => { /> ); }; + +export const infiniteListWithCustomComponent = () => { + const pageSize = 10; + const contentRef = useRef(null); + const fetchRecords = (page: number, pageSize: number) => { + return Array.from({ length: pageSize }, (_, i) => ({ + name: `user ${page * pageSize + i}`, + index: page * pageSize + i, + age: Math.floor(Math.random() * 41) + 20, // Random age between 20 and 60 + })); + }; + + const fetchMore = async (page: number, pageSize: number, filter: string) => { + console.log(filter); + const records = fetchRecords(page, pageSize); + + return records; + }; + + const Card = ({ user }: { user: User }) => { + return ( + +
+
+
{user.name}
+
+
+
Age {user.age}
+
+
+
+ ); + }; + + return ( +
+ + } + loadingComponent={
Loading...
} + noMoreDataComponent={
No more data to fetch!
} + >
+
+
+ ); +}; diff --git a/packages/apsara-ui/src/Listing/Listing.styles.tsx b/packages/apsara-ui/src/Listing/Listing.styles.tsx index bd553fef..d3cc3caa 100644 --- a/packages/apsara-ui/src/Listing/Listing.styles.tsx +++ b/packages/apsara-ui/src/Listing/Listing.styles.tsx @@ -19,3 +19,89 @@ export const FilterActions = styled.span` display: flex; align-items: center; `; + +export const ScrollableList = styled.div` + position: absolute; + 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` + 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; + } + } + } +`; diff --git a/packages/apsara-ui/src/Listing/index.tsx b/packages/apsara-ui/src/Listing/index.tsx index 884b8a94..47d071ca 100644 --- a/packages/apsara-ui/src/Listing/index.tsx +++ b/packages/apsara-ui/src/Listing/index.tsx @@ -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"; diff --git a/packages/apsara-ui/src/index.ts b/packages/apsara-ui/src/index.ts index 40f8f568..09e81786 100644 --- a/packages/apsara-ui/src/index.ts +++ b/packages/apsara-ui/src/index.ts @@ -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"; @@ -103,6 +103,7 @@ export { Slider, Themes, InfoModal, + InfiniteScroll, DiffTimeline, InputNumber, DatePicker, From c040fc49418cbdeec60e03af93af729cfadbd462 Mon Sep 17 00:00:00 2001 From: Ayushi Sharma Date: Sun, 21 Apr 2024 21:33:21 +0530 Subject: [PATCH 2/9] feat: fixed filter functionality --- .../apsara-ui/src/Listing/InfiniteScroll.tsx | 48 +++++++------------ .../apsara-ui/src/Listing/Listing.stories.tsx | 43 +++++++++++++---- 2 files changed, 51 insertions(+), 40 deletions(-) diff --git a/packages/apsara-ui/src/Listing/InfiniteScroll.tsx b/packages/apsara-ui/src/Listing/InfiniteScroll.tsx index 528a8deb..65899160 100644 --- a/packages/apsara-ui/src/Listing/InfiniteScroll.tsx +++ b/packages/apsara-ui/src/Listing/InfiniteScroll.tsx @@ -4,8 +4,10 @@ import React, { useState, useEffect } from "react"; interface InfiniteScrollProps { fetchMoreData: (page: number, pageSize: number, filters: any) => Promise; contentRef: React.RefObject; + page: number; pageSize?: number; filters?: any; + threshold?: number; renderItem: (item: T) => React.ReactNode; loadingComponent?: React.ReactNode; noMoreDataComponent?: React.ReactNode; @@ -14,26 +16,30 @@ interface InfiniteScrollProps { const InfiniteScroll: React.FC> = ({ fetchMoreData, contentRef, + page, pageSize = 10, + threshold = 0, filters, renderItem, loadingComponent, noMoreDataComponent, }) => { const [data, setData] = useState([]); - const [page, setPage] = useState(1); const [isLoading, setIsLoading] = useState(false); const [hasMoreData, setHasMoreData] = useState(true); - const [isFirstFetch, setIsFirstFetch] = useState(true); - const onBottomHit = debounce(async () => { + useEffect(() => { + setData([]); + setHasMoreData(true); + }, [filters]); + + const fetchMore = debounce(async () => { if (!isLoading && hasMoreData && isBottom(contentRef)) { setIsLoading(true); try { const newData = await fetchMoreData(page, pageSize, filters); setData((prevData) => prevData.concat(newData)); - setPage((prevPage) => prevPage + 1); - setHasMoreData(newData.length > 0); + setHasMoreData(newData.length < pageSize ? false : true); } finally { setIsLoading(false); } @@ -41,23 +47,17 @@ const InfiniteScroll: React.FC> = ({ }, 200); useEffect(() => { - setPage(1); - setHasMoreData(true); - setData([]); - }, [filters]); - - useEffect(() => { - if (isFirstFetch) { - onBottomHit(); + if (page == 1 && !data.length) { + fetchMore(); } - setIsFirstFetch(false); const onScroll = () => { - onBottomHit(); + if (page != 1) { + fetchMore(); + } }; const currentContentRef = contentRef.current; - if (currentContentRef) { currentContentRef.addEventListener("scroll", onScroll); } @@ -67,19 +67,7 @@ const InfiniteScroll: React.FC> = ({ currentContentRef.removeEventListener("scroll", onScroll); } }; - }, [ - contentRef, - isLoading, - hasMoreData, - page, - filters, - fetchMoreData, - setData, - setPage, - setHasMoreData, - isFirstFetch, - onBottomHit, - ]); + }, [contentRef, fetchMore, page, filters]); const isBottom = (ref: React.RefObject) => { if (!ref.current) { @@ -88,7 +76,7 @@ const InfiniteScroll: React.FC> = ({ const { scrollTop, scrollHeight, clientHeight } = ref.current; - return scrollTop + clientHeight >= scrollHeight; + return scrollTop + clientHeight >= scrollHeight - threshold; }; return ( diff --git a/packages/apsara-ui/src/Listing/Listing.stories.tsx b/packages/apsara-ui/src/Listing/Listing.stories.tsx index 1d4a3d68..d985f6db 100644 --- a/packages/apsara-ui/src/Listing/Listing.stories.tsx +++ b/packages/apsara-ui/src/Listing/Listing.stories.tsx @@ -1,8 +1,9 @@ -import React, { useRef, 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", @@ -194,18 +195,20 @@ export const infiniteListingWithApply = () => { export const infiniteListWithCustomComponent = () => { const pageSize = 10; const contentRef = useRef(null); - const fetchRecords = (page: number, pageSize: number) => { - return Array.from({ length: pageSize }, (_, i) => ({ - name: `user ${page * pageSize + i}`, - index: page * pageSize + i, - age: Math.floor(Math.random() * 41) + 20, // Random age between 20 and 60 - })); - }; + const [searchTerm, setSearchTerm] = useState(""); + const [filter, setFilter] = useState(); + const [page, setPage] = useState(1); const fetchMore = async (page: number, pageSize: number, filter: string) => { - console.log(filter); - const records = fetchRecords(page, pageSize); + 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; }; @@ -224,11 +227,31 @@ export const infiniteListWithCustomComponent = () => { ); }; + const onSubmit = (e: FormEvent) => { + e.preventDefault(); + setPage(1); + setFilter(searchTerm); + }; + return (
+
+ { + return setSearchTerm(e.target.value); + }} + placeholder="Type your search query here.." + // @ts-ignore + secondary={true} + /> + } From 66b17017be6c8fb9a3683495f7dda439df790e27 Mon Sep 17 00:00:00 2001 From: Ayushi Sharma Date: Tue, 23 Apr 2024 00:53:17 +0530 Subject: [PATCH 3/9] feat(InfiniteScroll): added useCallback in fetchMore func --- .../apsara-ui/src/Listing/InfiniteScroll.tsx | 39 ++++++++++--------- .../apsara-ui/src/Listing/Listing.stories.tsx | 2 +- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/packages/apsara-ui/src/Listing/InfiniteScroll.tsx b/packages/apsara-ui/src/Listing/InfiniteScroll.tsx index 65899160..2a0dd655 100644 --- a/packages/apsara-ui/src/Listing/InfiniteScroll.tsx +++ b/packages/apsara-ui/src/Listing/InfiniteScroll.tsx @@ -1,5 +1,5 @@ import { debounce } from "lodash"; -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useCallback } from "react"; interface InfiniteScrollProps { fetchMoreData: (page: number, pageSize: number, filters: any) => Promise; @@ -33,8 +33,18 @@ const InfiniteScroll: React.FC> = ({ setHasMoreData(true); }, [filters]); - const fetchMore = debounce(async () => { - if (!isLoading && hasMoreData && isBottom(contentRef)) { + const isBottom = (ref: React.RefObject) => { + if (!ref.current) { + return false; + } + + const { scrollTop, scrollHeight, clientHeight } = ref.current; + + return scrollTop + clientHeight >= scrollHeight - threshold; + }; + + const fetchMore = useCallback( + debounce(async () => { setIsLoading(true); try { const newData = await fetchMoreData(page, pageSize, filters); @@ -43,16 +53,19 @@ const InfiniteScroll: React.FC> = ({ } finally { setIsLoading(false); } - } - }, 200); + }, 200), + [page, filters, isLoading], + ); useEffect(() => { - if (page == 1 && !data.length) { + if (page === 1 && !data.length && !isLoading && filters !== undefined) { + console.log("fetchMore on initial fetch"); fetchMore(); } const onScroll = () => { - if (page != 1) { + if (!isLoading && hasMoreData && isBottom(contentRef)) { + console.log("fetchMore on scroll to bottom"); fetchMore(); } }; @@ -67,17 +80,7 @@ const InfiniteScroll: React.FC> = ({ currentContentRef.removeEventListener("scroll", onScroll); } }; - }, [contentRef, fetchMore, page, filters]); - - const isBottom = (ref: React.RefObject) => { - if (!ref.current) { - return false; - } - - const { scrollTop, scrollHeight, clientHeight } = ref.current; - - return scrollTop + clientHeight >= scrollHeight - threshold; - }; + }, [contentRef, fetchMore, filters, data, hasMoreData, isBottom, isLoading, page]); return (
diff --git a/packages/apsara-ui/src/Listing/Listing.stories.tsx b/packages/apsara-ui/src/Listing/Listing.stories.tsx index d985f6db..93bee81f 100644 --- a/packages/apsara-ui/src/Listing/Listing.stories.tsx +++ b/packages/apsara-ui/src/Listing/Listing.stories.tsx @@ -196,7 +196,7 @@ export const infiniteListWithCustomComponent = () => { const pageSize = 10; const contentRef = useRef(null); const [searchTerm, setSearchTerm] = useState(""); - const [filter, setFilter] = useState(); + const [filter, setFilter] = useState(""); const [page, setPage] = useState(1); const fetchMore = async (page: number, pageSize: number, filter: string) => { From 5dbb2ca846fd6fc54a0f8c556f2c34fb9007294d Mon Sep 17 00:00:00 2001 From: Ayushi Sharma Date: Tue, 23 Apr 2024 08:48:00 +0530 Subject: [PATCH 4/9] feat(InfiniteScroll): make contentRef prop optional if not present take reference of infinite-list div --- .../apsara-ui/src/Listing/InfiniteScroll.tsx | 20 +++++++++++-------- .../apsara-ui/src/Listing/Listing.stories.tsx | 8 +++----- .../apsara-ui/src/Listing/Listing.styles.tsx | 4 ++-- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/apsara-ui/src/Listing/InfiniteScroll.tsx b/packages/apsara-ui/src/Listing/InfiniteScroll.tsx index 2a0dd655..3ca2995f 100644 --- a/packages/apsara-ui/src/Listing/InfiniteScroll.tsx +++ b/packages/apsara-ui/src/Listing/InfiniteScroll.tsx @@ -1,9 +1,9 @@ import { debounce } from "lodash"; -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useRef } from "react"; interface InfiniteScrollProps { fetchMoreData: (page: number, pageSize: number, filters: any) => Promise; - contentRef: React.RefObject; + contentRef?: React.RefObject; page: number; pageSize?: number; filters?: any; @@ -28,6 +28,8 @@ const InfiniteScroll: React.FC> = ({ const [isLoading, setIsLoading] = useState(false); const [hasMoreData, setHasMoreData] = useState(true); + const defaultContentRef = useRef(null); + const refToUse = contentRef || defaultContentRef; useEffect(() => { setData([]); setHasMoreData(true); @@ -59,18 +61,16 @@ const InfiniteScroll: React.FC> = ({ useEffect(() => { if (page === 1 && !data.length && !isLoading && filters !== undefined) { - console.log("fetchMore on initial fetch"); fetchMore(); } const onScroll = () => { - if (!isLoading && hasMoreData && isBottom(contentRef)) { - console.log("fetchMore on scroll to bottom"); + if (!isLoading && hasMoreData && isBottom(refToUse)) { fetchMore(); } }; - const currentContentRef = contentRef.current; + const currentContentRef = refToUse.current; if (currentContentRef) { currentContentRef.addEventListener("scroll", onScroll); } @@ -80,10 +80,14 @@ const InfiniteScroll: React.FC> = ({ currentContentRef.removeEventListener("scroll", onScroll); } }; - }, [contentRef, fetchMore, filters, data, hasMoreData, isBottom, isLoading, page]); + }, [contentRef, defaultContentRef, fetchMore, filters, data, hasMoreData, isBottom, isLoading, page]); return ( -
+
{data.map(renderItem)} {isLoading ? loadingComponent : null} {!hasMoreData && noMoreDataComponent} diff --git a/packages/apsara-ui/src/Listing/Listing.stories.tsx b/packages/apsara-ui/src/Listing/Listing.stories.tsx index 93bee81f..c4048e17 100644 --- a/packages/apsara-ui/src/Listing/Listing.stories.tsx +++ b/packages/apsara-ui/src/Listing/Listing.stories.tsx @@ -1,7 +1,7 @@ import React, { FormEvent, useRef, useState } from "react"; import Listing from "./Listing"; import InfiniteListing from "./InfiniteListing"; -import { ScrollableList, UserCard } from "./Listing.styles"; +import { ListContainer, UserCard } from "./Listing.styles"; import InfiniteScroll from "./InfiniteScroll"; import Search from "../Search"; @@ -194,7 +194,6 @@ export const infiniteListingWithApply = () => { export const infiniteListWithCustomComponent = () => { const pageSize = 10; - const contentRef = useRef(null); const [searchTerm, setSearchTerm] = useState(""); const [filter, setFilter] = useState(""); const [page, setPage] = useState(1); @@ -247,18 +246,17 @@ export const infiniteListWithCustomComponent = () => { secondary={true} /> - + } loadingComponent={
Loading...
} noMoreDataComponent={
No more data to fetch!
} >
-
+
); }; diff --git a/packages/apsara-ui/src/Listing/Listing.styles.tsx b/packages/apsara-ui/src/Listing/Listing.styles.tsx index d3cc3caa..df431141 100644 --- a/packages/apsara-ui/src/Listing/Listing.styles.tsx +++ b/packages/apsara-ui/src/Listing/Listing.styles.tsx @@ -20,7 +20,7 @@ export const FilterActions = styled.span` align-items: center; `; -export const ScrollableList = styled.div` +export const ListContainer = styled.div` position: absolute; top: 50%; left: 50%; @@ -28,7 +28,7 @@ export const ScrollableList = styled.div` width: 90%; height: 70%; background-color: ${({ theme }) => theme.backgroundColor || "white"}; - overflow-y: auto; + overflow: hidden; box-shadow: rgb(179 179 179 / 31%) 0px 1px 5px; `; From 9056b4ca4c456462c3d0a03a2e5fdec42547893a Mon Sep 17 00:00:00 2001 From: Ayushi Sharma Date: Tue, 23 Apr 2024 09:55:19 +0530 Subject: [PATCH 5/9] feat(InfiniteScroll): make InfiniteScroll a separate componet --- .../InfiniteScroll/InfiniteScroll.styles.tsx | 48 +++++++ .../InfiniteScroll.tsx | 2 +- .../src/InfiniteScroll/Listing.stories.tsx | 118 ++++++++++++++++++ .../apsara-ui/src/InfiniteScroll/index.tsx | 1 + .../apsara-ui/src/Listing/Listing.stories.tsx | 74 +---------- .../apsara-ui/src/Listing/Listing.styles.tsx | 86 ------------- packages/apsara-ui/src/Listing/index.tsx | 1 - packages/apsara-ui/src/index.ts | 3 +- 8 files changed, 171 insertions(+), 162 deletions(-) create mode 100644 packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.styles.tsx rename packages/apsara-ui/src/{Listing => InfiniteScroll}/InfiniteScroll.tsx (96%) create mode 100644 packages/apsara-ui/src/InfiniteScroll/Listing.stories.tsx create mode 100644 packages/apsara-ui/src/InfiniteScroll/index.tsx diff --git a/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.styles.tsx b/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.styles.tsx new file mode 100644 index 00000000..8643a36a --- /dev/null +++ b/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.styles.tsx @@ -0,0 +1,48 @@ +import styled from "styled-components"; + +export 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; + } + } + } +`; diff --git a/packages/apsara-ui/src/Listing/InfiniteScroll.tsx b/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.tsx similarity index 96% rename from packages/apsara-ui/src/Listing/InfiniteScroll.tsx rename to packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.tsx index 3ca2995f..b7ac5943 100644 --- a/packages/apsara-ui/src/Listing/InfiniteScroll.tsx +++ b/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.tsx @@ -80,7 +80,7 @@ const InfiniteScroll: React.FC> = ({ currentContentRef.removeEventListener("scroll", onScroll); } }; - }, [contentRef, defaultContentRef, fetchMore, filters, data, hasMoreData, isBottom, isLoading, page]); + }, [refToUse, contentRef, defaultContentRef, fetchMore, filters, data, hasMoreData, isBottom, isLoading, page]); return (
{ + return { + key: index, + name: `name ${index}`, + status: index % 2 ? "active" : "inactive", + age: index, + address: `A${index} Downing Street`, + }; + }); +} + +export const infiniteListWithCustomComponent = () => { + const pageSize = 10; + const [searchTerm, setSearchTerm] = useState(""); + const [filter, setFilter] = useState(""); + const [page, setPage] = useState(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 ( + +
{user.name}
+
+
+
Address
+
{user.address}
+
+
+
Age
+
{user.age}
+
+
+
+ ); + }; + + const onSubmit = (e: FormEvent) => { + e.preventDefault(); + setPage(1); + setFilter(searchTerm); + }; + + return ( +
+
+ { + return setSearchTerm(e.target.value); + }} + placeholder="Type your search query here.." + // @ts-ignore + secondary={true} + /> + +
+ } + loadingComponent={
Loading...
} + noMoreDataComponent={
No more data to fetch!
} + >
+
+
+ ); +}; diff --git a/packages/apsara-ui/src/InfiniteScroll/index.tsx b/packages/apsara-ui/src/InfiniteScroll/index.tsx new file mode 100644 index 00000000..4d39641c --- /dev/null +++ b/packages/apsara-ui/src/InfiniteScroll/index.tsx @@ -0,0 +1 @@ +export default "./InfiniteScroll"; diff --git a/packages/apsara-ui/src/Listing/Listing.stories.tsx b/packages/apsara-ui/src/Listing/Listing.stories.tsx index c4048e17..e62bf3c8 100644 --- a/packages/apsara-ui/src/Listing/Listing.stories.tsx +++ b/packages/apsara-ui/src/Listing/Listing.stories.tsx @@ -1,9 +1,6 @@ -import React, { FormEvent, useRef, useState } from "react"; +import React, { useState } from "react"; import Listing from "./Listing"; import InfiniteListing from "./InfiniteListing"; -import { ListContainer, UserCard } from "./Listing.styles"; -import InfiniteScroll from "./InfiniteScroll"; -import Search from "../Search"; export default { title: "Data Display/Listing", @@ -191,72 +188,3 @@ export const infiniteListingWithApply = () => { /> ); }; - -export const infiniteListWithCustomComponent = () => { - const pageSize = 10; - const [searchTerm, setSearchTerm] = useState(""); - const [filter, setFilter] = useState(""); - const [page, setPage] = useState(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 ( - -
-
-
{user.name}
-
-
-
Age {user.age}
-
-
-
- ); - }; - - const onSubmit = (e: FormEvent) => { - e.preventDefault(); - setPage(1); - setFilter(searchTerm); - }; - - return ( -
-
- { - return setSearchTerm(e.target.value); - }} - placeholder="Type your search query here.." - // @ts-ignore - secondary={true} - /> - - - } - loadingComponent={
Loading...
} - noMoreDataComponent={
No more data to fetch!
} - >
-
-
- ); -}; diff --git a/packages/apsara-ui/src/Listing/Listing.styles.tsx b/packages/apsara-ui/src/Listing/Listing.styles.tsx index df431141..bd553fef 100644 --- a/packages/apsara-ui/src/Listing/Listing.styles.tsx +++ b/packages/apsara-ui/src/Listing/Listing.styles.tsx @@ -19,89 +19,3 @@ export const FilterActions = styled.span` display: flex; align-items: center; `; - -export const ListContainer = styled.div` - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 90%; - height: 70%; - background-color: ${({ theme }) => theme.backgroundColor || "white"}; - overflow: hidden; - box-shadow: rgb(179 179 179 / 31%) 0px 1px 5px; -`; - -export const UserCard = styled.div` - 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; - } - } - } -`; diff --git a/packages/apsara-ui/src/Listing/index.tsx b/packages/apsara-ui/src/Listing/index.tsx index 47d071ca..884b8a94 100644 --- a/packages/apsara-ui/src/Listing/index.tsx +++ b/packages/apsara-ui/src/Listing/index.tsx @@ -1,5 +1,4 @@ import Listing from "./Listing"; -export { default as InfiniteScroll } from "./InfiniteScroll"; export { default as InfiniteListing } from "./InfiniteListing"; export { default as useSearchFilter } from "./hooks/useSearchFilter"; diff --git a/packages/apsara-ui/src/index.ts b/packages/apsara-ui/src/index.ts index 09e81786..ac625ec8 100644 --- a/packages/apsara-ui/src/index.ts +++ b/packages/apsara-ui/src/index.ts @@ -1,6 +1,6 @@ import Button from "./Button"; import Icon, { IconImage } from "./Icon"; -import Listing, { useSearchFilter, InfiniteListing, InfiniteScroll } from "./Listing"; +import Listing, { useSearchFilter, InfiniteListing } from "./Listing"; import Search from "./Search"; import Table from "./Table"; import VTable from "./Table/VirtualisedTable"; @@ -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"; From e297069acd4adf7c19211aef69d8bfca65f760f9 Mon Sep 17 00:00:00 2001 From: Stewart Jingga Date: Tue, 23 Apr 2024 14:51:23 +0700 Subject: [PATCH 6/9] feat(infinite-scroll): change props contract --- .../InfiniteScroll/InfiniteScroll.styles.tsx | 50 +--- .../src/InfiniteScroll/InfiniteScroll.tsx | 127 ++++----- .../src/InfiniteScroll/Listing.stories.tsx | 259 ++++++++++++------ 3 files changed, 233 insertions(+), 203 deletions(-) diff --git a/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.styles.tsx b/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.styles.tsx index 8643a36a..f5b7b08b 100644 --- a/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.styles.tsx +++ b/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.styles.tsx @@ -1,48 +1,8 @@ import styled from "styled-components"; -export 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; - } - } - } +export const InfiniteScrollWrapper = styled.div` + position: relative; + height: calc(100% - 0px); + height: calc(100% - 200px); + overflow-y: scroll; `; diff --git a/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.tsx b/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.tsx index b7ac5943..166df768 100644 --- a/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.tsx +++ b/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.tsx @@ -1,98 +1,81 @@ -import { debounce } from "lodash"; -import React, { useState, useEffect, useCallback, useRef } from "react"; +import React, { useEffect, useRef } from "react"; + +import { InfiniteScrollWrapper } from "./InfiniteScroll.styles"; interface InfiniteScrollProps { - fetchMoreData: (page: number, pageSize: number, filters: any) => Promise; - contentRef?: React.RefObject; - page: number; - pageSize?: number; - filters?: any; - threshold?: number; renderItem: (item: T) => React.ReactNode; + onBottomScroll: () => void; + items: T[]; + style?: React.CSSProperties; + isLoading?: boolean; + noMoreData?: boolean; + containerRef?: React.RefObject; + threshold?: number; loadingComponent?: React.ReactNode; noMoreDataComponent?: React.ReactNode; } -const InfiniteScroll: React.FC> = ({ - fetchMoreData, - contentRef, - page, - pageSize = 10, - threshold = 0, - filters, +const InfiniteScroll = ({ renderItem, + onBottomScroll, + items, + style, + isLoading, + noMoreData, + containerRef, + threshold = 0, loadingComponent, noMoreDataComponent, -}) => { - const [data, setData] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [hasMoreData, setHasMoreData] = useState(true); - - const defaultContentRef = useRef(null); - const refToUse = contentRef || defaultContentRef; - useEffect(() => { - setData([]); - setHasMoreData(true); - }, [filters]); - - const isBottom = (ref: React.RefObject) => { - if (!ref.current) { - return false; - } - - const { scrollTop, scrollHeight, clientHeight } = ref.current; - - return scrollTop + clientHeight >= scrollHeight - threshold; - }; - - const fetchMore = useCallback( - debounce(async () => { - 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), - [page, filters, isLoading], - ); + ...props +}: InfiniteScrollProps) => { + const defaultContainerRef = useRef(null); + const containerElem = containerRef ? containerRef.current : defaultContainerRef.current; useEffect(() => { - if (page === 1 && !data.length && !isLoading && filters !== undefined) { - fetchMore(); - } + if (!containerElem) return; const onScroll = () => { - if (!isLoading && hasMoreData && isBottom(refToUse)) { - fetchMore(); + if (isBottom(containerElem, threshold)) { + onBottomScroll(); } - }; - - const currentContentRef = refToUse.current; - if (currentContentRef) { - currentContentRef.addEventListener("scroll", onScroll); } + containerElem.addEventListener("scroll", onScroll); + return () => { - if (currentContentRef) { - currentContentRef.removeEventListener("scroll", onScroll); - } + containerElem.removeEventListener("scroll", onScroll); }; - }, [refToUse, contentRef, defaultContentRef, fetchMore, filters, data, hasMoreData, isBottom, isLoading, page]); + }, [containerElem, onBottomScroll, threshold]); + + const loadingComp = loadingComponent || ; return ( -
- {data.map(renderItem)} - {isLoading ? loadingComponent : null} - {!hasMoreData && noMoreDataComponent} -
+ {items?.map(renderItem)} + {isLoading && loadingComp} + {!noMoreData && noMoreDataComponent} + ); }; +const DefaultLoading = () =>
Loading...
; + +const isBottom = (elem: HTMLElement, threshold: number): boolean => { + if (!elem) return false; + + const { scrollTop, scrollHeight, clientHeight } = elem; + + const a = scrollTop + clientHeight; + const b = scrollHeight - threshold; + + console.log(a); + console.log(b); + return a >= b; +}; + export default InfiniteScroll; diff --git a/packages/apsara-ui/src/InfiniteScroll/Listing.stories.tsx b/packages/apsara-ui/src/InfiniteScroll/Listing.stories.tsx index cb690573..7a2e4714 100644 --- a/packages/apsara-ui/src/InfiniteScroll/Listing.stories.tsx +++ b/packages/apsara-ui/src/InfiniteScroll/Listing.stories.tsx @@ -1,7 +1,7 @@ -import React, { FormEvent, useState } from "react"; -import { UserCard } from "./InfiniteScroll.styles"; +import React, { FormEvent, useCallback, useEffect, useState } from "react"; import InfiniteScroll from "./InfiniteScroll"; import Search from "../Search"; +import styled from "styled-components"; export default { title: "Data Display/InfiniteScroll", @@ -16,8 +16,8 @@ interface User { address: string; } -function getData(page = 1): User[] { - return new Array(page * 100).fill(0).map((_, index) => { +function getData(page = 1, page_size = 10): User[] { + return new Array(page * page_size).fill(0).map((_, index) => { return { key: index, name: `name ${index}`, @@ -28,91 +28,178 @@ function getData(page = 1): User[] { }); } -export const infiniteListWithCustomComponent = () => { - const pageSize = 10; - const [searchTerm, setSearchTerm] = useState(""); - const [filter, setFilter] = useState(""); - const [page, setPage] = useState(1); +function getDataAsync(page = 1, page_size = 10): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve(getData(page, page_size)); + }, 1000); + }); +} - const fetchMore = async (page: number, pageSize: number, filter: string) => { - if (page === 4) return []; +export const basic = () => { + const [data, setData] = useState([]); + const [isFetching, setIsFetching] = useState(false); - let records = getData(page); - if (filter) { - records = records.filter((record) => record.name.toLowerCase().includes(filter.toLowerCase())); - } + const fetchData = useCallback(() => { + setIsFetching(true) + getDataAsync() + .then(d => setData(curr => curr.concat(d))) + .finally(() => setIsFetching(false)) + }, []); - setPage((prevPage) => prevPage + 1); - records = records.slice(0, pageSize); - return records; - }; - - const Card = ({ user }: { user: User }) => { - return ( - -
{user.name}
-
-
-
Address
-
{user.address}
-
-
-
Age
-
{user.age}
-
-
-
- ); - }; - - const onSubmit = (e: FormEvent) => { - e.preventDefault(); - setPage(1); - setFilter(searchTerm); - }; + useEffect(() => { + fetchData(); + }, [fetchData]) + + const onScroll = () => { + if (isFetching) return; + + fetchData(); + } return ( -
-
- { - return setSearchTerm(e.target.value); - }} - placeholder="Type your search query here.." - // @ts-ignore - secondary={true} - /> - -
- } - loadingComponent={
Loading...
} - noMoreDataComponent={
No more data to fetch!
} - >
-
+
+ + items={data} + renderItem={(item) =>
{item.name}
} + onBottomScroll={onScroll} + isLoading={isFetching} + />
- ); -}; + ) +} + +// export const infiniteListWithCustomComponent = () => { +// const pageSize = 10; +// const [searchTerm, setSearchTerm] = useState(""); +// const [filter, setFilter] = useState(""); +// const [page, setPage] = useState(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 ( +// +//
{user.name}
+//
+//
+//
Address
+//
{user.address}
+//
+//
+//
Age
+//
{user.age}
+//
+//
+//
+// ); +// }; + +// const onSubmit = (e: FormEvent) => { +// e.preventDefault(); +// setPage(1); +// setFilter(searchTerm); +// }; + +// return ( +//
+//
+// { +// return setSearchTerm(e.target.value); +// }} +// placeholder="Type your search query here.." +// // @ts-ignore +// secondary={true} +// /> +// +//
+// +// fetchMoreData={fetchMore} +// filters={filter} +// pageSize={pageSize} +// renderItem={(user: User) => } +// loadingComponent={
Loading...
} +// noMoreDataComponent={
No more data to fetch!
} +// > +//
+//
+// ); +// }; + +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; + } + } + } +`; From 5a2df372352ad83556659460725918fa4c817eca Mon Sep 17 00:00:00 2001 From: Ayushi Sharma Date: Wed, 24 Apr 2024 08:52:59 +0530 Subject: [PATCH 7/9] feat: overload component height if provided in props --- .../InfiniteScroll/InfiniteScroll.stories.tsx | 134 ++++++++++++ .../InfiniteScroll/InfiniteScroll.styles.tsx | 8 - .../src/InfiniteScroll/InfiniteScroll.tsx | 31 +-- .../src/InfiniteScroll/Listing.stories.tsx | 205 ------------------ .../apsara-ui/src/Listing/Listing.stories.tsx | 2 +- 5 files changed, 151 insertions(+), 229 deletions(-) create mode 100644 packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.stories.tsx delete mode 100644 packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.styles.tsx delete mode 100644 packages/apsara-ui/src/InfiniteScroll/Listing.stories.tsx diff --git a/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.stories.tsx b/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.stories.tsx new file mode 100644 index 00000000..2c640ee5 --- /dev/null +++ b/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.stories.tsx @@ -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 { + return new Promise((resolve) => { + setTimeout(() => { + resolve(getData(page, page_size)); + }, 1000); + }); +} + +export const basic = () => { + const [data, setData] = useState([]); + const [isFetching, setIsFetching] = useState(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 ( +
+ + items={data} + renderItem={(item) => } + onBottomScroll={onScroll} + isLoading={isFetching} + /> +
+ ); +}; + +const Card = ({ user }: { user: User }) => { + return ( + +
{user.name}
+
+
+
Address
+
{user.address}
+
+
+
Age
+
{user.age}
+
+
+
+ ); +}; + +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; + } + } + } +`; diff --git a/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.styles.tsx b/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.styles.tsx deleted file mode 100644 index f5b7b08b..00000000 --- a/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.styles.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import styled from "styled-components"; - -export const InfiniteScrollWrapper = styled.div` - position: relative; - height: calc(100% - 0px); - height: calc(100% - 200px); - overflow-y: scroll; -`; diff --git a/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.tsx b/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.tsx index 166df768..e920a981 100644 --- a/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.tsx +++ b/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.tsx @@ -1,11 +1,10 @@ import React, { useEffect, useRef } from "react"; -import { InfiniteScrollWrapper } from "./InfiniteScroll.styles"; - interface InfiniteScrollProps { renderItem: (item: T) => React.ReactNode; onBottomScroll: () => void; items: T[]; + height?: string; style?: React.CSSProperties; isLoading?: boolean; noMoreData?: boolean; @@ -20,13 +19,13 @@ const InfiniteScroll = ({ onBottomScroll, items, style, + height, isLoading, noMoreData, containerRef, - threshold = 0, + threshold = 1, loadingComponent, noMoreDataComponent, - ...props }: InfiniteScrollProps) => { const defaultContainerRef = useRef(null); const containerElem = containerRef ? containerRef.current : defaultContainerRef.current; @@ -38,7 +37,7 @@ const InfiniteScroll = ({ if (isBottom(containerElem, threshold)) { onBottomScroll(); } - } + }; containerElem.addEventListener("scroll", onScroll); @@ -47,19 +46,23 @@ const InfiniteScroll = ({ }; }, [containerElem, onBottomScroll, threshold]); - const loadingComp = loadingComponent || ; + const loadingComp = loadingComponent || ; + + const scrollStyle: React.CSSProperties = !containerRef + ? { + height: height || "100%", + overflow: "scroll", + position: "relative", + ...style, + } + : {}; return ( - +
{items?.map(renderItem)} {isLoading && loadingComp} {!noMoreData && noMoreDataComponent} - +
); }; @@ -73,8 +76,6 @@ const isBottom = (elem: HTMLElement, threshold: number): boolean => { const a = scrollTop + clientHeight; const b = scrollHeight - threshold; - console.log(a); - console.log(b); return a >= b; }; diff --git a/packages/apsara-ui/src/InfiniteScroll/Listing.stories.tsx b/packages/apsara-ui/src/InfiniteScroll/Listing.stories.tsx deleted file mode 100644 index 7a2e4714..00000000 --- a/packages/apsara-ui/src/InfiniteScroll/Listing.stories.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import React, { FormEvent, useCallback, useEffect, useState } from "react"; -import InfiniteScroll from "./InfiniteScroll"; -import Search from "../Search"; -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 { - return new Promise((resolve) => { - setTimeout(() => { - resolve(getData(page, page_size)); - }, 1000); - }); -} - -export const basic = () => { - const [data, setData] = useState([]); - const [isFetching, setIsFetching] = useState(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 ( -
- - items={data} - renderItem={(item) =>
{item.name}
} - onBottomScroll={onScroll} - isLoading={isFetching} - /> -
- ) -} - -// export const infiniteListWithCustomComponent = () => { -// const pageSize = 10; -// const [searchTerm, setSearchTerm] = useState(""); -// const [filter, setFilter] = useState(""); -// const [page, setPage] = useState(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 ( -// -//
{user.name}
-//
-//
-//
Address
-//
{user.address}
-//
-//
-//
Age
-//
{user.age}
-//
-//
-//
-// ); -// }; - -// const onSubmit = (e: FormEvent) => { -// e.preventDefault(); -// setPage(1); -// setFilter(searchTerm); -// }; - -// return ( -//
-//
-// { -// return setSearchTerm(e.target.value); -// }} -// placeholder="Type your search query here.." -// // @ts-ignore -// secondary={true} -// /> -// -//
-// -// fetchMoreData={fetchMore} -// filters={filter} -// pageSize={pageSize} -// renderItem={(user: User) => } -// loadingComponent={
Loading...
} -// noMoreDataComponent={
No more data to fetch!
} -// > -//
-//
-// ); -// }; - -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; - } - } - } -`; diff --git a/packages/apsara-ui/src/Listing/Listing.stories.tsx b/packages/apsara-ui/src/Listing/Listing.stories.tsx index e62bf3c8..88677439 100644 --- a/packages/apsara-ui/src/Listing/Listing.stories.tsx +++ b/packages/apsara-ui/src/Listing/Listing.stories.tsx @@ -28,7 +28,7 @@ function getData(page = 1): User[] { } export const listing = () => ( -
+

Listing

loading={false} From 6324665463cffdea3a9cf8f316cef74329bd7565 Mon Sep 17 00:00:00 2001 From: Ayushi Sharma Date: Thu, 25 Apr 2024 10:31:29 +0530 Subject: [PATCH 8/9] feat: make default height (100%-0px) --- packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.tsx b/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.tsx index e920a981..6bc08281 100644 --- a/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.tsx +++ b/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.tsx @@ -50,7 +50,7 @@ const InfiniteScroll = ({ const scrollStyle: React.CSSProperties = !containerRef ? { - height: height || "100%", + height: height || "calc(100%-0px)", overflow: "scroll", position: "relative", ...style, From 10de210f7e09d3467f960fc7bceb0c4363cabbc7 Mon Sep 17 00:00:00 2001 From: Ayushi Sharma Date: Thu, 25 Apr 2024 10:35:24 +0530 Subject: [PATCH 9/9] feat: move containerElem inside useEffect --- packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.tsx b/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.tsx index 6bc08281..ad182bed 100644 --- a/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.tsx +++ b/packages/apsara-ui/src/InfiniteScroll/InfiniteScroll.tsx @@ -28,9 +28,9 @@ const InfiniteScroll = ({ noMoreDataComponent, }: InfiniteScrollProps) => { const defaultContainerRef = useRef(null); - const containerElem = containerRef ? containerRef.current : defaultContainerRef.current; useEffect(() => { + const containerElem = containerRef ? containerRef.current : defaultContainerRef.current; if (!containerElem) return; const onScroll = () => { @@ -44,7 +44,7 @@ const InfiniteScroll = ({ return () => { containerElem.removeEventListener("scroll", onScroll); }; - }, [containerElem, onBottomScroll, threshold]); + }, [containerRef, defaultContainerRef, onBottomScroll, threshold]); const loadingComp = loadingComponent || ;