Skip to content

Commit 19df5a8

Browse files
NickGerlemanyayvery
authored andcommitted
Fix invariant violation when maintainVisibleContentPosition adjustment moves window before list start (facebook#38655)
Summary: Pull Request resolved: facebook#38655 facebook#35993 added logic in VirtualizedList to support `maintainVisibleContentPosition`. This logic makes sure that a previously visible cell being used as an anchor remains rendered after new content is added. The strategy here is to calculate the difference in previous and new positions of the anchor, and move the render window to its new location during item change. `minIndexForVisible` is used as this anchor. When an item change moves the anchor to a position below `minIndexForVisible`, shifting the render window may result in a window which starts before zero. This fixes up `_constrainToItemCount()` to handle this. Changelog: [General][Fixed] - Fix invariant violation when `maintainVisibleContentPosition` adjustment moves window before list start Reviewed By: yungsters Differential Revision: D47846165 fbshipit-source-id: 8a36f66fdad321acb255745dad85618d28c54dba
1 parent 8e4c87a commit 19df5a8

File tree

3 files changed

+321
-3
lines changed

3 files changed

+321
-3
lines changed

packages/virtualized-lists/Lists/VirtualizedList.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -882,15 +882,19 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
882882
props: Props,
883883
): {first: number, last: number} {
884884
const itemCount = props.getItemCount(props.data);
885-
const last = Math.min(itemCount - 1, cells.last);
885+
const lastPossibleCellIndex = itemCount - 1;
886886

887+
// Constraining `last` may significantly shrink the window. Adjust `first`
888+
// to expand the window if the new `last` results in a new window smaller
889+
// than the number of cells rendered per batch.
887890
const maxToRenderPerBatch = maxToRenderPerBatchOrDefault(
888891
props.maxToRenderPerBatch,
889892
);
893+
const maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch);
890894

891895
return {
892-
first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first),
893-
last,
896+
first: clamp(0, cells.first, maxFirst),
897+
last: Math.min(lastPossibleCellIndex, cells.last),
894898
};
895899
}
896900

packages/virtualized-lists/Lists/__tests__/VirtualizedList-test.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2224,6 +2224,53 @@ it('handles maintainVisibleContentPosition', () => {
22242224
expect(component).toMatchSnapshot();
22252225
});
22262226

2227+
it('handles maintainVisibleContentPosition when anchor moves before minIndexForVisible', () => {
2228+
const items = generateItems(20);
2229+
const ITEM_HEIGHT = 10;
2230+
2231+
// Render a list with `minIndexForVisible: 1`
2232+
let component;
2233+
ReactTestRenderer.act(() => {
2234+
component = ReactTestRenderer.create(
2235+
<VirtualizedList
2236+
initialNumToRender={1}
2237+
windowSize={1}
2238+
maintainVisibleContentPosition={{minIndexForVisible: 1}}
2239+
{...baseItemProps(items)}
2240+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
2241+
/>,
2242+
);
2243+
});
2244+
2245+
ReactTestRenderer.act(() => {
2246+
simulateLayout(component, {
2247+
viewport: {width: 10, height: 50},
2248+
content: {width: 10, height: items.length * ITEM_HEIGHT},
2249+
});
2250+
2251+
performAllBatches();
2252+
});
2253+
2254+
expect(component).toMatchSnapshot();
2255+
2256+
// Remove the first item to shift the previous anchor to be before
2257+
// `minIndexForVisible`.
2258+
const [, ...restItems] = items;
2259+
ReactTestRenderer.act(() => {
2260+
component.update(
2261+
<VirtualizedList
2262+
initialNumToRender={1}
2263+
windowSize={1}
2264+
maintainVisibleContentPosition={{minIndexForVisible: 1}}
2265+
{...baseItemProps(restItems)}
2266+
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
2267+
/>,
2268+
);
2269+
});
2270+
2271+
expect(component).toMatchSnapshot();
2272+
});
2273+
22272274
function generateItems(count, startKey = 0) {
22282275
return Array(count)
22292276
.fill()

packages/virtualized-lists/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3659,6 +3659,273 @@ exports[`handles maintainVisibleContentPosition 3`] = `
36593659
</RCTScrollView>
36603660
`;
36613661

3662+
exports[`handles maintainVisibleContentPosition when anchor moves before minIndexForVisible 1`] = `
3663+
<RCTScrollView
3664+
data={
3665+
Array [
3666+
Object {
3667+
"key": 0,
3668+
},
3669+
Object {
3670+
"key": 1,
3671+
},
3672+
Object {
3673+
"key": 2,
3674+
},
3675+
Object {
3676+
"key": 3,
3677+
},
3678+
Object {
3679+
"key": 4,
3680+
},
3681+
Object {
3682+
"key": 5,
3683+
},
3684+
Object {
3685+
"key": 6,
3686+
},
3687+
Object {
3688+
"key": 7,
3689+
},
3690+
Object {
3691+
"key": 8,
3692+
},
3693+
Object {
3694+
"key": 9,
3695+
},
3696+
Object {
3697+
"key": 10,
3698+
},
3699+
Object {
3700+
"key": 11,
3701+
},
3702+
Object {
3703+
"key": 12,
3704+
},
3705+
Object {
3706+
"key": 13,
3707+
},
3708+
Object {
3709+
"key": 14,
3710+
},
3711+
Object {
3712+
"key": 15,
3713+
},
3714+
Object {
3715+
"key": 16,
3716+
},
3717+
Object {
3718+
"key": 17,
3719+
},
3720+
Object {
3721+
"key": 18,
3722+
},
3723+
Object {
3724+
"key": 19,
3725+
},
3726+
]
3727+
}
3728+
getItem={[Function]}
3729+
getItemCount={[Function]}
3730+
getItemLayout={[Function]}
3731+
initialNumToRender={1}
3732+
maintainVisibleContentPosition={
3733+
Object {
3734+
"minIndexForVisible": 1,
3735+
}
3736+
}
3737+
onContentSizeChange={[Function]}
3738+
onLayout={[Function]}
3739+
onMomentumScrollBegin={[Function]}
3740+
onMomentumScrollEnd={[Function]}
3741+
onScroll={[Function]}
3742+
onScrollBeginDrag={[Function]}
3743+
onScrollEndDrag={[Function]}
3744+
renderItem={[Function]}
3745+
scrollEventThrottle={50}
3746+
stickyHeaderIndices={Array []}
3747+
windowSize={1}
3748+
>
3749+
<View>
3750+
<View
3751+
onFocusCapture={[Function]}
3752+
style={null}
3753+
>
3754+
<MockCellItem
3755+
value={0}
3756+
/>
3757+
</View>
3758+
<View
3759+
onFocusCapture={[Function]}
3760+
style={null}
3761+
>
3762+
<MockCellItem
3763+
value={1}
3764+
/>
3765+
</View>
3766+
<View
3767+
onFocusCapture={[Function]}
3768+
style={null}
3769+
>
3770+
<MockCellItem
3771+
value={2}
3772+
/>
3773+
</View>
3774+
<View
3775+
onFocusCapture={[Function]}
3776+
style={null}
3777+
>
3778+
<MockCellItem
3779+
value={3}
3780+
/>
3781+
</View>
3782+
<View
3783+
onFocusCapture={[Function]}
3784+
style={null}
3785+
>
3786+
<MockCellItem
3787+
value={4}
3788+
/>
3789+
</View>
3790+
<View
3791+
style={
3792+
Object {
3793+
"height": 150,
3794+
}
3795+
}
3796+
/>
3797+
</View>
3798+
</RCTScrollView>
3799+
`;
3800+
3801+
exports[`handles maintainVisibleContentPosition when anchor moves before minIndexForVisible 2`] = `
3802+
<RCTScrollView
3803+
data={
3804+
Array [
3805+
Object {
3806+
"key": 1,
3807+
},
3808+
Object {
3809+
"key": 2,
3810+
},
3811+
Object {
3812+
"key": 3,
3813+
},
3814+
Object {
3815+
"key": 4,
3816+
},
3817+
Object {
3818+
"key": 5,
3819+
},
3820+
Object {
3821+
"key": 6,
3822+
},
3823+
Object {
3824+
"key": 7,
3825+
},
3826+
Object {
3827+
"key": 8,
3828+
},
3829+
Object {
3830+
"key": 9,
3831+
},
3832+
Object {
3833+
"key": 10,
3834+
},
3835+
Object {
3836+
"key": 11,
3837+
},
3838+
Object {
3839+
"key": 12,
3840+
},
3841+
Object {
3842+
"key": 13,
3843+
},
3844+
Object {
3845+
"key": 14,
3846+
},
3847+
Object {
3848+
"key": 15,
3849+
},
3850+
Object {
3851+
"key": 16,
3852+
},
3853+
Object {
3854+
"key": 17,
3855+
},
3856+
Object {
3857+
"key": 18,
3858+
},
3859+
Object {
3860+
"key": 19,
3861+
},
3862+
]
3863+
}
3864+
getItem={[Function]}
3865+
getItemCount={[Function]}
3866+
getItemLayout={[Function]}
3867+
initialNumToRender={1}
3868+
maintainVisibleContentPosition={
3869+
Object {
3870+
"minIndexForVisible": 1,
3871+
}
3872+
}
3873+
onContentSizeChange={[Function]}
3874+
onLayout={[Function]}
3875+
onMomentumScrollBegin={[Function]}
3876+
onMomentumScrollEnd={[Function]}
3877+
onScroll={[Function]}
3878+
onScrollBeginDrag={[Function]}
3879+
onScrollEndDrag={[Function]}
3880+
renderItem={[Function]}
3881+
scrollEventThrottle={50}
3882+
stickyHeaderIndices={Array []}
3883+
windowSize={1}
3884+
>
3885+
<View>
3886+
<View
3887+
onFocusCapture={[Function]}
3888+
style={null}
3889+
>
3890+
<MockCellItem
3891+
value={1}
3892+
/>
3893+
</View>
3894+
<View
3895+
onFocusCapture={[Function]}
3896+
style={null}
3897+
>
3898+
<MockCellItem
3899+
value={2}
3900+
/>
3901+
</View>
3902+
<View
3903+
onFocusCapture={[Function]}
3904+
style={null}
3905+
>
3906+
<MockCellItem
3907+
value={3}
3908+
/>
3909+
</View>
3910+
<View
3911+
onFocusCapture={[Function]}
3912+
style={null}
3913+
>
3914+
<MockCellItem
3915+
value={4}
3916+
/>
3917+
</View>
3918+
<View
3919+
style={
3920+
Object {
3921+
"height": 150,
3922+
}
3923+
}
3924+
/>
3925+
</View>
3926+
</RCTScrollView>
3927+
`;
3928+
36623929
exports[`initially renders nothing when initialNumToRender is 0 1`] = `
36633930
<RCTScrollView
36643931
data={

0 commit comments

Comments
 (0)