react-native-web
Version:
React Native for Web
1,152 lines (1,115 loc) • 64.2 kB
JavaScript
import _createForOfIteratorHelperLoose from "@babel/runtime/helpers/createForOfIteratorHelperLoose";
import _extends from "@babel/runtime/helpers/extends";
import _objectSpread from "@babel/runtime/helpers/objectSpread2";
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*
* @format
*/
import RefreshControl from '../../../exports/RefreshControl';
import ScrollView from '../../../exports/ScrollView';
import View from '../../../exports/View';
import StyleSheet from '../../../exports/StyleSheet';
import Batchinator from '../Batchinator';
import clamp from '../Utilities/clamp';
import infoLog from '../infoLog';
import { CellRenderMask } from './CellRenderMask';
import ChildListCollection from './ChildListCollection';
import FillRateHelper from '../FillRateHelper';
import StateSafePureComponent from './StateSafePureComponent';
import ViewabilityHelper from '../ViewabilityHelper';
import CellRenderer from './VirtualizedListCellRenderer';
import { VirtualizedListCellContextProvider, VirtualizedListContext, VirtualizedListContextProvider } from './VirtualizedListContext.js';
import { computeWindowedRenderLimits, keyExtractor as defaultKeyExtractor } from '../VirtualizeUtils';
import invariant from 'fbjs/lib/invariant';
import nullthrows from 'nullthrows';
import * as React from 'react';
var __DEV__ = process.env.NODE_ENV !== 'production';
var ON_EDGE_REACHED_EPSILON = 0.001;
var _usedIndexForKey = false;
var _keylessItemComponentName = '';
/**
* Default Props Helper Functions
* Use the following helper functions for default values
*/
// horizontalOrDefault(this.props.horizontal)
function horizontalOrDefault(horizontal) {
return horizontal !== null && horizontal !== void 0 ? horizontal : false;
}
// initialNumToRenderOrDefault(this.props.initialNumToRender)
function initialNumToRenderOrDefault(initialNumToRender) {
return initialNumToRender !== null && initialNumToRender !== void 0 ? initialNumToRender : 10;
}
// maxToRenderPerBatchOrDefault(this.props.maxToRenderPerBatch)
function maxToRenderPerBatchOrDefault(maxToRenderPerBatch) {
return maxToRenderPerBatch !== null && maxToRenderPerBatch !== void 0 ? maxToRenderPerBatch : 10;
}
// onStartReachedThresholdOrDefault(this.props.onStartReachedThreshold)
function onStartReachedThresholdOrDefault(onStartReachedThreshold) {
return onStartReachedThreshold !== null && onStartReachedThreshold !== void 0 ? onStartReachedThreshold : 2;
}
// onEndReachedThresholdOrDefault(this.props.onEndReachedThreshold)
function onEndReachedThresholdOrDefault(onEndReachedThreshold) {
return onEndReachedThreshold !== null && onEndReachedThreshold !== void 0 ? onEndReachedThreshold : 2;
}
// getScrollingThreshold(visibleLength, onEndReachedThreshold)
function getScrollingThreshold(threshold, visibleLength) {
return threshold * visibleLength / 2;
}
// scrollEventThrottleOrDefault(this.props.scrollEventThrottle)
function scrollEventThrottleOrDefault(scrollEventThrottle) {
return scrollEventThrottle !== null && scrollEventThrottle !== void 0 ? scrollEventThrottle : 50;
}
// windowSizeOrDefault(this.props.windowSize)
function windowSizeOrDefault(windowSize) {
return windowSize !== null && windowSize !== void 0 ? windowSize : 21;
}
function findLastWhere(arr, predicate) {
for (var i = arr.length - 1; i >= 0; i--) {
if (predicate(arr[i])) {
return arr[i];
}
}
return null;
}
/**
* Base implementation for the more convenient [`<FlatList>`](https://reactnative.dev/docs/flatlist)
* and [`<SectionList>`](https://reactnative.dev/docs/sectionlist) components, which are also better
* documented. In general, this should only really be used if you need more flexibility than
* `FlatList` provides, e.g. for use with immutable data instead of plain arrays.
*
* Virtualization massively improves memory consumption and performance of large lists by
* maintaining a finite render window of active items and replacing all items outside of the render
* window with appropriately sized blank space. The window adapts to scrolling behavior, and items
* are rendered incrementally with low-pri (after any running interactions) if they are far from the
* visible area, or with hi-pri otherwise to minimize the potential of seeing blank space.
*
* Some caveats:
*
* - Internal state is not preserved when content scrolls out of the render window. Make sure all
* your data is captured in the item data or external stores like Flux, Redux, or Relay.
* - This is a `PureComponent` which means that it will not re-render if `props` remain shallow-
* equal. Make sure that everything your `renderItem` function depends on is passed as a prop
* (e.g. `extraData`) that is not `===` after updates, otherwise your UI may not update on
* changes. This includes the `data` prop and parent component state.
* - In order to constrain memory and enable smooth scrolling, content is rendered asynchronously
* offscreen. This means it's possible to scroll faster than the fill rate ands momentarily see
* blank content. This is a tradeoff that can be adjusted to suit the needs of each application,
* and we are working on improving it behind the scenes.
* - By default, the list looks for a `key` or `id` prop on each item and uses that for the React key.
* Alternatively, you can provide a custom `keyExtractor` prop.
* - As an effort to remove defaultProps, use helper functions when referencing certain props
*
*/
class VirtualizedList extends StateSafePureComponent {
// scrollToEnd may be janky without getItemLayout prop
scrollToEnd(params) {
var animated = params ? params.animated : true;
var veryLast = this.props.getItemCount(this.props.data) - 1;
if (veryLast < 0) {
return;
}
var frame = this.__getFrameMetricsApprox(veryLast, this.props);
var offset = Math.max(0, frame.offset + frame.length + this._footerLength - this._scrollMetrics.visibleLength);
if (this._scrollRef == null) {
return;
}
if (this._scrollRef.scrollTo == null) {
console.warn('No scrollTo method provided. This may be because you have two nested ' + 'VirtualizedLists with the same orientation, or because you are ' + 'using a custom component that does not implement scrollTo.');
return;
}
this._scrollRef.scrollTo(horizontalOrDefault(this.props.horizontal) ? {
x: offset,
animated
} : {
y: offset,
animated
});
}
// scrollToIndex may be janky without getItemLayout prop
scrollToIndex(params) {
var _this$props = this.props,
data = _this$props.data,
horizontal = _this$props.horizontal,
getItemCount = _this$props.getItemCount,
getItemLayout = _this$props.getItemLayout,
onScrollToIndexFailed = _this$props.onScrollToIndexFailed;
var animated = params.animated,
index = params.index,
viewOffset = params.viewOffset,
viewPosition = params.viewPosition;
invariant(index >= 0, "scrollToIndex out of range: requested index " + index + " but minimum is 0");
invariant(getItemCount(data) >= 1, "scrollToIndex out of range: item length " + getItemCount(data) + " but minimum is 1");
invariant(index < getItemCount(data), "scrollToIndex out of range: requested index " + index + " is out of 0 to " + (getItemCount(data) - 1));
if (!getItemLayout && index > this._highestMeasuredFrameIndex) {
invariant(!!onScrollToIndexFailed, 'scrollToIndex should be used in conjunction with getItemLayout or onScrollToIndexFailed, ' + 'otherwise there is no way to know the location of offscreen indices or handle failures.');
onScrollToIndexFailed({
averageItemLength: this._averageCellLength,
highestMeasuredFrameIndex: this._highestMeasuredFrameIndex,
index
});
return;
}
var frame = this.__getFrameMetricsApprox(Math.floor(index), this.props);
var offset = Math.max(0, this._getOffsetApprox(index, this.props) - (viewPosition || 0) * (this._scrollMetrics.visibleLength - frame.length)) - (viewOffset || 0);
if (this._scrollRef == null) {
return;
}
if (this._scrollRef.scrollTo == null) {
console.warn('No scrollTo method provided. This may be because you have two nested ' + 'VirtualizedLists with the same orientation, or because you are ' + 'using a custom component that does not implement scrollTo.');
return;
}
this._scrollRef.scrollTo(horizontal ? {
x: offset,
animated
} : {
y: offset,
animated
});
}
// scrollToItem may be janky without getItemLayout prop. Required linear scan through items -
// use scrollToIndex instead if possible.
scrollToItem(params) {
var item = params.item;
var _this$props2 = this.props,
data = _this$props2.data,
getItem = _this$props2.getItem,
getItemCount = _this$props2.getItemCount;
var itemCount = getItemCount(data);
for (var _index = 0; _index < itemCount; _index++) {
if (getItem(data, _index) === item) {
this.scrollToIndex(_objectSpread(_objectSpread({}, params), {}, {
index: _index
}));
break;
}
}
}
/**
* Scroll to a specific content pixel offset in the list.
*
* Param `offset` expects the offset to scroll to.
* In case of `horizontal` is true, the offset is the x-value,
* in any other case the offset is the y-value.
*
* Param `animated` (`true` by default) defines whether the list
* should do an animation while scrolling.
*/
scrollToOffset(params) {
var animated = params.animated,
offset = params.offset;
if (this._scrollRef == null) {
return;
}
if (this._scrollRef.scrollTo == null) {
console.warn('No scrollTo method provided. This may be because you have two nested ' + 'VirtualizedLists with the same orientation, or because you are ' + 'using a custom component that does not implement scrollTo.');
return;
}
this._scrollRef.scrollTo(horizontalOrDefault(this.props.horizontal) ? {
x: offset,
animated
} : {
y: offset,
animated
});
}
recordInteraction() {
this._nestedChildLists.forEach(childList => {
childList.recordInteraction();
});
this._viewabilityTuples.forEach(t => {
t.viewabilityHelper.recordInteraction();
});
this._updateViewableItems(this.props, this.state.cellsAroundViewport);
}
flashScrollIndicators() {
if (this._scrollRef == null) {
return;
}
this._scrollRef.flashScrollIndicators();
}
/**
* Provides a handle to the underlying scroll responder.
* Note that `this._scrollRef` might not be a `ScrollView`, so we
* need to check that it responds to `getScrollResponder` before calling it.
*/
getScrollResponder() {
if (this._scrollRef && this._scrollRef.getScrollResponder) {
return this._scrollRef.getScrollResponder();
}
}
getScrollableNode() {
if (this._scrollRef && this._scrollRef.getScrollableNode) {
return this._scrollRef.getScrollableNode();
} else {
return this._scrollRef;
}
}
getScrollRef() {
if (this._scrollRef && this._scrollRef.getScrollRef) {
return this._scrollRef.getScrollRef();
} else {
return this._scrollRef;
}
}
_getCellKey() {
var _this$context;
return ((_this$context = this.context) == null ? void 0 : _this$context.cellKey) || 'rootList';
}
// $FlowFixMe[missing-local-annot]
hasMore() {
return this._hasMore;
}
// $FlowFixMe[missing-local-annot]
// REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller.
constructor(_props) {
var _this$props$updateCel;
super(_props);
this._getScrollMetrics = () => {
return this._scrollMetrics;
};
this._getOutermostParentListRef = () => {
if (this._isNestedWithSameOrientation()) {
return this.context.getOutermostParentListRef();
} else {
return this;
}
};
this._registerAsNestedChild = childList => {
this._nestedChildLists.add(childList.ref, childList.cellKey);
if (this._hasInteracted) {
childList.ref.recordInteraction();
}
};
this._unregisterAsNestedChild = childList => {
this._nestedChildLists.remove(childList.ref);
};
this._onUpdateSeparators = (keys, newProps) => {
keys.forEach(key => {
var ref = key != null && this._cellRefs[key];
ref && ref.updateSeparatorProps(newProps);
});
};
this._getSpacerKey = isVertical => isVertical ? 'height' : 'width';
this._averageCellLength = 0;
this._cellRefs = {};
this._frames = {};
this._footerLength = 0;
this._hasTriggeredInitialScrollToIndex = false;
this._hasInteracted = false;
this._hasMore = false;
this._hasWarned = {};
this._headerLength = 0;
this._hiPriInProgress = false;
this._highestMeasuredFrameIndex = 0;
this._indicesToKeys = new Map();
this._lastFocusedCellKey = null;
this._nestedChildLists = new ChildListCollection();
this._offsetFromParentVirtualizedList = 0;
this._prevParentOffset = 0;
this._scrollMetrics = {
contentLength: 0,
dOffset: 0,
dt: 10,
offset: 0,
timestamp: 0,
velocity: 0,
visibleLength: 0,
zoomScale: 1
};
this._scrollRef = null;
this._sentStartForContentLength = 0;
this._sentEndForContentLength = 0;
this._totalCellLength = 0;
this._totalCellsMeasured = 0;
this._viewabilityTuples = [];
this._captureScrollRef = ref => {
this._scrollRef = ref;
};
this._defaultRenderScrollComponent = props => {
var onRefresh = props.onRefresh;
if (this._isNestedWithSameOrientation()) {
// $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors
return /*#__PURE__*/React.createElement(View, props);
} else if (onRefresh) {
var _props$refreshing;
invariant(typeof props.refreshing === 'boolean', '`refreshing` prop must be set as a boolean in order to use `onRefresh`, but got `' + JSON.stringify((_props$refreshing = props.refreshing) !== null && _props$refreshing !== void 0 ? _props$refreshing : 'undefined') + '`');
return (
/*#__PURE__*/
// $FlowFixMe[prop-missing] Invalid prop usage
// $FlowFixMe[incompatible-use]
React.createElement(ScrollView, _extends({}, props, {
refreshControl: props.refreshControl == null ? /*#__PURE__*/React.createElement(RefreshControl
// $FlowFixMe[incompatible-type]
, {
refreshing: props.refreshing,
onRefresh: onRefresh,
progressViewOffset: props.progressViewOffset
}) : props.refreshControl
}))
);
} else {
// $FlowFixMe[prop-missing] Invalid prop usage
// $FlowFixMe[incompatible-use]
return /*#__PURE__*/React.createElement(ScrollView, props);
}
};
this._onCellLayout = (e, cellKey, index) => {
var layout = e.nativeEvent.layout;
var next = {
offset: this._selectOffset(layout),
length: this._selectLength(layout),
index,
inLayout: true
};
var curr = this._frames[cellKey];
if (!curr || next.offset !== curr.offset || next.length !== curr.length || index !== curr.index) {
this._totalCellLength += next.length - (curr ? curr.length : 0);
this._totalCellsMeasured += curr ? 0 : 1;
this._averageCellLength = this._totalCellLength / this._totalCellsMeasured;
this._frames[cellKey] = next;
this._highestMeasuredFrameIndex = Math.max(this._highestMeasuredFrameIndex, index);
this._scheduleCellsToRenderUpdate();
} else {
this._frames[cellKey].inLayout = true;
}
this._triggerRemeasureForChildListsInCell(cellKey);
this._computeBlankness();
this._updateViewableItems(this.props, this.state.cellsAroundViewport);
};
this._onCellUnmount = cellKey => {
delete this._cellRefs[cellKey];
var curr = this._frames[cellKey];
if (curr) {
this._frames[cellKey] = _objectSpread(_objectSpread({}, curr), {}, {
inLayout: false
});
}
};
this._onLayout = e => {
if (this._isNestedWithSameOrientation()) {
// Need to adjust our scroll metrics to be relative to our containing
// VirtualizedList before we can make claims about list item viewability
this.measureLayoutRelativeToContainingList();
} else {
this._scrollMetrics.visibleLength = this._selectLength(e.nativeEvent.layout);
}
this.props.onLayout && this.props.onLayout(e);
this._scheduleCellsToRenderUpdate();
this._maybeCallOnEdgeReached();
};
this._onLayoutEmpty = e => {
this.props.onLayout && this.props.onLayout(e);
};
this._onLayoutFooter = e => {
this._triggerRemeasureForChildListsInCell(this._getFooterCellKey());
this._footerLength = this._selectLength(e.nativeEvent.layout);
};
this._onLayoutHeader = e => {
this._headerLength = this._selectLength(e.nativeEvent.layout);
};
this._onContentSizeChange = (width, height) => {
if (width > 0 && height > 0 && this.props.initialScrollIndex != null && this.props.initialScrollIndex > 0 && !this._hasTriggeredInitialScrollToIndex) {
if (this.props.contentOffset == null) {
if (this.props.initialScrollIndex < this.props.getItemCount(this.props.data)) {
this.scrollToIndex({
animated: false,
index: nullthrows(this.props.initialScrollIndex)
});
} else {
this.scrollToEnd({
animated: false
});
}
}
this._hasTriggeredInitialScrollToIndex = true;
}
if (this.props.onContentSizeChange) {
this.props.onContentSizeChange(width, height);
}
this._scrollMetrics.contentLength = this._selectLength({
height,
width
});
this._scheduleCellsToRenderUpdate();
this._maybeCallOnEdgeReached();
};
this._convertParentScrollMetrics = metrics => {
// Offset of the top of the nested list relative to the top of its parent's viewport
var offset = metrics.offset - this._offsetFromParentVirtualizedList;
// Child's visible length is the same as its parent's
var visibleLength = metrics.visibleLength;
var dOffset = offset - this._scrollMetrics.offset;
var contentLength = this._scrollMetrics.contentLength;
return {
visibleLength,
contentLength,
offset,
dOffset
};
};
this._onScroll = e => {
this._nestedChildLists.forEach(childList => {
childList._onScroll(e);
});
if (this.props.onScroll) {
this.props.onScroll(e);
}
var timestamp = e.timeStamp;
var visibleLength = this._selectLength(e.nativeEvent.layoutMeasurement);
var contentLength = this._selectLength(e.nativeEvent.contentSize);
var offset = this._selectOffset(e.nativeEvent.contentOffset);
var dOffset = offset - this._scrollMetrics.offset;
if (this._isNestedWithSameOrientation()) {
if (this._scrollMetrics.contentLength === 0) {
// Ignore scroll events until onLayout has been called and we
// know our offset from our offset from our parent
return;
}
var _this$_convertParentS = this._convertParentScrollMetrics({
visibleLength,
offset
});
visibleLength = _this$_convertParentS.visibleLength;
contentLength = _this$_convertParentS.contentLength;
offset = _this$_convertParentS.offset;
dOffset = _this$_convertParentS.dOffset;
}
var dt = this._scrollMetrics.timestamp ? Math.max(1, timestamp - this._scrollMetrics.timestamp) : 1;
var velocity = dOffset / dt;
if (dt > 500 && this._scrollMetrics.dt > 500 && contentLength > 5 * visibleLength && !this._hasWarned.perf) {
infoLog('VirtualizedList: You have a large list that is slow to update - make sure your ' + 'renderItem function renders components that follow React performance best practices ' + 'like PureComponent, shouldComponentUpdate, etc.', {
dt,
prevDt: this._scrollMetrics.dt,
contentLength
});
this._hasWarned.perf = true;
}
// For invalid negative values (w/ RTL), set this to 1.
var zoomScale = e.nativeEvent.zoomScale < 0 ? 1 : e.nativeEvent.zoomScale;
this._scrollMetrics = {
contentLength,
dt,
dOffset,
offset,
timestamp,
velocity,
visibleLength,
zoomScale
};
this._updateViewableItems(this.props, this.state.cellsAroundViewport);
if (!this.props) {
return;
}
this._maybeCallOnEdgeReached();
if (velocity !== 0) {
this._fillRateHelper.activate();
}
this._computeBlankness();
this._scheduleCellsToRenderUpdate();
};
this._onScrollBeginDrag = e => {
this._nestedChildLists.forEach(childList => {
childList._onScrollBeginDrag(e);
});
this._viewabilityTuples.forEach(tuple => {
tuple.viewabilityHelper.recordInteraction();
});
this._hasInteracted = true;
this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e);
};
this._onScrollEndDrag = e => {
this._nestedChildLists.forEach(childList => {
childList._onScrollEndDrag(e);
});
var velocity = e.nativeEvent.velocity;
if (velocity) {
this._scrollMetrics.velocity = this._selectOffset(velocity);
}
this._computeBlankness();
this.props.onScrollEndDrag && this.props.onScrollEndDrag(e);
};
this._onMomentumScrollBegin = e => {
this._nestedChildLists.forEach(childList => {
childList._onMomentumScrollBegin(e);
});
this.props.onMomentumScrollBegin && this.props.onMomentumScrollBegin(e);
};
this._onMomentumScrollEnd = e => {
this._nestedChildLists.forEach(childList => {
childList._onMomentumScrollEnd(e);
});
this._scrollMetrics.velocity = 0;
this._computeBlankness();
this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e);
};
this._updateCellsToRender = () => {
this._updateViewableItems(this.props, this.state.cellsAroundViewport);
this.setState((state, props) => {
var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport);
var renderMask = VirtualizedList._createRenderMask(props, cellsAroundViewport, this._getNonViewportRenderRegions(props));
if (cellsAroundViewport.first === state.cellsAroundViewport.first && cellsAroundViewport.last === state.cellsAroundViewport.last && renderMask.equals(state.renderMask)) {
return null;
}
return {
cellsAroundViewport,
renderMask
};
});
};
this._createViewToken = (index, isViewable, props
// $FlowFixMe[missing-local-annot]
) => {
var data = props.data,
getItem = props.getItem;
var item = getItem(data, index);
return {
index,
item,
key: this._keyExtractor(item, index, props),
isViewable
};
};
this._getOffsetApprox = (index, props) => {
if (Number.isInteger(index)) {
return this.__getFrameMetricsApprox(index, props).offset;
} else {
var frameMetrics = this.__getFrameMetricsApprox(Math.floor(index), props);
var remainder = index - Math.floor(index);
return frameMetrics.offset + remainder * frameMetrics.length;
}
};
this.__getFrameMetricsApprox = (index, props) => {
var frame = this._getFrameMetrics(index, props);
if (frame && frame.index === index) {
// check for invalid frames due to row re-ordering
return frame;
} else {
var data = props.data,
getItemCount = props.getItemCount,
getItemLayout = props.getItemLayout;
invariant(index >= 0 && index < getItemCount(data), 'Tried to get frame for out of range index ' + index);
invariant(!getItemLayout, 'Should not have to estimate frames when a measurement metrics function is provided');
return {
length: this._averageCellLength,
offset: this._averageCellLength * index
};
}
};
this._getFrameMetrics = (index, props) => {
var data = props.data,
getItem = props.getItem,
getItemCount = props.getItemCount,
getItemLayout = props.getItemLayout;
invariant(index >= 0 && index < getItemCount(data), 'Tried to get frame for out of range index ' + index);
var item = getItem(data, index);
var frame = this._frames[this._keyExtractor(item, index, props)];
if (!frame || frame.index !== index) {
if (getItemLayout) {
/* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment
* suppresses an error found when Flow v0.63 was deployed. To see the error
* delete this comment and run Flow. */
return getItemLayout(data, index);
}
}
return frame;
};
this._getNonViewportRenderRegions = props => {
// Keep a viewport's worth of content around the last focused cell to allow
// random navigation around it without any blanking. E.g. tabbing from one
// focused item out of viewport to another.
if (!(this._lastFocusedCellKey && this._cellRefs[this._lastFocusedCellKey])) {
return [];
}
var lastFocusedCellRenderer = this._cellRefs[this._lastFocusedCellKey];
var focusedCellIndex = lastFocusedCellRenderer.props.index;
var itemCount = props.getItemCount(props.data);
// The last cell we rendered may be at a new index. Bail if we don't know
// where it is.
if (focusedCellIndex >= itemCount || this._keyExtractor(props.getItem(props.data, focusedCellIndex), focusedCellIndex, props) !== this._lastFocusedCellKey) {
return [];
}
var first = focusedCellIndex;
var heightOfCellsBeforeFocused = 0;
for (var i = first - 1; i >= 0 && heightOfCellsBeforeFocused < this._scrollMetrics.visibleLength; i--) {
first--;
heightOfCellsBeforeFocused += this.__getFrameMetricsApprox(i, props).length;
}
var last = focusedCellIndex;
var heightOfCellsAfterFocused = 0;
for (var _i = last + 1; _i < itemCount && heightOfCellsAfterFocused < this._scrollMetrics.visibleLength; _i++) {
last++;
heightOfCellsAfterFocused += this.__getFrameMetricsApprox(_i, props).length;
}
return [{
first,
last
}];
};
this._checkProps(_props);
this._fillRateHelper = new FillRateHelper(this._getFrameMetrics);
this._updateCellsToRenderBatcher = new Batchinator(this._updateCellsToRender, (_this$props$updateCel = this.props.updateCellsBatchingPeriod) !== null && _this$props$updateCel !== void 0 ? _this$props$updateCel : 50);
if (this.props.viewabilityConfigCallbackPairs) {
this._viewabilityTuples = this.props.viewabilityConfigCallbackPairs.map(pair => ({
viewabilityHelper: new ViewabilityHelper(pair.viewabilityConfig),
onViewableItemsChanged: pair.onViewableItemsChanged
}));
} else {
var _this$props3 = this.props,
onViewableItemsChanged = _this$props3.onViewableItemsChanged,
viewabilityConfig = _this$props3.viewabilityConfig;
if (onViewableItemsChanged) {
this._viewabilityTuples.push({
viewabilityHelper: new ViewabilityHelper(viewabilityConfig),
onViewableItemsChanged: onViewableItemsChanged
});
}
}
var initialRenderRegion = VirtualizedList._initialRenderRegion(_props);
this.state = {
cellsAroundViewport: initialRenderRegion,
renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion)
};
// REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller.
// For issue https://github.com/necolas/react-native-web/issues/995
this.invertedWheelEventHandler = ev => {
var scrollOffset = this.props.horizontal ? ev.target.scrollLeft : ev.target.scrollTop;
var scrollLength = this.props.horizontal ? ev.target.scrollWidth : ev.target.scrollHeight;
var clientLength = this.props.horizontal ? ev.target.clientWidth : ev.target.clientHeight;
var isEventTargetScrollable = scrollLength > clientLength;
var delta = this.props.horizontal ? ev.deltaX || ev.wheelDeltaX : ev.deltaY || ev.wheelDeltaY;
var leftoverDelta = delta;
if (isEventTargetScrollable) {
leftoverDelta = delta < 0 ? Math.min(delta + scrollOffset, 0) : Math.max(delta - (scrollLength - clientLength - scrollOffset), 0);
}
var targetDelta = delta - leftoverDelta;
if (this.props.inverted && this._scrollRef && this._scrollRef.getScrollableNode) {
var node = this._scrollRef.getScrollableNode();
if (this.props.horizontal) {
ev.target.scrollLeft += targetDelta;
var nextScrollLeft = node.scrollLeft - leftoverDelta;
node.scrollLeft = !this.props.getItemLayout ? Math.min(nextScrollLeft, this._totalCellLength) : nextScrollLeft;
} else {
ev.target.scrollTop += targetDelta;
var nextScrollTop = node.scrollTop - leftoverDelta;
node.scrollTop = !this.props.getItemLayout ? Math.min(nextScrollTop, this._totalCellLength) : nextScrollTop;
}
ev.preventDefault();
}
};
}
_checkProps(props) {
var onScroll = props.onScroll,
windowSize = props.windowSize,
getItemCount = props.getItemCount,
data = props.data,
initialScrollIndex = props.initialScrollIndex;
invariant(
// $FlowFixMe[prop-missing]
!onScroll || !onScroll.__isNative, 'Components based on VirtualizedList must be wrapped with Animated.createAnimatedComponent ' + 'to support native onScroll events with useNativeDriver');
invariant(windowSizeOrDefault(windowSize) > 0, 'VirtualizedList: The windowSize prop must be present and set to a value greater than 0.');
invariant(getItemCount, 'VirtualizedList: The "getItemCount" prop must be provided');
var itemCount = getItemCount(data);
if (initialScrollIndex != null && !this._hasTriggeredInitialScrollToIndex && (initialScrollIndex < 0 || itemCount > 0 && initialScrollIndex >= itemCount) && !this._hasWarned.initialScrollIndex) {
console.warn("initialScrollIndex \"" + initialScrollIndex + "\" is not valid (list has " + itemCount + " items)");
this._hasWarned.initialScrollIndex = true;
}
if (__DEV__ && !this._hasWarned.flexWrap) {
// $FlowFixMe[underconstrained-implicit-instantiation]
var flatStyles = StyleSheet.flatten(this.props.contentContainerStyle);
if (flatStyles != null && flatStyles.flexWrap === 'wrap') {
console.warn('`flexWrap: `wrap`` is not supported with the `VirtualizedList` components.' + 'Consider using `numColumns` with `FlatList` instead.');
this._hasWarned.flexWrap = true;
}
}
}
static _createRenderMask(props, cellsAroundViewport, additionalRegions) {
var itemCount = props.getItemCount(props.data);
invariant(cellsAroundViewport.first >= 0 && cellsAroundViewport.last >= cellsAroundViewport.first - 1 && cellsAroundViewport.last < itemCount, "Invalid cells around viewport \"[" + cellsAroundViewport.first + ", " + cellsAroundViewport.last + "]\" was passed to VirtualizedList._createRenderMask");
var renderMask = new CellRenderMask(itemCount);
if (itemCount > 0) {
var allRegions = [cellsAroundViewport, ...(additionalRegions !== null && additionalRegions !== void 0 ? additionalRegions : [])];
for (var _i2 = 0, _allRegions = allRegions; _i2 < _allRegions.length; _i2++) {
var region = _allRegions[_i2];
renderMask.addCells(region);
}
// The initially rendered cells are retained as part of the
// "scroll-to-top" optimization
if (props.initialScrollIndex == null || props.initialScrollIndex <= 0) {
var initialRegion = VirtualizedList._initialRenderRegion(props);
renderMask.addCells(initialRegion);
}
// The layout coordinates of sticker headers may be off-screen while the
// actual header is on-screen. Keep the most recent before the viewport
// rendered, even if its layout coordinates are not in viewport.
var stickyIndicesSet = new Set(props.stickyHeaderIndices);
VirtualizedList._ensureClosestStickyHeader(props, stickyIndicesSet, renderMask, cellsAroundViewport.first);
}
return renderMask;
}
static _initialRenderRegion(props) {
var _props$initialScrollI;
var itemCount = props.getItemCount(props.data);
var firstCellIndex = Math.max(0, Math.min(itemCount - 1, Math.floor((_props$initialScrollI = props.initialScrollIndex) !== null && _props$initialScrollI !== void 0 ? _props$initialScrollI : 0)));
var lastCellIndex = Math.min(itemCount, firstCellIndex + initialNumToRenderOrDefault(props.initialNumToRender)) - 1;
return {
first: firstCellIndex,
last: lastCellIndex
};
}
static _ensureClosestStickyHeader(props, stickyIndicesSet, renderMask, cellIdx) {
var stickyOffset = props.ListHeaderComponent ? 1 : 0;
for (var itemIdx = cellIdx - 1; itemIdx >= 0; itemIdx--) {
if (stickyIndicesSet.has(itemIdx + stickyOffset)) {
renderMask.addCells({
first: itemIdx,
last: itemIdx
});
break;
}
}
}
_adjustCellsAroundViewport(props, cellsAroundViewport) {
var data = props.data,
getItemCount = props.getItemCount;
var onEndReachedThreshold = onEndReachedThresholdOrDefault(props.onEndReachedThreshold);
var _this$_scrollMetrics = this._scrollMetrics,
contentLength = _this$_scrollMetrics.contentLength,
offset = _this$_scrollMetrics.offset,
visibleLength = _this$_scrollMetrics.visibleLength;
var distanceFromEnd = contentLength - visibleLength - offset;
// Wait until the scroll view metrics have been set up. And until then,
// we will trust the initialNumToRender suggestion
if (visibleLength <= 0 || contentLength <= 0) {
return cellsAroundViewport.last >= getItemCount(data) ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) : cellsAroundViewport;
}
var newCellsAroundViewport;
if (props.disableVirtualization) {
var renderAhead = distanceFromEnd < onEndReachedThreshold * visibleLength ? maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch) : 0;
newCellsAroundViewport = {
first: 0,
last: Math.min(cellsAroundViewport.last + renderAhead, getItemCount(data) - 1)
};
} else {
// If we have a non-zero initialScrollIndex and run this before we've scrolled,
// we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex.
// So let's wait until we've scrolled the view to the right place. And until then,
// we will trust the initialScrollIndex suggestion.
// Thus, we want to recalculate the windowed render limits if any of the following hold:
// - initialScrollIndex is undefined or is 0
// - initialScrollIndex > 0 AND scrolling is complete
// - initialScrollIndex > 0 AND the end of the list is visible (this handles the case
// where the list is shorter than the visible area)
if (props.initialScrollIndex && !this._scrollMetrics.offset && Math.abs(distanceFromEnd) >= Number.EPSILON) {
return cellsAroundViewport.last >= getItemCount(data) ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) : cellsAroundViewport;
}
newCellsAroundViewport = computeWindowedRenderLimits(props, maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), windowSizeOrDefault(props.windowSize), cellsAroundViewport, this.__getFrameMetricsApprox, this._scrollMetrics);
invariant(newCellsAroundViewport.last < getItemCount(data), 'computeWindowedRenderLimits() should return range in-bounds');
}
if (this._nestedChildLists.size() > 0) {
// If some cell in the new state has a child list in it, we should only render
// up through that item, so that we give that list a chance to render.
// Otherwise there's churn from multiple child lists mounting and un-mounting
// their items.
// Will this prevent rendering if the nested list doesn't realize the end?
var childIdx = this._findFirstChildWithMore(newCellsAroundViewport.first, newCellsAroundViewport.last);
newCellsAroundViewport.last = childIdx !== null && childIdx !== void 0 ? childIdx : newCellsAroundViewport.last;
}
return newCellsAroundViewport;
}
_findFirstChildWithMore(first, last) {
for (var ii = first; ii <= last; ii++) {
var cellKeyForIndex = this._indicesToKeys.get(ii);
if (cellKeyForIndex != null && this._nestedChildLists.anyInCell(cellKeyForIndex, childList => childList.hasMore())) {
return ii;
}
}
return null;
}
componentDidMount() {
if (this._isNestedWithSameOrientation()) {
this.context.registerAsNestedChild({
ref: this,
cellKey: this.context.cellKey
});
}
// REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller.
this.setupWebWheelHandler();
}
componentWillUnmount() {
if (this._isNestedWithSameOrientation()) {
this.context.unregisterAsNestedChild({
ref: this
});
}
this._updateCellsToRenderBatcher.dispose({
abort: true
});
this._viewabilityTuples.forEach(tuple => {
tuple.viewabilityHelper.dispose();
});
this._fillRateHelper.deactivateAndFlush();
// REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller.
this.teardownWebWheelHandler();
}
// REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller.
setupWebWheelHandler() {
if (this._scrollRef && this._scrollRef.getScrollableNode) {
this._scrollRef.getScrollableNode().addEventListener('wheel', this.invertedWheelEventHandler);
} else {
setTimeout(() => this.setupWebWheelHandler(), 50);
return;
}
}
// REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller.
teardownWebWheelHandler() {
if (this._scrollRef && this._scrollRef.getScrollableNode) {
this._scrollRef.getScrollableNode().removeEventListener('wheel', this.invertedWheelEventHandler);
}
}
static getDerivedStateFromProps(newProps, prevState) {
// first and last could be stale (e.g. if a new, shorter items props is passed in), so we make
// sure we're rendering a reasonable range here.
var itemCount = newProps.getItemCount(newProps.data);
if (itemCount === prevState.renderMask.numCells()) {
return prevState;
}
var constrainedCells = VirtualizedList._constrainToItemCount(prevState.cellsAroundViewport, newProps);
return {
cellsAroundViewport: constrainedCells,
renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells)
};
}
_pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, first, last, inversionStyle) {
var _this = this;
var _this$props4 = this.props,
CellRendererComponent = _this$props4.CellRendererComponent,
ItemSeparatorComponent = _this$props4.ItemSeparatorComponent,
ListHeaderComponent = _this$props4.ListHeaderComponent,
ListItemComponent = _this$props4.ListItemComponent,
data = _this$props4.data,
debug = _this$props4.debug,
getItem = _this$props4.getItem,
getItemCount = _this$props4.getItemCount,
getItemLayout = _this$props4.getItemLayout,
horizontal = _this$props4.horizontal,
renderItem = _this$props4.renderItem;
var stickyOffset = ListHeaderComponent ? 1 : 0;
var end = getItemCount(data) - 1;
var prevCellKey;
last = Math.min(end, last);
var _loop = function _loop() {
var item = getItem(data, ii);
var key = _this._keyExtractor(item, ii, _this.props);
_this._indicesToKeys.set(ii, key);
if (stickyIndicesFromProps.has(ii + stickyOffset)) {
stickyHeaderIndices.push(cells.length);
}
var shouldListenForLayout = getItemLayout == null || debug || _this._fillRateHelper.enabled();
cells.push(/*#__PURE__*/React.createElement(CellRenderer, _extends({
CellRendererComponent: CellRendererComponent,
ItemSeparatorComponent: ii < end ? ItemSeparatorComponent : undefined,
ListItemComponent: ListItemComponent,
cellKey: key,
horizontal: horizontal,
index: ii,
inversionStyle: inversionStyle,
item: item,
key: key,
prevCellKey: prevCellKey,
onUpdateSeparators: _this._onUpdateSeparators,
onCellFocusCapture: e => _this._onCellFocusCapture(key),
onUnmount: _this._onCellUnmount,
ref: _ref => {
_this._cellRefs[key] = _ref;
},
renderItem: renderItem
}, shouldListenForLayout && {
onCellLayout: _this._onCellLayout
})));
prevCellKey = key;
};
for (var ii = first; ii <= last; ii++) {
_loop();
}
}
static _constrainToItemCount(cells, props) {
var itemCount = props.getItemCount(props.data);
var last = Math.min(itemCount - 1, cells.last);
var maxToRenderPerBatch = maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch);
return {
first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first),
last
};
}
_isNestedWithSameOrientation() {
var nestedContext = this.context;
return !!(nestedContext && !!nestedContext.horizontal === horizontalOrDefault(this.props.horizontal));
}
_keyExtractor(item, index, props
// $FlowFixMe[missing-local-annot]
) {
if (props.keyExtractor != null) {
return props.keyExtractor(item, index);
}
var key = defaultKeyExtractor(item, index);
if (key === String(index)) {
_usedIndexForKey = true;
if (item.type && item.type.displayName) {
_keylessItemComponentName = item.type.displayName;
}
}
return key;
}
render() {
this._checkProps(this.props);
var _this$props5 = this.props,
ListEmptyComponent = _this$props5.ListEmptyComponent,
ListFooterComponent = _this$props5.ListFooterComponent,
ListHeaderComponent = _this$props5.ListHeaderComponent;
var _this$props6 = this.props,
data = _this$props6.data,
horizontal = _this$props6.horizontal;
var inversionStyle = this.props.inverted ? horizontalOrDefault(this.props.horizontal) ? styles.horizontallyInverted : styles.verticallyInverted : null;
var cells = [];
var stickyIndicesFromProps = new Set(this.props.stickyHeaderIndices);
var stickyHeaderIndices = [];
// 1. Add cell for ListHeaderComponent
if (ListHeaderComponent) {
if (stickyIndicesFromProps.has(0)) {
stickyHeaderIndices.push(0);
}
var _element = /*#__PURE__*/React.isValidElement(ListHeaderComponent) ? ListHeaderComponent :
/*#__PURE__*/
// $FlowFixMe[not-a-component]
// $FlowFixMe[incompatible-type-arg]
React.createElement(ListHeaderComponent, null);
cells.push(/*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, {
cellKey: this._getCellKey() + '-header',
key: "$header"
}, /*#__PURE__*/React.createElement(View, {
onLayout: this._onLayoutHeader,
style: [inversionStyle, this.props.ListHeaderComponentStyle]
},
// $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors
_element)));
}
// 2a. Add a cell for ListEmptyComponent if applicable
var itemCount = this.props.getItemCount(data);
if (itemCount === 0 && ListEmptyComponent) {
var _element2 = /*#__PURE__*/React.isValidElement(ListEmptyComponent) ? ListEmptyComponent :
/*#__PURE__*/
// $FlowFixMe[not-a-component]
// $FlowFixMe[incompatible-type-arg]
React.createElement(ListEmptyComponent, null);
cells.push(/*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, {
cellKey: this._getCellKey() + '-empty',
key: "$empty"
}, /*#__PURE__*/React.cloneElement(_element2, {
onLayout: event => {
this._onLayoutEmpty(event);
if (_element2.props.onLayout) {
_element2.props.onLayout(event);
}
},
style: [inversionStyle, _element2.props.style]
})));
}
// 2b. Add cells and spacers for each item
if (itemCount > 0) {
_usedIndexForKey = false;
_keylessItemComponentName = '';
var spacerKey = this._getSpacerKey(!horizontal);
var renderRegions = this.state.renderMask.enumerateRegions();
var lastSpacer = findLastWhere(renderRegions, r => r.isSpacer);
for (var _iterator = _createForOfIteratorHelperLoose(renderRegions), _step; !(_step = _iterator()).done;) {
var section = _step.value;
if (section.isSpacer) {
// Legacy behavior is to avoid spacers when virtualization is
// disabled (including head spacers on initial render).
if (this.props.disableVirtualization) {
continue;
}
// Without getItemLayout, we limit our tail spacer to the _highestMeasuredFrameIndex to
// prevent the user for hyperscrolling into un-measured area because otherwise content will
// likely jump around as it renders in above the viewport.
var isLastSpacer = section === lastSpacer;
var constrainToMeasured = isLastSpacer && !this.props.getItemLayout;
var last = constrainToMeasured ? clamp(section.first - 1, section.last, this._highestMeasuredFrameIndex) : section.last;
var firstMetrics = this.__getFrameMetricsApprox(section.first, this.props);
var lastMetrics = this.__getFrameMetricsApprox(last, this.props);
var spacerSize = lastMetrics.offset + lastMetrics.length - firstMetrics.offset;
cells.push(/*#__PURE__*/React.createElement(View, {
key: "$spacer-" + section.first,
style: {
[spacerKey]: spacerSize
}
}));
} else {
this._pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, section.first, section.last, inversionStyle);
}
}
if (!this._hasWarned.keys && _usedIndexForKey) {
console.warn('VirtualizedList: missing keys for items, make sure to specify a key or id property on each ' + 'item or provide a custom keyExtractor.', _keylessItemComponentName);
this._hasWarned.keys = true;
}
}
// 3. Add cell for ListFooterComponent
if (ListFooterComponent) {
var _element3 = /*#__PURE__*/React.isValidElement(ListFooterComponent) ? ListFooterComponent :
/*#__PURE__*/
// $FlowFixMe[not-a-component]
// $FlowFixMe[incompatible-type-arg]
React.createElement(ListFooterComponent, null);
cells.push(/*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, {
cellKey: this._getFooterCellKey(),
key: "$footer"
}, /*#__PURE__*/React.createElement(View, {
onLayout: this._onLayoutFooter,
style: [inversionStyle, this.props.ListFooterComponentStyle]
},
// $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors
_element3)));
}
// 4. Render the ScrollView
var scrollProps = _objectSpread(_objectSpread({}, this.props), {}, {
onContentSizeChange: this._onContentSizeChange,
onLayout: this._onLayout,
onScroll: this._onScroll,
onScrollBeginDrag: this._onScrollBeginDrag,
onScrollEndDrag: this._onScrollEndDrag,
onMomentumScrollBegin: this._onMomentumScrollBegin,
onMomentumScrollEnd: this._onMomentumScrollEnd,
scrollEventThrottle: scrollEventThrottleOrDefault(this.props.scrollEventThrottle),
// TODO: Android support
invertStickyHeaders: this.props.invertStickyHeaders !== undefined ? this.props.invertStickyHeaders : this.props.inverted,
stickyHeaderIndices,
style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style
});
this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1;
var innerRet = /*#__PURE__*/React.createElement(VirtualizedListContextProvider, {
value: {
cellKey: null,
getScrollMetrics: this._getScrollMetrics,
horizontal: horizontalOrDefault(this.props.horizontal),
getOutermostParentListRef: this._getOutermostParentListRef,
registerAsNestedChild: this._registerAsNestedChild,
unregisterAsNestedChild: this._unregisterAsNestedChild
}
}, /*#__PURE__*/React.cloneElement((this.props.renderScrollComponent || this._defaultRenderScrollComponent)(scrollProps), {
ref: this._captureScrollRef
}, cells));
var ret = innerRet;
/* https://github.com/necolas/react-native-web/issues/2239: Re-enable when ScrollView.Context.Consumer is available.
if (__DEV__) {
ret = (
<ScrollView.Context.Consumer>
{scrollContext => {
if (
scrollContext != null &&
!scrollContext.horizontal ===
!horizontalOrDefault(this.props.horizont