From 1cff8dc16c6ff17b0b843fef671a6dcfb7a688f1 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Sun, 21 Nov 2021 14:48:01 -0800 Subject: [PATCH] Enable Generic Discontiguous Regions within VirtualizedList Builds upon the `CellRenderMask` data structure added with https://github.com/facebook/react-native/pull/31420, and VirtualizedList coverage added with https://github.com/facebook/react-native/pull/31401. VirtualizedList currently keeps a [first, last] range as state, tracking the region of cells to render. The render functions uses this as an input, along with a few special cases to render more (sticky headers, initial render region.) This change moves to instead keep state which describes discontiguous render regions. This mask is continually updated as the viewport changes, batch renders expand the region, etc. Special cases are baked into the render mask, with a relatively simple tranformation from the mask to render function. This representation makes it much easier to support keyboarding scenarios, which require keeping distinct regions (e.g. for last focused) realized while out of viewport. MS/FB folks have a video discussion about VirtualizedList here: https://msit.microsoftstream.com/video/fe01a1ff-0400-94b1-d4f1-f1eb924b1809 facebook/react-native#31401 added quite a few snapshot tests, centering around the logic this change is touching. I manually validated RNTester FlatList examples (and their should be some upstream UI testing for them). --- Libraries/Lists/CellRenderMask.js | 4 + Libraries/Lists/VirtualizeUtils.js | 2 - Libraries/Lists/VirtualizedList.js | 528 +++++++++++------- .../Lists/__tests__/VirtualizedList-test.js | 11 +- .../VirtualizedList-test.js.snap | 385 ++++--------- 5 files changed, 428 insertions(+), 502 deletions(-) diff --git a/Libraries/Lists/CellRenderMask.js b/Libraries/Lists/CellRenderMask.js index 786ae7b00bad42..15ce117dbe638d 100644 --- a/Libraries/Lists/CellRenderMask.js +++ b/Libraries/Lists/CellRenderMask.js @@ -110,6 +110,10 @@ export class CellRenderMask { ); } + numCells(): number { + return this._numCells; + } + equals(other: CellRenderMask): boolean { return ( this._numCells === other._numCells && diff --git a/Libraries/Lists/VirtualizeUtils.js b/Libraries/Lists/VirtualizeUtils.js index 11dfd8cc20365b..09d448ae285f50 100644 --- a/Libraries/Lists/VirtualizeUtils.js +++ b/Libraries/Lists/VirtualizeUtils.js @@ -92,7 +92,6 @@ export function computeWindowedRenderLimits( prev: { first: number, last: number, - ... }, getFrameMetricsApprox: (index: number) => { length: number, @@ -109,7 +108,6 @@ export function computeWindowedRenderLimits( ): { first: number, last: number, - ... } { const itemCount = getItemCount(data); if (itemCount === 0) { diff --git a/Libraries/Lists/VirtualizedList.js b/Libraries/Lists/VirtualizedList.js index c411032c269c38..66e94e7bdcf71c 100644 --- a/Libraries/Lists/VirtualizedList.js +++ b/Libraries/Lists/VirtualizedList.js @@ -40,7 +40,10 @@ import { VirtualizedListContextProvider, type ChildListState, type ListDebugInfo, -} from './VirtualizedListContext.js'; +} from './VirtualizedListContext'; + +import {CellRenderMask} from './CellRenderMask'; +import clamp from '../Utilities/clamp'; type Item = any; @@ -309,8 +312,8 @@ let _usedIndexForKey = false; let _keylessItemComponentName: string = ''; type State = { - first: number, - last: number, + renderMask: CellRenderMask, + cellsAroundViewport: {first: number, last: number}, }; /** @@ -348,6 +351,19 @@ function windowSizeOrDefault(windowSize: ?number) { return windowSize ?? 21; } +function findLastWhere( + arr: $ReadOnlyArray, + predicate: (element: T) => boolean, +): T | null { + for (let i = arr.length - 1; i >= 0; i--) { + if (predicate(arr[i])) { + return arr[i]; + } + } + + return null; +} + /** * Base implementation for the more convenient [``](https://reactnative.dev/docs/flatlist.html) * and [``](https://reactnative.dev/docs/sectionlist.html) components, which are also better @@ -690,6 +706,11 @@ class VirtualizedList extends React.PureComponent { 'VirtualizedList: The windowSize prop must be present and set to a value greater than 0.', ); + invariant( + props.getItemCount, + 'VirtualizedList: The "getItemCount" prop must be provided', + ); + this._fillRateHelper = new FillRateHelper(this._getFrameMetrics); this._updateCellsToRenderBatcher = new Batchinator( this._updateCellsToRender, @@ -711,14 +732,11 @@ class VirtualizedList extends React.PureComponent { }); } - let initialState = { - first: this.props.initialScrollIndex || 0, - last: - Math.min( - this.props.getItemCount(this.props.data), - (this.props.initialScrollIndex || 0) + - initialNumToRenderOrDefault(this.props.initialNumToRender), - ) - 1, + const initialRenderRegion = VirtualizedList._initialRenderRegion(props); + + let initialState: State = { + cellsAroundViewport: initialRenderRegion, + renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion), }; if (this._isNestedWithSameOrientation()) { @@ -733,6 +751,166 @@ class VirtualizedList extends React.PureComponent { this.state = initialState; } + static _createRenderMask( + props: Props, + cellsAroundViewport: {first: number, last: number}, + ): CellRenderMask { + const 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`, + ); + + const renderMask = new CellRenderMask(itemCount); + + if (itemCount > 0) { + renderMask.addCells(cellsAroundViewport); + + // The initially rendered cells are retained as part of the + // "scroll-to-top" optimization + if (props.initialScrollIndex == null || props.initialScrollIndex === 0) { + const 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. + const stickyIndicesSet = new Set(props.stickyHeaderIndices); + VirtualizedList._ensureClosestStickyHeader( + props, + stickyIndicesSet, + renderMask, + cellsAroundViewport.first, + ); + } + + return renderMask; + } + + static _initialRenderRegion(props: Props): {first: number, last: number} { + const itemCount = props.getItemCount(props.data); + const scrollIndex = props.initialScrollIndex || 0; + + return { + first: scrollIndex, + last: + Math.min( + itemCount, + scrollIndex + initialNumToRenderOrDefault(props.initialNumToRender), + ) - 1, + }; + } + + static _ensureClosestStickyHeader( + props: Props, + stickyIndicesSet: Set, + renderMask: CellRenderMask, + cellIdx: number, + ) { + const stickyOffset = props.ListHeaderComponent ? 1 : 0; + + for (let itemIdx = cellIdx - 1; itemIdx >= 0; itemIdx--) { + if (stickyIndicesSet.has(itemIdx + stickyOffset)) { + renderMask.addCells({first: itemIdx, last: itemIdx}); + break; + } + } + } + + _adjustCellsAroundViewport( + props: Props, + cellsAroundViewport: {first: number, last: number}, + ): {first: number, last: number} { + const {data, getItemCount} = props; + const onEndReachedThreshold = onEndReachedThresholdOrDefault( + props.onEndReachedThreshold, + ); + this._updateViewableItems(data); + + const {contentLength, offset, visibleLength} = this._scrollMetrics; + + // 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; + } + + let newCellsAroundViewport: {first: number, last: number}; + if (this._isVirtualizationDisabled()) { + const distanceFromEnd = contentLength - visibleLength - offset; + const renderAhead = + distanceFromEnd < onEndReachedThreshold * visibleLength + ? maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch) + : 0; + + newCellsAroundViewport = { + first: 0, + last: Math.min( + this.state.cellsAroundViewport.last + renderAhead, + getItemCount(data) - 1, + ), + }; + } else { + // If we have a non-zero initialScrollIndex and run this before we've scrolled, + // wait until we've scrolled the view to the right place. And until then, + // we will trust the initialScrollIndex suggestion. + if (this.props.initialScrollIndex && !this._scrollMetrics.offset) { + return cellsAroundViewport; + } + + newCellsAroundViewport = computeWindowedRenderLimits( + props.data, + props.getItemCount, + maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), + windowSizeOrDefault(props.windowSize), + cellsAroundViewport, + this._getFrameMetricsApprox, + this._scrollMetrics, + ); + } + + 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? + const childIdx = this._findFirstChildWithMore( + newCellsAroundViewport.first, + newCellsAroundViewport.last, + ); + + newCellsAroundViewport.last = childIdx ?? newCellsAroundViewport.last; + } + + return newCellsAroundViewport; + } + + _findFirstChildWithMore(first: number, last: number): number | null { + for (let ii = first; ii <= last; ii++) { + const cellKeyForIndex = this._indicesToKeys.get(ii); + const childListKeys = + cellKeyForIndex && this._cellKeysToChildListKeys.get(cellKeyForIndex); + if (!childListKeys) { + continue; + } + // For each cell, need to check whether any child list in it has more elements to render + for (let childKey of childListKeys) { + const childList = this._nestedChildLists.get(childKey); + if (childList && childList.ref && childList.ref.hasMore()) { + return ii; + } + } + } + + return null; + } + componentDidMount() { if (this._isNestedWithSameOrientation()) { this.context.registerAsNestedChild({ @@ -753,8 +931,7 @@ class VirtualizedList extends React.PureComponent { this.context.unregisterAsNestedChild({ key: this._getListKey(), state: { - first: this.state.first, - last: this.state.last, + ...this.state, frames: this._frames, }, }); @@ -768,18 +945,21 @@ class VirtualizedList extends React.PureComponent { } static getDerivedStateFromProps(newProps: Props, prevState: State): State { - const {data, getItemCount} = newProps; - const maxToRenderPerBatch = maxToRenderPerBatchOrDefault( - newProps.maxToRenderPerBatch, - ); // 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. + const itemCount = newProps.getItemCount(newProps.data); + if (itemCount === prevState.renderMask.numCells()) { + return prevState; + } + + const constrainedCells = VirtualizedList._constrainToItemCount( + prevState.cellsAroundViewport, + newProps, + ); + return { - first: Math.max( - 0, - Math.min(prevState.first, getItemCount(data) - 1 - maxToRenderPerBatch), - ), - last: Math.max(0, Math.min(prevState.last, getItemCount(data) - 1)), + cellsAroundViewport: prevState.cellsAroundViewport, + renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), }; } @@ -835,6 +1015,23 @@ class VirtualizedList extends React.PureComponent { } } + static _constrainToItemCount( + cells: {first: number, last: number}, + props: Props, + ): {first: number, last: number} { + const itemCount = props.getItemCount(props.data); + const last = Math.min(itemCount - 1, cells.last); + + const maxToRenderPerBatch = maxToRenderPerBatchOrDefault( + props.maxToRenderPerBatch, + ); + + return { + first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), + last, + }; + } + _onUpdateSeparators = (keys: Array, newProps: Object) => { keys.forEach(key => { const ref = key != null && this._cellRefs[key]; @@ -885,7 +1082,6 @@ class VirtualizedList extends React.PureComponent { const {ListEmptyComponent, ListFooterComponent, ListHeaderComponent} = this.props; const {data, horizontal} = this.props; - const isVirtualizationDisabled = this._isVirtualizationDisabled(); const inversionStyle = this.props.inverted ? horizontalOrDefault(this.props.horizontal) ? styles.horizontallyInverted @@ -894,6 +1090,8 @@ class VirtualizedList extends React.PureComponent { const cells = []; const stickyIndicesFromProps = new Set(this.props.stickyHeaderIndices); const stickyHeaderIndices = []; + + // 1. Add cell for ListHeaderComponent if (ListHeaderComponent) { if (stickyIndicesFromProps.has(0)) { stickyHeaderIndices.push(0); @@ -923,103 +1121,10 @@ class VirtualizedList extends React.PureComponent { , ); } + + // 2a. Add a cell for ListEmptyComponent if applicable const itemCount = this.props.getItemCount(data); - if (itemCount > 0) { - _usedIndexForKey = false; - _keylessItemComponentName = ''; - const spacerKey = this._getSpacerKey(!horizontal); - const lastInitialIndex = this.props.initialScrollIndex - ? -1 - : initialNumToRenderOrDefault(this.props.initialNumToRender) - 1; - const {first, last} = this.state; - this._pushCells( - cells, - stickyHeaderIndices, - stickyIndicesFromProps, - 0, - lastInitialIndex, - inversionStyle, - ); - const firstAfterInitial = Math.max(lastInitialIndex + 1, first); - if (!isVirtualizationDisabled && first > lastInitialIndex + 1) { - let insertedStickySpacer = false; - if (stickyIndicesFromProps.size > 0) { - const stickyOffset = ListHeaderComponent ? 1 : 0; - // See if there are any sticky headers in the virtualized space that we need to render. - for (let ii = firstAfterInitial - 1; ii > lastInitialIndex; ii--) { - if (stickyIndicesFromProps.has(ii + stickyOffset)) { - const initBlock = this._getFrameMetricsApprox(lastInitialIndex); - const stickyBlock = this._getFrameMetricsApprox(ii); - const leadSpace = - stickyBlock.offset - - initBlock.offset - - (this.props.initialScrollIndex ? 0 : initBlock.length); - cells.push( - , - ); - this._pushCells( - cells, - stickyHeaderIndices, - stickyIndicesFromProps, - ii, - ii, - inversionStyle, - ); - const trailSpace = - this._getFrameMetricsApprox(first).offset - - (stickyBlock.offset + stickyBlock.length); - cells.push( - , - ); - insertedStickySpacer = true; - break; - } - } - } - if (!insertedStickySpacer) { - const initBlock = this._getFrameMetricsApprox(lastInitialIndex); - const firstSpace = - this._getFrameMetricsApprox(first).offset - - (initBlock.offset + initBlock.length); - cells.push( - , - ); - } - } - this._pushCells( - cells, - stickyHeaderIndices, - stickyIndicesFromProps, - firstAfterInitial, - 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; - } - if (!isVirtualizationDisabled && last < itemCount - 1) { - const lastFrame = this._getFrameMetricsApprox(last); - // 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. - const end = this.props.getItemLayout - ? itemCount - 1 - : Math.min(itemCount - 1, this._highestMeasuredFrameIndex); - const endFrame = this._getFrameMetricsApprox(end); - const tailSpacerLength = - endFrame.offset + - endFrame.length - - (lastFrame.offset + lastFrame.length); - cells.push( - , - ); - } - } else if (ListEmptyComponent) { + if (itemCount === 0 && ListEmptyComponent) { const element: React.Element = ((React.isValidElement( ListEmptyComponent, ) ? ( @@ -1042,6 +1147,70 @@ class VirtualizedList extends React.PureComponent { }), ); } + + // 2b. Add cells and spacers for each item + if (itemCount > 0) { + _usedIndexForKey = false; + _keylessItemComponentName = ''; + const spacerKey = this._getSpacerKey(!horizontal); + + const renderRegions = this.state.renderMask.enumerateRegions(); + const lastSpacer = findLastWhere(renderRegions, r => r.isSpacer); + + for (const section of renderRegions) { + if (section.isSpacer) { + // Legacy behavior is to avoid spacers when virtualization is + // disabled (including head spacers on initial render). + if (this._isVirtualizationDisabled()) { + 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. + const isLastSpacer = section === lastSpacer; + const constrainToMeasured = isLastSpacer && !this.props.getItemLayout; + const last = constrainToMeasured + ? clamp( + section.first - 1, + section.last, + this._highestMeasuredFrameIndex, + ) + : section.last; + + const firstMetrics = this._getFrameMetricsApprox(section.first); + const lastMetrics = this._getFrameMetricsApprox(last); + const spacerSize = + lastMetrics.offset + lastMetrics.length - firstMetrics.offset; + cells.push( + , + ); + } 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) { const element = React.isValidElement(ListFooterComponent) ? ( ListFooterComponent @@ -1068,6 +1237,8 @@ class VirtualizedList extends React.PureComponent { , ); } + + // 4. Render the ScrollView const scrollProps = { ...this.props, onContentSizeChange: this._onContentSizeChange, @@ -1090,8 +1261,7 @@ class VirtualizedList extends React.PureComponent { : this.props.style, }; - this._hasMore = - this.state.last < this.props.getItemCount(this.props.data) - 1; + this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; const innerRet = ( { _computeBlankness() { this._fillRateHelper.computeBlankness( this.props, - this.state, + this.state.cellsAroundViewport, this._scrollMetrics, ); } @@ -1425,8 +1595,12 @@ class VirtualizedList extends React.PureComponent { framesInLayout.push(frame); } } - const windowTop = this._getFrameMetricsApprox(this.state.first).offset; - const frameLast = this._getFrameMetricsApprox(this.state.last); + const windowTop = this._getFrameMetricsApprox( + this.state.cellsAroundViewport.first, + ).offset; + const frameLast = this._getFrameMetricsApprox( + this.state.cellsAroundViewport.last, + ); const windowLen = frameLast.offset + frameLast.length - windowTop; const visTop = this._scrollMetrics.offset; const visLen = this._scrollMetrics.visibleLength; @@ -1501,7 +1675,7 @@ class VirtualizedList extends React.PureComponent { onEndReachedThreshold != null ? onEndReachedThreshold * visibleLength : 2; if ( onEndReached && - this.state.last === getItemCount(data) - 1 && + this.state.cellsAroundViewport.last === getItemCount(data) - 1 && distanceFromEnd < threshold && this._scrollMetrics.contentLength !== this._sentEndForContentLength ) { @@ -1629,7 +1803,7 @@ class VirtualizedList extends React.PureComponent { }; _scheduleCellsToRenderUpdate() { - const {first, last} = this.state; + const {first, last} = this.state.cellsAroundViewport; const {offset, visibleLength, velocity} = this._scrollMetrics; const itemCount = this.props.getItemCount(this.props.data); let hiPri = false; @@ -1717,93 +1891,25 @@ class VirtualizedList extends React.PureComponent { }; _updateCellsToRender = () => { - const { - data, - getItemCount, - onEndReachedThreshold: _onEndReachedThreshold, - } = this.props; - const onEndReachedThreshold = onEndReachedThresholdOrDefault( - _onEndReachedThreshold, - ); - const isVirtualizationDisabled = this._isVirtualizationDisabled(); - this._updateViewableItems(data); - if (!data) { - return; - } - this.setState(state => { - let newState; - const {contentLength, offset, visibleLength} = this._scrollMetrics; - if (!isVirtualizationDisabled) { - // If we run this with bogus data, we'll force-render window {first: 0, last: 0}, - // and wipe out the initialNumToRender rendered elements. - // So let's wait until the scroll view metrics have been set up. And until then, - // we will trust the initialNumToRender suggestion - if (visibleLength > 0 && contentLength > 0) { - // 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. - if (!this.props.initialScrollIndex || this._scrollMetrics.offset) { - newState = computeWindowedRenderLimits( - this.props.data, - this.props.getItemCount, - maxToRenderPerBatchOrDefault(this.props.maxToRenderPerBatch), - windowSizeOrDefault(this.props.windowSize), - state, - this._getFrameMetricsApprox, - this._scrollMetrics, - ); - } - } - } else { - const distanceFromEnd = contentLength - visibleLength - offset; - const renderAhead = - distanceFromEnd < onEndReachedThreshold * visibleLength - ? maxToRenderPerBatchOrDefault(this.props.maxToRenderPerBatch) - : 0; - newState = { - first: 0, - last: Math.min(state.last + renderAhead, getItemCount(data) - 1), - }; - } - if (newState && this._nestedChildLists.size > 0) { - const newFirst = newState.first; - const newLast = newState.last; - // 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. - for (let ii = newFirst; ii <= newLast; ii++) { - const cellKeyForIndex = this._indicesToKeys.get(ii); - const childListKeys = - cellKeyForIndex && - this._cellKeysToChildListKeys.get(cellKeyForIndex); - if (!childListKeys) { - continue; - } - let someChildHasMore = false; - // For each cell, need to check whether any child list in it has more elements to render - for (let childKey of childListKeys) { - const childList = this._nestedChildLists.get(childKey); - if (childList && childList.ref && childList.ref.hasMore()) { - someChildHasMore = true; - break; - } - } - if (someChildHasMore) { - newState.last = ii; - break; - } - } - } + this.setState((state, props) => { + const cellsAroundViewport = this._adjustCellsAroundViewport( + props, + state.cellsAroundViewport, + ); + const renderMask = VirtualizedList._createRenderMask( + props, + cellsAroundViewport, + ); + if ( - newState != null && - newState.first === state.first && - newState.last === state.last + cellsAroundViewport.first === state.cellsAroundViewport.first && + cellsAroundViewport.last === state.cellsAroundViewport.last && + renderMask.equals(state.renderMask) ) { - newState = null; + return null; } - return newState; + + return {cellsAroundViewport, renderMask}; }); }; @@ -1875,7 +1981,7 @@ class VirtualizedList extends React.PureComponent { this._getFrameMetrics, this._createViewToken, tuple.onViewableItemsChanged, - this.state, + this.state.cellsAroundViewport, ); }); } diff --git a/Libraries/Lists/__tests__/VirtualizedList-test.js b/Libraries/Lists/__tests__/VirtualizedList-test.js index b2093ce07c1b10..26223e43fe3beb 100644 --- a/Libraries/Lists/__tests__/VirtualizedList-test.js +++ b/Libraries/Lists/__tests__/VirtualizedList-test.js @@ -965,13 +965,12 @@ it('renders no spacers up to initialScrollIndex on first render when virtualizat ); }); - // There should be no spacers present in an offset initial render with - // virtualiztion disabled. Only initialNumToRender items starting at - // initialScrollIndex. + // We should render initialNumToRender items with no spacers on initial render + // when virtualization is disabled expect(component).toMatchSnapshot(); }); -it('expands first in viewport to render up to maxToRenderPerBatch on initial render', () => { +it('renders initialNumToRender when initialScrollIndex is offset', () => { const items = generateItems(10); const ITEM_HEIGHT = 10; @@ -988,9 +987,7 @@ it('expands first in viewport to render up to maxToRenderPerBatch on initial ren ); }); - // When virtualization is disabled we may render items before initialItemIndex - // if initialItemIndex + initialNumToRender < maToRenderPerBatch. Expect cells - // 0-3 to be rendered in this example, even though initialScrollIndex is 4. + // We should render initialNumToRender items starting at initialScrollIndex. expect(component).toMatchSnapshot(); }); diff --git a/Libraries/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap b/Libraries/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap index ad93703101b2ad..0784444ad79f4d 100644 --- a/Libraries/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap +++ b/Libraries/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap @@ -850,10 +850,8 @@ exports[`VirtualizedList keeps sticky headers above viewport visualized 1`] = ` Array [ 0, 2, - 4, - 7, - 10, - 13, + 5, + 8, ] } windowSize={1} @@ -870,7 +868,7 @@ exports[`VirtualizedList keeps sticky headers above viewport visualized 1`] = ` @@ -879,53 +877,16 @@ exports[`VirtualizedList keeps sticky headers above viewport visualized 1`] = ` > - - - - - - - - - - - - - - - @@ -1993,45 +1954,10 @@ exports[`discards intitial render if initialScrollIndex != 0 1`] = ` - - - - - - - - - - - - - - - @@ -2399,33 +2325,12 @@ exports[`does not over-render when there is less than initialNumToRender cells 1 > - - - - - - - - - - - + style={ + Object { + "height": 40, + } + } + /> @@ -2602,113 +2507,6 @@ exports[`eventually renders all items when virtualization disabled 1`] = ` `; -exports[`expands first in viewport to render up to maxToRenderPerBatch on initial render 1`] = ` - - - - - - - - - - - - - - - - - - - - - - - -`; - exports[`expands render area by maxToRenderPerBatch on tick 1`] = ` + + + + + +`; + +exports[`renders initialNumToRender when initialScrollIndex is offset 1`] = ` + + + @@ -3138,6 +3008,13 @@ exports[`renders initialNumToRender cells when virtualization disabled 1`] = ` value={5} /> + `; @@ -3377,33 +3254,12 @@ exports[`renders offset cells in initial render when initialScrollIndex set 1`] > - - - - - - - - - - - + style={ + Object { + "height": 40, + } + } + /> @@ -3635,7 +3491,7 @@ exports[`renders tail spacer up to last measured with irregular layout when getI @@ -4405,45 +4261,10 @@ exports[`retains intitial render if initialScrollIndex == 0 1`] = ` - - - - - - - - - - - - - - -