mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-06 20:10:12 +02:00
refactor item grid sizer handling to prevent blanks on resize
This commit is contained in:
@@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user