refactor item grid sizer handling to prevent blanks on resize

This commit is contained in:
jeffvli
2025-11-25 11:54:15 -08:00
parent 359e442947
commit 22fae938c4
@@ -47,6 +47,7 @@ import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
import { LibraryItem } from '/@/shared/types/domain-types'; import { LibraryItem } from '/@/shared/types/domain-types';
interface VirtualizedGridListProps { interface VirtualizedGridListProps {
_tableMetaVersion: number; // Used to trigger rerenders via React.memo comparison
controls: ItemControls; controls: ItemControls;
data: unknown[]; data: unknown[];
enableDrag?: boolean; enableDrag?: boolean;
@@ -63,11 +64,11 @@ interface VirtualizedGridListProps {
outerRef: RefObject<any>; outerRef: RefObject<any>;
ref: RefObject<FixedSizeList<GridItemProps> | null>; ref: RefObject<FixedSizeList<GridItemProps> | null>;
rows?: ItemCardProps['rows']; rows?: ItemCardProps['rows'];
tableMeta: null | { tableMetaRef: RefObject<null | {
columnCount: number; columnCount: number;
itemHeight: number; itemHeight: number;
rowCount: number; rowCount: number;
}; }>;
width: number; width: number;
} }
@@ -89,9 +90,11 @@ const VirtualizedGridList = React.memo(
outerRef, outerRef,
ref, ref,
rows, rows,
tableMeta, tableMetaRef,
width, width,
}: VirtualizedGridListProps) => { }: VirtualizedGridListProps) => {
const tableMeta = tableMetaRef.current;
const itemData: GridItemProps = useMemo(() => { const itemData: GridItemProps = useMemo(() => {
return { return {
columns: tableMeta?.columnCount || 0, columns: tableMeta?.columnCount || 0,
@@ -129,12 +132,13 @@ const VirtualizedGridList = React.memo(
const handleOnItemsRendered = useCallback( const handleOnItemsRendered = useCallback(
(items: ListOnItemsRenderedProps) => { (items: ListOnItemsRenderedProps) => {
const columnCount = tableMetaRef.current?.columnCount || 0;
onRangeChanged?.({ onRangeChanged?.({
startIndex: items.visibleStartIndex * (tableMeta?.columnCount || 0), startIndex: items.visibleStartIndex * columnCount,
stopIndex: items.visibleStopIndex * (tableMeta?.columnCount || 0), stopIndex: items.visibleStopIndex * columnCount,
}); });
}, },
[onRangeChanged, tableMeta?.columnCount], [onRangeChanged, tableMetaRef],
); );
if (!tableMeta) { if (!tableMeta) {
@@ -148,17 +152,19 @@ const VirtualizedGridList = React.memo(
return initialTop.to; return initialTop.to;
} }
const rowIndex = Math.floor(initialTop.to / (tableMeta?.columnCount || 1)); const columnCount = tableMeta?.columnCount || 1;
return rowIndex * (tableMeta?.itemHeight || 0); const itemHeight = tableMeta?.itemHeight || 0;
const rowIndex = Math.floor(initialTop.to / columnCount);
return rowIndex * itemHeight;
}; };
return ( return (
<FixedSizeList <FixedSizeList
height={height} height={height}
initialScrollOffset={calculateInitialScrollOffset()} initialScrollOffset={calculateInitialScrollOffset()}
itemCount={itemData.tableMeta?.rowCount || 0} itemCount={tableMeta.rowCount || 0}
itemData={itemData} itemData={itemData}
itemSize={itemData.tableMeta?.itemHeight || 0} itemSize={tableMeta.itemHeight || 0}
onItemsRendered={handleOnItemsRendered} onItemsRendered={handleOnItemsRendered}
onScroll={handleOnScroll} onScroll={handleOnScroll}
outerRef={outerRef} outerRef={outerRef}
@@ -316,17 +322,20 @@ export const ItemGridList = ({
const hasExpanded = internalState.hasExpanded(); const hasExpanded = internalState.hasExpanded();
const [tableMeta, setTableMeta] = useState<null | { const tableMetaRef = useRef<null | {
columnCount: number; columnCount: number;
itemHeight: number; itemHeight: number;
rowCount: number; rowCount: number;
}>(null); }>(null);
const [tableMetaVersion, setTableMetaVersion] = useState(0);
const isOverlayScrollbarsInitialized = useRef(false);
useEffect(() => { useEffect(() => {
const { current: root } = rootRef; const { current: root } = rootRef;
const { current: outer } = outerRef; const { current: outer } = outerRef;
if (!tableMeta || !root || !outer) { if (!tableMetaRef.current || !root || !outer || isOverlayScrollbarsInitialized.current) {
return; return;
} }
@@ -337,6 +346,10 @@ export const ItemGridList = ({
target: root, target: root,
}); });
isOverlayScrollbarsInitialized.current = true;
}, [initialize, tableMetaVersion]);
useEffect(() => {
return () => { return () => {
try { try {
const instance = osInstance(); const instance = osInstance();
@@ -358,15 +371,34 @@ export const ItemGridList = ({
// Ignore error // Ignore error
} }
}; };
}, [initialize, osInstance, tableMeta]); }, [osInstance]);
const throttledSetTableMeta = useMemo(() => { const throttledSetTableMeta = useMemo(() => {
return createThrottledSetTableMeta(itemsPerRow, rows?.length); return createThrottledSetTableMeta(itemsPerRow, rows?.length);
}, [itemsPerRow, rows?.length]); }, [itemsPerRow, rows?.length]);
useLayoutEffect(() => { useLayoutEffect(() => {
throttledSetTableMeta(containerWidth, data.length, setTableMeta); const { current: container } = containerRef;
}, [containerWidth, data.length, throttledSetTableMeta]); if (!container) return;
throttledSetTableMeta(containerWidth, data.length, (meta) => {
if (!meta) return;
const current = tableMetaRef.current;
if (
!current ||
current.columnCount !== meta.columnCount ||
current.itemHeight !== meta.itemHeight ||
current.rowCount !== meta.rowCount
) {
tableMetaRef.current = meta;
container.style.setProperty('--grid-column-count', String(meta.columnCount));
container.style.setProperty('--grid-item-height', `${meta.itemHeight}px`);
container.style.setProperty('--grid-row-count', String(meta.rowCount));
setTableMetaVersion((v) => v + 1);
}
});
}, [containerWidth, data.length, throttledSetTableMeta, containerRef]);
const controls = useDefaultItemListControls({ overrides: overrideControls }); const controls = useDefaultItemListControls({ overrides: overrideControls });
@@ -375,8 +407,8 @@ export const ItemGridList = ({
index: number, index: number,
options?: { align?: 'bottom' | 'center' | 'top'; behavior?: 'auto' | 'smooth' }, options?: { align?: 'bottom' | 'center' | 'top'; behavior?: 'auto' | 'smooth' },
) => { ) => {
if (!listRef.current || !tableMeta) return; if (!listRef.current || !tableMetaRef.current) return;
const row = Math.floor(index / tableMeta.columnCount); const row = Math.floor(index / tableMetaRef.current.columnCount);
// Map alignment options to react-window's alignment // Map alignment options to react-window's alignment
let alignment: 'auto' | 'center' | 'end' | 'smart' | 'start' = 'smart'; let alignment: 'auto' | 'center' | 'end' | 'smart' | 'start' = 'smart';
@@ -390,7 +422,7 @@ export const ItemGridList = ({
listRef.current.scrollToItem(row, alignment); listRef.current.scrollToItem(row, alignment);
}, },
[tableMeta], [],
); );
const scrollToOffset = useCallback((offset: number) => { const scrollToOffset = useCallback((offset: number) => {
@@ -401,7 +433,7 @@ export const ItemGridList = ({
// Handle keyboard navigation // Handle keyboard navigation
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => { (e: React.KeyboardEvent<HTMLDivElement>) => {
if (!enableSelection || !tableMeta) return; if (!enableSelection || !tableMetaRef.current) return;
if ( if (
e.key !== 'ArrowDown' && e.key !== 'ArrowDown' &&
e.key !== 'ArrowUp' && e.key !== 'ArrowUp' &&
@@ -428,9 +460,12 @@ export const ItemGridList = ({
// Calculate grid position // Calculate grid position
const currentRow = const currentRow =
currentIndex !== -1 ? Math.floor(currentIndex / tableMeta.columnCount) : 0; currentIndex !== -1
const currentCol = currentIndex !== -1 ? currentIndex % tableMeta.columnCount : 0; ? Math.floor(currentIndex / tableMetaRef.current.columnCount)
const totalRows = Math.ceil(data.length / tableMeta.columnCount); : 0;
const currentCol =
currentIndex !== -1 ? currentIndex % tableMetaRef.current.columnCount : 0;
const totalRows = Math.ceil(data.length / tableMetaRef.current.columnCount);
let newIndex = 0; let newIndex = 0;
if (currentIndex !== -1) { if (currentIndex !== -1) {
@@ -439,9 +474,9 @@ export const ItemGridList = ({
// Move down one row // Move down one row
const nextRow = currentRow + 1; const nextRow = currentRow + 1;
if (nextRow < totalRows) { if (nextRow < totalRows) {
const nextRowStart = nextRow * tableMeta.columnCount; const nextRowStart = nextRow * tableMetaRef.current.columnCount;
const nextRowEnd = Math.min( const nextRowEnd = Math.min(
nextRowStart + tableMeta.columnCount - 1, nextRowStart + tableMetaRef.current.columnCount - 1,
data.length - 1, data.length - 1,
); );
// Keep same column position, or use last item in row if column doesn't exist // Keep same column position, or use last item in row if column doesn't exist
@@ -458,8 +493,8 @@ export const ItemGridList = ({
} else if (currentRow > 0) { } else if (currentRow > 0) {
// Wrap to end of previous row // Wrap to end of previous row
newIndex = Math.max( newIndex = Math.max(
(currentRow - 1) * tableMeta.columnCount + (currentRow - 1) * tableMetaRef.current.columnCount +
tableMeta.columnCount - tableMetaRef.current.columnCount -
1, 1,
0, 0,
); );
@@ -472,14 +507,14 @@ export const ItemGridList = ({
case 'ArrowRight': { case 'ArrowRight': {
// Move right, wrap to next row if at end of row // Move right, wrap to next row if at end of row
if ( if (
currentCol < tableMeta.columnCount - 1 && currentCol < tableMetaRef.current.columnCount - 1 &&
currentIndex < data.length - 1 currentIndex < data.length - 1
) { ) {
newIndex = currentIndex + 1; newIndex = currentIndex + 1;
} else if (currentRow < totalRows - 1) { } else if (currentRow < totalRows - 1) {
// Wrap to start of next row // Wrap to start of next row
newIndex = Math.min( newIndex = Math.min(
(currentRow + 1) * tableMeta.columnCount, (currentRow + 1) * tableMetaRef.current.columnCount,
data.length - 1, data.length - 1,
); );
} else { } else {
@@ -491,9 +526,9 @@ export const ItemGridList = ({
// Move up one row // Move up one row
const prevRow = currentRow - 1; const prevRow = currentRow - 1;
if (prevRow >= 0) { if (prevRow >= 0) {
const prevRowStart = prevRow * tableMeta.columnCount; const prevRowStart = prevRow * tableMetaRef.current.columnCount;
const prevRowEnd = Math.min( const prevRowEnd = Math.min(
prevRowStart + tableMeta.columnCount - 1, prevRowStart + tableMetaRef.current.columnCount - 1,
data.length - 1, data.length - 1,
); );
// Keep same column position, or use last item in row if column doesn't exist // Keep same column position, or use last item in row if column doesn't exist
@@ -599,7 +634,7 @@ export const ItemGridList = ({
scrollToIndex(newIndex); scrollToIndex(newIndex);
}, },
[data, enableSelection, internalState, tableMeta, scrollToIndex], [data, enableSelection, internalState, scrollToIndex],
); );
const imperativeHandle: ItemListHandle = useMemo(() => { const imperativeHandle: ItemListHandle = useMemo(() => {
@@ -634,6 +669,7 @@ export const ItemGridList = ({
<AutoSizer> <AutoSizer>
{({ height, width }) => ( {({ height, width }) => (
<VirtualizedGridList <VirtualizedGridList
_tableMetaVersion={tableMetaVersion}
controls={controls} controls={controls}
data={data} data={data}
enableDrag={enableDrag} enableDrag={enableDrag}
@@ -650,7 +686,7 @@ export const ItemGridList = ({
outerRef={outerRef} outerRef={outerRef}
ref={listRef} ref={listRef}
rows={rows} rows={rows}
tableMeta={tableMeta} tableMetaRef={tableMetaRef}
width={width} width={width}
/> />
)} )}