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; }