Skip to content

Commit 53fc0eb

Browse files
NickGerlemanlsdimagine
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 bae63d4 commit 53fc0eb

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
@@ -858,15 +858,19 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
858858
props: Props,
859859
): {first: number, last: number} {
860860
const itemCount = props.getItemCount(props.data);
861-
const last = Math.min(itemCount - 1, cells.last);
861+
const lastPossibleCellIndex = itemCount - 1;
862862

863+
// Constraining `last` may significantly shrink the window. Adjust `first`
864+
// to expand the window if the new `last` results in a new window smaller
865+
// than the number of cells rendered per batch.
863866
const maxToRenderPerBatch = maxToRenderPerBatchOrDefault(
864867
props.maxToRenderPerBatch,
865868
);
869+
const maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch);
866870

867871
return {
868-
first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first),
869-
last,
872+
first: clamp(0, cells.first, maxFirst),
873+
last: Math.min(lastPossibleCellIndex, cells.last),
870874
};
871875
}
872876

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
@@ -3658,6 +3658,273 @@ exports[`handles maintainVisibleContentPosition 3`] = `
36583658
</RCTScrollView>
36593659
`;
36603660

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

0 commit comments

Comments
 (0)