Skip to content

Commit 98a673a

Browse files
committed
virtual-scroll: add support for scrollToOffset and scrollToIndex (#12272)
Note: `scrollToIndex` currently does not work with the `AutoSizeVirtualScrollStrategy`. Support for this will be added in a future PR. This PR is based off #11498 by @amcdnl. I removed the `lazy` option from #11498 to avoid bloating the API. I'm not sure how common the use case is, and its relatively simple to implement on top using the current APIs.
1 parent b6f7205 commit 98a673a

9 files changed

+142
-4
lines changed

src/cdk-experimental/scrolling/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ ng_module(
1212
deps = [
1313
"//src/cdk/coercion",
1414
"//src/cdk/collections",
15+
"//src/cdk/platform",
1516
"@rxjs",
1617
],
1718
tsconfig = "//src/cdk-experimental:tsconfig-build.json",

src/cdk-experimental/scrolling/auto-size-virtual-scroll.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,13 @@ export class AutoSizeVirtualScrollStrategy implements VirtualScrollStrategy {
151151
}
152152
}
153153

154+
/** Scroll to the offset for the given index. */
155+
scrollToIndex(): void {
156+
// TODO(mmalerba): Implement.
157+
throw new Error('cdk-virtual-scroll: scrollToIndex is currently not supported for the autosize'
158+
+ ' scroll strategy');
159+
}
160+
154161
/**
155162
* Update the buffer parameters.
156163
* @param minBufferPx The minimum amount of buffer rendered beyond the viewport (in pixels).

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,17 @@ export class FixedSizeVirtualScrollStrategy implements VirtualScrollStrategy {
7777
/** @docs-private Implemented as part of VirtualScrollStrategy. */
7878
onRenderedOffsetChanged() { /* no-op */ }
7979

80+
/**
81+
* Scroll to the offset for the given index.
82+
* @param index The index of the element to scroll to.
83+
* @param behavior The ScrollBehavior to use when scrolling.
84+
*/
85+
scrollToIndex(index: number, behavior: ScrollBehavior): void {
86+
if (this._viewport) {
87+
this._viewport.scrollToOffset(index * this._itemSize, behavior);
88+
}
89+
}
90+
8091
/** Update the viewport's total content size. */
8192
private _updateTotalContentSize() {
8293
if (!this._viewport) {

src/cdk-experimental/scrolling/virtual-scroll-strategy.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,11 @@ export interface VirtualScrollStrategy {
3737

3838
/** Called when the offset of the rendered items changed. */
3939
onRenderedOffsetChanged();
40+
41+
/**
42+
* Scroll to the offset for the given index.
43+
* @param index The index of the element to scroll to.
44+
* @param behavior The ScrollBehavior to use when scrolling.
45+
*/
46+
scrollToIndex(index: number, behavior: ScrollBehavior): void;
4047
}

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,56 @@ describe('CdkVirtualScrollViewport', () => {
156156
expect(viewport.getRenderedRange()).toEqual({start: 2, end: 6});
157157
}));
158158

159+
it('should scroll to offset', fakeAsync(() => {
160+
finishInit(fixture);
161+
viewport.scrollToOffset(testComponent.itemSize * 2);
162+
163+
triggerScroll(viewport);
164+
fixture.detectChanges();
165+
flush();
166+
167+
expect(viewport.elementRef.nativeElement.scrollTop).toBe(testComponent.itemSize * 2);
168+
expect(viewport.getRenderedRange()).toEqual({start: 2, end: 6});
169+
}));
170+
171+
it('should scroll to index', fakeAsync(() => {
172+
finishInit(fixture);
173+
viewport.scrollToIndex(2);
174+
175+
triggerScroll(viewport);
176+
fixture.detectChanges();
177+
flush();
178+
179+
expect(viewport.elementRef.nativeElement.scrollTop).toBe(testComponent.itemSize * 2);
180+
expect(viewport.getRenderedRange()).toEqual({start: 2, end: 6});
181+
}));
182+
183+
it('should scroll to offset in horizontal mode', fakeAsync(() => {
184+
testComponent.orientation = 'horizontal';
185+
finishInit(fixture);
186+
viewport.scrollToOffset(testComponent.itemSize * 2);
187+
188+
triggerScroll(viewport);
189+
fixture.detectChanges();
190+
flush();
191+
192+
expect(viewport.elementRef.nativeElement.scrollLeft).toBe(testComponent.itemSize * 2);
193+
expect(viewport.getRenderedRange()).toEqual({start: 2, end: 6});
194+
}));
195+
196+
it('should scroll to index in horizontal mode', fakeAsync(() => {
197+
testComponent.orientation = 'horizontal';
198+
finishInit(fixture);
199+
viewport.scrollToIndex(2);
200+
201+
triggerScroll(viewport);
202+
fixture.detectChanges();
203+
flush();
204+
205+
expect(viewport.elementRef.nativeElement.scrollLeft).toBe(testComponent.itemSize * 2);
206+
expect(viewport.getRenderedRange()).toEqual({start: 2, end: 6});
207+
}));
208+
159209
it('should update viewport as user scrolls down', fakeAsync(() => {
160210
finishInit(fixture);
161211

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import {ListRange} from '@angular/cdk/collections';
10+
import {supportsScrollBehavior} from '@angular/cdk/platform';
1011
import {
1112
ChangeDetectionStrategy,
1213
ChangeDetectorRef,
@@ -245,7 +246,36 @@ export class CdkVirtualScrollViewport implements OnInit, OnDestroy {
245246
}
246247
}
247248

248-
/** Sets the scroll offset on the viewport. */
249+
/**
250+
* Scrolls to the offset on the viewport.
251+
* @param offset The offset to scroll to.
252+
* @param behavior The ScrollBehavior to use when scrolling. Default is behavior is `auto`.
253+
*/
254+
scrollToOffset(offset: number, behavior: ScrollBehavior = 'auto') {
255+
const viewportElement = this.elementRef.nativeElement;
256+
257+
if (supportsScrollBehavior()) {
258+
const offsetDirection = this.orientation === 'horizontal' ? 'left' : 'top';
259+
viewportElement.scrollTo({[offsetDirection]: offset, behavior});
260+
} else {
261+
if (this.orientation === 'horizontal') {
262+
viewportElement.scrollLeft = offset;
263+
} else {
264+
viewportElement.scrollTop = offset;
265+
}
266+
}
267+
}
268+
269+
/**
270+
* Scrolls to the offset for the given index.
271+
* @param index The index of the element to scroll to.
272+
* @param behavior The ScrollBehavior to use when scrolling. Default is behavior is `auto`.
273+
*/
274+
scrollToIndex(index: number, behavior: ScrollBehavior = 'auto') {
275+
this._scrollStrategy.scrollToIndex(index, behavior);
276+
}
277+
278+
/** @docs-private Internal method to set the scroll offset on the viewport. */
249279
setScrollOffset(offset: number) {
250280
// Rather than setting the offset immediately, we batch it up to be applied along with other DOM
251281
// writes during the next change detection cycle.

src/cdk/platform/features.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ export function supportsPassiveEventListeners(): boolean {
2727
return supportsPassiveEvents;
2828
}
2929

30+
/** Check whether the browser supports scroll behaviors. */
31+
export function supportsScrollBehavior(): boolean {
32+
return !!(document && document.documentElement && document.documentElement.style &&
33+
'scrollBehavior' in document.documentElement.style);
34+
}
35+
3036
/** Cached result Set of input types support by the current browser. */
3137
let supportedInputTypes: Set<string>;
3238

src/demo-app/virtual-scroll/virtual-scroll-demo.html

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,30 @@ <h3>Random size</h3>
3434

3535
<h2>Fixed size</h2>
3636

37-
<cdk-virtual-scroll-viewport class="demo-viewport" [itemSize]="50">
37+
<mat-form-field>
38+
<mat-label>Behavior</mat-label>
39+
<mat-select [(ngModel)]="scrollToBehavior">
40+
<mat-option value="auto">Auto</mat-option>
41+
<mat-option value="instant">Instant</mat-option>
42+
<mat-option value="smooth">Smooth</mat-option>
43+
</mat-select>
44+
</mat-form-field>
45+
<mat-form-field>
46+
<mat-label>Offset</mat-label>
47+
<input matInput type="number" [(ngModel)]="scrollToOffset">
48+
</mat-form-field>
49+
<button mat-button (click)="viewport1.scrollToOffset(scrollToOffset, scrollToBehavior)">
50+
Go to offset
51+
</button>
52+
<mat-form-field>
53+
<mat-label>Index</mat-label>
54+
<input matInput type="number" [(ngModel)]="scrollToIndex">
55+
</mat-form-field>
56+
<button mat-button (click)="viewport1.scrollToIndex(scrollToIndex, scrollToBehavior)">
57+
Go to index
58+
</button>
59+
60+
<cdk-virtual-scroll-viewport class="demo-viewport" [itemSize]="50" #viewport1>
3861
<div *cdkVirtualFor="let size of fixedSizeData; let i = index" class="demo-item"
3962
[style.height.px]="size">
4063
Item #{{i}} - ({{size}}px)
@@ -97,8 +120,8 @@ <h2>trackBy state name</h2>
97120

98121
<h2>Use with <code>&lt;ol&gt;</code></h2>
99122

100-
<cdk-virtual-scroll-viewport class="demo-viewport" autosize #viewport>
101-
<ol class="demo-ol" [start]="viewport.getRenderedRange().start + 1">
123+
<cdk-virtual-scroll-viewport class="demo-viewport" autosize #viewport2>
124+
<ol class="demo-ol" [start]="viewport2.getRenderedRange().start + 1">
102125
<li *cdkVirtualFor="let state of statesObservable | async" class="demo-li">
103126
{{state.name}} - {{state.capital}}
104127
</li>

src/demo-app/virtual-scroll/virtual-scroll-demo.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ type State = {
2525
changeDetection: ChangeDetectionStrategy.OnPush,
2626
})
2727
export class VirtualScrollDemo {
28+
scrollToOffset = 0;
29+
scrollToIndex = 0;
30+
scrollToBehavior: ScrollBehavior = 'auto';
2831
fixedSizeData = Array(10000).fill(50);
2932
increasingSizeData = Array(10000).fill(0).map((_, i) => (1 + Math.floor(i / 1000)) * 20);
3033
decreasingSizeData = Array(10000).fill(0)

0 commit comments

Comments
 (0)