From f4e7fd213c954b57a3b0e3682ae38d5898ae1a70 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Sun, 3 Feb 2019 10:57:53 +0100 Subject: [PATCH] feat(overlay): allow for scroll strategy to be swapped out With #12306 we added the ability to swap out position strategies, however that won't be enough to handle all use cases since the consumer is still locked into the scroll strategy that they chose at the start. E.g. when switching an overlay from a dialog to a dropdown, it might not make sense to block scrolling anymore. These changes add an API to allow for the scroll strategy to be swapped. --- src/cdk/overlay/overlay-ref.ts | 46 ++++++-- src/cdk/overlay/overlay.spec.ts | 110 ++++++++++++++++-- .../overlay/scroll/close-scroll-strategy.ts | 5 + .../scroll/reposition-scroll-strategy.ts | 5 + src/cdk/overlay/scroll/scroll-strategy.ts | 3 + tools/public_api_guard/cdk/overlay.d.ts | 6 +- 6 files changed, 155 insertions(+), 20 deletions(-) diff --git a/src/cdk/overlay/overlay-ref.ts b/src/cdk/overlay/overlay-ref.ts index 7852c5a3c070..4abf91a8c702 100644 --- a/src/cdk/overlay/overlay-ref.ts +++ b/src/cdk/overlay/overlay-ref.ts @@ -17,6 +17,7 @@ import {OverlayConfig} from './overlay-config'; import {coerceCssPixelValue, coerceArray} from '@angular/cdk/coercion'; import {OverlayReference} from './overlay-reference'; import {PositionStrategy} from './position/position-strategy'; +import {ScrollStrategy} from './scroll'; /** An object where all of its properties cannot be written. */ @@ -34,6 +35,7 @@ export class OverlayRef implements PortalOutlet, OverlayReference { private _attachments = new Subject(); private _detachments = new Subject(); private _positionStrategy: PositionStrategy | undefined; + private _scrollStrategy: ScrollStrategy | undefined; private _locationChanges: SubscriptionLike = Subscription.EMPTY; /** @@ -71,7 +73,8 @@ export class OverlayRef implements PortalOutlet, OverlayReference { private _location?: Location) { if (_config.scrollStrategy) { - _config.scrollStrategy.attach(this); + this._scrollStrategy = _config.scrollStrategy; + this._scrollStrategy.attach(this); } this._positionStrategy = _config.positionStrategy; @@ -123,8 +126,8 @@ export class OverlayRef implements PortalOutlet, OverlayReference { this._updateElementSize(); this._updateElementDirection(); - if (this._config.scrollStrategy) { - this._config.scrollStrategy.enable(); + if (this._scrollStrategy) { + this._scrollStrategy.enable(); } // Update the position once the zone is stable so that the overlay will be fully rendered @@ -186,8 +189,8 @@ export class OverlayRef implements PortalOutlet, OverlayReference { this._positionStrategy.detach(); } - if (this._config.scrollStrategy) { - this._config.scrollStrategy.disable(); + if (this._scrollStrategy) { + this._scrollStrategy.disable(); } const detachmentResult = this._portalOutlet.detach(); @@ -216,10 +219,7 @@ export class OverlayRef implements PortalOutlet, OverlayReference { this._positionStrategy.dispose(); } - if (this._config.scrollStrategy) { - this._config.scrollStrategy.disable(); - } - + this._disposeScrollStrategy(); this.detachBackdrop(); this._locationChanges.unsubscribe(); this._keyboardDispatcher.remove(this); @@ -336,6 +336,21 @@ export class OverlayRef implements PortalOutlet, OverlayReference { return typeof direction === 'string' ? direction : direction.value; } + /** Switches to a new scroll strategy. */ + updateScrollStrategy(strategy: ScrollStrategy): void { + if (strategy === this._scrollStrategy) { + return; + } + + this._disposeScrollStrategy(); + this._scrollStrategy = strategy; + + if (this.hasAttached()) { + strategy.attach(this); + strategy.enable(); + } + } + /** Updates the text direction of the overlay panel. */ private _updateElementDirection() { this._host.setAttribute('dir', this.getDirection()); @@ -490,6 +505,19 @@ export class OverlayRef implements PortalOutlet, OverlayReference { }); }); } + + /** Disposes of a scroll strategy. */ + private _disposeScrollStrategy() { + const scrollStrategy = this._scrollStrategy; + + if (scrollStrategy) { + scrollStrategy.disable(); + + if (scrollStrategy.detach) { + scrollStrategy.detach(); + } + } + } } diff --git a/src/cdk/overlay/overlay.spec.ts b/src/cdk/overlay/overlay.spec.ts index f7da3fc088e7..8ab3b2224f7e 100644 --- a/src/cdk/overlay/overlay.spec.ts +++ b/src/cdk/overlay/overlay.spec.ts @@ -816,27 +816,29 @@ describe('Overlay', () => { }); describe('scroll strategy', () => { - let fakeScrollStrategy: FakeScrollStrategy; - let config: OverlayConfig; - let overlayRef: OverlayRef; - - beforeEach(() => { - fakeScrollStrategy = new FakeScrollStrategy(); - config = new OverlayConfig({scrollStrategy: fakeScrollStrategy}); - overlayRef = overlay.create(config); - }); - it('should attach the overlay ref to the scroll strategy', () => { + const fakeScrollStrategy = new FakeScrollStrategy(); + const config = new OverlayConfig({scrollStrategy: fakeScrollStrategy}); + const overlayRef = overlay.create(config); + expect(fakeScrollStrategy.overlayRef).toBe(overlayRef, 'Expected scroll strategy to have been attached to the current overlay ref.'); }); it('should enable the scroll strategy when the overlay is attached', () => { + const fakeScrollStrategy = new FakeScrollStrategy(); + const config = new OverlayConfig({scrollStrategy: fakeScrollStrategy}); + const overlayRef = overlay.create(config); + overlayRef.attach(componentPortal); expect(fakeScrollStrategy.isEnabled).toBe(true, 'Expected scroll strategy to be enabled.'); }); it('should disable the scroll strategy once the overlay is detached', () => { + const fakeScrollStrategy = new FakeScrollStrategy(); + const config = new OverlayConfig({scrollStrategy: fakeScrollStrategy}); + const overlayRef = overlay.create(config); + overlayRef.attach(componentPortal); expect(fakeScrollStrategy.isEnabled).toBe(true, 'Expected scroll strategy to be enabled.'); @@ -845,9 +847,93 @@ describe('Overlay', () => { }); it('should disable the scroll strategy when the overlay is destroyed', () => { + const fakeScrollStrategy = new FakeScrollStrategy(); + const config = new OverlayConfig({scrollStrategy: fakeScrollStrategy}); + const overlayRef = overlay.create(config); + overlayRef.dispose(); expect(fakeScrollStrategy.isEnabled).toBe(false, 'Expected scroll strategy to be disabled.'); }); + + it('should detach the scroll strategy when the overlay is destroyed', () => { + const fakeScrollStrategy = new FakeScrollStrategy(); + const config = new OverlayConfig({scrollStrategy: fakeScrollStrategy}); + const overlayRef = overlay.create(config); + + expect(fakeScrollStrategy.overlayRef).toBe(overlayRef); + + overlayRef.dispose(); + + expect(fakeScrollStrategy.overlayRef).toBeNull(); + }); + + it('should be able to swap scroll strategies', fakeAsync(() => { + const firstStrategy = new FakeScrollStrategy(); + const secondStrategy = new FakeScrollStrategy(); + + [firstStrategy, secondStrategy].forEach(strategy => { + spyOn(strategy, 'attach'); + spyOn(strategy, 'enable'); + spyOn(strategy, 'disable'); + spyOn(strategy, 'detach'); + }); + + const overlayRef = overlay.create({scrollStrategy: firstStrategy}); + + overlayRef.attach(componentPortal); + viewContainerFixture.detectChanges(); + zone.simulateZoneExit(); + tick(); + + expect(firstStrategy.attach).toHaveBeenCalledTimes(1); + expect(firstStrategy.enable).toHaveBeenCalledTimes(1); + + expect(secondStrategy.attach).not.toHaveBeenCalled(); + expect(secondStrategy.enable).not.toHaveBeenCalled(); + + overlayRef.updateScrollStrategy(secondStrategy); + viewContainerFixture.detectChanges(); + tick(); + + expect(firstStrategy.attach).toHaveBeenCalledTimes(1); + expect(firstStrategy.enable).toHaveBeenCalledTimes(1); + expect(firstStrategy.disable).toHaveBeenCalledTimes(1); + expect(firstStrategy.detach).toHaveBeenCalledTimes(1); + + expect(secondStrategy.attach).toHaveBeenCalledTimes(1); + expect(secondStrategy.enable).toHaveBeenCalledTimes(1); + })); + + it('should not do anything when trying to swap a strategy with itself', fakeAsync(() => { + const strategy = new FakeScrollStrategy(); + + spyOn(strategy, 'attach'); + spyOn(strategy, 'enable'); + spyOn(strategy, 'disable'); + spyOn(strategy, 'detach'); + + const overlayRef = overlay.create({scrollStrategy: strategy}); + + overlayRef.attach(componentPortal); + viewContainerFixture.detectChanges(); + zone.simulateZoneExit(); + tick(); + + expect(strategy.attach).toHaveBeenCalledTimes(1); + expect(strategy.enable).toHaveBeenCalledTimes(1); + expect(strategy.disable).not.toHaveBeenCalled(); + expect(strategy.detach).not.toHaveBeenCalled(); + + overlayRef.updateScrollStrategy(strategy); + viewContainerFixture.detectChanges(); + tick(); + + expect(strategy.attach).toHaveBeenCalledTimes(1); + expect(strategy.enable).toHaveBeenCalledTimes(1); + expect(strategy.disable).not.toHaveBeenCalled(); + expect(strategy.detach).not.toHaveBeenCalled(); + })); + }); }); @@ -908,4 +994,8 @@ class FakeScrollStrategy implements ScrollStrategy { disable() { this.isEnabled = false; } + + detach() { + this.overlayRef = null!; + } } diff --git a/src/cdk/overlay/scroll/close-scroll-strategy.ts b/src/cdk/overlay/scroll/close-scroll-strategy.ts index c1d77dfb8c1b..cd3bf822ae0f 100644 --- a/src/cdk/overlay/scroll/close-scroll-strategy.ts +++ b/src/cdk/overlay/scroll/close-scroll-strategy.ts @@ -75,6 +75,11 @@ export class CloseScrollStrategy implements ScrollStrategy { } } + detach() { + this.disable(); + this._overlayRef = null!; + } + /** Detaches the overlay ref and disables the scroll strategy. */ private _detach = () => { this.disable(); diff --git a/src/cdk/overlay/scroll/reposition-scroll-strategy.ts b/src/cdk/overlay/scroll/reposition-scroll-strategy.ts index fc5f8f7b674d..43e260724665 100644 --- a/src/cdk/overlay/scroll/reposition-scroll-strategy.ts +++ b/src/cdk/overlay/scroll/reposition-scroll-strategy.ts @@ -79,4 +79,9 @@ export class RepositionScrollStrategy implements ScrollStrategy { this._scrollSubscription = null; } } + + detach() { + this.disable(); + this._overlayRef = null!; + } } diff --git a/src/cdk/overlay/scroll/scroll-strategy.ts b/src/cdk/overlay/scroll/scroll-strategy.ts index 8d60f2539d0f..24b2c2944a92 100644 --- a/src/cdk/overlay/scroll/scroll-strategy.ts +++ b/src/cdk/overlay/scroll/scroll-strategy.ts @@ -20,6 +20,9 @@ export interface ScrollStrategy { /** Attaches this `ScrollStrategy` to an overlay. */ attach: (overlayRef: OverlayReference) => void; + + /** Detaches the scroll strategy from the current overlay. */ + detach?: () => void; } /** diff --git a/tools/public_api_guard/cdk/overlay.d.ts b/tools/public_api_guard/cdk/overlay.d.ts index b759a9484ac1..4728aeb96b77 100644 --- a/tools/public_api_guard/cdk/overlay.d.ts +++ b/tools/public_api_guard/cdk/overlay.d.ts @@ -45,6 +45,7 @@ export declare class CdkOverlayOrigin { export declare class CloseScrollStrategy implements ScrollStrategy { constructor(_scrollDispatcher: ScrollDispatcher, _ngZone: NgZone, _viewportRuler: ViewportRuler, _config?: CloseScrollStrategyConfig | undefined); attach(overlayRef: OverlayReference): void; + detach(): void; disable(): void; enable(): void; } @@ -228,9 +229,9 @@ export declare class OverlayRef implements PortalOutlet, OverlayReference { readonly overlayElement: HTMLElement; constructor(_portalOutlet: PortalOutlet, _host: HTMLElement, _pane: HTMLElement, _config: ImmutableObject, _ngZone: NgZone, _keyboardDispatcher: OverlayKeyboardDispatcher, _document: Document, _location?: Location | undefined); addPanelClass(classes: string | string[]): void; - attach(portal: any): any; attach(portal: ComponentPortal): ComponentRef; attach(portal: TemplatePortal): EmbeddedViewRef; + attach(portal: any): any; attachments(): Observable; backdropClick(): Observable; detach(): any; @@ -245,6 +246,7 @@ export declare class OverlayRef implements PortalOutlet, OverlayReference { setDirection(dir: Direction | Directionality): void; updatePosition(): void; updatePositionStrategy(strategy: PositionStrategy): void; + updateScrollStrategy(strategy: ScrollStrategy): void; updateSize(sizeConfig: OverlaySizeConfig): void; } @@ -267,6 +269,7 @@ export interface PositionStrategy { export declare class RepositionScrollStrategy implements ScrollStrategy { constructor(_scrollDispatcher: ScrollDispatcher, _viewportRuler: ViewportRuler, _ngZone: NgZone, _config?: RepositionScrollStrategyConfig | undefined); attach(overlayRef: OverlayReference): void; + detach(): void; disable(): void; enable(): void; } @@ -285,6 +288,7 @@ export declare class ScrollingVisibility { export interface ScrollStrategy { attach: (overlayRef: OverlayReference) => void; + detach?: () => void; disable: () => void; enable: () => void; }