Skip to content

Commit 176213d

Browse files
authored
feat(cdk/scrolling): make scroller element configurable for virtual scrolling (#24394)
1 parent 799cf7c commit 176213d

11 files changed

+452
-33
lines changed

src/cdk/scrolling/public-api.ts

+3
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@ export * from './virtual-for-of';
1515
export * from './virtual-scroll-strategy';
1616
export * from './virtual-scroll-viewport';
1717
export * from './virtual-scroll-repeater';
18+
export * from './virtual-scrollable';
19+
export * from './virtual-scrollable-element';
20+
export * from './virtual-scrollable-window';

src/cdk/scrolling/scrollable.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ export type ExtendedScrollToOptions = _XAxis & _YAxis & ScrollOptions;
4545
selector: '[cdk-scrollable], [cdkScrollable]',
4646
})
4747
export class CdkScrollable implements OnInit, OnDestroy {
48-
private readonly _destroyed = new Subject<void>();
48+
protected readonly _destroyed = new Subject<void>();
4949

50-
private _elementScrolled: Observable<Event> = new Observable((observer: Observer<Event>) =>
50+
protected _elementScrolled: Observable<Event> = new Observable((observer: Observer<Event>) =>
5151
this.ngZone.runOutsideAngular(() =>
5252
fromEvent(this.elementRef.nativeElement, 'scroll')
5353
.pipe(takeUntil(this._destroyed))

src/cdk/scrolling/scrolling-module.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {CdkFixedSizeVirtualScroll} from './fixed-size-virtual-scroll';
1212
import {CdkScrollable} from './scrollable';
1313
import {CdkVirtualForOf} from './virtual-for-of';
1414
import {CdkVirtualScrollViewport} from './virtual-scroll-viewport';
15+
import {CdkVirtualScrollableElement} from './virtual-scrollable-element';
16+
import {CdkVirtualScrollableWindow} from './virtual-scrollable-window';
1517

1618
@NgModule({
1719
exports: [CdkScrollable],
@@ -30,7 +32,15 @@ export class CdkScrollableModule {}
3032
CdkFixedSizeVirtualScroll,
3133
CdkVirtualForOf,
3234
CdkVirtualScrollViewport,
35+
CdkVirtualScrollableWindow,
36+
CdkVirtualScrollableElement,
37+
],
38+
declarations: [
39+
CdkFixedSizeVirtualScroll,
40+
CdkVirtualForOf,
41+
CdkVirtualScrollViewport,
42+
CdkVirtualScrollableWindow,
43+
CdkVirtualScrollableElement,
3344
],
34-
declarations: [CdkFixedSizeVirtualScroll, CdkVirtualForOf, CdkVirtualScrollViewport],
3545
})
3646
export class ScrollingModule {}

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

+8-9
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,18 @@
2727
}
2828

2929

30-
// Scrolling container.
30+
// viewport
3131
cdk-virtual-scroll-viewport {
3232
display: block;
3333
position: relative;
34-
overflow: auto;
35-
contain: strict;
3634
transform: translateZ(0);
35+
}
36+
37+
// Scrolling container.
38+
.cdk-virtual-scrollable {
39+
overflow: auto;
3740
will-change: scroll-position;
41+
contain: strict;
3842
-webkit-overflow-scrolling: touch;
3943
}
4044

@@ -69,19 +73,14 @@ cdk-virtual-scroll-viewport {
6973
// set if it were rendered all at once. This ensures that the scrollable content region is the
7074
// correct size.
7175
.cdk-virtual-scroll-spacer {
72-
position: absolute;
73-
top: 0;
74-
left: 0;
7576
height: 1px;
76-
width: 1px;
7777
transform-origin: 0 0;
78+
flex: 0 0 auto; // prevents spacer from collapsing if display: flex is applied
7879

7980
// Note: We can't put `will-change: transform;` here because it causes Safari to not update the
8081
// viewport's `scrollHeight` when the spacer's transform changes.
8182

8283
[dir='rtl'] & {
83-
right: 0;
84-
left: auto;
8584
transform-origin: 100% 0;
8685
}
8786
}

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

+158-3
Original file line numberDiff line numberDiff line change
@@ -772,9 +772,9 @@ describe('CdkVirtualScrollViewport', () => {
772772
spyOn(dispatcher, 'register').and.callThrough();
773773
spyOn(dispatcher, 'deregister').and.callThrough();
774774
finishInit(fixture);
775-
expect(dispatcher.register).toHaveBeenCalledWith(testComponent.viewport);
775+
expect(dispatcher.register).toHaveBeenCalledWith(testComponent.viewport.scrollable);
776776
fixture.destroy();
777-
expect(dispatcher.deregister).toHaveBeenCalledWith(testComponent.viewport);
777+
expect(dispatcher.deregister).toHaveBeenCalledWith(testComponent.viewport.scrollable);
778778
}),
779779
));
780780

@@ -1086,6 +1086,76 @@ 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(waitForAsync(() => {
1096+
TestBed.configureTestingModule({
1097+
imports: [ScrollingModule],
1098+
declarations: [VirtualScrollWithCustomScrollingElement],
1099+
}).compileComponents();
1100+
}));
1101+
1102+
beforeEach(() => {
1103+
fixture = TestBed.createComponent(VirtualScrollWithCustomScrollingElement);
1104+
testComponent = fixture.componentInstance;
1105+
viewport = testComponent.viewport;
1106+
});
1107+
1108+
it('should measure viewport offset', fakeAsync(() => {
1109+
finishInit(fixture);
1110+
1111+
expect(viewport.measureViewportOffset('top'))
1112+
.withContext('with scrolling-element padding-top: 50 offset should be 50')
1113+
.toBe(50);
1114+
}));
1115+
1116+
it('should measure scroll offset', fakeAsync(() => {
1117+
finishInit(fixture);
1118+
triggerScroll(viewport, 100);
1119+
fixture.detectChanges();
1120+
flush();
1121+
1122+
expect(viewport.measureScrollOffset('top'))
1123+
.withContext('should be 50 (actual scroll offset - viewport offset)')
1124+
.toBe(50);
1125+
}));
1126+
});
1127+
1128+
describe('with scrollable window', () => {
1129+
let fixture: ComponentFixture<VirtualScrollWithScrollableWindow>;
1130+
let testComponent: VirtualScrollWithScrollableWindow;
1131+
let viewport: CdkVirtualScrollViewport;
1132+
1133+
beforeEach(waitForAsync(() => {
1134+
TestBed.configureTestingModule({
1135+
imports: [ScrollingModule],
1136+
declarations: [VirtualScrollWithScrollableWindow],
1137+
}).compileComponents();
1138+
}));
1139+
1140+
beforeEach(() => {
1141+
fixture = TestBed.createComponent(VirtualScrollWithScrollableWindow);
1142+
testComponent = fixture.componentInstance;
1143+
viewport = testComponent.viewport;
1144+
});
1145+
1146+
it('should measure scroll offset', fakeAsync(() => {
1147+
finishInit(fixture);
1148+
viewport.scrollToOffset(100 + 8); // the +8 is due to a horizontal scrollbar
1149+
dispatchFakeEvent(window, 'scroll', true);
1150+
tick();
1151+
fixture.detectChanges();
1152+
flush();
1153+
1154+
expect(viewport.measureScrollOffset('top'))
1155+
.withContext('should be 50 (actual scroll offset - viewport offset)')
1156+
.toBe(50);
1157+
}));
1158+
});
10891159
});
10901160

10911161
/** Finish initializing the virtual scroll component at the beginning of a test. */
@@ -1109,7 +1179,7 @@ function triggerScroll(viewport: CdkVirtualScrollViewport, offset?: number) {
11091179
if (offset !== undefined) {
11101180
viewport.scrollToOffset(offset);
11111181
}
1112-
dispatchFakeEvent(viewport.elementRef.nativeElement, 'scroll');
1182+
dispatchFakeEvent(viewport.scrollable.getElementRef().nativeElement, 'scroll');
11131183
animationFrameScheduler.flush();
11141184
}
11151185

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

0 commit comments

Comments
 (0)