@bestyled/contrib-flatlist
Version:
Implementation of FlatList for the BeStyled Design System
191 lines (190 loc) • 7.68 kB
JavaScript
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
/* Forked from react-virtualized and react-tiny-virtual-list and adapted to Hooks 💖 */
import * as React from 'react';
import { useResizeObserver, useForceUpdate, useInstanceRef } from '@bestyled/contrib-common';
import SizeAndPositionManager from './SizeAndPositionManager';
const STYLE_WRAPPER = {
overflow: 'auto',
willChange: 'transform',
WebkitOverflowScrolling: 'touch',
boxSizing: 'content-box'
};
const STYLE_INNER = {
position: 'relative',
display: 'flex',
flexDirection: 'column',
minHeight: '100%',
marginRight: '10px'
};
const STYLE_ITEM = {
flex: '0 0 auto'
};
export const FlatListChild = React.memo(props => {
const { measure, index, children, style } = props;
const [containerRef, height] = useResizeObserver();
React.useEffect(() => {
if (height !== null) {
measure(index, height);
}
}, [height, index]);
return (React.createElement("div", { itemType: "FlatListChild", "data-index": index, style: style, ref: containerRef }, children));
});
export const FlatList = props => {
const forceUpdate = useForceUpdate();
/* prop variables */
const { className, estimatedItemSize = 50, height, itemCount, overscanCount = 3, scrollToIndex, scrollToAlignment, onScroll, renderItem, data, id } = props, rest = __rest(props
/* instance variables */
, ["className", "estimatedItemSize", "height", "itemCount", "overscanCount", "scrollToIndex", "scrollToAlignment", "onScroll", "renderItem", "data", "id"]);
/* instance variables */
const rootNode = React.useRef(null);
const sizeAndPositionManager = useInstanceRef(() => new SizeAndPositionManager({
itemCount,
estimatedItemSize
}));
const actualOffset = React.useRef(0);
const targetScrollToIndex = React.useRef(scrollToIndex);
React.useEffect(() => {
if (scrollToIndex !== null) {
targetScrollToIndex.current = scrollToIndex;
forceUpdate();
}
}, [scrollToIndex]);
React.useEffect(() => {
sizeAndPositionManager.current.resize({
itemCount,
estimatedItemSize
});
forceUpdate();
}, [itemCount, estimatedItemSize]);
const getStartStop = React.useCallback(() => {
return sizeAndPositionManager.current.getVisibleRange({
containerSize: height,
offset: actualOffset.current,
overscanCount
});
}, [height, overscanCount]);
const visibleRange = useInstanceRef(() => getStartStop());
const getOffsetForIndex = React.useCallback((index) => {
if (index < 0 || index >= itemCount) {
index = 0;
}
return sizeAndPositionManager.current.getUpdatedOffsetForIndex({
align: scrollToAlignment,
containerSize: height,
currentOffset: actualOffset.current || 0,
targetIndex: index
});
}, [scrollToAlignment, height, itemCount]);
/* component mounted and unmounted */
/* props change handlers */
React.useEffect(() => {
const { start, stop, paddingTop } = getStartStop();
visibleRange.current.start = start;
visibleRange.current.stop = stop;
visibleRange.current.paddingTop = paddingTop;
forceUpdate();
}, [
getStartStop,
itemCount,
estimatedItemSize,
height,
overscanCount,
scrollToIndex
]);
/* additional private methods */
const scrollTo = (value) => {
if (value == null) {
return;
}
rootNode.current.scrollTo({
top: value,
behavior: rootNode.current.scrollTop / value > 0.8 ? 'smooth' : 'auto'
});
};
const handleScroll = React.useCallback((event) => {
const newOffset = getNodeOffset();
if (newOffset < 0 ||
actualOffset.current === newOffset ||
event.target !== rootNode.current) {
return;
}
actualOffset.current = newOffset;
const { start, stop, paddingTop } = getStartStop();
if (start !== visibleRange.current.start ||
stop !== visibleRange.current.stop) {
visibleRange.current.start = start;
visibleRange.current.stop = stop;
visibleRange.current.paddingTop = paddingTop;
forceUpdate();
}
if (typeof onScroll === 'function') {
onScroll(newOffset, event);
}
}, [getStartStop, onScroll]);
React.useEffect(() => {
rootNode.current.addEventListener('scroll', handleScroll, {
passive: true
});
if (scrollToIndex != null) {
scrollTo(getOffsetForIndex(scrollToIndex));
}
return () => {
rootNode.current.removeEventListener('scroll', handleScroll);
};
}, [handleScroll]);
const getNodeOffset = () => {
return rootNode.current.scrollTop;
};
const didChangeSize = React.useCallback((index, height) => {
if (!sizeAndPositionManager.current.setItem(index, height)) {
return;
}
if (targetScrollToIndex.current !== undefined && scrollToIndex !== null) {
actualOffset.current =
(targetScrollToIndex.current != null &&
getOffsetForIndex(targetScrollToIndex.current)) ||
0;
}
const { start, stop, paddingTop } = getStartStop();
if (start !== visibleRange.current.start ||
stop !== visibleRange.current.stop) {
visibleRange.current.start = start;
visibleRange.current.stop = stop;
visibleRange.current.paddingTop = paddingTop;
forceUpdate();
}
if (index === targetScrollToIndex.current) {
setTimeout(() => {
targetScrollToIndex.current = undefined;
}, 2000);
}
if (targetScrollToIndex.current !== undefined) {
scrollTo(actualOffset.current);
}
}, [scrollToAlignment, height, itemCount]);
const items = [];
const wrapperStyle = Object.assign(Object.assign({}, STYLE_WRAPPER), { height });
const innerStyle = Object.assign(Object.assign({}, STYLE_INNER), { height: sizeAndPositionManager.current.getTotalSize(), paddingTop: visibleRange.current.paddingTop });
if (typeof visibleRange.current.start !== 'undefined' &&
typeof visibleRange.current.stop !== 'undefined') {
for (let index = visibleRange.current.start; index <= visibleRange.current.stop; index++) {
const item = data[index];
items.push(React.createElement(FlatListChild, { key: item ? item.key || -index : -index, measure: didChangeSize, index: index, style: STYLE_ITEM }, renderItem({ item, index })));
}
}
return (React.createElement("div", Object.assign({ ref: rootNode }, rest, { style: wrapperStyle }),
React.createElement("div", { id: id, itemType: "FlatListInner", "data-start": visibleRange.current.start, "data-stop": visibleRange.current.stop, style: innerStyle }, items)));
};
FlatList.defaultProps = {
overscanCount: 3
};