Files
oranguru/src/Griddy/rendering/VirtualBody.tsx
Hein 7244bd33fc refactor(advancedSearch): reorder exports and improve type definitions
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
2026-02-15 19:54:33 +02:00

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>
);
}