Skip to content

Instantly share code, notes, and snippets.

@lyleunderwood
Created April 16, 2020 19:22
Show Gist options
  • Save lyleunderwood/b7615b3b17b61922757ece4b28d70cb5 to your computer and use it in GitHub Desktop.
Save lyleunderwood/b7615b3b17b61922757ece4b28d70cb5 to your computer and use it in GitHub Desktop.
// @flow
import * as React from 'react';
import { List, AutoSizer, CellMeasurer, CellMeasurerCache } from 'react-virtualized';
import styled from 'styled-components';
import isFirefox from 'is-firefox';
import Translate from '../translate';
import Spinner from '../spinner';
export type RowRenderer = $ReadOnly<{|
index: number,
isScrolling: boolean,
isVisible: boolean,
key: string,
parent: Object,
style: Object,
|}> => React.Node;
export type LoadingRenderer = $ReadOnly<{|
index: number,
defaultHeight: number,
style: { [string]: mixed },
key: string,
parent: React.AbstractComponent<mixed>,
isScrolling: boolean,
isVisible: boolean,
width: number,
|}> => React.Node;
export type Props = $ReadOnly<{|
scrollTo: number | void,
scrollToAlignment?: string,
onScrollTo: (number) => void,
defaultHeight: number,
rowRenderer: RowRenderer,
rowCount: number,
overscanRowCount?: number,
onRowsRendered?: ({| +startIndex: number, +stopIndex: number |}) => void,
onVisibleRowsChanged?: $ReadOnlyArray<number> => void,
registerChild?: any => void,
isRowLoaded?: ({| +index: number |}) => boolean,
loadingRenderer?: LoadingRenderer,
noRowsRenderer?: () => React.Node;
rowsToClearFromCache?: $ReadOnlyArray<number>,
|}>;
const FlexRow = styled.div`
display: flex;
flex-direction: row;
flex-wrap: wrap;
`;
const NoRows = styled.div`
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
align-items: center;
height: 100%;
`;
const LoadingRow = styled.div`
height: ${({ height }) => String(height)}px;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
`;
const handleRowsRendered = (onScrollTo, onRowsRendered) =>
({ startIndex, stopIndex }) => {
onScrollTo(startIndex);
onRowsRendered({
startIndex,
stopIndex,
});
};
// eslint-disable-next-line react/prop-types
const defaultLoadingRenderer: LoadingRenderer = ({ defaultHeight }) => (
<LoadingRow height={defaultHeight}><Spinner /></LoadingRow>
);
const defaultNoRowsRenderer = () => (
<NoRows><Translate>no_data_to_display</Translate></NoRows>
);
const visibleRows = (grid: any): $ReadOnlyArray<number> =>
((
// eslint-disable-next-line no-underscore-dangle
Object.entries(grid._styleCache).filter(([, value]) => value): any
): $ReadOnlyArray<[string, { +top: number, +height: number }]>)
.filter(([, { top, height }]) =>
(top < (grid.state.scrollTop + grid.props.height)) &&
(top + height > grid.state.scrollTop),
)
.map(([key]) => Number(key.split('-')[0]));
/**
* This is a list component which is designed for rows with contents which may
* wrap one or more lines as per-row items increase, increasing the row height.
* It uses a CellMeasurer to JIT measure the height of rows to account for
* fully dynamic row heights. It also expands to fill the space of its
* container.
*/
const RowList = ({
scrollTo,
scrollToAlignment,
onScrollTo,
defaultHeight,
rowRenderer,
rowCount,
overscanRowCount = 10,
onRowsRendered = () => {},
onVisibleRowsChanged,
registerChild,
isRowLoaded = () => true,
loadingRenderer = defaultLoadingRenderer,
noRowsRenderer = defaultNoRowsRenderer,
rowsToClearFromCache,
}: Props) => {
const { current: cache } = React.useRef(new CellMeasurerCache({
fixedWidth: true,
}));
if (rowsToClearFromCache) {
rowsToClearFromCache.forEach(row => cache.clear(row, 0));
}
const gridRef = React.useRef(null);
// this effect is to handle this ridiculous issue, which can only be caused by
// some wacky bug in firefox:
//
// https://git.io/Jv9ji
//
// i was also able to make this issue go away by removing overflow: hidden
// from the top-level dojo app element and .dijitLayoutContainer for some
// reason, but i didn't know what side-effects that might have. anyway,
// apparently willChange was set to transform in order to sidestep some chrome
// bug which caused some performance issue:
//
// https://git.io/Jv9j6
//
// so, maybe this won't make FF performance terrible, but we'll see.
//
// TODO: report this at bvaughn/react-virtualized
React.useEffect(() => {
if (!isFirefox) return;
const grid = gridRef.current;
// eslint-disable-next-line no-underscore-dangle
const node = (grid || {})._scrollingContainer;
if (!(node instanceof HTMLElement)) return;
// eslint-disable-next-line fp/no-mutation
node.style.willChange = 'unset';
}, []);
const loadableRowRenderer = (width: number) => ({
key,
parent,
index,
style,
isScrolling,
isVisible,
}: {
key: string,
parent: React.AbstractComponent<mixed>,
index: number,
style: { [string]: mixed },
isScrolling: boolean,
isVisible: boolean,
}) => (
<CellMeasurer
cache={cache}
rowIndex={index}
columnIndex={0}
parent={parent}
key={key}
minHeight={defaultHeight}
defaultHeight={defaultHeight}
>
<FlexRow style={style} key={key}>
{
isRowLoaded({ index })
? rowRenderer({ key, parent, index, style, isScrolling, isVisible })
: loadingRenderer({
width,
key,
style,
index,
parent,
isScrolling,
isVisible,
defaultHeight,
})
}
</FlexRow>
</CellMeasurer>
);
const handleVisibleRowsChanged = React.useCallback(() => {
if (!onVisibleRowsChanged || !gridRef.current) return;
onVisibleRowsChanged(visibleRows(gridRef.current));
}, [onVisibleRowsChanged]);
const handleRegisterChild = (child) => {
if (registerChild) registerChild(child);
if (!child || gridRef.current === child.Grid) return;
// eslint-disable-next-line fp/no-mutation
gridRef.current = child.Grid;
handleVisibleRowsChanged();
};
const handleScroll = () => {
if (!gridRef.current) return;
handleVisibleRowsChanged();
};
return (
<AutoSizer
onResize={() => { cache.clearAll(); handleVisibleRowsChanged(); }}
defaultHeight={200}
>
{({ width, height }) => (
<List
ref={handleRegisterChild}
scrollToIndex={scrollTo}
scrollToAlignment={scrollToAlignment}
onRowsRendered={handleRowsRendered(onScrollTo, onRowsRendered)}
onScroll={handleScroll}
rowRenderer={loadableRowRenderer(width)}
rowCount={rowCount}
height={height}
width={width}
rowHeight={cache.rowHeight}
overscanRowCount={overscanRowCount}
deferredMeasurementCache={cache}
noRowsRenderer={noRowsRenderer}
/>
)}
</AutoSizer>
);
};
export default RowList;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment