Skip to content

Commit 1d6ae58

Browse files
authored
fix(cdk/scrolling): fixed-size-virtual-scroll wrong range and position when items length changes and current scroll is greater than new data length (#19578)
* fix(cdk/scrolling): fixed-size-virtual-scroll wrong range and position when items length changes and current scroll is greater than new data length * Added comments and fixed behavior when new dataLength matches current range end
1 parent 0a6e4d6 commit 1d6ae58

File tree

2 files changed

+89
-6
lines changed

2 files changed

+89
-6
lines changed

src/cdk/scrolling/fixed-size-virtual-scroll.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,30 @@ export class FixedSizeVirtualScrollStrategy implements VirtualScrollStrategy {
120120
return;
121121
}
122122

123-
const scrollOffset = this._viewport.measureScrollOffset();
124-
const firstVisibleIndex = scrollOffset / this._itemSize;
125123
const renderedRange = this._viewport.getRenderedRange();
126124
const newRange = {start: renderedRange.start, end: renderedRange.end};
127125
const viewportSize = this._viewport.getViewportSize();
128126
const dataLength = this._viewport.getDataLength();
127+
let scrollOffset = this._viewport.measureScrollOffset();
128+
let firstVisibleIndex = scrollOffset / this._itemSize;
129+
130+
// If user scrolls to the bottom of the list and data changes to a smaller list
131+
if (newRange.end > dataLength) {
132+
// We have to recalculate the first visible index based on new data length and viewport size.
133+
const maxVisibleItems = Math.ceil(viewportSize / this._itemSize);
134+
const newVisibleIndex = Math.max(0,
135+
Math.min(firstVisibleIndex, dataLength - maxVisibleItems));
136+
137+
// If first visible index changed we must update scroll offset to handle start/end buffers
138+
// Current range must also be adjusted to cover the new position (bottom of new list).
139+
if (firstVisibleIndex != newVisibleIndex) {
140+
firstVisibleIndex = newVisibleIndex;
141+
scrollOffset = newVisibleIndex * this._itemSize;
142+
newRange.start = Math.floor(firstVisibleIndex);
143+
}
144+
145+
newRange.end = Math.max(0, Math.min(dataLength, newRange.start + maxVisibleItems));
146+
}
129147

130148
const startBuffer = scrollOffset - newRange.start * this._itemSize;
131149
if (startBuffer < this._minBufferPx && newRange.start != 0) {

src/cdk/scrolling/virtual-scroll-viewport.spec.ts

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -412,12 +412,46 @@ describe('CdkVirtualScrollViewport', () => {
412412
fixture.detectChanges();
413413
flush();
414414

415-
triggerScroll(viewport);
415+
expect(viewport.getOffsetToRenderedContentStart())
416+
.toBe(testComponent.itemSize, 'should be scrolled to bottom of 5 item list');
417+
}));
418+
419+
it('should handle dynamic item array with dynamic buffer', fakeAsync(() => {
420+
finishInit(fixture);
421+
triggerScroll(viewport, testComponent.itemSize * 6);
416422
fixture.detectChanges();
417423
flush();
418424

419425
expect(viewport.getOffsetToRenderedContentStart())
420-
.toBe(testComponent.itemSize, 'should be scrolled to bottom of 5 item list');
426+
.toBe(testComponent.itemSize * 6, 'should be scrolled to bottom of 10 item list');
427+
428+
testComponent.items = Array(5).fill(0);
429+
testComponent.minBufferPx = testComponent.itemSize;
430+
testComponent.maxBufferPx = testComponent.itemSize;
431+
432+
fixture.detectChanges();
433+
flush();
434+
435+
expect(viewport.getOffsetToRenderedContentStart())
436+
.toBe(0, 'should render from first item');
437+
}));
438+
439+
it('should handle dynamic item array keeping position when possibile', fakeAsync(() => {
440+
testComponent.items = Array(100).fill(0);
441+
finishInit(fixture);
442+
triggerScroll(viewport, testComponent.itemSize * 50);
443+
fixture.detectChanges();
444+
flush();
445+
446+
expect(viewport.getOffsetToRenderedContentStart())
447+
.toBe(testComponent.itemSize * 50, 'should be scrolled to index 50 item list');
448+
449+
testComponent.items = Array(54).fill(0);
450+
fixture.detectChanges();
451+
flush();
452+
453+
expect(viewport.getOffsetToRenderedContentStart())
454+
.toBe(testComponent.itemSize * 50, 'should be kept the scroll position');
421455
}));
422456

423457
it('should update viewport as user scrolls right in horizontal mode', fakeAsync(() => {
@@ -900,6 +934,15 @@ function triggerScroll(viewport: CdkVirtualScrollViewport, offset?: number) {
900934
.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper {
901935
flex-direction: row;
902936
}
937+
938+
.cdk-virtual-scroll-viewport {
939+
background-color: #f5f5f5;
940+
}
941+
942+
.item {
943+
box-sizing: border-box;
944+
border: 1px dashed #ccc;
945+
}
903946
`],
904947
encapsulation: ViewEncapsulation.None,
905948
})
@@ -952,6 +995,15 @@ class FixedSizeVirtualScroll {
952995
.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper {
953996
flex-direction: row;
954997
}
998+
999+
.cdk-virtual-scroll-viewport {
1000+
background-color: #f5f5f5;
1001+
}
1002+
1003+
.item {
1004+
box-sizing: border-box;
1005+
border: 1px dashed #ccc;
1006+
}
9551007
`],
9561008
encapsulation: ViewEncapsulation.None,
9571009
})
@@ -982,9 +1034,19 @@ class FixedSizeVirtualScrollWithRtlDirection {
9821034
@Component({
9831035
template: `
9841036
<cdk-virtual-scroll-viewport>
985-
<div *cdkVirtualFor="let item of items">{{item}}</div>
1037+
<div class="item" *cdkVirtualFor="let item of items">{{item}}</div>
9861038
</cdk-virtual-scroll-viewport>
987-
`
1039+
`,
1040+
styles: [`
1041+
.cdk-virtual-scroll-viewport {
1042+
background-color: #f5f5f5;
1043+
}
1044+
1045+
.item {
1046+
box-sizing: border-box;
1047+
border: 1px dashed #ccc;
1048+
}
1049+
`]
9881050
})
9891051
class VirtualScrollWithNoStrategy {
9901052
items = [];
@@ -1013,11 +1075,14 @@ class InjectsViewContainer {
10131075
.cdk-virtual-scroll-viewport {
10141076
width: 200px;
10151077
height: 200px;
1078+
background-color: #f5f5f5;
10161079
}
10171080
10181081
.item {
10191082
width: 100%;
10201083
height: 50px;
1084+
box-sizing: border-box;
1085+
border: 1px dashed #ccc;
10211086
}
10221087
`],
10231088
encapsulation: ViewEncapsulation.None

0 commit comments

Comments
 (0)