UNPKG

@bestyled/contrib-flatlist

Version:

Implementation of FlatList for the BeStyled Design System

191 lines (190 loc) 7.68 kB
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 };