From 58fa171a8e29d3df8f115b8f2fe953f7622ba85c Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Fri, 17 Jan 2025 11:39:20 -0700 Subject: [PATCH] more elegant infinite list component Co-Authored-By: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- .../PermissionListDatasetGroupContainer.tsx | 27 +++---- web/src/app/test/list/test2/page.tsx | 56 ++++++++++++++ .../BusterInfiniteList/BusterInfiniteList.tsx | 73 +++++++++++-------- 3 files changed, 112 insertions(+), 44 deletions(-) create mode 100644 web/src/app/test/list/test2/page.tsx diff --git a/web/src/app/app/datasets/[datasetId]/permissions/_PermissionDatasetGroups/PermissionListDatasetGroupContainer.tsx b/web/src/app/app/datasets/[datasetId]/permissions/_PermissionDatasetGroups/PermissionListDatasetGroupContainer.tsx index 35c6acfdb..d5b235853 100644 --- a/web/src/app/app/datasets/[datasetId]/permissions/_PermissionDatasetGroups/PermissionListDatasetGroupContainer.tsx +++ b/web/src/app/app/datasets/[datasetId]/permissions/_PermissionDatasetGroups/PermissionListDatasetGroupContainer.tsx @@ -156,20 +156,17 @@ const DatasetGroupAssignedCell: React.FC<{ id: string; assigned: boolean; onSelect: (params: { id: string; assigned: boolean }) => Promise; -}> = React.memo( - ({ id, assigned, onSelect }) => { - return ( - { + onSelect({ id, assigned: value }); + }} + /> + ); +}); DatasetGroupAssignedCell.displayName = 'DatasetGroupAssignedCell'; diff --git a/web/src/app/test/list/test2/page.tsx b/web/src/app/test/list/test2/page.tsx new file mode 100644 index 000000000..4ecdd53c4 --- /dev/null +++ b/web/src/app/test/list/test2/page.tsx @@ -0,0 +1,56 @@ +'use client'; + +import { useRef } from 'react'; +import { Virtualizer } from 'virtua'; +import React from 'react'; +import { useMount } from 'ahooks'; + +const headerHeight = 300; + +const ItemSwag = React.memo(({ index }: { index: number }) => { + useMount(() => { + console.log('useMount', index); + }); + return
Swag {index}
; +}); +ItemSwag.displayName = 'ItemSwag'; + +const createRows = (numberToGenerate: number) => { + return Array.from({ length: numberToGenerate }, (_, index) => { + return ; + }); +}; + +export default function ListTest2() { + const ref = useRef(null); + const outerPadding = 30; + const innerPadding = 50; + + return ( +
+
+
+ + {createRows(1000)} + +
+
+
+ ); +} diff --git a/web/src/components/list/BusterInfiniteList/BusterInfiniteList.tsx b/web/src/components/list/BusterInfiniteList/BusterInfiniteList.tsx index a8eb07cea..7be8383be 100644 --- a/web/src/components/list/BusterInfiniteList/BusterInfiniteList.tsx +++ b/web/src/components/list/BusterInfiniteList/BusterInfiniteList.tsx @@ -1,9 +1,8 @@ -import React from 'react'; +import React, { useRef } from 'react'; import { useMemoizedFn } from 'ahooks'; import { BusterListProps } from '../BusterList'; import { getAllIdsInSection } from '../BusterList/helpers'; -import { WindowVirtualizer } from 'virtua'; -import { useEffect, useMemo, useRef, useCallback } from 'react'; +import { useEffect, useMemo } from 'react'; import { BusterListHeader } from '../BusterList/BusterListHeader'; import { BusterListRowComponentSelector } from '../BusterList/BusterListRowComponentSelector'; @@ -26,8 +25,10 @@ export const BusterInfiniteList: React.FC = ({ showSelectAll = true, onScrollEnd, loadingNewContent, - scrollEndThreshold = 200 // Default threshold of 200px + scrollEndThreshold = 48 // Default threshold of 200px }) => { + const containerRef = useRef(null); + const scrollRef = useRef(null); const showEmptyState = useMemo( () => (!rows || rows.length === 0 || !rows.some((row) => !row.rowSection)) && !!emptyState, [rows, emptyState] @@ -86,27 +87,44 @@ export const BusterInfiniteList: React.FC = ({ selectedRowKeys ]); - // Add scroll handler - const handleScroll = useCallback(() => { - // const { scrollTop, scrollHeight, clientHeight } = containerRef.current; - // const distanceToBottom = scrollHeight - scrollTop - clientHeight; - // console.log('distanceToBottom', distanceToBottom); - // if (distanceToBottom <= scrollEndThreshold) { - // onScrollEnd(); - // console.log('onScrollEnd'); - // } + useEffect(() => { + if (!onScrollEnd) return; + + // Find the first scrollable parent element + const findScrollableParent = (element: HTMLElement | null): HTMLDivElement | null => { + while (element) { + const { overflowY } = window.getComputedStyle(element); + if (overflowY === 'auto' || overflowY === 'scroll') { + return element as HTMLDivElement; + } + element = element.parentElement; + } + return null; + }; + + const scrollableParent = findScrollableParent(containerRef.current?.parentElement ?? null); + if (!scrollableParent) return; + + scrollRef.current = scrollableParent; + + // Check if we've scrolled near the bottom + const handleScroll = () => { + if (!scrollRef.current) return; + + const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; + const distanceFromBottom = scrollHeight - scrollTop - clientHeight; + + if (distanceFromBottom <= scrollEndThreshold) { + onScrollEnd(); + } + }; + + scrollableParent.addEventListener('scroll', handleScroll); + return () => scrollableParent.removeEventListener('scroll', handleScroll); }, [onScrollEnd, scrollEndThreshold]); - // Add scroll event listener - useEffect(() => { - // const container = containerRef.current; - // if (!container || !onScrollEnd) return; - // container.addEventListener('scroll', handleScroll); - // return () => container.removeEventListener('scroll', handleScroll); - }, [handleScroll, onScrollEnd]); - return ( -
+
{showHeader && !showEmptyState && ( = ({ /> )} - {!showEmptyState && ( - <> - {rows.map((row) => ( - - ))} - - )} + {!showEmptyState && + rows.map((row) => ( + + ))} {showEmptyState && (
{emptyState}