diff --git a/packages/react-native/Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap b/packages/react-native/Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap
index 2fec3a235fb118..86901053a00d9f 100644
--- a/packages/react-native/Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap
+++ b/packages/react-native/Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap
@@ -111,6 +111,7 @@ exports[`FlatList renders all the bells and whistles 1`] = `
diff --git a/packages/react-native/Libraries/Lists/__tests__/__snapshots__/SectionList-test.js.snap b/packages/react-native/Libraries/Lists/__tests__/__snapshots__/SectionList-test.js.snap
index ea1021883c1d92..a700cdb7be2a73 100644
--- a/packages/react-native/Libraries/Lists/__tests__/__snapshots__/SectionList-test.js.snap
+++ b/packages/react-native/Libraries/Lists/__tests__/__snapshots__/SectionList-test.js.snap
@@ -243,6 +243,7 @@ exports[`SectionList renders all the bells and whistles 1`] = `
{
+function genItemData(i: number): Item {
+ const itemHash = Math.abs(hashCode('Item ' + i));
+ return {
+ title: 'Item ' + i,
+ text: LOREM_IPSUM.substr(0, (itemHash % 301) + 20),
+ key: String(i),
+ pressed: false,
+ };
+}
+
+function genNewerItems(count: number, start: number = 0): Array- {
+ const dataBlob = [];
+ for (let i = start; i < count + start; i++) {
+ dataBlob.push(genItemData(i));
+ }
+ return dataBlob;
+}
+
+function genOlderItems(count: number, start: number = 0): Array
- {
const dataBlob = [];
- for (let ii = start; ii < count + start; ii++) {
- const itemHash = Math.abs(hashCode('Item ' + ii));
- dataBlob.push({
- title: 'Item ' + ii,
- text: LOREM_IPSUM.substr(0, (itemHash % 301) + 20),
- key: String(ii),
- pressed: false,
- });
+ for (let i = count; i > 0; i--) {
+ dataBlob.push(genItemData(start - i));
}
return dataBlob;
}
@@ -147,6 +160,12 @@ class SeparatorComponent extends React.PureComponent<{...}> {
}
}
+const LoadingComponent: React.ComponentType<{}> = React.memo(() => (
+
+
+
+));
+
class ItemSeparatorComponent extends React.PureComponent<$FlowFixMeProps> {
render(): React.Node {
const style = this.props.highlighted
@@ -352,6 +371,13 @@ const styles = StyleSheet.create({
text: {
flex: 1,
},
+ loadingContainer: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ height: 100,
+ borderTopWidth: 1,
+ borderTopColor: 'rgb(200, 199, 204)',
+ },
});
module.exports = {
@@ -362,8 +388,10 @@ module.exports = {
ItemSeparatorComponent,
PlainInput,
SeparatorComponent,
+ LoadingComponent,
Spindicator,
- genItemData,
+ genNewerItems,
+ genOlderItems,
getItemLayout,
pressItem,
renderSmallSwitchOption,
diff --git a/packages/rn-tester/js/examples/FlatList/FlatList-basic.js b/packages/rn-tester/js/examples/FlatList/FlatList-basic.js
index abf37ca4c331ea..360540c61a2277 100644
--- a/packages/rn-tester/js/examples/FlatList/FlatList-basic.js
+++ b/packages/rn-tester/js/examples/FlatList/FlatList-basic.js
@@ -35,8 +35,10 @@ import {
ItemSeparatorComponent,
PlainInput,
SeparatorComponent,
+ LoadingComponent,
Spindicator,
- genItemData,
+ genNewerItems,
+ genOlderItems,
getItemLayout,
pressItem,
renderSmallSwitchOption,
@@ -44,6 +46,11 @@ import {
import type {Item} from '../../components/ListExampleShared';
+const PAGE_SIZE = 100;
+const NUM_PAGES = 10;
+const INITIAL_PAGE_OFFSET = Math.floor(NUM_PAGES / 2);
+const LOAD_TIME = 2000;
+
const VIEWABILITY_CONFIG = {
minimumViewTime: 3000,
viewAreaCoveragePercentThreshold: 100,
@@ -53,6 +60,8 @@ const VIEWABILITY_CONFIG = {
type Props = $ReadOnly<{||}>;
type State = {|
data: Array
- ,
+ first: number,
+ last: number,
debug: boolean,
horizontal: boolean,
inverted: boolean,
@@ -66,13 +75,18 @@ type State = {|
onPressDisabled: boolean,
textSelectable: boolean,
isRTL: boolean,
+ maintainVisibleContentPosition: boolean,
+ previousLoading: boolean,
+ nextLoading: boolean,
|};
const IS_RTL = I18nManager.isRTL;
class FlatListExample extends React.PureComponent {
state: State = {
- data: genItemData(100),
+ data: genNewerItems(PAGE_SIZE, PAGE_SIZE * INITIAL_PAGE_OFFSET),
+ first: PAGE_SIZE * INITIAL_PAGE_OFFSET,
+ last: PAGE_SIZE + PAGE_SIZE * INITIAL_PAGE_OFFSET,
debug: false,
horizontal: false,
inverted: false,
@@ -86,6 +100,9 @@ class FlatListExample extends React.PureComponent {
onPressDisabled: false,
textSelectable: true,
isRTL: IS_RTL,
+ maintainVisibleContentPosition: true,
+ previousLoading: false,
+ nextLoading: false,
};
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
@@ -206,6 +223,11 @@ class FlatListExample extends React.PureComponent {
this.state.isRTL,
this._setIsRTL,
)}
+ {renderSmallSwitchOption(
+ 'Maintain content position',
+ this.state.maintainVisibleContentPosition,
+ this._setBooleanValue('maintainVisibleContentPosition'),
+ )}
{Platform.OS === 'android' && (
{
}
- ListFooterComponent={FooterComponent}
+ ListHeaderComponent={
+ this.state.previousLoading ? LoadingComponent : HeaderComponent
+ }
+ ListFooterComponent={
+ this.state.nextLoading ? LoadingComponent : FooterComponent
+ }
ListEmptyComponent={ListEmptyComponent}
// $FlowFixMe[missing-empty-array-annot]
data={this.state.empty ? [] : filteredData}
@@ -247,6 +273,8 @@ class FlatListExample extends React.PureComponent {
keyboardShouldPersistTaps="always"
keyboardDismissMode="on-drag"
numColumns={1}
+ onStartReached={this._onStartReached}
+ initialScrollIndex={Math.floor(PAGE_SIZE / 2)}
onEndReached={this._onEndReached}
onRefresh={this._onRefresh}
onScroll={
@@ -257,6 +285,11 @@ class FlatListExample extends React.PureComponent {
refreshing={false}
contentContainerStyle={styles.list}
viewabilityConfig={VIEWABILITY_CONFIG}
+ maintainVisibleContentPosition={
+ this.state.maintainVisibleContentPosition
+ ? {minIndexForVisible: 0}
+ : undefined
+ }
{...flatListItemRendererProps}
/>
@@ -277,13 +310,33 @@ class FlatListExample extends React.PureComponent {
_getItemLayout = (data: any, index: number) => {
return getItemLayout(data, index, this.state.horizontal);
};
+ _onStartReached = () => {
+ if (this.state.first <= 0 || this.state.previousLoading) {
+ return;
+ }
+
+ this.setState({previousLoading: true});
+ setTimeout(() => {
+ this.setState(state => ({
+ previousLoading: false,
+ data: genOlderItems(PAGE_SIZE, state.first).concat(state.data),
+ first: state.first - PAGE_SIZE,
+ }));
+ }, LOAD_TIME);
+ };
_onEndReached = () => {
- if (this.state.data.length >= 1000) {
+ if (this.state.last >= PAGE_SIZE * NUM_PAGES || this.state.nextLoading) {
return;
}
- this.setState(state => ({
- data: state.data.concat(genItemData(100, state.data.length)),
- }));
+
+ this.setState({nextLoading: true});
+ setTimeout(() => {
+ this.setState(state => ({
+ nextLoading: false,
+ data: state.data.concat(genNewerItems(PAGE_SIZE, state.last)),
+ last: state.last + PAGE_SIZE,
+ }));
+ }, LOAD_TIME);
};
// $FlowFixMe[missing-local-annot]
_onPressCallback = () => {
@@ -340,7 +393,7 @@ class FlatListExample extends React.PureComponent {
_pressItem = (key: string) => {
this._listRef?.recordInteraction();
- const index = Number(key);
+ const index = this.state.data.findIndex(item => item.key === key);
const itemState = pressItem(this.state.data[index]);
this.setState(state => ({
...state,
diff --git a/packages/rn-tester/js/examples/FlatList/FlatList-multiColumn.js b/packages/rn-tester/js/examples/FlatList/FlatList-multiColumn.js
index 4c5ea5ddd554ab..fd8d10a43474c3 100644
--- a/packages/rn-tester/js/examples/FlatList/FlatList-multiColumn.js
+++ b/packages/rn-tester/js/examples/FlatList/FlatList-multiColumn.js
@@ -23,7 +23,7 @@ const {
ItemComponent,
PlainInput,
SeparatorComponent,
- genItemData,
+ genNewerItems,
getItemLayout,
pressItem,
renderSmallSwitchOption,
@@ -46,7 +46,7 @@ class MultiColumnExample extends React.PureComponent<
numColumns: number,
virtualized: boolean,
|} = {
- data: genItemData(1000),
+ data: genNewerItems(1000),
filterText: '',
fixedHeight: true,
logViewable: false,
diff --git a/packages/rn-tester/js/examples/SectionList/SectionList-scrollable.js b/packages/rn-tester/js/examples/SectionList/SectionList-scrollable.js
index 83e989a8a78429..b0039c98eb3c36 100644
--- a/packages/rn-tester/js/examples/SectionList/SectionList-scrollable.js
+++ b/packages/rn-tester/js/examples/SectionList/SectionList-scrollable.js
@@ -22,7 +22,7 @@ const {
PlainInput,
SeparatorComponent,
Spindicator,
- genItemData,
+ genNewerItems,
pressItem,
renderSmallSwitchOption,
renderStackedItem,
@@ -170,7 +170,7 @@ export function SectionList_scrollable(Props: {
const [logViewable, setLogViewable] = React.useState(false);
const [debug, setDebug] = React.useState(false);
const [inverted, setInverted] = React.useState(false);
- const [data, setData] = React.useState(genItemData(1000));
+ const [data, setData] = React.useState(genNewerItems(1000));
const filterRegex = new RegExp(String(filterText), 'i');
const filter = (item: Item) =>
diff --git a/packages/virtualized-lists/Lists/VirtualizedList.js b/packages/virtualized-lists/Lists/VirtualizedList.js
index 2aca79a2ea2503..8a23e81cbef1a6 100644
--- a/packages/virtualized-lists/Lists/VirtualizedList.js
+++ b/packages/virtualized-lists/Lists/VirtualizedList.js
@@ -73,6 +73,10 @@ type ViewabilityHelperCallbackTuple = {
type State = {
renderMask: CellRenderMask,
cellsAroundViewport: {first: number, last: number},
+ // Used to track items added at the start of the list for maintainVisibleContentPosition.
+ firstVisibleItemKey: ?string,
+ // When > 0 the scroll position available in JS is considered stale and should not be used.
+ pendingScrollUpdateCount: number,
};
/**
@@ -448,9 +452,24 @@ class VirtualizedList extends StateSafePureComponent {
const initialRenderRegion = VirtualizedList._initialRenderRegion(props);
+ const minIndexForVisible =
+ this.props.maintainVisibleContentPosition?.minIndexForVisible ?? 0;
+
this.state = {
cellsAroundViewport: initialRenderRegion,
renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion),
+ firstVisibleItemKey:
+ this.props.getItemCount(this.props.data) > minIndexForVisible
+ ? VirtualizedList._getItemKey(this.props, minIndexForVisible)
+ : null,
+ // When we have a non-zero initialScrollIndex, we will receive a
+ // scroll event later so this will prevent the window from updating
+ // until we get a valid offset.
+ pendingScrollUpdateCount:
+ this.props.initialScrollIndex != null &&
+ this.props.initialScrollIndex > 0
+ ? 1
+ : 0,
};
}
@@ -502,6 +521,40 @@ class VirtualizedList extends StateSafePureComponent {
}
}
+ static _findItemIndexWithKey(
+ props: Props,
+ key: string,
+ hint: ?number,
+ ): ?number {
+ const itemCount = props.getItemCount(props.data);
+ if (hint != null && hint >= 0 && hint < itemCount) {
+ const curKey = VirtualizedList._getItemKey(props, hint);
+ if (curKey === key) {
+ return hint;
+ }
+ }
+ for (let ii = 0; ii < itemCount; ii++) {
+ const curKey = VirtualizedList._getItemKey(props, ii);
+ if (curKey === key) {
+ return ii;
+ }
+ }
+ return null;
+ }
+
+ static _getItemKey(
+ props: {
+ data: Props['data'],
+ getItem: Props['getItem'],
+ keyExtractor: Props['keyExtractor'],
+ ...
+ },
+ index: number,
+ ): string {
+ const item = props.getItem(props.data, index);
+ return VirtualizedList._keyExtractor(item, index, props);
+ }
+
static _createRenderMask(
props: Props,
cellsAroundViewport: {first: number, last: number},
@@ -585,6 +638,7 @@ class VirtualizedList extends StateSafePureComponent {
_adjustCellsAroundViewport(
props: Props,
cellsAroundViewport: {first: number, last: number},
+ pendingScrollUpdateCount: number,
): {first: number, last: number} {
const {data, getItemCount} = props;
const onEndReachedThreshold = onEndReachedThresholdOrDefault(
@@ -616,22 +670,9 @@ class VirtualizedList extends StateSafePureComponent {
),
};
} else {
- // If we have a positive 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 != null &&
- props.initialScrollIndex > 0 &&
- !this._scrollMetrics.offset &&
- Math.abs(distanceFromEnd) >= Number.EPSILON
- ) {
+ // If we have a pending scroll update, we should not adjust the render window as it
+ // might override the correct window.
+ if (pendingScrollUpdateCount > 0) {
return cellsAroundViewport.last >= getItemCount(data)
? VirtualizedList._constrainToItemCount(cellsAroundViewport, props)
: cellsAroundViewport;
@@ -713,14 +754,59 @@ class VirtualizedList extends StateSafePureComponent {
return prevState;
}
+ let maintainVisibleContentPositionAdjustment: ?number = null;
+ const prevFirstVisibleItemKey = prevState.firstVisibleItemKey;
+ const minIndexForVisible =
+ newProps.maintainVisibleContentPosition?.minIndexForVisible ?? 0;
+ const newFirstVisibleItemKey =
+ newProps.getItemCount(newProps.data) > minIndexForVisible
+ ? VirtualizedList._getItemKey(newProps, minIndexForVisible)
+ : null;
+ if (
+ newProps.maintainVisibleContentPosition != null &&
+ prevFirstVisibleItemKey != null &&
+ newFirstVisibleItemKey != null
+ ) {
+ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) {
+ // Fast path if items were added at the start of the list.
+ const hint =
+ itemCount - prevState.renderMask.numCells() + minIndexForVisible;
+ const firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey(
+ newProps,
+ prevFirstVisibleItemKey,
+ hint,
+ );
+ maintainVisibleContentPositionAdjustment =
+ firstVisibleItemIndex != null
+ ? firstVisibleItemIndex - minIndexForVisible
+ : null;
+ } else {
+ maintainVisibleContentPositionAdjustment = null;
+ }
+ }
+
const constrainedCells = VirtualizedList._constrainToItemCount(
- prevState.cellsAroundViewport,
+ maintainVisibleContentPositionAdjustment != null
+ ? {
+ first:
+ prevState.cellsAroundViewport.first +
+ maintainVisibleContentPositionAdjustment,
+ last:
+ prevState.cellsAroundViewport.last +
+ maintainVisibleContentPositionAdjustment,
+ }
+ : prevState.cellsAroundViewport,
newProps,
);
return {
cellsAroundViewport: constrainedCells,
renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells),
+ firstVisibleItemKey: newFirstVisibleItemKey,
+ pendingScrollUpdateCount:
+ maintainVisibleContentPositionAdjustment != null
+ ? prevState.pendingScrollUpdateCount + 1
+ : prevState.pendingScrollUpdateCount,
};
}
@@ -752,7 +838,7 @@ class VirtualizedList extends StateSafePureComponent {
for (let ii = first; ii <= last; ii++) {
const item = getItem(data, ii);
- const key = this._keyExtractor(item, ii, this.props);
+ const key = VirtualizedList._keyExtractor(item, ii, this.props);
this._indicesToKeys.set(ii, key);
if (stickyIndicesFromProps.has(ii + stickyOffset)) {
@@ -825,15 +911,14 @@ class VirtualizedList extends StateSafePureComponent {
_getSpacerKey = (isVertical: boolean): string =>
isVertical ? 'height' : 'width';
- _keyExtractor(
+ static _keyExtractor(
item: Item,
index: number,
props: {
keyExtractor?: ?(item: Item, index: number) => string,
...
},
- // $FlowFixMe[missing-local-annot]
- ) {
+ ): string {
if (props.keyExtractor != null) {
return props.keyExtractor(item, index);
}
@@ -879,6 +964,10 @@ class VirtualizedList extends StateSafePureComponent {
cellKey={this._getCellKey() + '-header'}
key="$header">
{
style: inversionStyle
? [inversionStyle, this.props.style]
: this.props.style,
+ maintainVisibleContentPosition:
+ this.props.maintainVisibleContentPosition != null
+ ? {
+ ...this.props.maintainVisibleContentPosition,
+ // Adjust index to account for ListHeaderComponent.
+ minIndexForVisible:
+ this.props.maintainVisibleContentPosition.minIndexForVisible +
+ (this.props.ListHeaderComponent ? 1 : 0),
+ }
+ : undefined,
};
this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1;
@@ -1457,8 +1556,13 @@ class VirtualizedList extends StateSafePureComponent {
onStartReachedThreshold,
onEndReached,
onEndReachedThreshold,
- initialScrollIndex,
} = this.props;
+ // If we have any pending scroll updates it means that the scroll metrics
+ // are out of date and we should not call any of the edge reached callbacks.
+ if (this.state.pendingScrollUpdateCount > 0) {
+ return;
+ }
+
const {contentLength, visibleLength, offset} = this._scrollMetrics;
let distanceFromStart = offset;
let distanceFromEnd = contentLength - visibleLength - offset;
@@ -1510,14 +1614,8 @@ class VirtualizedList extends StateSafePureComponent {
isWithinStartThreshold &&
this._scrollMetrics.contentLength !== this._sentStartForContentLength
) {
- // On initial mount when using initialScrollIndex the offset will be 0 initially
- // and will trigger an unexpected onStartReached. To avoid this we can use
- // timestamp to differentiate between the initial scroll metrics and when we actually
- // received the first scroll event.
- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) {
- this._sentStartForContentLength = this._scrollMetrics.contentLength;
- onStartReached({distanceFromStart});
- }
+ this._sentStartForContentLength = this._scrollMetrics.contentLength;
+ onStartReached({distanceFromStart});
}
// If the user scrolls away from the start or end and back again,
@@ -1644,6 +1742,11 @@ class VirtualizedList extends StateSafePureComponent {
visibleLength,
zoomScale,
};
+ if (this.state.pendingScrollUpdateCount > 0) {
+ this.setState(state => ({
+ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1,
+ }));
+ }
this._updateViewableItems(this.props, this.state.cellsAroundViewport);
if (!this.props) {
return;
@@ -1759,6 +1862,7 @@ class VirtualizedList extends StateSafePureComponent {
const cellsAroundViewport = this._adjustCellsAroundViewport(
props,
state.cellsAroundViewport,
+ state.pendingScrollUpdateCount,
);
const renderMask = VirtualizedList._createRenderMask(
props,
@@ -1789,7 +1893,7 @@ class VirtualizedList extends StateSafePureComponent {
return {
index,
item,
- key: this._keyExtractor(item, index, props),
+ key: VirtualizedList._keyExtractor(item, index, props),
isViewable,
};
};
@@ -1850,13 +1954,12 @@ class VirtualizedList extends StateSafePureComponent {
inLayout?: boolean,
...
} => {
- const {data, getItem, getItemCount, getItemLayout} = props;
+ const {data, getItemCount, getItemLayout} = props;
invariant(
index >= 0 && index < getItemCount(data),
'Tried to get frame for out of range index ' + index,
);
- const item = getItem(data, index);
- const frame = this._frames[this._keyExtractor(item, index, props)];
+ const frame = this._frames[VirtualizedList._getItemKey(props, index)];
if (!frame || frame.index !== index) {
if (getItemLayout) {
/* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment
@@ -1891,11 +1994,8 @@ class VirtualizedList extends StateSafePureComponent {
// where it is.
if (
focusedCellIndex >= itemCount ||
- this._keyExtractor(
- props.getItem(props.data, focusedCellIndex),
- focusedCellIndex,
- props,
- ) !== this._lastFocusedCellKey
+ VirtualizedList._getItemKey(props, focusedCellIndex) !==
+ this._lastFocusedCellKey
) {
return [];
}
@@ -1936,6 +2036,11 @@ class VirtualizedList extends StateSafePureComponent {
props: FrameMetricProps,
cellsAroundViewport: {first: number, last: number},
) {
+ // If we have any pending scroll updates it means that the scroll metrics
+ // are out of date and we should not call any of the visibility callbacks.
+ if (this.state.pendingScrollUpdateCount > 0) {
+ return;
+ }
this._viewabilityTuples.forEach(tuple => {
tuple.viewabilityHelper.onUpdate(
props,
diff --git a/packages/virtualized-lists/Lists/__tests__/VirtualizedList-test.js b/packages/virtualized-lists/Lists/__tests__/VirtualizedList-test.js
index 218f7c1cae71db..c030e8a2d7e690 100644
--- a/packages/virtualized-lists/Lists/__tests__/VirtualizedList-test.js
+++ b/packages/virtualized-lists/Lists/__tests__/VirtualizedList-test.js
@@ -2164,10 +2164,70 @@ it('virtualizes away last focused index if item removed', () => {
expect(component).toMatchSnapshot();
});
-function generateItems(count) {
+it('handles maintainVisibleContentPosition', () => {
+ const items = generateItems(20);
+ const ITEM_HEIGHT = 10;
+
+ let component;
+ ReactTestRenderer.act(() => {
+ component = ReactTestRenderer.create(
+ ,
+ );
+ });
+
+ ReactTestRenderer.act(() => {
+ simulateLayout(component, {
+ viewport: {width: 10, height: 50},
+ content: {width: 10, height: items.length * ITEM_HEIGHT},
+ });
+
+ performAllBatches();
+ });
+
+ // Initial render.
+ expect(component).toMatchSnapshot();
+
+ // Add new items at the start of the list to trigger the maintainVisibleContentPosition adjustment.
+ const newItems = [...generateItems(10, items.length), ...items];
+ ReactTestRenderer.act(() => {
+ component.update(
+ ,
+ );
+ });
+
+ // Previously rendered cells should be rendered still.
+ expect(component).toMatchSnapshot();
+
+ // Simulate scroll adjustment from native maintainVisibleContentPosition.
+ ReactTestRenderer.act(() => {
+ simulateContentLayout(component, {
+ width: 10,
+ height: newItems.length * ITEM_HEIGHT,
+ });
+ simulateScroll(component, {x: 0, y: 10 * ITEM_HEIGHT});
+ performAllBatches();
+ });
+
+ // Previously rendered cells should be rendered still + starting to render new cells ahead.
+ expect(component).toMatchSnapshot();
+});
+
+function generateItems(count, startKey = 0) {
return Array(count)
.fill()
- .map((_, i) => ({key: i}));
+ .map((_, i) => ({key: i + startKey}));
}
function generateItemsStickyEveryN(count, n) {
@@ -2225,7 +2285,7 @@ function simulateContentLayout(component, dimensions) {
function simulateCellLayout(component, items, itemIndex, dimensions) {
const instance = component.getInstance();
- const cellKey = instance._keyExtractor(
+ const cellKey = VirtualizedList._keyExtractor(
items[itemIndex],
itemIndex,
instance.props,
diff --git a/packages/virtualized-lists/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap b/packages/virtualized-lists/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap
index cd174da04cca2c..b877e3da84d999 100644
--- a/packages/virtualized-lists/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap
+++ b/packages/virtualized-lists/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap
@@ -65,6 +65,7 @@ exports[`VirtualizedList forwards correct stickyHeaderIndices when ListHeaderCom
>
@@ -1048,6 +1049,7 @@ exports[`VirtualizedList renders all the bells and whistles 1`] = `
@@ -1834,45 +1837,12 @@ exports[`adjusts render area with non-zero initialScrollIndex 1`] = `
/>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ style={
+ Object {
+ "height": 50,
+ }
+ }
+ />
`;
@@ -3173,7 +3143,7 @@ exports[`gracefully handles negative initialScrollIndex 1`] = `
`;
-exports[`initially renders nothing when initialNumToRender is 0 1`] = `
+exports[`handles maintainVisibleContentPosition 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -3236,10 +3282,40 @@ exports[`initially renders nothing when initialNumToRender is 0 1`] = `
`;
-exports[`keeps viewport above last focused rendered 1`] = `
+exports[`handles maintainVisibleContentPosition 2`] = `
-
-
-
-
-
-
-
-
-
-
-
-
@@ -3371,23 +3420,7 @@ exports[`keeps viewport above last focused rendered 1`] = `
style={null}
>
-
-
-
-
-
-
-
-
+ style={
+ Object {
+ "height": 150,
+ }
+ }
+ />
`;
-exports[`keeps viewport below last focused rendered 1`] = `
+exports[`handles maintainVisibleContentPosition 3`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`initially renders nothing when initialNumToRender is 0 1`] = `
+
+
+
+
+
+`;
+
+exports[`keeps viewport above last focused rendered 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`keeps viewport below last focused rendered 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+ style={
+ Object {
+ "height": 20,
+ }
+ }
+ />
`;
diff --git a/packages/virtualized-lists/Lists/__tests__/__snapshots__/VirtualizedSectionList-test.js.snap b/packages/virtualized-lists/Lists/__tests__/__snapshots__/VirtualizedSectionList-test.js.snap
index 9f2d41bf9aa513..3f64c8856611e6 100644
--- a/packages/virtualized-lists/Lists/__tests__/__snapshots__/VirtualizedSectionList-test.js.snap
+++ b/packages/virtualized-lists/Lists/__tests__/__snapshots__/VirtualizedSectionList-test.js.snap
@@ -831,6 +831,7 @@ exports[`VirtualizedSectionList renders all the bells and whistles 1`] = `