Skip to content

Commit 3308187

Browse files
crisbetojelbourn
authored andcommitted
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.
1 parent 01dcf7e commit 3308187

File tree

6 files changed

+155
-20
lines changed

6 files changed

+155
-20
lines changed

src/cdk/overlay/overlay-ref.ts

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {OverlayConfig} from './overlay-config';
1717
import {coerceCssPixelValue, coerceArray} from '@angular/cdk/coercion';
1818
import {OverlayReference} from './overlay-reference';
1919
import {PositionStrategy} from './position/position-strategy';
20+
import {ScrollStrategy} from './scroll';
2021

2122

2223
/** An object where all of its properties cannot be written. */
@@ -34,6 +35,7 @@ export class OverlayRef implements PortalOutlet, OverlayReference {
3435
private _attachments = new Subject<void>();
3536
private _detachments = new Subject<void>();
3637
private _positionStrategy: PositionStrategy | undefined;
38+
private _scrollStrategy: ScrollStrategy | undefined;
3739
private _locationChanges: SubscriptionLike = Subscription.EMPTY;
3840

3941
/**
@@ -71,7 +73,8 @@ export class OverlayRef implements PortalOutlet, OverlayReference {
7173
private _location?: Location) {
7274

7375
if (_config.scrollStrategy) {
74-
_config.scrollStrategy.attach(this);
76+
this._scrollStrategy = _config.scrollStrategy;
77+
this._scrollStrategy.attach(this);
7578
}
7679

7780
this._positionStrategy = _config.positionStrategy;
@@ -123,8 +126,8 @@ export class OverlayRef implements PortalOutlet, OverlayReference {
123126
this._updateElementSize();
124127
this._updateElementDirection();
125128

126-
if (this._config.scrollStrategy) {
127-
this._config.scrollStrategy.enable();
129+
if (this._scrollStrategy) {
130+
this._scrollStrategy.enable();
128131
}
129132

130133
// 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 {
186189
this._positionStrategy.detach();
187190
}
188191

189-
if (this._config.scrollStrategy) {
190-
this._config.scrollStrategy.disable();
192+
if (this._scrollStrategy) {
193+
this._scrollStrategy.disable();
191194
}
192195

193196
const detachmentResult = this._portalOutlet.detach();
@@ -216,10 +219,7 @@ export class OverlayRef implements PortalOutlet, OverlayReference {
216219
this._positionStrategy.dispose();
217220
}
218221

219-
if (this._config.scrollStrategy) {
220-
this._config.scrollStrategy.disable();
221-
}
222-
222+
this._disposeScrollStrategy();
223223
this.detachBackdrop();
224224
this._locationChanges.unsubscribe();
225225
this._keyboardDispatcher.remove(this);
@@ -336,6 +336,21 @@ export class OverlayRef implements PortalOutlet, OverlayReference {
336336
return typeof direction === 'string' ? direction : direction.value;
337337
}
338338

339+
/** Switches to a new scroll strategy. */
340+
updateScrollStrategy(strategy: ScrollStrategy): void {
341+
if (strategy === this._scrollStrategy) {
342+
return;
343+
}
344+
345+
this._disposeScrollStrategy();
346+
this._scrollStrategy = strategy;
347+
348+
if (this.hasAttached()) {
349+
strategy.attach(this);
350+
strategy.enable();
351+
}
352+
}
353+
339354
/** Updates the text direction of the overlay panel. */
340355
private _updateElementDirection() {
341356
this._host.setAttribute('dir', this.getDirection());
@@ -490,6 +505,19 @@ export class OverlayRef implements PortalOutlet, OverlayReference {
490505
});
491506
});
492507
}
508+
509+
/** Disposes of a scroll strategy. */
510+
private _disposeScrollStrategy() {
511+
const scrollStrategy = this._scrollStrategy;
512+
513+
if (scrollStrategy) {
514+
scrollStrategy.disable();
515+
516+
if (scrollStrategy.detach) {
517+
scrollStrategy.detach();
518+
}
519+
}
520+
}
493521
}
494522

495523

src/cdk/overlay/overlay.spec.ts

Lines changed: 100 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -816,27 +816,29 @@ describe('Overlay', () => {
816816
});
817817

818818
describe('scroll strategy', () => {
819-
let fakeScrollStrategy: FakeScrollStrategy;
820-
let config: OverlayConfig;
821-
let overlayRef: OverlayRef;
822-
823-
beforeEach(() => {
824-
fakeScrollStrategy = new FakeScrollStrategy();
825-
config = new OverlayConfig({scrollStrategy: fakeScrollStrategy});
826-
overlayRef = overlay.create(config);
827-
});
828-
829819
it('should attach the overlay ref to the scroll strategy', () => {
820+
const fakeScrollStrategy = new FakeScrollStrategy();
821+
const config = new OverlayConfig({scrollStrategy: fakeScrollStrategy});
822+
const overlayRef = overlay.create(config);
823+
830824
expect(fakeScrollStrategy.overlayRef).toBe(overlayRef,
831825
'Expected scroll strategy to have been attached to the current overlay ref.');
832826
});
833827

834828
it('should enable the scroll strategy when the overlay is attached', () => {
829+
const fakeScrollStrategy = new FakeScrollStrategy();
830+
const config = new OverlayConfig({scrollStrategy: fakeScrollStrategy});
831+
const overlayRef = overlay.create(config);
832+
835833
overlayRef.attach(componentPortal);
836834
expect(fakeScrollStrategy.isEnabled).toBe(true, 'Expected scroll strategy to be enabled.');
837835
});
838836

839837
it('should disable the scroll strategy once the overlay is detached', () => {
838+
const fakeScrollStrategy = new FakeScrollStrategy();
839+
const config = new OverlayConfig({scrollStrategy: fakeScrollStrategy});
840+
const overlayRef = overlay.create(config);
841+
840842
overlayRef.attach(componentPortal);
841843
expect(fakeScrollStrategy.isEnabled).toBe(true, 'Expected scroll strategy to be enabled.');
842844

@@ -845,9 +847,93 @@ describe('Overlay', () => {
845847
});
846848

847849
it('should disable the scroll strategy when the overlay is destroyed', () => {
850+
const fakeScrollStrategy = new FakeScrollStrategy();
851+
const config = new OverlayConfig({scrollStrategy: fakeScrollStrategy});
852+
const overlayRef = overlay.create(config);
853+
848854
overlayRef.dispose();
849855
expect(fakeScrollStrategy.isEnabled).toBe(false, 'Expected scroll strategy to be disabled.');
850856
});
857+
858+
it('should detach the scroll strategy when the overlay is destroyed', () => {
859+
const fakeScrollStrategy = new FakeScrollStrategy();
860+
const config = new OverlayConfig({scrollStrategy: fakeScrollStrategy});
861+
const overlayRef = overlay.create(config);
862+
863+
expect(fakeScrollStrategy.overlayRef).toBe(overlayRef);
864+
865+
overlayRef.dispose();
866+
867+
expect(fakeScrollStrategy.overlayRef).toBeNull();
868+
});
869+
870+
it('should be able to swap scroll strategies', fakeAsync(() => {
871+
const firstStrategy = new FakeScrollStrategy();
872+
const secondStrategy = new FakeScrollStrategy();
873+
874+
[firstStrategy, secondStrategy].forEach(strategy => {
875+
spyOn(strategy, 'attach');
876+
spyOn(strategy, 'enable');
877+
spyOn(strategy, 'disable');
878+
spyOn(strategy, 'detach');
879+
});
880+
881+
const overlayRef = overlay.create({scrollStrategy: firstStrategy});
882+
883+
overlayRef.attach(componentPortal);
884+
viewContainerFixture.detectChanges();
885+
zone.simulateZoneExit();
886+
tick();
887+
888+
expect(firstStrategy.attach).toHaveBeenCalledTimes(1);
889+
expect(firstStrategy.enable).toHaveBeenCalledTimes(1);
890+
891+
expect(secondStrategy.attach).not.toHaveBeenCalled();
892+
expect(secondStrategy.enable).not.toHaveBeenCalled();
893+
894+
overlayRef.updateScrollStrategy(secondStrategy);
895+
viewContainerFixture.detectChanges();
896+
tick();
897+
898+
expect(firstStrategy.attach).toHaveBeenCalledTimes(1);
899+
expect(firstStrategy.enable).toHaveBeenCalledTimes(1);
900+
expect(firstStrategy.disable).toHaveBeenCalledTimes(1);
901+
expect(firstStrategy.detach).toHaveBeenCalledTimes(1);
902+
903+
expect(secondStrategy.attach).toHaveBeenCalledTimes(1);
904+
expect(secondStrategy.enable).toHaveBeenCalledTimes(1);
905+
}));
906+
907+
it('should not do anything when trying to swap a strategy with itself', fakeAsync(() => {
908+
const strategy = new FakeScrollStrategy();
909+
910+
spyOn(strategy, 'attach');
911+
spyOn(strategy, 'enable');
912+
spyOn(strategy, 'disable');
913+
spyOn(strategy, 'detach');
914+
915+
const overlayRef = overlay.create({scrollStrategy: strategy});
916+
917+
overlayRef.attach(componentPortal);
918+
viewContainerFixture.detectChanges();
919+
zone.simulateZoneExit();
920+
tick();
921+
922+
expect(strategy.attach).toHaveBeenCalledTimes(1);
923+
expect(strategy.enable).toHaveBeenCalledTimes(1);
924+
expect(strategy.disable).not.toHaveBeenCalled();
925+
expect(strategy.detach).not.toHaveBeenCalled();
926+
927+
overlayRef.updateScrollStrategy(strategy);
928+
viewContainerFixture.detectChanges();
929+
tick();
930+
931+
expect(strategy.attach).toHaveBeenCalledTimes(1);
932+
expect(strategy.enable).toHaveBeenCalledTimes(1);
933+
expect(strategy.disable).not.toHaveBeenCalled();
934+
expect(strategy.detach).not.toHaveBeenCalled();
935+
}));
936+
851937
});
852938
});
853939

@@ -908,4 +994,8 @@ class FakeScrollStrategy implements ScrollStrategy {
908994
disable() {
909995
this.isEnabled = false;
910996
}
997+
998+
detach() {
999+
this.overlayRef = null!;
1000+
}
9111001
}

src/cdk/overlay/scroll/close-scroll-strategy.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ export class CloseScrollStrategy implements ScrollStrategy {
7575
}
7676
}
7777

78+
detach() {
79+
this.disable();
80+
this._overlayRef = null!;
81+
}
82+
7883
/** Detaches the overlay ref and disables the scroll strategy. */
7984
private _detach = () => {
8085
this.disable();

src/cdk/overlay/scroll/reposition-scroll-strategy.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,9 @@ export class RepositionScrollStrategy implements ScrollStrategy {
7979
this._scrollSubscription = null;
8080
}
8181
}
82+
83+
detach() {
84+
this.disable();
85+
this._overlayRef = null!;
86+
}
8287
}

src/cdk/overlay/scroll/scroll-strategy.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ export interface ScrollStrategy {
2020

2121
/** Attaches this `ScrollStrategy` to an overlay. */
2222
attach: (overlayRef: OverlayReference) => void;
23+
24+
/** Detaches the scroll strategy from the current overlay. */
25+
detach?: () => void;
2326
}
2427

2528
/**

tools/public_api_guard/cdk/overlay.d.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export declare class CdkOverlayOrigin {
4545
export declare class CloseScrollStrategy implements ScrollStrategy {
4646
constructor(_scrollDispatcher: ScrollDispatcher, _ngZone: NgZone, _viewportRuler: ViewportRuler, _config?: CloseScrollStrategyConfig | undefined);
4747
attach(overlayRef: OverlayReference): void;
48+
detach(): void;
4849
disable(): void;
4950
enable(): void;
5051
}
@@ -228,9 +229,9 @@ export declare class OverlayRef implements PortalOutlet, OverlayReference {
228229
readonly overlayElement: HTMLElement;
229230
constructor(_portalOutlet: PortalOutlet, _host: HTMLElement, _pane: HTMLElement, _config: ImmutableObject<OverlayConfig>, _ngZone: NgZone, _keyboardDispatcher: OverlayKeyboardDispatcher, _document: Document, _location?: Location | undefined);
230231
addPanelClass(classes: string | string[]): void;
231-
attach(portal: any): any;
232232
attach<T>(portal: ComponentPortal<T>): ComponentRef<T>;
233233
attach<T>(portal: TemplatePortal<T>): EmbeddedViewRef<T>;
234+
attach(portal: any): any;
234235
attachments(): Observable<void>;
235236
backdropClick(): Observable<MouseEvent>;
236237
detach(): any;
@@ -245,6 +246,7 @@ export declare class OverlayRef implements PortalOutlet, OverlayReference {
245246
setDirection(dir: Direction | Directionality): void;
246247
updatePosition(): void;
247248
updatePositionStrategy(strategy: PositionStrategy): void;
249+
updateScrollStrategy(strategy: ScrollStrategy): void;
248250
updateSize(sizeConfig: OverlaySizeConfig): void;
249251
}
250252

@@ -267,6 +269,7 @@ export interface PositionStrategy {
267269
export declare class RepositionScrollStrategy implements ScrollStrategy {
268270
constructor(_scrollDispatcher: ScrollDispatcher, _viewportRuler: ViewportRuler, _ngZone: NgZone, _config?: RepositionScrollStrategyConfig | undefined);
269271
attach(overlayRef: OverlayReference): void;
272+
detach(): void;
270273
disable(): void;
271274
enable(): void;
272275
}
@@ -285,6 +288,7 @@ export declare class ScrollingVisibility {
285288

286289
export interface ScrollStrategy {
287290
attach: (overlayRef: OverlayReference) => void;
291+
detach?: () => void;
288292
disable: () => void;
289293
enable: () => void;
290294
}

0 commit comments

Comments
 (0)