diff --git a/src/cdk-experimental/scrolling/BUILD.bazel b/src/cdk-experimental/scrolling/BUILD.bazel index 9fdbbb7b5752..054f9f5aae51 100644 --- a/src/cdk-experimental/scrolling/BUILD.bazel +++ b/src/cdk-experimental/scrolling/BUILD.bazel @@ -12,6 +12,7 @@ ng_module( deps = [ "//src/cdk/coercion", "//src/cdk/collections", + "//src/cdk/platform", "@rxjs", ], tsconfig = "//src/cdk-experimental:tsconfig-build.json", diff --git a/src/cdk-experimental/scrolling/auto-size-virtual-scroll.ts b/src/cdk-experimental/scrolling/auto-size-virtual-scroll.ts index f93d79427750..92cc65497b15 100644 --- a/src/cdk-experimental/scrolling/auto-size-virtual-scroll.ts +++ b/src/cdk-experimental/scrolling/auto-size-virtual-scroll.ts @@ -151,6 +151,13 @@ export class AutoSizeVirtualScrollStrategy implements VirtualScrollStrategy { } } + /** Scroll to the offset for the given index. */ + scrollToIndex(): void { + // TODO(mmalerba): Implement. + throw new Error('cdk-virtual-scroll: scrollToIndex is currently not supported for the autosize' + + ' scroll strategy'); + } + /** * Update the buffer parameters. * @param minBufferPx The minimum amount of buffer rendered beyond the viewport (in pixels). diff --git a/src/cdk-experimental/scrolling/fixed-size-virtual-scroll.ts b/src/cdk-experimental/scrolling/fixed-size-virtual-scroll.ts index 06e5d2ea376f..324bc18f030d 100644 --- a/src/cdk-experimental/scrolling/fixed-size-virtual-scroll.ts +++ b/src/cdk-experimental/scrolling/fixed-size-virtual-scroll.ts @@ -77,6 +77,17 @@ export class FixedSizeVirtualScrollStrategy implements VirtualScrollStrategy { /** @docs-private Implemented as part of VirtualScrollStrategy. */ onRenderedOffsetChanged() { /* no-op */ } + /** + * Scroll to the offset for the given index. + * @param index The index of the element to scroll to. + * @param behavior The ScrollBehavior to use when scrolling. + */ + scrollToIndex(index: number, behavior: ScrollBehavior): void { + if (this._viewport) { + this._viewport.scrollToOffset(index * this._itemSize, behavior); + } + } + /** Update the viewport's total content size. */ private _updateTotalContentSize() { if (!this._viewport) { diff --git a/src/cdk-experimental/scrolling/virtual-scroll-strategy.ts b/src/cdk-experimental/scrolling/virtual-scroll-strategy.ts index cbb667201709..033f54e1c355 100644 --- a/src/cdk-experimental/scrolling/virtual-scroll-strategy.ts +++ b/src/cdk-experimental/scrolling/virtual-scroll-strategy.ts @@ -37,4 +37,11 @@ export interface VirtualScrollStrategy { /** Called when the offset of the rendered items changed. */ onRenderedOffsetChanged(); + + /** + * Scroll to the offset for the given index. + * @param index The index of the element to scroll to. + * @param behavior The ScrollBehavior to use when scrolling. + */ + scrollToIndex(index: number, behavior: ScrollBehavior): void; } diff --git a/src/cdk-experimental/scrolling/virtual-scroll-viewport.spec.ts b/src/cdk-experimental/scrolling/virtual-scroll-viewport.spec.ts index 78e1a88c9c07..5614b8a30a97 100644 --- a/src/cdk-experimental/scrolling/virtual-scroll-viewport.spec.ts +++ b/src/cdk-experimental/scrolling/virtual-scroll-viewport.spec.ts @@ -156,6 +156,56 @@ describe('CdkVirtualScrollViewport', () => { expect(viewport.getRenderedRange()).toEqual({start: 2, end: 6}); })); + it('should scroll to offset', fakeAsync(() => { + finishInit(fixture); + viewport.scrollToOffset(testComponent.itemSize * 2); + + triggerScroll(viewport); + fixture.detectChanges(); + flush(); + + expect(viewport.elementRef.nativeElement.scrollTop).toBe(testComponent.itemSize * 2); + expect(viewport.getRenderedRange()).toEqual({start: 2, end: 6}); + })); + + it('should scroll to index', fakeAsync(() => { + finishInit(fixture); + viewport.scrollToIndex(2); + + triggerScroll(viewport); + fixture.detectChanges(); + flush(); + + expect(viewport.elementRef.nativeElement.scrollTop).toBe(testComponent.itemSize * 2); + expect(viewport.getRenderedRange()).toEqual({start: 2, end: 6}); + })); + + it('should scroll to offset in horizontal mode', fakeAsync(() => { + testComponent.orientation = 'horizontal'; + finishInit(fixture); + viewport.scrollToOffset(testComponent.itemSize * 2); + + triggerScroll(viewport); + fixture.detectChanges(); + flush(); + + expect(viewport.elementRef.nativeElement.scrollLeft).toBe(testComponent.itemSize * 2); + expect(viewport.getRenderedRange()).toEqual({start: 2, end: 6}); + })); + + it('should scroll to index in horizontal mode', fakeAsync(() => { + testComponent.orientation = 'horizontal'; + finishInit(fixture); + viewport.scrollToIndex(2); + + triggerScroll(viewport); + fixture.detectChanges(); + flush(); + + expect(viewport.elementRef.nativeElement.scrollLeft).toBe(testComponent.itemSize * 2); + expect(viewport.getRenderedRange()).toEqual({start: 2, end: 6}); + })); + it('should update viewport as user scrolls down', fakeAsync(() => { finishInit(fixture); diff --git a/src/cdk-experimental/scrolling/virtual-scroll-viewport.ts b/src/cdk-experimental/scrolling/virtual-scroll-viewport.ts index ed0d6669d3b7..3f6e7bb3aaa8 100644 --- a/src/cdk-experimental/scrolling/virtual-scroll-viewport.ts +++ b/src/cdk-experimental/scrolling/virtual-scroll-viewport.ts @@ -7,6 +7,7 @@ */ import {ListRange} from '@angular/cdk/collections'; +import {supportsScrollBehavior} from '@angular/cdk/platform'; import { ChangeDetectionStrategy, ChangeDetectorRef, @@ -245,7 +246,36 @@ export class CdkVirtualScrollViewport implements OnInit, OnDestroy { } } - /** Sets the scroll offset on the viewport. */ + /** + * Scrolls to the offset on the viewport. + * @param offset The offset to scroll to. + * @param behavior The ScrollBehavior to use when scrolling. Default is behavior is `auto`. + */ + scrollToOffset(offset: number, behavior: ScrollBehavior = 'auto') { + const viewportElement = this.elementRef.nativeElement; + + if (supportsScrollBehavior()) { + const offsetDirection = this.orientation === 'horizontal' ? 'left' : 'top'; + viewportElement.scrollTo({[offsetDirection]: offset, behavior}); + } else { + if (this.orientation === 'horizontal') { + viewportElement.scrollLeft = offset; + } else { + viewportElement.scrollTop = offset; + } + } + } + + /** + * Scrolls to the offset for the given index. + * @param index The index of the element to scroll to. + * @param behavior The ScrollBehavior to use when scrolling. Default is behavior is `auto`. + */ + scrollToIndex(index: number, behavior: ScrollBehavior = 'auto') { + this._scrollStrategy.scrollToIndex(index, behavior); + } + + /** @docs-private Internal method to set the scroll offset on the viewport. */ setScrollOffset(offset: number) { // Rather than setting the offset immediately, we batch it up to be applied along with other DOM // writes during the next change detection cycle. diff --git a/src/cdk/platform/features.ts b/src/cdk/platform/features.ts index 10cd148f40f6..6b31384fc376 100644 --- a/src/cdk/platform/features.ts +++ b/src/cdk/platform/features.ts @@ -27,6 +27,12 @@ export function supportsPassiveEventListeners(): boolean { return supportsPassiveEvents; } +/** Check whether the browser supports scroll behaviors. */ +export function supportsScrollBehavior(): boolean { + return !!(document && document.documentElement && document.documentElement.style && + 'scrollBehavior' in document.documentElement.style); +} + /** Cached result Set of input types support by the current browser. */ let supportedInputTypes: Set; diff --git a/src/demo-app/virtual-scroll/virtual-scroll-demo.html b/src/demo-app/virtual-scroll/virtual-scroll-demo.html index f479593e64bb..30b12deb7f8a 100644 --- a/src/demo-app/virtual-scroll/virtual-scroll-demo.html +++ b/src/demo-app/virtual-scroll/virtual-scroll-demo.html @@ -34,7 +34,30 @@

Random size

Fixed size

- + + Behavior + + Auto + Instant + Smooth + + + + Offset + + + + + Index + + + + +
Item #{{i}} - ({{size}}px) @@ -97,8 +120,8 @@

trackBy state name

Use with <ol>

- -
    + +
    1. {{state.name}} - {{state.capital}}
    2. diff --git a/src/demo-app/virtual-scroll/virtual-scroll-demo.ts b/src/demo-app/virtual-scroll/virtual-scroll-demo.ts index c5570f197641..0f35340ff1c4 100644 --- a/src/demo-app/virtual-scroll/virtual-scroll-demo.ts +++ b/src/demo-app/virtual-scroll/virtual-scroll-demo.ts @@ -25,6 +25,9 @@ type State = { changeDetection: ChangeDetectionStrategy.OnPush, }) export class VirtualScrollDemo { + scrollToOffset = 0; + scrollToIndex = 0; + scrollToBehavior: ScrollBehavior = 'auto'; fixedSizeData = Array(10000).fill(50); increasingSizeData = Array(10000).fill(0).map((_, i) => (1 + Math.floor(i / 1000)) * 20); decreasingSizeData = Array(10000).fill(0)