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;
assigned: boolean;
onSelect: (params: { id: string; assigned: boolean }) => Promise<void>;
}> = React.memo(
({ id, assigned, onSelect }) => {
}> = React.memo(({ id, assigned, onSelect }) => {
return (
<Select
options={options}
defaultValue={assigned || false}
value={assigned || false}
popupMatchSelectWidth
onSelect={(value) => {
onSelect({ id, assigned: value });
}}
/>
);
},
() => true
);
});
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 { 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<BusterInfiniteListProps> = ({
showSelectAll = true,
onScrollEnd,
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(
() => (!rows || rows.length === 0 || !rows.some((row) => !row.rowSection)) && !!emptyState,
[rows, emptyState]
@ -86,27 +87,44 @@ export const BusterInfiniteList: React.FC<BusterInfiniteListProps> = ({
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 (
<div className="infinite-list-container relative flex h-full w-full flex-col">
<div ref={containerRef} className="infinite-list-container relative">
{showHeader && !showEmptyState && (
<BusterListHeader
columns={columns}
@ -117,13 +135,10 @@ export const BusterInfiniteList: React.FC<BusterInfiniteListProps> = ({
/>
)}
{!showEmptyState && (
<>
{rows.map((row) => (
{!showEmptyState &&
rows.map((row) => (
<BusterListRowComponentSelector key={row.id} row={row} id={row.id} {...itemData} />
))}
</>
)}
{showEmptyState && (
<div className="flex h-full items-center justify-center">{emptyState}</div>