refactor(types): reorganize SearchCondition and AdvancedSearchState interfaces refactor(filterPresets): streamline useFilterPresets hook and localStorage handling refactor(filtering): clean up ColumnFilterButton and ColumnFilterPopover components refactor(loading): separate GriddyLoadingOverlay from GriddyLoadingSkeleton refactor(searchHistory): enhance useSearchHistory hook with persistence refactor(index): update exports for adapters and core components refactor(rendering): improve EditableCell and TableCell components for clarity refactor(rendering): enhance TableHeader and VirtualBody components for better readability
99 lines
2.9 KiB
TypeScript
99 lines
2.9 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
|
|
import { CSS } from '../core/constants';
|
|
import { useGriddyStore } from '../core/GriddyStore';
|
|
import styles from '../styles/griddy.module.css';
|
|
import { TableRow } from './TableRow';
|
|
|
|
export function VirtualBody() {
|
|
const table = useGriddyStore((s) => s._table);
|
|
const virtualizer = useGriddyStore((s) => s._virtualizer);
|
|
const setTotalRows = useGriddyStore((s) => s.setTotalRows);
|
|
const infiniteScroll = useGriddyStore((s) => s.infiniteScroll);
|
|
|
|
const rows = table?.getRowModel().rows;
|
|
const virtualRows = virtualizer?.getVirtualItems();
|
|
const totalSize = virtualizer?.getTotalSize() ?? 0;
|
|
|
|
// Track if we're currently loading to prevent multiple simultaneous calls
|
|
const isLoadingRef = useRef(false);
|
|
|
|
// Sync row count to store for keyboard navigation bounds
|
|
useEffect(() => {
|
|
if (rows) {
|
|
setTotalRows(rows.length);
|
|
}
|
|
}, [rows?.length, setTotalRows]);
|
|
|
|
// Infinite scroll: detect when approaching the end
|
|
useEffect(() => {
|
|
if (!infiniteScroll?.enabled || !infiniteScroll.onLoadMore || !virtualRows || !rows) {
|
|
return;
|
|
}
|
|
|
|
const { hasMore = true, isLoading = false, threshold = 10 } = infiniteScroll;
|
|
|
|
// Don't trigger if already loading or no more data
|
|
if (isLoading || !hasMore || isLoadingRef.current) {
|
|
return;
|
|
}
|
|
|
|
// Check if the last rendered virtual row is within threshold of the end
|
|
const lastVirtualRow = virtualRows[virtualRows.length - 1];
|
|
if (!lastVirtualRow) return;
|
|
|
|
const lastVirtualIndex = lastVirtualRow.index;
|
|
const totalRows = rows.length;
|
|
const distanceFromEnd = totalRows - lastVirtualIndex - 1;
|
|
|
|
if (distanceFromEnd <= threshold) {
|
|
isLoadingRef.current = true;
|
|
const loadPromise = infiniteScroll.onLoadMore();
|
|
|
|
if (loadPromise instanceof Promise) {
|
|
loadPromise.finally(() => {
|
|
isLoadingRef.current = false;
|
|
});
|
|
} else {
|
|
isLoadingRef.current = false;
|
|
}
|
|
}
|
|
}, [virtualRows, rows, infiniteScroll]);
|
|
|
|
if (!table || !virtualizer || !rows || !virtualRows) return null;
|
|
|
|
const showLoadingIndicator = infiniteScroll?.enabled && infiniteScroll.isLoading;
|
|
|
|
return (
|
|
<div
|
|
className={styles[CSS.tbody]}
|
|
role="rowgroup"
|
|
style={{
|
|
height: totalSize,
|
|
position: 'relative',
|
|
width: '100%',
|
|
}}
|
|
>
|
|
{virtualRows.map((virtualRow) => {
|
|
const row = rows[virtualRow.index];
|
|
if (!row) return null;
|
|
|
|
return <TableRow key={row.id} row={row} size={virtualRow.size} start={virtualRow.start} />;
|
|
})}
|
|
{showLoadingIndicator && (
|
|
<div
|
|
className={styles['griddy-loading-indicator']}
|
|
style={{
|
|
bottom: 0,
|
|
left: 0,
|
|
position: 'absolute',
|
|
right: 0,
|
|
}}
|
|
>
|
|
<div className={styles['griddy-loading-spinner']}>Loading more...</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|