Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions packages/virtualized-lists/Lists/VirtualizedList.js
Original file line number Diff line number Diff line change
Expand Up @@ -713,18 +713,26 @@ class VirtualizedList extends StateSafePureComponent<
// 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;
}

let maintainVisibleContentPositionAdjustment: ?number = null;
const prevFirstVisibleItemKey = prevState.firstVisibleItemKey;
const minIndexForVisible =
newProps.maintainVisibleContentPosition?.minIndexForVisible ?? 0;
const newFirstVisibleItemKey =
newProps.getItemCount(newProps.data) > minIndexForVisible
itemCount > minIndexForVisible
? VirtualizedList._getItemKey(newProps, minIndexForVisible)
: null;
// React applies queued state updaters before getDerivedStateFromProps, so
// `_updateCellsToRender` may have already sized `renderMask` for the new
// data. An unchanged count then doesn't rule out a prepend, so with
// maintainVisibleContentPosition the key must be unchanged as well.
if (
itemCount === prevState.renderMask.numCells() &&
(newProps.maintainVisibleContentPosition == null ||
newFirstVisibleItemKey === prevFirstVisibleItemKey)
) {
return prevState;
}

let maintainVisibleContentPositionAdjustment: ?number = null;
if (
newProps.maintainVisibleContentPosition != null &&
prevFirstVisibleItemKey != null &&
Expand Down
53 changes: 53 additions & 0 deletions packages/virtualized-lists/Lists/__tests__/VirtualizedList-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2640,6 +2640,59 @@ it('handles maintainVisibleContentPosition', async () => {
expect(component).toMatchSnapshot();
});

// Trigger: Items prepended in the same render as a queued cells update, e.g. when a scroll event takes
// the high-priority path of `_scheduleCellsToRenderUpdate` and the app updates its data from that event.
// Expected: The prepend is handled as if it rendered on its own.
it('handles a maintainVisibleContentPosition prepend batched with a cells update', async () => {
const items = generateItems(20);
const ITEM_HEIGHT = 10;

let component;
await act(() => {
component = create(
<VirtualizedList
initialNumToRender={1}
windowSize={1}
maintainVisibleContentPosition={{minIndexForVisible: 0}}
{...baseItemProps(items)}
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
/>,
);
});

await act(() => {
simulateLayout(component, {
viewport: {width: 10, height: 50},
content: {width: 10, height: items.length * ITEM_HEIGHT},
});

performAllBatches();
});

const instance = component.getInstance();
expect(instance.state.cellsAroundViewport).toEqual({first: 0, last: 4});

const newItems = [...generateItems(10, items.length), ...items];
await act(() => {
instance._updateCellsToRender();
component.update(
<VirtualizedList
initialNumToRender={1}
windowSize={1}
maintainVisibleContentPosition={{minIndexForVisible: 0}}
{...baseItemProps(newItems)}
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
/>,
);
});

// The render window moves with the previously rendered cells, and the
// adjustment is pending until native reports the new offset.
expect(instance.state.cellsAroundViewport).toEqual({first: 10, last: 14});
expect(instance.state.pendingScrollUpdateCount).toBe(1);
expect(instance.state.firstVisibleItemKey).toBe(20);
});

// Trigger: Item at anchor position removed from data array.
// Expected: Anchor shifts to next visible item. MVCP captures new anchor's frame, computes delta, adjusts scroll.
it('handles maintainVisibleContentPosition when anchor moves before minIndexForVisible', async () => {
Expand Down
Loading