diff --git a/src/cdk/overlay/scroll/close-scroll-strategy.spec.ts b/src/cdk/overlay/scroll/close-scroll-strategy.spec.ts index 503c795e4326..f98d008cbb43 100644 --- a/src/cdk/overlay/scroll/close-scroll-strategy.spec.ts +++ b/src/cdk/overlay/scroll/close-scroll-strategy.spec.ts @@ -1,8 +1,8 @@ -import {inject, TestBed, async} from '@angular/core/testing'; +import {inject, TestBed, fakeAsync} from '@angular/core/testing'; import {NgModule, Component, NgZone} from '@angular/core'; import {Subject} from 'rxjs/Subject'; import {ComponentPortal, PortalModule} from '@angular/cdk/portal'; -import {ScrollDispatcher} from '@angular/cdk/scrolling'; +import {ScrollDispatcher, ViewportRuler} from '@angular/cdk/scrolling'; import { Overlay, OverlayConfig, @@ -16,13 +16,19 @@ describe('CloseScrollStrategy', () => { let overlayRef: OverlayRef; let componentPortal: ComponentPortal; let scrolledSubject = new Subject(); + let scrollPosition: number; + + beforeEach(fakeAsync(() => { + scrollPosition = 0; - beforeEach(async(() => { TestBed.configureTestingModule({ imports: [OverlayModule, PortalModule, OverlayTestModule], providers: [ {provide: ScrollDispatcher, useFactory: () => ({ scrolled: () => scrolledSubject.asObservable() + })}, + {provide: ViewportRuler, useFactory: () => ({ + getViewportScrollPosition: () => ({top: scrollPosition}) })} ] }); @@ -70,6 +76,60 @@ describe('CloseScrollStrategy', () => { subscription.unsubscribe(); }); + it('should be able to reposition the overlay up to a certain threshold before closing', + inject([Overlay], (overlay: Overlay) => { + overlayRef.dispose(); + + overlayRef = overlay.create({ + scrollStrategy: overlay.scrollStrategies.close({threshold: 50}) + }); + + overlayRef.attach(componentPortal); + spyOn(overlayRef, 'updatePosition'); + spyOn(overlayRef, 'detach'); + + for (let i = 0; i < 50; i++) { + scrollPosition++; + scrolledSubject.next(); + } + + expect(overlayRef.updatePosition).toHaveBeenCalledTimes(50); + expect(overlayRef.detach).not.toHaveBeenCalled(); + + scrollPosition++; + scrolledSubject.next(); + + expect(overlayRef.detach).toHaveBeenCalledTimes(1); + })); + + it('should not close if the user starts scrolling away and comes back', + inject([Overlay], (overlay: Overlay) => { + overlayRef.dispose(); + scrollPosition = 100; + + overlayRef = overlay.create({ + scrollStrategy: overlay.scrollStrategies.close({threshold: 50}) + }); + + overlayRef.attach(componentPortal); + spyOn(overlayRef, 'updatePosition'); + spyOn(overlayRef, 'detach'); + + // Scroll down 30px. + for (let i = 0; i < 30; i++) { + scrollPosition++; + scrolledSubject.next(); + } + + // Scroll back up 30px. + for (let i = 0; i < 30; i++) { + scrollPosition--; + scrolledSubject.next(); + } + + expect(overlayRef.updatePosition).toHaveBeenCalledTimes(60); + expect(overlayRef.detach).not.toHaveBeenCalled(); + })); }); diff --git a/src/cdk/overlay/scroll/close-scroll-strategy.ts b/src/cdk/overlay/scroll/close-scroll-strategy.ts index 970e3afcebe5..df6c5cbc3ac8 100644 --- a/src/cdk/overlay/scroll/close-scroll-strategy.ts +++ b/src/cdk/overlay/scroll/close-scroll-strategy.ts @@ -5,13 +5,19 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - import {NgZone} from '@angular/core'; import {ScrollStrategy, getMatScrollStrategyAlreadyAttachedError} from './scroll-strategy'; import {OverlayRef} from '../overlay-ref'; import {Subscription} from 'rxjs/Subscription'; -import {ScrollDispatcher} from '@angular/cdk/scrolling'; +import {ScrollDispatcher, ViewportRuler} from '@angular/cdk/scrolling'; +/** + * Config options for the CloseScrollStrategy. + */ +export interface CloseScrollStrategyConfig { + /** Amount of pixels the user has to scroll before the overlay is closed. */ + threshold?: number; +} /** * Strategy that will close the overlay as soon as the user starts scrolling. @@ -19,8 +25,13 @@ import {ScrollDispatcher} from '@angular/cdk/scrolling'; export class CloseScrollStrategy implements ScrollStrategy { private _scrollSubscription: Subscription|null = null; private _overlayRef: OverlayRef; + private _initialScrollPosition: number; - constructor(private _scrollDispatcher: ScrollDispatcher, private _ngZone: NgZone) { } + constructor( + private _scrollDispatcher: ScrollDispatcher, + private _ngZone: NgZone, + private _viewportRuler: ViewportRuler, + private _config?: CloseScrollStrategyConfig) {} /** Attaches this scroll strategy to an overlay. */ attach(overlayRef: OverlayRef) { @@ -31,18 +42,28 @@ export class CloseScrollStrategy implements ScrollStrategy { this._overlayRef = overlayRef; } - /** Enables the closing of the attached on scroll. */ + /** Enables the closing of the attached overlay on scroll. */ enable() { - if (!this._scrollSubscription) { - this._scrollSubscription = this._scrollDispatcher.scrolled(0).subscribe(() => { - this._ngZone.run(() => { - this.disable(); - - if (this._overlayRef.hasAttached()) { - this._overlayRef.detach(); - } - }); + if (this._scrollSubscription) { + return; + } + + const stream = this._scrollDispatcher.scrolled(0); + + if (this._config && this._config.threshold && this._config.threshold > 1) { + this._initialScrollPosition = this._viewportRuler.getViewportScrollPosition().top; + + this._scrollSubscription = stream.subscribe(() => { + const scrollPosition = this._viewportRuler.getViewportScrollPosition().top; + + if (Math.abs(scrollPosition - this._initialScrollPosition) > this._config!.threshold!) { + this._detach(); + } else { + this._overlayRef.updatePosition(); + } }); + } else { + this._scrollSubscription = stream.subscribe(this._detach); } } @@ -53,4 +74,13 @@ export class CloseScrollStrategy implements ScrollStrategy { this._scrollSubscription = null; } } + + /** Detaches the overlay ref and disables the scroll strategy. */ + private _detach = () => { + this.disable(); + + if (this._overlayRef.hasAttached()) { + this._ngZone.run(() => this._overlayRef.detach()); + } + } } diff --git a/src/cdk/overlay/scroll/scroll-strategy-options.ts b/src/cdk/overlay/scroll/scroll-strategy-options.ts index 1185da1544e1..18fcd6e38c99 100644 --- a/src/cdk/overlay/scroll/scroll-strategy-options.ts +++ b/src/cdk/overlay/scroll/scroll-strategy-options.ts @@ -5,9 +5,8 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - import {Injectable, NgZone} from '@angular/core'; -import {CloseScrollStrategy} from './close-scroll-strategy'; +import {CloseScrollStrategy, CloseScrollStrategyConfig} from './close-scroll-strategy'; import {NoopScrollStrategy} from './noop-scroll-strategy'; import {BlockScrollStrategy} from './block-scroll-strategy'; import {ScrollDispatcher} from '@angular/cdk/scrolling'; @@ -34,8 +33,12 @@ export class ScrollStrategyOptions { /** Do nothing on scroll. */ noop = () => new NoopScrollStrategy(); - /** Close the overlay as soon as the user scrolls. */ - close = () => new CloseScrollStrategy(this._scrollDispatcher, this._ngZone); + /** + * Close the overlay as soon as the user scrolls. + * @param config Configuration to be used inside the scroll strategy. + */ + close = (config?: CloseScrollStrategyConfig) => new CloseScrollStrategy(this._scrollDispatcher, + this._ngZone, this._viewportRuler, config) /** Block scrolling. */ block = () => new BlockScrollStrategy(this._viewportRuler);