mirror of
https://github.com/Radarr/Radarr
synced 2025-12-06 08:28:50 +01:00
167 lines
4.6 KiB
TypeScript
167 lines
4.6 KiB
TypeScript
import React, { ReactNode, useEffect, useRef } from 'react';
|
|
import { Grid, GridCellProps, WindowScroller } from 'react-virtualized';
|
|
import ModelBase from 'App/ModelBase';
|
|
import Scroller from 'Components/Scroller/Scroller';
|
|
import useMeasure from 'Helpers/Hooks/useMeasure';
|
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
|
import { scrollDirections } from 'Helpers/Props';
|
|
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
|
import styles from './VirtualTable.css';
|
|
|
|
const ROW_HEIGHT = 38;
|
|
|
|
function overscanIndicesGetter(options: {
|
|
cellCount: number;
|
|
overscanCellsCount: number;
|
|
startIndex: number;
|
|
stopIndex: number;
|
|
}) {
|
|
const { cellCount, overscanCellsCount, startIndex, stopIndex } = options;
|
|
|
|
// The default getter takes the scroll direction into account,
|
|
// but that can cause issues. Ignore the scroll direction and
|
|
// always over return more items.
|
|
|
|
const overscanStartIndex = startIndex - overscanCellsCount;
|
|
const overscanStopIndex = stopIndex + overscanCellsCount;
|
|
|
|
return {
|
|
overscanStartIndex: Math.max(0, overscanStartIndex),
|
|
overscanStopIndex: Math.min(cellCount - 1, overscanStopIndex),
|
|
};
|
|
}
|
|
|
|
interface VirtualTableProps<T extends ModelBase> {
|
|
isSmallScreen: boolean;
|
|
className?: string;
|
|
items: T[];
|
|
scrollIndex?: number;
|
|
scrollTop?: number;
|
|
scroller: Element;
|
|
header: React.ReactNode;
|
|
headerHeight?: number;
|
|
rowRenderer: (rowProps: GridCellProps) => ReactNode;
|
|
rowHeight?: number;
|
|
}
|
|
|
|
function VirtualTable<T extends ModelBase>({
|
|
isSmallScreen,
|
|
className = styles.tableContainer,
|
|
items,
|
|
scroller,
|
|
scrollIndex,
|
|
scrollTop,
|
|
header,
|
|
headerHeight = 38,
|
|
rowHeight = ROW_HEIGHT,
|
|
rowRenderer,
|
|
...otherProps
|
|
}: VirtualTableProps<T>) {
|
|
const [measureRef, bounds] = useMeasure();
|
|
const gridRef = useRef<Grid>(null);
|
|
const scrollRestored = useRef(false);
|
|
const previousScrollIndex = usePrevious(scrollIndex);
|
|
const previousItems = usePrevious(items);
|
|
|
|
const width = bounds.width;
|
|
|
|
const gridStyle = {
|
|
boxSizing: undefined,
|
|
direction: undefined,
|
|
height: undefined,
|
|
position: undefined,
|
|
willChange: undefined,
|
|
overflow: undefined,
|
|
width: undefined,
|
|
};
|
|
|
|
const containerStyle = {
|
|
position: undefined,
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (gridRef.current && width > 0) {
|
|
gridRef.current.recomputeGridSize();
|
|
}
|
|
}, [width]);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
gridRef.current &&
|
|
previousItems &&
|
|
hasDifferentItemsOrOrder(previousItems, items)
|
|
) {
|
|
gridRef.current.recomputeGridSize();
|
|
}
|
|
}, [items, previousItems]);
|
|
|
|
useEffect(() => {
|
|
if (gridRef.current && scrollTop && !scrollRestored.current) {
|
|
gridRef.current.scrollToPosition({ scrollLeft: 0, scrollTop });
|
|
scrollRestored.current = true;
|
|
}
|
|
}, [scrollTop]);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
gridRef.current &&
|
|
scrollIndex != null &&
|
|
scrollIndex !== previousScrollIndex
|
|
) {
|
|
gridRef.current.scrollToCell({
|
|
rowIndex: scrollIndex,
|
|
columnIndex: 0,
|
|
});
|
|
}
|
|
}, [scrollIndex, previousScrollIndex]);
|
|
|
|
return (
|
|
<WindowScroller scrollElement={isSmallScreen ? undefined : scroller}>
|
|
{({ height, registerChild, onChildScroll, scrollTop }) => {
|
|
if (!height) {
|
|
return null;
|
|
}
|
|
return (
|
|
<div ref={measureRef}>
|
|
<Scroller
|
|
className={className}
|
|
scrollDirection={scrollDirections.HORIZONTAL}
|
|
>
|
|
{header}
|
|
|
|
{/* @ts-expect-error - ref type is incompatible */}
|
|
<div ref={registerChild}>
|
|
<Grid
|
|
{...otherProps}
|
|
ref={gridRef}
|
|
autoContainerWidth={true}
|
|
autoHeight={true}
|
|
autoWidth={true}
|
|
width={width}
|
|
height={height}
|
|
headerHeight={height - headerHeight}
|
|
rowHeight={rowHeight}
|
|
rowCount={items.length}
|
|
columnCount={1}
|
|
columnWidth={width}
|
|
scrollTop={scrollTop}
|
|
overscanRowCount={2}
|
|
cellRenderer={rowRenderer}
|
|
overscanIndicesGetter={overscanIndicesGetter}
|
|
scrollToAlignment="start"
|
|
isScrollingOptout={true}
|
|
className={styles.tableBodyContainer}
|
|
style={gridStyle}
|
|
containerStyle={containerStyle}
|
|
onScroll={onChildScroll}
|
|
/>
|
|
</div>
|
|
</Scroller>
|
|
</div>
|
|
);
|
|
}}
|
|
</WindowScroller>
|
|
);
|
|
}
|
|
|
|
export default VirtualTable;
|