more elegant infinite list component

Co-Authored-By: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
This commit is contained in:
Nate Kelley 2025-01-17 11:39:20 -07:00
parent 221a4a6280
commit 58fa171a8e
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
3 changed files with 112 additions and 44 deletions

View File

@ -156,20 +156,17 @@ const DatasetGroupAssignedCell: React.FC<{
id: string; id: string;
assigned: boolean; assigned: boolean;
onSelect: (params: { id: string; assigned: boolean }) => Promise<void>; onSelect: (params: { id: string; assigned: boolean }) => Promise<void>;
}> = React.memo( }> = React.memo(({ id, assigned, onSelect }) => {
({ id, assigned, onSelect }) => { return (
return ( <Select
<Select options={options}
options={options} value={assigned || false}
defaultValue={assigned || false} popupMatchSelectWidth
popupMatchSelectWidth onSelect={(value) => {
onSelect={(value) => { onSelect({ id, assigned: value });
onSelect({ id, assigned: value }); }}
}} />
/> );
); });
},
() => true
);
DatasetGroupAssignedCell.displayName = 'DatasetGroupAssignedCell'; DatasetGroupAssignedCell.displayName = 'DatasetGroupAssignedCell';

View File

@ -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 <div className="h-[48px] border bg-red-200">Swag {index}</div>;
});
ItemSwag.displayName = 'ItemSwag';
const createRows = (numberToGenerate: number) => {
return Array.from({ length: numberToGenerate }, (_, index) => {
return <ItemSwag key={index} index={index} />;
});
};
export default function ListTest2() {
const ref = useRef<HTMLDivElement>(null);
const outerPadding = 30;
const innerPadding = 50;
return (
<div
ref={ref}
style={{
width: '100%',
height: '100vh',
overflowY: 'auto',
// opt out browser's scroll anchoring on header/footer because it will conflict to scroll anchoring of virtualizer
overflowAnchor: 'none'
}}>
<div
style={{
backgroundColor: 'burlywood',
padding: outerPadding
}}>
<div
style={{
backgroundColor: 'steelblue',
padding: innerPadding
}}>
<Virtualizer scrollRef={ref} startMargin={outerPadding + innerPadding}>
{createRows(1000)}
</Virtualizer>
</div>
</div>
</div>
);
}

View File

@ -1,9 +1,8 @@
import React from 'react'; import React, { useRef } from 'react';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { BusterListProps } from '../BusterList'; import { BusterListProps } from '../BusterList';
import { getAllIdsInSection } from '../BusterList/helpers'; import { getAllIdsInSection } from '../BusterList/helpers';
import { WindowVirtualizer } from 'virtua'; import { useEffect, useMemo } from 'react';
import { useEffect, useMemo, useRef, useCallback } from 'react';
import { BusterListHeader } from '../BusterList/BusterListHeader'; import { BusterListHeader } from '../BusterList/BusterListHeader';
import { BusterListRowComponentSelector } from '../BusterList/BusterListRowComponentSelector'; import { BusterListRowComponentSelector } from '../BusterList/BusterListRowComponentSelector';
@ -26,8 +25,10 @@ export const BusterInfiniteList: React.FC<BusterInfiniteListProps> = ({
showSelectAll = true, showSelectAll = true,
onScrollEnd, onScrollEnd,
loadingNewContent, loadingNewContent,
scrollEndThreshold = 200 // Default threshold of 200px scrollEndThreshold = 48 // Default threshold of 200px
}) => { }) => {
const containerRef = useRef<HTMLDivElement>(null);
const scrollRef = useRef<HTMLDivElement | null>(null);
const showEmptyState = useMemo( const showEmptyState = useMemo(
() => (!rows || rows.length === 0 || !rows.some((row) => !row.rowSection)) && !!emptyState, () => (!rows || rows.length === 0 || !rows.some((row) => !row.rowSection)) && !!emptyState,
[rows, emptyState] [rows, emptyState]
@ -86,27 +87,44 @@ export const BusterInfiniteList: React.FC<BusterInfiniteListProps> = ({
selectedRowKeys selectedRowKeys
]); ]);
// Add scroll handler useEffect(() => {
const handleScroll = useCallback(() => { if (!onScrollEnd) return;
// const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
// const distanceToBottom = scrollHeight - scrollTop - clientHeight; // Find the first scrollable parent element
// console.log('distanceToBottom', distanceToBottom); const findScrollableParent = (element: HTMLElement | null): HTMLDivElement | null => {
// if (distanceToBottom <= scrollEndThreshold) { while (element) {
// onScrollEnd(); const { overflowY } = window.getComputedStyle(element);
// console.log('onScrollEnd'); 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]); }, [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 ( return (
<div className="infinite-list-container relative flex h-full w-full flex-col"> <div ref={containerRef} className="infinite-list-container relative">
{showHeader && !showEmptyState && ( {showHeader && !showEmptyState && (
<BusterListHeader <BusterListHeader
columns={columns} columns={columns}
@ -117,13 +135,10 @@ export const BusterInfiniteList: React.FC<BusterInfiniteListProps> = ({
/> />
)} )}
{!showEmptyState && ( {!showEmptyState &&
<> rows.map((row) => (
{rows.map((row) => ( <BusterListRowComponentSelector key={row.id} row={row} id={row.id} {...itemData} />
<BusterListRowComponentSelector key={row.id} row={row} id={row.id} {...itemData} /> ))}
))}
</>
)}
{showEmptyState && ( {showEmptyState && (
<div className="flex h-full items-center justify-center">{emptyState}</div> <div className="flex h-full items-center justify-center">{emptyState}</div>