From f3d3ba9c727a764c25a834ad8138ce9cd676efa0 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Mon, 20 Nov 2017 19:14:10 +0100 Subject: [PATCH] feat(reposition-scroll-strategy): add option for closing once the user scrolls away Adds an option to the `RepositionScrollStrategy` that tells it to close the overlay once the user has scrolled away. This is a steps towards moving the scroll clipping logic away from the `ConnectedPositionStrategy`. --- .../scroll/reposition-scroll-strategy.spec.ts | 45 +++++++++++++++++-- .../scroll/reposition-scroll-strategy.ts | 27 ++++++++++- .../overlay/scroll/scroll-strategy-options.ts | 4 +- 3 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/cdk/overlay/scroll/reposition-scroll-strategy.spec.ts b/src/cdk/overlay/scroll/reposition-scroll-strategy.spec.ts index 0969222af312..91f7e3a436a7 100644 --- a/src/cdk/overlay/scroll/reposition-scroll-strategy.spec.ts +++ b/src/cdk/overlay/scroll/reposition-scroll-strategy.spec.ts @@ -14,6 +14,7 @@ import { describe('RepositionScrollStrategy', () => { let overlayRef: OverlayRef; + let overlay: Overlay; let componentPortal: ComponentPortal; let scrolledSubject = new Subject(); @@ -30,9 +31,8 @@ describe('RepositionScrollStrategy', () => { TestBed.compileComponents(); })); - beforeEach(inject([Overlay], (overlay: Overlay) => { - let overlayConfig = new OverlayConfig({scrollStrategy: overlay.scrollStrategies.reposition()}); - overlayRef = overlay.create(overlayConfig); + beforeEach(inject([Overlay], (o: Overlay) => { + overlay = o; componentPortal = new ComponentPortal(PastaMsg); })); @@ -42,6 +42,11 @@ describe('RepositionScrollStrategy', () => { })); it('should update the overlay position when the page is scrolled', () => { + const overlayConfig = new OverlayConfig({ + scrollStrategy: overlay.scrollStrategies.reposition() + }); + + overlayRef = overlay.create(overlayConfig); overlayRef.attach(componentPortal); spyOn(overlayRef, 'updatePosition'); @@ -53,6 +58,11 @@ describe('RepositionScrollStrategy', () => { }); it('should not be updating the position after the overlay is detached', () => { + const overlayConfig = new OverlayConfig({ + scrollStrategy: overlay.scrollStrategies.reposition() + }); + + overlayRef = overlay.create(overlayConfig); overlayRef.attach(componentPortal); spyOn(overlayRef, 'updatePosition'); @@ -63,6 +73,11 @@ describe('RepositionScrollStrategy', () => { }); it('should not be updating the position after the overlay is destroyed', () => { + const overlayConfig = new OverlayConfig({ + scrollStrategy: overlay.scrollStrategies.reposition() + }); + + overlayRef = overlay.create(overlayConfig); overlayRef.attach(componentPortal); spyOn(overlayRef, 'updatePosition'); @@ -72,6 +87,30 @@ describe('RepositionScrollStrategy', () => { expect(overlayRef.updatePosition).not.toHaveBeenCalled(); }); + it('should be able to close the overlay once it is out of view', () => { + const overlayConfig = new OverlayConfig({ + scrollStrategy: overlay.scrollStrategies.reposition({ + autoClose: true + }) + }); + + overlayRef = overlay.create(overlayConfig); + overlayRef.attach(componentPortal); + spyOn(overlayRef, 'updatePosition'); + spyOn(overlayRef, 'detach'); + spyOn(overlayRef.overlayElement, 'getBoundingClientRect').and.returnValue({ + top: -1000, + bottom: -900, + left: 0, + right: 100, + width: 100, + height: 100 + }); + + scrolledSubject.next(); + expect(overlayRef.detach).toHaveBeenCalledTimes(1); + }); + }); diff --git a/src/cdk/overlay/scroll/reposition-scroll-strategy.ts b/src/cdk/overlay/scroll/reposition-scroll-strategy.ts index 716adec4c2c3..457f649c2844 100644 --- a/src/cdk/overlay/scroll/reposition-scroll-strategy.ts +++ b/src/cdk/overlay/scroll/reposition-scroll-strategy.ts @@ -6,16 +6,22 @@ * found in the LICENSE file at https://angular.io/license */ +import {NgZone} from '@angular/core'; import {Subscription} from 'rxjs/Subscription'; import {ScrollStrategy, getMatScrollStrategyAlreadyAttachedError} from './scroll-strategy'; import {OverlayRef} from '../overlay-ref'; -import {ScrollDispatcher} from '@angular/cdk/scrolling'; +import {ScrollDispatcher, ViewportRuler} from '@angular/cdk/scrolling'; +import {isElementScrolledOutsideView} from '../position/scroll-clip'; /** * Config options for the RepositionScrollStrategy. */ export interface RepositionScrollStrategyConfig { + /** Time in milliseconds to throttle the scroll events. */ scrollThrottle?: number; + + /** Whether to close the overlay once the user has scrolled away completely. */ + autoClose?: boolean; } /** @@ -27,6 +33,8 @@ export class RepositionScrollStrategy implements ScrollStrategy { constructor( private _scrollDispatcher: ScrollDispatcher, + private _viewportRuler: ViewportRuler, + private _ngZone: NgZone, private _config?: RepositionScrollStrategyConfig) { } /** Attaches this scroll strategy to an overlay. */ @@ -41,10 +49,25 @@ export class RepositionScrollStrategy implements ScrollStrategy { /** Enables repositioning of the attached overlay on scroll. */ enable() { if (!this._scrollSubscription) { - let throttle = this._config ? this._config.scrollThrottle : 0; + const throttle = this._config ? this._config.scrollThrottle : 0; this._scrollSubscription = this._scrollDispatcher.scrolled(throttle).subscribe(() => { this._overlayRef.updatePosition(); + + // TODO(crisbeto): make `close` on by default once all components can handle it. + if (this._config && this._config.autoClose) { + const overlayRect = this._overlayRef.overlayElement.getBoundingClientRect(); + const {width, height} = this._viewportRuler.getViewportSize(); + + // TODO(crisbeto): include all ancestor scroll containers here once + // we have a way of exposing the trigger element to the scroll strategy. + const parentRects = [{width, height, bottom: height, right: width, top: 0, left: 0}]; + + if (isElementScrolledOutsideView(overlayRect, parentRects)) { + this.disable(); + 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 07473fd51ef4..1185da1544e1 100644 --- a/src/cdk/overlay/scroll/scroll-strategy-options.ts +++ b/src/cdk/overlay/scroll/scroll-strategy-options.ts @@ -45,6 +45,6 @@ export class ScrollStrategyOptions { * @param config Configuration to be used inside the scroll strategy. * Allows debouncing the reposition calls. */ - reposition = (config?: RepositionScrollStrategyConfig) => - new RepositionScrollStrategy(this._scrollDispatcher, config) + reposition = (config?: RepositionScrollStrategyConfig) => new RepositionScrollStrategy( + this._scrollDispatcher, this._viewportRuler, this._ngZone, config) }