Skip to content

Commit 6b79cd0

Browse files
committed
fixup! feat(cdk/scrolling): make scroller element configurable for virtual scrolling
1 parent 55f41ba commit 6b79cd0

5 files changed

+194
-6
lines changed

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

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1086,6 +1086,80 @@ describe('CdkVirtualScrollViewport', () => {
10861086
expect(viewport.getOffsetToRenderedContentStart()).toBe(0);
10871087
}));
10881088
});
1089+
1090+
describe('with custom scrolling element', () => {
1091+
let fixture: ComponentFixture<VirtualScrollWithCustomScrollingElement>;
1092+
let testComponent: VirtualScrollWithCustomScrollingElement;
1093+
let viewport: CdkVirtualScrollViewport;
1094+
1095+
beforeEach(
1096+
waitForAsync(() => {
1097+
TestBed.configureTestingModule({
1098+
imports: [ScrollingModule],
1099+
declarations: [VirtualScrollWithCustomScrollingElement],
1100+
}).compileComponents();
1101+
}),
1102+
);
1103+
1104+
beforeEach(() => {
1105+
fixture = TestBed.createComponent(VirtualScrollWithCustomScrollingElement);
1106+
testComponent = fixture.componentInstance;
1107+
viewport = testComponent.viewport;
1108+
});
1109+
1110+
it('should measure viewport offset', fakeAsync(() => {
1111+
finishInit(fixture);
1112+
1113+
expect(viewport.measureViewportOffset('top'))
1114+
.withContext('with scrolling-element padding-top: 50 offset should be 50')
1115+
.toBe(50);
1116+
}));
1117+
1118+
it('should measure scroll offset', fakeAsync(() => {
1119+
finishInit(fixture);
1120+
triggerScroll(viewport, 100);
1121+
fixture.detectChanges();
1122+
flush();
1123+
1124+
expect(viewport.measureScrollOffset('top'))
1125+
.withContext('should be 50 (actual scroll offset - viewport offset)')
1126+
.toBe(50);
1127+
}));
1128+
});
1129+
1130+
describe('with scrollable window', () => {
1131+
let fixture: ComponentFixture<VirtualScrollWithScrollableWindow>;
1132+
let testComponent: VirtualScrollWithScrollableWindow;
1133+
let viewport: CdkVirtualScrollViewport;
1134+
1135+
beforeEach(
1136+
waitForAsync(() => {
1137+
TestBed.configureTestingModule({
1138+
imports: [ScrollingModule],
1139+
declarations: [VirtualScrollWithScrollableWindow],
1140+
}).compileComponents();
1141+
}),
1142+
);
1143+
1144+
beforeEach(() => {
1145+
fixture = TestBed.createComponent(VirtualScrollWithScrollableWindow);
1146+
testComponent = fixture.componentInstance;
1147+
viewport = testComponent.viewport;
1148+
});
1149+
1150+
it('should measure scroll offset', fakeAsync(() => {
1151+
finishInit(fixture);
1152+
viewport.scrollToOffset(100 + 8); // the +8 is due to a horizontal scrollbar
1153+
dispatchFakeEvent(window, 'scroll', true);
1154+
tick();
1155+
fixture.detectChanges();
1156+
flush();
1157+
1158+
expect(viewport.measureScrollOffset('top'))
1159+
.withContext('should be 50 (actual scroll offset - viewport offset)')
1160+
.toBe(50);
1161+
}));
1162+
});
10891163
});
10901164

10911165
/** Finish initializing the virtual scroll component at the beginning of a test. */
@@ -1109,7 +1183,7 @@ function triggerScroll(viewport: CdkVirtualScrollViewport, offset?: number) {
11091183
if (offset !== undefined) {
11101184
viewport.scrollToOffset(offset);
11111185
}
1112-
dispatchFakeEvent(viewport.elementRef.nativeElement, 'scroll');
1186+
dispatchFakeEvent(viewport.scrollable.getElementRef().nativeElement, 'scroll');
11131187
animationFrameScheduler.flush();
11141188
}
11151189

@@ -1391,3 +1465,88 @@ class VirtualScrollWithAppendOnly {
13911465
.fill(0)
13921466
.map((_, i) => i);
13931467
}
1468+
1469+
@Component({
1470+
template: `
1471+
<div cdk-virtual-scrollable-element class="scrolling-element">
1472+
<cdk-virtual-scroll-viewport itemSize="50">
1473+
<div class="item" *cdkVirtualFor="let item of items">{{item}}</div>
1474+
</cdk-virtual-scroll-viewport>
1475+
</div>
1476+
`,
1477+
styles: [
1478+
`
1479+
.cdk-virtual-scroll-content-wrapper {
1480+
display: flex;
1481+
flex-direction: column;
1482+
}
1483+
1484+
.cdk-virtual-scroll-viewport {
1485+
width: 200px;
1486+
height: 200px;
1487+
background-color: #f5f5f5;
1488+
}
1489+
1490+
.item {
1491+
width: 100%;
1492+
height: 50px;
1493+
box-sizing: border-box;
1494+
border: 1px dashed #ccc;
1495+
}
1496+
1497+
.scrolling-element {
1498+
padding-top: 50px;
1499+
}
1500+
`,
1501+
],
1502+
encapsulation: ViewEncapsulation.None,
1503+
})
1504+
class VirtualScrollWithCustomScrollingElement {
1505+
@ViewChild(CdkVirtualScrollViewport, {static: true}) viewport: CdkVirtualScrollViewport;
1506+
itemSize = 50;
1507+
items = Array(20000)
1508+
.fill(0)
1509+
.map((_, i) => i);
1510+
}
1511+
1512+
@Component({
1513+
template: `
1514+
<div class="before-virtual-viewport"></div>
1515+
<cdk-virtual-scroll-viewport scrollable-window itemSize="50">
1516+
<div class="item" *cdkVirtualFor="let item of items">{{item}}</div>
1517+
</cdk-virtual-scroll-viewport>
1518+
`,
1519+
styles: [
1520+
`
1521+
.cdk-virtual-scroll-content-wrapper {
1522+
display: flex;
1523+
flex-direction: column;
1524+
}
1525+
1526+
.cdk-virtual-scroll-viewport {
1527+
width: 200px;
1528+
height: 200px;
1529+
background-color: #f5f5f5;
1530+
}
1531+
1532+
.item {
1533+
width: 100%;
1534+
height: 50px;
1535+
box-sizing: border-box;
1536+
border: 1px dashed #ccc;
1537+
}
1538+
1539+
.before-virtual-viewport {
1540+
height: 50px;
1541+
}
1542+
`,
1543+
],
1544+
encapsulation: ViewEncapsulation.None,
1545+
})
1546+
class VirtualScrollWithScrollableWindow {
1547+
@ViewChild(CdkVirtualScrollViewport, {static: true}) viewport: CdkVirtualScrollViewport;
1548+
itemSize = 50;
1549+
items = Array(20000)
1550+
.fill(0)
1551+
.map((_, i) => i);
1552+
}

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ export class CdkVirtualScrollViewport extends CdkVirtualScrollable implements On
294294
return this._renderedRange;
295295
}
296296

297-
getBoundingClientRectWithScrollOffset(from: 'left' | 'top' | 'right' | 'bottom'): number {
297+
measureBoundingClientRectWithScrollOffset(from: 'left' | 'top' | 'right' | 'bottom'): number {
298298
return this.getElementRef().nativeElement.getBoundingClientRect()[from];
299299
}
300300

@@ -416,6 +416,10 @@ export class CdkVirtualScrollViewport extends CdkVirtualScrollable implements On
416416
);
417417
}
418418

419+
/**
420+
* Measures the offset of the viewport from the scrolling container
421+
* @param from The edge to measure from.
422+
*/
419423
measureViewportOffset(from?: 'top' | 'left' | 'right' | 'bottom' | 'start' | 'end') {
420424
let fromRect: 'left' | 'top' | 'right' | 'bottom';
421425
const LEFT = 'left';
@@ -431,7 +435,7 @@ export class CdkVirtualScrollViewport extends CdkVirtualScrollable implements On
431435
fromRect = this.orientation === 'horizontal' ? 'left' : 'top';
432436
}
433437

434-
const scrollerClientRect = this.scrollable.getBoundingClientRectWithScrollOffset(fromRect);
438+
const scrollerClientRect = this.scrollable.measureBoundingClientRectWithScrollOffset(fromRect);
435439
const viewportClientRect = this.elementRef.nativeElement.getBoundingClientRect()[fromRect];
436440

437441
return viewportClientRect - scrollerClientRect;

src/cdk/scrolling/virtual-scrollable-element.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import {Directive, ElementRef, NgZone, Optional} from '@angular/core';
1111
import {ScrollDispatcher} from './scroll-dispatcher';
1212
import {CdkVirtualScrollable, VIRTUAL_SCROLLABLE} from './virtual-scrollable';
1313

14+
/**
15+
* Provides a virtual scrollable for the element it is attached to.
16+
*/
1417
@Directive({
1518
selector: '[cdk-virtual-scrollable-element], [cdkVirtualScrollableElement]',
1619
providers: [{provide: VIRTUAL_SCROLLABLE, useExisting: CdkVirtualScrollableElement}],
@@ -28,7 +31,9 @@ export class CdkVirtualScrollableElement extends CdkVirtualScrollable {
2831
super(elementRef, scrollDispatcher, ngZone, dir);
2932
}
3033

31-
getBoundingClientRectWithScrollOffset(from: 'left' | 'top' | 'right' | 'bottom'): number {
34+
override measureBoundingClientRectWithScrollOffset(
35+
from: 'left' | 'top' | 'right' | 'bottom',
36+
): number {
3237
return (
3338
this.getElementRef().nativeElement.getBoundingClientRect()[from] -
3439
this.measureScrollOffset(from)

src/cdk/scrolling/virtual-scrollable-window.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import {takeUntil} from 'rxjs/operators';
1313
import {ScrollDispatcher} from './scroll-dispatcher';
1414
import {CdkVirtualScrollable, VIRTUAL_SCROLLABLE} from './virtual-scrollable';
1515

16+
/**
17+
* Provides as virtual scrollable for the global / window scrollbar.
18+
*/
1619
@Directive({
1720
selector: 'cdk-virtual-scroll-viewport[scrollable-window]',
1821
providers: [{provide: VIRTUAL_SCROLLABLE, useExisting: CdkVirtualScrollableWindow}],
@@ -29,7 +32,9 @@ export class CdkVirtualScrollableWindow extends CdkVirtualScrollable {
2932
super(new ElementRef(document.documentElement), scrollDispatcher, ngZone, dir);
3033
}
3134

32-
getBoundingClientRectWithScrollOffset(from: 'left' | 'top' | 'right' | 'bottom'): number {
35+
override measureBoundingClientRectWithScrollOffset(
36+
from: 'left' | 'top' | 'right' | 'bottom',
37+
): number {
3338
return this.getElementRef().nativeElement.getBoundingClientRect()[from];
3439
}
3540
}

src/cdk/scrolling/virtual-scrollable.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import {CdkScrollable} from './scrollable';
1313

1414
export const VIRTUAL_SCROLLABLE = new InjectionToken<CdkVirtualScrollable>('VIRTUAL_SCROLLABLE');
1515

16+
/**
17+
* Extending the {@link CdkScrollable} to be used as scrolling container for virtual scrolling.
18+
*/
1619
@Directive()
1720
export abstract class CdkVirtualScrollable extends CdkScrollable {
1821
constructor(
@@ -24,10 +27,22 @@ export abstract class CdkVirtualScrollable extends CdkScrollable {
2427
super(elementRef, scrollDispatcher, ngZone, dir);
2528
}
2629

30+
/**
31+
* Measure the viewport size for the provided orientation.
32+
*
33+
* @param orientation The orientation to measure the size from.
34+
*/
2735
measureViewportSize(orientation: 'horizontal' | 'vertical') {
2836
const viewportEl = this.elementRef.nativeElement;
2937
return orientation === 'horizontal' ? viewportEl.clientWidth : viewportEl.clientHeight;
3038
}
3139

32-
abstract getBoundingClientRectWithScrollOffset(from: 'left' | 'top' | 'right' | 'bottom'): number;
40+
/**
41+
* Measure the bounding ClientRect size including the scroll offset.
42+
*
43+
* @param from The edge to measure from.
44+
*/
45+
abstract measureBoundingClientRectWithScrollOffset(
46+
from: 'left' | 'top' | 'right' | 'bottom',
47+
): number;
3348
}

0 commit comments

Comments
 (0)