feat(overlay): allow for scroll strategy to be swapped out (#15067)

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.
This commit is contained in:
Kristiyan Kostadinov
2019-02-05 16:21:56 +01:00
committed by Jeremy Elbourn
parent 01dcf7ee7d
commit 33081878ab
6 changed files with 155 additions and 20 deletions
+37 -9
View File
@@ -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<void>();
private _detachments = new Subject<void>();
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();
}
}
}
}
+100 -10
View File
@@ -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!;
}
}
@@ -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();
@@ -79,4 +79,9 @@ export class RepositionScrollStrategy implements ScrollStrategy {
this._scrollSubscription = null;
}
}
detach() {
this.disable();
this._overlayRef = null!;
}
}
@@ -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;
}
/**
+5 -1
View File
@@ -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<OverlayConfig>, _ngZone: NgZone, _keyboardDispatcher: OverlayKeyboardDispatcher, _document: Document, _location?: Location | undefined);
addPanelClass(classes: string | string[]): void;
attach(portal: any): any;
attach<T>(portal: ComponentPortal<T>): ComponentRef<T>;
attach<T>(portal: TemplatePortal<T>): EmbeddedViewRef<T>;
attach(portal: any): any;
attachments(): Observable<void>;
backdropClick(): Observable<MouseEvent>;
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;
}