Created
April 16, 2020 19:22
-
-
Save lyleunderwood/b7615b3b17b61922757ece4b28d70cb5 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @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