optimize item table

This commit is contained in:
jeffvli
2026-01-02 21:56:01 -08:00
parent d06d1674d1
commit ffe3f08705
2 changed files with 414 additions and 237 deletions
@@ -611,33 +611,52 @@ export const TableColumnTextContainer = (
: props.rowIndex === props.data.length - 1); : props.rowIndex === props.data.length - 1);
useEffect(() => { useEffect(() => {
if (!isDataRow || !containerRef.current) return; if (!isDataRow || !containerRef.current || !props.enableRowHoverHighlight) return;
const container = containerRef.current; const container = containerRef.current;
const rowIndex = props.rowIndex; const rowIndex = props.rowIndex;
const rowSelector = `[data-row-index="${props.tableId}-${rowIndex}"]`;
let rafId: null | number = null;
let cachedCells: NodeListOf<Element> | null = null;
const getCells = () => {
if (!cachedCells) {
cachedCells = document.querySelectorAll(rowSelector);
}
return cachedCells;
};
const handleMouseEnter = () => { const handleMouseEnter = () => {
// Find all cells in the same row and add hover class if (rafId !== null) {
const allCells = document.querySelectorAll( cancelAnimationFrame(rafId);
`[data-row-index="${props.tableId}-${rowIndex}"]`, }
); rafId = requestAnimationFrame(() => {
allCells.forEach((cell) => cell.classList.add(styles.rowHovered)); const cells = getCells();
cells.forEach((cell) => cell.classList.add(styles.rowHovered));
});
}; };
const handleMouseLeave = () => { const handleMouseLeave = () => {
// Remove hover class from all cells in the same row if (rafId !== null) {
const allCells = document.querySelectorAll( cancelAnimationFrame(rafId);
`[data-row-index="${props.tableId}-${rowIndex}"]`, }
); rafId = requestAnimationFrame(() => {
allCells.forEach((cell) => cell.classList.remove(styles.rowHovered)); const cells = getCells();
cells.forEach((cell) => cell.classList.remove(styles.rowHovered));
cachedCells = null;
});
}; };
container.addEventListener('mouseenter', handleMouseEnter); container.addEventListener('mouseenter', handleMouseEnter);
container.addEventListener('mouseleave', handleMouseLeave); container.addEventListener('mouseleave', handleMouseLeave);
return () => { return () => {
if (rafId !== null) {
cancelAnimationFrame(rafId);
}
container.removeEventListener('mouseenter', handleMouseEnter); container.removeEventListener('mouseenter', handleMouseEnter);
container.removeEventListener('mouseleave', handleMouseLeave); container.removeEventListener('mouseleave', handleMouseLeave);
cachedCells = null;
}; };
}, [isDataRow, props.rowIndex, props.enableRowHoverHighlight, props.tableId]); }, [isDataRow, props.rowIndex, props.enableRowHoverHighlight, props.tableId]);
@@ -647,13 +666,26 @@ export const TableColumnTextContainer = (
const rowIndex = props.rowIndex; const rowIndex = props.rowIndex;
const draggedOverState = props.isDraggedOver; const draggedOverState = props.isDraggedOver;
const rowSelector = `[data-row-index="${props.tableId}-${rowIndex}"]`;
let rafId: null | number = null;
let cachedCells: NodeListOf<Element> | null = null;
const getCells = () => {
if (!cachedCells) {
cachedCells = document.querySelectorAll(rowSelector);
}
return cachedCells;
};
if (rafId !== null) {
cancelAnimationFrame(rafId);
}
rafId = requestAnimationFrame(() => {
const cells = getCells();
if (draggedOverState) { if (draggedOverState) {
// Find all cells in the same row and add dragged over class cells.forEach((cell, index) => {
const allCells = document.querySelectorAll(
`[data-row-index="${props.tableId}-${rowIndex}"]`,
);
allCells.forEach((cell, index) => {
if (draggedOverState === 'top') { if (draggedOverState === 'top') {
cell.classList.add(styles.draggedOverTop); cell.classList.add(styles.draggedOverTop);
cell.classList.remove(styles.draggedOverBottom); cell.classList.remove(styles.draggedOverBottom);
@@ -676,15 +708,22 @@ export const TableColumnTextContainer = (
}); });
} else { } else {
// Remove dragged over classes from all cells in the same row // Remove dragged over classes from all cells in the same row
const allCells = document.querySelectorAll( cells.forEach((cell) => {
`[data-row-index="${props.tableId}-${rowIndex}"]`,
);
allCells.forEach((cell) => {
cell.classList.remove(styles.draggedOverTop); cell.classList.remove(styles.draggedOverTop);
cell.classList.remove(styles.draggedOverBottom); cell.classList.remove(styles.draggedOverBottom);
cell.classList.remove(styles.draggedOverFirstCell); cell.classList.remove(styles.draggedOverFirstCell);
}); });
// Clear cache when state is cleared
cachedCells = null;
} }
});
return () => {
if (rafId !== null) {
cancelAnimationFrame(rafId);
}
cachedCells = null;
};
}, [isDataRow, props.rowIndex, props.isDraggedOver, props.tableId]); }, [isDataRow, props.rowIndex, props.isDraggedOver, props.tableId]);
const handleClick = useDoubleClick({ const handleClick = useDoubleClick({
@@ -824,33 +863,52 @@ export const TableColumnContainer = (
: props.rowIndex === props.data.length - 1); : props.rowIndex === props.data.length - 1);
useEffect(() => { useEffect(() => {
if (!isDataRow || !containerRef.current) return; if (!isDataRow || !containerRef.current || !props.enableRowHoverHighlight) return;
const container = containerRef.current; const container = containerRef.current;
const rowIndex = props.rowIndex; const rowIndex = props.rowIndex;
const rowSelector = `[data-row-index="${props.tableId}-${rowIndex}"]`;
let rafId: null | number = null;
let cachedCells: NodeListOf<Element> | null = null;
const getCells = () => {
if (!cachedCells) {
cachedCells = document.querySelectorAll(rowSelector);
}
return cachedCells;
};
const handleMouseEnter = () => { const handleMouseEnter = () => {
// Find all cells in the same row and add hover class if (rafId !== null) {
const allCells = document.querySelectorAll( cancelAnimationFrame(rafId);
`[data-row-index="${props.tableId}-${rowIndex}"]`, }
); rafId = requestAnimationFrame(() => {
allCells.forEach((cell) => cell.classList.add(styles.rowHovered)); const cells = getCells();
cells.forEach((cell) => cell.classList.add(styles.rowHovered));
});
}; };
const handleMouseLeave = () => { const handleMouseLeave = () => {
// Remove hover class from all cells in the same row if (rafId !== null) {
const allCells = document.querySelectorAll( cancelAnimationFrame(rafId);
`[data-row-index="${props.tableId}-${rowIndex}"]`, }
); rafId = requestAnimationFrame(() => {
allCells.forEach((cell) => cell.classList.remove(styles.rowHovered)); const cells = getCells();
cells.forEach((cell) => cell.classList.remove(styles.rowHovered));
cachedCells = null;
});
}; };
container.addEventListener('mouseenter', handleMouseEnter); container.addEventListener('mouseenter', handleMouseEnter);
container.addEventListener('mouseleave', handleMouseLeave); container.addEventListener('mouseleave', handleMouseLeave);
return () => { return () => {
if (rafId !== null) {
cancelAnimationFrame(rafId);
}
container.removeEventListener('mouseenter', handleMouseEnter); container.removeEventListener('mouseenter', handleMouseEnter);
container.removeEventListener('mouseleave', handleMouseLeave); container.removeEventListener('mouseleave', handleMouseLeave);
cachedCells = null;
}; };
}, [isDataRow, props.rowIndex, props.enableRowHoverHighlight, props.tableId]); }, [isDataRow, props.rowIndex, props.enableRowHoverHighlight, props.tableId]);
@@ -860,13 +918,26 @@ export const TableColumnContainer = (
const rowIndex = props.rowIndex; const rowIndex = props.rowIndex;
const draggedOverState = props.isDraggedOver; const draggedOverState = props.isDraggedOver;
const rowSelector = `[data-row-index="${props.tableId}-${rowIndex}"]`;
let rafId: null | number = null;
let cachedCells: NodeListOf<Element> | null = null;
const getCells = () => {
if (!cachedCells) {
cachedCells = document.querySelectorAll(rowSelector);
}
return cachedCells;
};
if (rafId !== null) {
cancelAnimationFrame(rafId);
}
rafId = requestAnimationFrame(() => {
const cells = getCells();
if (draggedOverState) { if (draggedOverState) {
// Find all cells in the same row and add dragged over class cells.forEach((cell, index) => {
const allCells = document.querySelectorAll(
`[data-row-index="${props.tableId}-${rowIndex}"]`,
);
allCells.forEach((cell, index) => {
if (draggedOverState === 'top') { if (draggedOverState === 'top') {
cell.classList.add(styles.draggedOverTop); cell.classList.add(styles.draggedOverTop);
cell.classList.remove(styles.draggedOverBottom); cell.classList.remove(styles.draggedOverBottom);
@@ -889,15 +960,22 @@ export const TableColumnContainer = (
}); });
} else { } else {
// Remove dragged over classes from all cells in the same row // Remove dragged over classes from all cells in the same row
const allCells = document.querySelectorAll( cells.forEach((cell) => {
`[data-row-index="${props.tableId}-${rowIndex}"]`,
);
allCells.forEach((cell) => {
cell.classList.remove(styles.draggedOverTop); cell.classList.remove(styles.draggedOverTop);
cell.classList.remove(styles.draggedOverBottom); cell.classList.remove(styles.draggedOverBottom);
cell.classList.remove(styles.draggedOverFirstCell); cell.classList.remove(styles.draggedOverFirstCell);
}); });
// Clear cache when state is cleared
cachedCells = null;
} }
});
return () => {
if (rafId !== null) {
cancelAnimationFrame(rafId);
}
cachedCells = null;
};
}, [isDataRow, props.rowIndex, props.isDraggedOver, props.tableId]); }, [isDataRow, props.rowIndex, props.isDraggedOver, props.tableId]);
const handleClick = useDoubleClick({ const handleClick = useDoubleClick({
@@ -93,6 +93,7 @@ interface VirtualizedTableGridProps {
cellPadding: 'lg' | 'md' | 'sm' | 'xl' | 'xs'; cellPadding: 'lg' | 'md' | 'sm' | 'xl' | 'xs';
controls: ItemControls; controls: ItemControls;
data: unknown[]; data: unknown[];
dataWithGroups: (null | unknown)[];
enableAlternateRowColors: boolean; enableAlternateRowColors: boolean;
enableColumnReorder: boolean; enableColumnReorder: boolean;
enableColumnResize: boolean; enableColumnResize: boolean;
@@ -134,7 +135,8 @@ const VirtualizedTableGrid = ({
CellComponent, CellComponent,
cellPadding, cellPadding,
controls, controls,
data, // data,
dataWithGroups,
enableAlternateRowColors, enableAlternateRowColors,
enableColumnReorder, enableColumnReorder,
enableColumnResize, enableColumnResize,
@@ -185,53 +187,6 @@ const VirtualizedTableGrid = ({
); );
}, [pinnedRightColumnCount, pinnedLeftColumnCount, totalColumnCount, columnWidth]); }, [pinnedRightColumnCount, pinnedLeftColumnCount, totalColumnCount, columnWidth]);
// Create data array with group headers inserted as null values
// Groups are defined by itemCount, so we calculate indexes based on cumulative item counts
const dataWithGroups = useMemo(() => {
const result: (null | unknown)[] = enableHeader ? [null] : [];
if (!groups || groups.length === 0) {
// No groups, just add all data
result.push(...data);
return result;
}
// Calculate group header indexes based on itemCounts
const groupIndexes: number[] = [];
let cumulativeDataIndex = 0;
const headerOffset = enableHeader ? 1 : 0;
groups.forEach((group, groupIndex) => {
// Group header appears before its items
// Index = header offset + cumulative data index + number of previous group headers
const groupHeaderIndex = headerOffset + cumulativeDataIndex + groupIndex;
groupIndexes.push(groupHeaderIndex);
cumulativeDataIndex += group.itemCount;
});
let dataIndex = 0;
const startIndex = enableHeader ? 1 : 0;
let groupHeaderCount = 0;
// Iterate through the expanded row space (data + group headers)
for (
let rowIndex = startIndex;
rowIndex < startIndex + data.length + groupIndexes.length;
rowIndex++
) {
// Check if this row should have a group header
const expectedGroupIndex = groupIndexes[groupHeaderCount];
if (expectedGroupIndex !== undefined && rowIndex === expectedGroupIndex) {
result.push(null); // Group header row
groupHeaderCount++;
} else if (dataIndex < data.length) {
result.push(data[dataIndex]);
dataIndex++;
}
}
return result;
}, [data, enableHeader, groups]);
const adjustedRowIndexMap = useMemo(() => { const adjustedRowIndexMap = useMemo(() => {
const map = new Map<number, number>(); const map = new Map<number, number>();
@@ -275,71 +230,94 @@ const VirtualizedTableGrid = ({
return map; return map;
}, [dataWithGroups, enableHeader, groups]); }, [dataWithGroups, enableHeader, groups]);
const itemProps: TableItemProps = useMemo( const stableConfigProps = useMemo(
() => ({
cellPadding,
columns: parsedColumns,
controls,
enableHeader,
getRowHeight,
internalState,
itemType,
playerContext,
size,
tableId,
}),
[
cellPadding,
parsedColumns,
controls,
enableHeader,
getRowHeight,
internalState,
itemType,
playerContext,
size,
tableId,
],
);
const dynamicDataProps = useMemo(
() => ({ () => ({
activeRowId, activeRowId,
adjustedRowIndexMap, adjustedRowIndexMap,
calculatedColumnWidths, calculatedColumnWidths,
cellPadding,
columns: parsedColumns,
controls,
data: dataWithGroups, data: dataWithGroups,
enableAlternateRowColors,
enableColumnReorder,
enableColumnResize,
enableDrag,
enableExpansion,
enableHeader,
enableHorizontalBorders,
enableRowHoverHighlight,
enableSelection,
enableVerticalBorders,
getRowHeight,
groups,
internalState,
itemType,
pinnedLeftColumnCount, pinnedLeftColumnCount,
pinnedLeftColumnWidths, pinnedLeftColumnWidths,
pinnedRightColumnCount, pinnedRightColumnCount,
pinnedRightColumnWidths, pinnedRightColumnWidths,
playerContext,
size,
startRowIndex, startRowIndex,
tableId,
}), }),
[ [
activeRowId, activeRowId,
adjustedRowIndexMap, adjustedRowIndexMap,
calculatedColumnWidths, calculatedColumnWidths,
cellPadding,
parsedColumns,
controls,
dataWithGroups, dataWithGroups,
pinnedLeftColumnCount,
pinnedLeftColumnWidths,
pinnedRightColumnCount,
pinnedRightColumnWidths,
startRowIndex,
],
);
const featureFlags = useMemo(
() => ({
enableAlternateRowColors,
enableColumnReorder,
enableColumnResize,
enableDrag,
enableExpansion,
enableHorizontalBorders,
enableRowHoverHighlight,
enableSelection,
enableVerticalBorders,
groups,
}),
[
enableAlternateRowColors, enableAlternateRowColors,
enableColumnReorder, enableColumnReorder,
enableColumnResize, enableColumnResize,
enableDrag, enableDrag,
enableExpansion, enableExpansion,
enableHeader,
enableHorizontalBorders, enableHorizontalBorders,
enableRowHoverHighlight, enableRowHoverHighlight,
enableSelection, enableSelection,
enableVerticalBorders, enableVerticalBorders,
getRowHeight,
groups, groups,
internalState,
itemType,
pinnedLeftColumnCount,
pinnedLeftColumnWidths,
pinnedRightColumnCount,
pinnedRightColumnWidths,
playerContext,
size,
startRowIndex,
tableId,
], ],
); );
const itemProps: TableItemProps = useMemo(
() => ({
...stableConfigProps,
...dynamicDataProps,
...featureFlags,
}),
[stableConfigProps, dynamicDataProps, featureFlags],
);
const PinnedRowCell = useCallback( const PinnedRowCell = useCallback(
(cellProps: CellComponentProps & TableItemProps) => { (cellProps: CellComponentProps & TableItemProps) => {
return ( return (
@@ -612,6 +590,53 @@ const VirtualizedTableGrid = ({
VirtualizedTableGrid.displayName = 'VirtualizedTableGrid'; VirtualizedTableGrid.displayName = 'VirtualizedTableGrid';
const MemoizedVirtualizedTableGrid = memo(VirtualizedTableGrid, (prevProps, nextProps) => {
return (
prevProps.activeRowId === nextProps.activeRowId &&
prevProps.calculatedColumnWidths === nextProps.calculatedColumnWidths &&
prevProps.cellPadding === nextProps.cellPadding &&
prevProps.controls === nextProps.controls &&
prevProps.data === nextProps.data &&
prevProps.dataWithGroups === nextProps.dataWithGroups &&
prevProps.enableAlternateRowColors === nextProps.enableAlternateRowColors &&
prevProps.enableColumnReorder === nextProps.enableColumnReorder &&
prevProps.enableColumnResize === nextProps.enableColumnResize &&
prevProps.enableDrag === nextProps.enableDrag &&
prevProps.enableExpansion === nextProps.enableExpansion &&
prevProps.enableHeader === nextProps.enableHeader &&
prevProps.enableHorizontalBorders === nextProps.enableHorizontalBorders &&
prevProps.enableRowHoverHighlight === nextProps.enableRowHoverHighlight &&
prevProps.enableSelection === nextProps.enableSelection &&
prevProps.enableVerticalBorders === nextProps.enableVerticalBorders &&
prevProps.getRowHeight === nextProps.getRowHeight &&
prevProps.groups === nextProps.groups &&
prevProps.headerHeight === nextProps.headerHeight &&
prevProps.internalState === nextProps.internalState &&
prevProps.itemType === nextProps.itemType &&
prevProps.mergedRowRef === nextProps.mergedRowRef &&
prevProps.onRangeChanged === nextProps.onRangeChanged &&
prevProps.parsedColumns === nextProps.parsedColumns &&
prevProps.pinnedLeftColumnCount === nextProps.pinnedLeftColumnCount &&
prevProps.pinnedLeftColumnRef === nextProps.pinnedLeftColumnRef &&
prevProps.pinnedRightColumnCount === nextProps.pinnedRightColumnCount &&
prevProps.pinnedRightColumnRef === nextProps.pinnedRightColumnRef &&
prevProps.pinnedRowCount === nextProps.pinnedRowCount &&
prevProps.pinnedRowRef === nextProps.pinnedRowRef &&
prevProps.playerContext === nextProps.playerContext &&
prevProps.showLeftShadow === nextProps.showLeftShadow &&
prevProps.showRightShadow === nextProps.showRightShadow &&
prevProps.showTopShadow === nextProps.showTopShadow &&
prevProps.size === nextProps.size &&
prevProps.startRowIndex === nextProps.startRowIndex &&
prevProps.tableId === nextProps.tableId &&
prevProps.totalColumnCount === nextProps.totalColumnCount &&
prevProps.totalRowCount === nextProps.totalRowCount &&
prevProps.CellComponent === nextProps.CellComponent
);
});
MemoizedVirtualizedTableGrid.displayName = 'MemoizedVirtualizedTableGrid';
export interface TableGroupHeader { export interface TableGroupHeader {
itemCount: number; itemCount: number;
render: (props: { render: (props: {
@@ -738,6 +763,53 @@ const BaseItemTableList = ({
const [centerContainerWidth, setCenterContainerWidth] = useState(0); const [centerContainerWidth, setCenterContainerWidth] = useState(0);
const [totalContainerWidth, setTotalContainerWidth] = useState(0); const [totalContainerWidth, setTotalContainerWidth] = useState(0);
// Compute dataWithGroups once to avoid duplicate computation
// This is used by both VirtualizedTableGrid and getDataFn
const dataWithGroups = useMemo(() => {
const result: (null | unknown)[] = enableHeader ? [null] : [];
if (!groups || groups.length === 0) {
// No groups, just add all data
result.push(...data);
return result;
}
// Calculate group header indexes based on itemCounts
const groupIndexes: number[] = [];
let cumulativeDataIndex = 0;
const headerOffset = enableHeader ? 1 : 0;
groups.forEach((group, groupIndex) => {
// Group header appears before its items
// Index = header offset + cumulative data index + number of previous group headers
const groupHeaderIndex = headerOffset + cumulativeDataIndex + groupIndex;
groupIndexes.push(groupHeaderIndex);
cumulativeDataIndex += group.itemCount;
});
let dataIndex = 0;
const startIndex = enableHeader ? 1 : 0;
let groupHeaderCount = 0;
// Iterate through the expanded row space (data + group headers)
for (
let rowIndex = startIndex;
rowIndex < startIndex + data.length + groupIndexes.length;
rowIndex++
) {
// Check if this row should have a group header
const expectedGroupIndex = groupIndexes[groupHeaderCount];
if (expectedGroupIndex !== undefined && rowIndex === expectedGroupIndex) {
result.push(null); // Group header row
groupHeaderCount++;
} else if (dataIndex < data.length) {
result.push(data[dataIndex]);
dataIndex++;
}
}
return result;
}, [data, enableHeader, groups]);
// Compute distributed widths: unpinned columns with autoWidth will share any remaining space // Compute distributed widths: unpinned columns with autoWidth will share any remaining space
// When autoSizeColumns is true, all column widths are treated as proportions and scaled to fit the container // When autoSizeColumns is true, all column widths are treated as proportions and scaled to fit the container
const calculatedColumnWidths = useMemo(() => { const calculatedColumnWidths = useMemo(() => {
@@ -867,11 +939,20 @@ const BaseItemTableList = ({
const stickyHeader = stickyHeaderRef.current; const stickyHeader = stickyHeaderRef.current;
const container = containerRef.current; const container = containerRef.current;
let isMounted = true;
const updatePosition = () => { const updatePosition = () => {
// Guard against updates after unmount
if (!isMounted || !stickyHeader || !container) {
return;
}
try {
const containerRect = container.getBoundingClientRect(); const containerRect = container.getBoundingClientRect();
stickyHeader.style.left = `${containerRect.left}px`; stickyHeader.style.left = `${containerRect.left}px`;
stickyHeader.style.width = `${containerRect.width}px`; stickyHeader.style.width = `${containerRect.width}px`;
} catch {
// Silently handle errors if elements are no longer in DOM
}
}; };
updatePosition(); updatePosition();
@@ -880,6 +961,7 @@ const BaseItemTableList = ({
window.addEventListener('scroll', updatePosition, true); window.addEventListener('scroll', updatePosition, true);
return () => { return () => {
isMounted = false;
window.removeEventListener('resize', updatePosition); window.removeEventListener('resize', updatePosition);
window.removeEventListener('scroll', updatePosition, true); window.removeEventListener('scroll', updatePosition, true);
}; };
@@ -895,13 +977,22 @@ const BaseItemTableList = ({
updateWidth(); updateWidth();
let debounceTimeout: NodeJS.Timeout | null = null;
const resizeObserver = new ResizeObserver(() => { const resizeObserver = new ResizeObserver(() => {
if (debounceTimeout) {
clearTimeout(debounceTimeout);
}
debounceTimeout = setTimeout(() => {
updateWidth(); updateWidth();
}, 100);
}); });
resizeObserver.observe(el); resizeObserver.observe(el);
return () => { return () => {
if (debounceTimeout) {
clearTimeout(debounceTimeout);
}
resizeObserver.disconnect(); resizeObserver.disconnect();
}; };
}, []); }, []);
@@ -917,13 +1008,22 @@ const BaseItemTableList = ({
updateWidth(); updateWidth();
let debounceTimeout: NodeJS.Timeout | null = null;
const resizeObserver = new ResizeObserver(() => { const resizeObserver = new ResizeObserver(() => {
if (debounceTimeout) {
clearTimeout(debounceTimeout);
}
debounceTimeout = setTimeout(() => {
updateWidth(); updateWidth();
}, 100);
}); });
resizeObserver.observe(el); resizeObserver.observe(el);
return () => { return () => {
if (debounceTimeout) {
clearTimeout(debounceTimeout);
}
resizeObserver.disconnect(); resizeObserver.disconnect();
}; };
}, [autoFitColumns]); }, [autoFitColumns]);
@@ -1366,7 +1466,6 @@ const BaseItemTableList = ({
const scrollTop = (e.currentTarget as HTMLDivElement).scrollTop; const scrollTop = (e.currentTarget as HTMLDivElement).scrollTop;
const scrollLeft = (e.currentTarget as HTMLDivElement).scrollLeft; const scrollLeft = (e.currentTarget as HTMLDivElement).scrollLeft;
// Prevent recursive scroll events
const isScrolling = { const isScrolling = {
header: false, header: false,
pinnedLeft: false, pinnedLeft: false,
@@ -1490,8 +1589,14 @@ const BaseItemTableList = ({
} }
// Add resize observer to maintain height sync // Add resize observer to maintain height sync
let heightSyncDebounceTimeout: NodeJS.Timeout | null = null;
const resizeObserver = new ResizeObserver(() => { const resizeObserver = new ResizeObserver(() => {
if (heightSyncDebounceTimeout) {
clearTimeout(heightSyncDebounceTimeout);
}
heightSyncDebounceTimeout = setTimeout(() => {
syncHeights(); syncHeights();
}, 100);
}); });
resizeObserver.observe(row); resizeObserver.observe(row);
@@ -1526,6 +1631,9 @@ const BaseItemTableList = ({
pinnedRight.removeEventListener('wheel', setActiveElementFromWheel); pinnedRight.removeEventListener('wheel', setActiveElementFromWheel);
pinnedRight.removeEventListener('scroll', syncScroll); pinnedRight.removeEventListener('scroll', syncScroll);
} }
if (heightSyncDebounceTimeout) {
clearTimeout(heightSyncDebounceTimeout);
}
resizeObserver.disconnect(); resizeObserver.disconnect();
}; };
} }
@@ -1546,19 +1654,28 @@ const BaseItemTableList = ({
return () => clearTimeout(timeout); return () => clearTimeout(timeout);
} }
let debounceTimeout: NodeJS.Timeout | null = null;
const checkScrollPosition = () => { const checkScrollPosition = () => {
if (debounceTimeout) {
clearTimeout(debounceTimeout);
}
debounceTimeout = setTimeout(() => {
const scrollLeft = row.scrollLeft; const scrollLeft = row.scrollLeft;
const maxScrollLeft = row.scrollWidth - row.clientWidth; const maxScrollLeft = row.scrollWidth - row.clientWidth;
setShowLeftShadow(pinnedLeftColumnCount > 0 && scrollLeft > 0); setShowLeftShadow(pinnedLeftColumnCount > 0 && scrollLeft > 0);
setShowRightShadow(pinnedRightColumnCount > 0 && scrollLeft < maxScrollLeft); setShowRightShadow(pinnedRightColumnCount > 0 && scrollLeft < maxScrollLeft);
}, 50); // 50ms debounce for shadow visibility
}; };
checkScrollPosition(); checkScrollPosition();
row.addEventListener('scroll', checkScrollPosition); row.addEventListener('scroll', checkScrollPosition, { passive: true });
return () => { return () => {
if (debounceTimeout) {
clearTimeout(debounceTimeout);
}
row.removeEventListener('scroll', checkScrollPosition); row.removeEventListener('scroll', checkScrollPosition);
}; };
}, [pinnedLeftColumnCount, pinnedRightColumnCount]); }, [pinnedLeftColumnCount, pinnedRightColumnCount]);
@@ -1579,16 +1696,25 @@ const BaseItemTableList = ({
// When right-pinned columns exist, use right pinned column's scroll position // When right-pinned columns exist, use right pinned column's scroll position
const scrollElement = pinnedRightColumnCount > 0 && pinnedRight ? pinnedRight : row; const scrollElement = pinnedRightColumnCount > 0 && pinnedRight ? pinnedRight : row;
let debounceTimeout: NodeJS.Timeout | null = null;
const checkScrollPosition = () => { const checkScrollPosition = () => {
if (debounceTimeout) {
clearTimeout(debounceTimeout);
}
debounceTimeout = setTimeout(() => {
const currentScrollTop = scrollElement.scrollTop; const currentScrollTop = scrollElement.scrollTop;
setShowTopShadow(currentScrollTop > 0); setShowTopShadow(currentScrollTop > 0);
}, 50);
}; };
checkScrollPosition(); checkScrollPosition();
scrollElement.addEventListener('scroll', checkScrollPosition); scrollElement.addEventListener('scroll', checkScrollPosition, { passive: true });
return () => { return () => {
if (debounceTimeout) {
clearTimeout(debounceTimeout);
}
scrollElement.removeEventListener('scroll', checkScrollPosition); scrollElement.removeEventListener('scroll', checkScrollPosition);
}; };
}, [enableHeader, pinnedRightColumnCount]); }, [enableHeader, pinnedRightColumnCount]);
@@ -1653,11 +1779,20 @@ const BaseItemTableList = ({
const stickyGroupRow = stickyGroupRowRef.current; const stickyGroupRow = stickyGroupRowRef.current;
const container = containerRef.current; const container = containerRef.current;
let isMounted = true;
const updatePosition = () => { const updatePosition = () => {
// Guard against updates after unmount
if (!isMounted || !stickyGroupRow || !container) {
return;
}
try {
const containerRect = container.getBoundingClientRect(); const containerRect = container.getBoundingClientRect();
stickyGroupRow.style.left = `${containerRect.left}px`; stickyGroupRow.style.left = `${containerRect.left}px`;
stickyGroupRow.style.width = `${containerRect.width}px`; stickyGroupRow.style.width = `${containerRect.width}px`;
} catch {
// Silently handle errors if elements are no longer in DOM
}
}; };
updatePosition(); updatePosition();
@@ -1666,52 +1801,15 @@ const BaseItemTableList = ({
window.addEventListener('scroll', updatePosition, true); window.addEventListener('scroll', updatePosition, true);
return () => { return () => {
isMounted = false;
window.removeEventListener('resize', updatePosition); window.removeEventListener('resize', updatePosition);
window.removeEventListener('scroll', updatePosition, true); window.removeEventListener('scroll', updatePosition, true);
}; };
}, [shouldRenderStickyGroupRow]); }, [shouldRenderStickyGroupRow]);
const getDataFn = useCallback(() => { const getDataFn = useCallback(() => {
const result: (null | unknown)[] = enableHeader ? [null] : []; return dataWithGroups;
}, [dataWithGroups]);
if (!groups || groups.length === 0) {
// No groups, just add all data
result.push(...data);
return result;
}
// Calculate group header indexes based on itemCounts
const groupIndexes: number[] = [];
let cumulativeDataIndex = 0;
const headerOffset = enableHeader ? 1 : 0;
groups.forEach((group, groupIndex) => {
// Index = header offset + cumulative data index + number of previous group headers
const groupHeaderIndex = headerOffset + cumulativeDataIndex + groupIndex;
groupIndexes.push(groupHeaderIndex);
cumulativeDataIndex += group.itemCount;
});
let dataIndex = 0;
const startIndex = enableHeader ? 1 : 0;
let groupHeaderCount = 0;
for (
let rowIndex = startIndex;
rowIndex < startIndex + data.length + groupIndexes.length;
rowIndex++
) {
const expectedGroupIndex = groupIndexes[groupHeaderCount];
if (expectedGroupIndex !== undefined && rowIndex === expectedGroupIndex) {
result.push(null);
groupHeaderCount++;
} else if (dataIndex < data.length) {
result.push(data[dataIndex]);
dataIndex++;
}
}
return result;
}, [data, enableHeader, groups]);
const extractRowId = useMemo(() => createExtractRowId(getRowId), [getRowId]); const extractRowId = useMemo(() => createExtractRowId(getRowId), [getRowId]);
@@ -2287,13 +2385,14 @@ const BaseItemTableList = ({
> >
{StickyHeader} {StickyHeader}
{StickyGroupRow} {StickyGroupRow}
<VirtualizedTableGrid <MemoizedVirtualizedTableGrid
activeRowId={activeRowId} activeRowId={activeRowId}
calculatedColumnWidths={calculatedColumnWidths} calculatedColumnWidths={calculatedColumnWidths}
CellComponent={CellComponent} CellComponent={CellComponent}
cellPadding={cellPadding} cellPadding={cellPadding}
controls={controls} controls={controls}
data={data} data={data}
dataWithGroups={dataWithGroups}
enableAlternateRowColors={enableAlternateRowColors} enableAlternateRowColors={enableAlternateRowColors}
enableColumnReorder={!!onColumnReordered} enableColumnReorder={!!onColumnReordered}
enableColumnResize={!!onColumnResized} enableColumnResize={!!onColumnResized}