Skip to content

Commit 5892285

Browse files
crisbetojelbourn
authored andcommitted
fix(scrolling): virtual scroll not accounting for margin when measuring range (#19852)
Currently the `CdkVirtualForOf` determines the size of a rendered range by adding up the heights of all the elements, however this doesn't account for margins between them. These changes switch to doing it by taking the difference between the bottom of the last element and the top of the first. This should be a minor performance improvement as well, because we don't have to measure as many elements anymore. Fixes #19851. (cherry picked from commit a62a50a)
1 parent f58407d commit 5892285

File tree

2 files changed

+45
-13
lines changed

2 files changed

+45
-13
lines changed

src/cdk/scrolling/virtual-for-of.ts

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,20 @@ export type CdkVirtualForOfContext<T> = {
5757
};
5858

5959

60-
/** Helper to extract size from a DOM Node. */
61-
function getSize(orientation: 'horizontal' | 'vertical', node: Node): number {
60+
/** Helper to extract the offset of a DOM Node in a certain direction. */
61+
function getOffset(orientation: 'horizontal' | 'vertical', direction: 'start' | 'end', node: Node) {
6262
const el = node as Element;
6363
if (!el.getBoundingClientRect) {
6464
return 0;
6565
}
6666
const rect = el.getBoundingClientRect();
67-
return orientation == 'horizontal' ? rect.width : rect.height;
68-
}
6967

68+
if (orientation === 'horizontal') {
69+
return direction === 'start' ? rect.left : rect.right;
70+
}
71+
72+
return direction === 'start' ? rect.top : rect.bottom;
73+
}
7074

7175
/**
7276
* A directive similar to `ngForOf` to be used for rendering data inside a virtual scrolling
@@ -209,19 +213,33 @@ export class CdkVirtualForOf<T> implements
209213
// The length of the range we're measuring.
210214
const rangeLen = range.end - range.start;
211215

212-
// Loop over all root nodes for all items in the range and sum up their size.
213-
let totalSize = 0;
214-
let i = rangeLen;
215-
while (i--) {
216+
// Loop over all the views, find the first and land node and compute the size by subtracting
217+
// the top of the first node from the bottom of the last one.
218+
let firstNode: HTMLElement | undefined;
219+
let lastNode: HTMLElement | undefined;
220+
221+
// Find the first node by starting from the beginning and going forwards.
222+
for (let i = 0; i < rangeLen; i++) {
223+
const view = this._viewContainerRef.get(i + renderedStartIndex) as
224+
EmbeddedViewRef<CdkVirtualForOfContext<T>> | null;
225+
if (view && view.rootNodes.length) {
226+
firstNode = lastNode = view.rootNodes[0];
227+
break;
228+
}
229+
}
230+
231+
// Find the last node by starting from the end and going backwards.
232+
for (let i = rangeLen - 1; i > -1; i--) {
216233
const view = this._viewContainerRef.get(i + renderedStartIndex) as
217234
EmbeddedViewRef<CdkVirtualForOfContext<T>> | null;
218-
let j = view ? view.rootNodes.length : 0;
219-
while (j--) {
220-
totalSize += getSize(orientation, view!.rootNodes[j]);
235+
if (view && view.rootNodes.length) {
236+
lastNode = view.rootNodes[view.rootNodes.length - 1];
237+
break;
221238
}
222239
}
223240

224-
return totalSize;
241+
return firstNode && lastNode ?
242+
getOffset(orientation, 'end', lastNode) - getOffset(orientation, 'start', firstNode) : 0;
225243
}
226244

227245
ngDoCheck() {

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,14 @@ describe('CdkVirtualScrollViewport', () => {
134134
.toBe(testComponent.itemSize * 2, 'combined size of 2 50px items should be 100px');
135135
}));
136136

137+
it('should measure range size when items has a margin', fakeAsync(() => {
138+
fixture.componentInstance.hasMargin = true;
139+
finishInit(fixture);
140+
141+
expect(viewport.measureRangeSize({start: 1, end: 3})).toBe(testComponent.itemSize * 2 + 10,
142+
'combined size of 2 50px items with a 10px margin should be 110px');
143+
}));
144+
137145
it('should set total content size', fakeAsync(() => {
138146
finishInit(fixture);
139147

@@ -916,7 +924,8 @@ function triggerScroll(viewport: CdkVirtualScrollViewport, offset?: number) {
916924
<cdk-virtual-scroll-viewport
917925
[itemSize]="itemSize" [minBufferPx]="minBufferPx" [maxBufferPx]="maxBufferPx"
918926
[orientation]="orientation" [style.height.px]="viewportHeight"
919-
[style.width.px]="viewportWidth" (scrolledIndexChange)="scrolledToIndex = $event">
927+
[style.width.px]="viewportWidth" (scrolledIndexChange)="scrolledToIndex = $event"
928+
[class.has-margin]="hasMargin">
920929
<div class="item"
921930
*cdkVirtualFor="let item of items; let i = index; trackBy: trackBy; \
922931
templateCacheSize: templateCacheSize"
@@ -943,6 +952,10 @@ function triggerScroll(viewport: CdkVirtualScrollViewport, offset?: number) {
943952
box-sizing: border-box;
944953
border: 1px dashed #ccc;
945954
}
955+
956+
.has-margin .item {
957+
margin-bottom: 10px;
958+
}
946959
`],
947960
encapsulation: ViewEncapsulation.None,
948961
})
@@ -962,6 +975,7 @@ class FixedSizeVirtualScroll {
962975
@Input() templateCacheSize = 20;
963976

964977
scrolledToIndex = 0;
978+
hasMargin = false;
965979

966980
get viewportWidth() {
967981
return this.orientation == 'horizontal' ? this.viewportSize : this.viewportCrossSize;

0 commit comments

Comments
 (0)