Skip to content

Commit d48b1ba

Browse files
crisbetojelbourn
authored andcommitted
feat(overlay): add option to automatically dispose on navigation (#12592)
Adds the opt-in `disposeOnNavigation` option which will dispose of an overlay when the user goes back/forward in history. This is something that we had in `MatDialog` and `MatBottomSheet` already, but it's a common-enough case, especially for global overlays, that it makes sense to have it in the CDK. Fixes #12544.
1 parent 10b8353 commit d48b1ba

File tree

10 files changed

+79
-50
lines changed

10 files changed

+79
-50
lines changed

src/cdk/overlay/overlay-config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ export class OverlayConfig {
5353
*/
5454
direction?: Direction | Directionality;
5555

56+
/**
57+
* Whether the overlay should be disposed of when the user goes backwards/forwards in history.
58+
* Note that this usually doesn't include clicking on links (unless the user is using
59+
* the `HashLocationStrategy`).
60+
*/
61+
disposeOnNavigation?: boolean = false;
62+
5663
constructor(config?: OverlayConfig) {
5764
if (config) {
5865
Object.keys(config)

src/cdk/overlay/overlay-ref.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
import {Direction, Directionality} from '@angular/cdk/bidi';
1010
import {ComponentPortal, Portal, PortalOutlet, TemplatePortal} from '@angular/cdk/portal';
1111
import {ComponentRef, EmbeddedViewRef, NgZone} from '@angular/core';
12-
import {Observable, Subject, merge} from 'rxjs';
12+
import {Location} from '@angular/common';
13+
import {Observable, Subject, merge, SubscriptionLike, Subscription} from 'rxjs';
1314
import {take, takeUntil} from 'rxjs/operators';
1415
import {OverlayKeyboardDispatcher} from './keyboard/overlay-keyboard-dispatcher';
1516
import {OverlayConfig} from './overlay-config';
@@ -33,6 +34,7 @@ export class OverlayRef implements PortalOutlet, OverlayReference {
3334
private _attachments = new Subject<void>();
3435
private _detachments = new Subject<void>();
3536
private _positionStrategy: PositionStrategy | undefined;
37+
private _locationChanges: SubscriptionLike = Subscription.EMPTY;
3638

3739
/**
3840
* Reference to the parent of the `_host` at the time it was detached. Used to restore
@@ -63,7 +65,9 @@ export class OverlayRef implements PortalOutlet, OverlayReference {
6365
private _config: ImmutableObject<OverlayConfig>,
6466
private _ngZone: NgZone,
6567
private _keyboardDispatcher: OverlayKeyboardDispatcher,
66-
private _document: Document) {
68+
private _document: Document,
69+
// @breaking-change 8.0.0 `_location` parameter to be made required.
70+
private _location?: Location) {
6771

6872
if (_config.scrollStrategy) {
6973
_config.scrollStrategy.attach(this);
@@ -152,6 +156,12 @@ export class OverlayRef implements PortalOutlet, OverlayReference {
152156
// Track this overlay by the keyboard dispatcher
153157
this._keyboardDispatcher.add(this);
154158

159+
// @breaking-change 8.0.0 remove the null check for `_location`
160+
// once the constructor parameter is made required.
161+
if (this._config.disposeOnNavigation && this._location) {
162+
this._locationChanges = this._location.subscribe(() => this.dispose());
163+
}
164+
155165
return attachResult;
156166
}
157167

@@ -195,6 +205,9 @@ export class OverlayRef implements PortalOutlet, OverlayReference {
195205
// rendered, even though it's transparent and unclickable which is why we remove it.
196206
this._detachContentWhenStable();
197207

208+
// Stop listening for location changes.
209+
this._locationChanges.unsubscribe();
210+
198211
return detachmentResult;
199212
}
200213

@@ -211,6 +224,7 @@ export class OverlayRef implements PortalOutlet, OverlayReference {
211224
}
212225

213226
this.detachBackdrop();
227+
this._locationChanges.unsubscribe();
214228
this._keyboardDispatcher.remove(this);
215229
this._portalOutlet.dispose();
216230
this._attachments.complete();

src/cdk/overlay/overlay.spec.ts

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
TemplatePortal,
1818
CdkPortal
1919
} from '@angular/cdk/portal';
20+
import {Location} from '@angular/common';
21+
import {SpyLocation} from '@angular/common/testing';
2022
import {
2123
Overlay,
2224
OverlayContainer,
@@ -38,6 +40,7 @@ describe('Overlay', () => {
3840
let viewContainerFixture: ComponentFixture<TestComponentWithTemplatePortals>;
3941
let dir: Direction;
4042
let zone: MockNgZone;
43+
let mockLocation: SpyLocation;
4144

4245
beforeEach(async(() => {
4346
dir = 'ltr';
@@ -56,21 +59,27 @@ describe('Overlay', () => {
5659
provide: NgZone,
5760
useFactory: () => zone = new MockNgZone()
5861
},
62+
{
63+
provide: Location,
64+
useClass: SpyLocation
65+
},
5966
],
6067
}).compileComponents();
6168
}));
6269

63-
beforeEach(inject([Overlay, OverlayContainer], (o: Overlay, oc: OverlayContainer) => {
64-
overlay = o;
65-
overlayContainer = oc;
66-
overlayContainerElement = oc.getContainerElement();
67-
68-
let fixture = TestBed.createComponent(TestComponentWithTemplatePortals);
69-
fixture.detectChanges();
70-
templatePortal = fixture.componentInstance.templatePortal;
71-
componentPortal = new ComponentPortal(PizzaMsg, fixture.componentInstance.viewContainerRef);
72-
viewContainerFixture = fixture;
73-
}));
70+
beforeEach(inject([Overlay, OverlayContainer, Location],
71+
(o: Overlay, oc: OverlayContainer, l: Location) => {
72+
overlay = o;
73+
overlayContainer = oc;
74+
overlayContainerElement = oc.getContainerElement();
75+
76+
const fixture = TestBed.createComponent(TestComponentWithTemplatePortals);
77+
fixture.detectChanges();
78+
templatePortal = fixture.componentInstance.templatePortal;
79+
componentPortal = new ComponentPortal(PizzaMsg, fixture.componentInstance.viewContainerRef);
80+
viewContainerFixture = fixture;
81+
mockLocation = l as SpyLocation;
82+
}));
7483

7584
afterEach(() => {
7685
overlayContainer.ngOnDestroy();
@@ -378,6 +387,17 @@ describe('Overlay', () => {
378387
.toBeTruthy('Expected host element to be back in the DOM.');
379388
});
380389

390+
it('should be able to dispose an overlay on navigation', () => {
391+
const overlayRef = overlay.create({disposeOnNavigation: true});
392+
overlayRef.attach(componentPortal);
393+
394+
expect(overlayContainerElement.textContent).toContain('Pizza');
395+
396+
mockLocation.simulateUrlPop('');
397+
expect(overlayContainerElement.childNodes.length).toBe(0);
398+
expect(overlayContainerElement.textContent).toBe('');
399+
});
400+
381401
describe('positioning', () => {
382402
let config: OverlayConfig;
383403

src/cdk/overlay/overlay.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@
88

99
import {Directionality} from '@angular/cdk/bidi';
1010
import {DomPortalOutlet} from '@angular/cdk/portal';
11-
import {DOCUMENT} from '@angular/common';
11+
import {DOCUMENT, Location} from '@angular/common';
1212
import {
1313
ApplicationRef,
1414
ComponentFactoryResolver,
1515
Inject,
1616
Injectable,
1717
Injector,
1818
NgZone,
19+
Optional,
1920
} from '@angular/core';
2021
import {OverlayKeyboardDispatcher} from './keyboard/overlay-keyboard-dispatcher';
2122
import {OverlayConfig} from './overlay-config';
@@ -53,7 +54,9 @@ export class Overlay {
5354
private _injector: Injector,
5455
private _ngZone: NgZone,
5556
@Inject(DOCUMENT) private _document: any,
56-
private _directionality: Directionality) { }
57+
private _directionality: Directionality,
58+
// @breaking-change 8.0.0 `_location` parameter to be made required.
59+
@Optional() private _location?: Location) { }
5760

5861
/**
5962
* Creates an overlay.
@@ -69,7 +72,7 @@ export class Overlay {
6972
overlayConfig.direction = overlayConfig.direction || this._directionality.value;
7073

7174
return new OverlayRef(portalOutlet, host, pane, overlayConfig, this._ngZone,
72-
this._keyboardDispatcher, this._document);
75+
this._keyboardDispatcher, this._document, this._location);
7376
}
7477

7578
/**

src/lib/bottom-sheet/bottom-sheet-config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ export class MatBottomSheetConfig<D = any> {
4040
/** Aria label to assign to the bottom sheet element. */
4141
ariaLabel?: string | null = null;
4242

43-
/** Whether the bottom sheet should close when the user goes backwards/forwards in history. */
43+
/**
44+
* Whether the bottom sheet should close when the user goes backwards/forwards in history.
45+
* Note that this usually doesn't include clicking on links (unless the user is using
46+
* the `HashLocationStrategy`).
47+
*/
4448
closeOnNavigation?: boolean = true;
4549

4650
/** Whether the bottom sheet should focus the first focusable element on open. */

src/lib/bottom-sheet/bottom-sheet-ref.ts

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import {Location} from '@angular/common';
1010
import {ESCAPE} from '@angular/cdk/keycodes';
1111
import {OverlayRef} from '@angular/cdk/overlay';
12-
import {merge, Observable, Subject, SubscriptionLike, Subscription} from 'rxjs';
12+
import {merge, Observable, Subject} from 'rxjs';
1313
import {filter, take} from 'rxjs/operators';
1414
import {MatBottomSheetContainer} from './bottom-sheet-container';
1515

@@ -36,13 +36,11 @@ export class MatBottomSheetRef<T = any, R = any> {
3636
/** Result to be passed down to the `afterDismissed` stream. */
3737
private _result: R | undefined;
3838

39-
/** Subscription to changes in the user's location. */
40-
private _locationChanges: SubscriptionLike = Subscription.EMPTY;
41-
4239
constructor(
4340
containerInstance: MatBottomSheetContainer,
4441
private _overlayRef: OverlayRef,
45-
location?: Location) {
42+
// @breaking-change 8.0.0 `_location` parameter to be removed.
43+
_location?: Location) {
4644
this.containerInstance = containerInstance;
4745

4846
// Emit when opening animation completes
@@ -61,7 +59,6 @@ export class MatBottomSheetRef<T = any, R = any> {
6159
take(1)
6260
)
6361
.subscribe(() => {
64-
this._locationChanges.unsubscribe();
6562
this._overlayRef.dispose();
6663
this._afterDismissed.next(this._result);
6764
this._afterDismissed.complete();
@@ -73,14 +70,6 @@ export class MatBottomSheetRef<T = any, R = any> {
7370
_overlayRef.keydownEvents().pipe(filter(event => event.keyCode === ESCAPE))
7471
).subscribe(() => this.dismiss());
7572
}
76-
77-
if (location) {
78-
this._locationChanges = location.subscribe(() => {
79-
if (containerInstance.bottomSheetConfig.closeOnNavigation) {
80-
this.dismiss();
81-
}
82-
});
83-
}
8473
}
8574

8675
/**

src/lib/bottom-sheet/bottom-sheet.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export class MatBottomSheet {
127127
const overlayConfig = new OverlayConfig({
128128
direction: config.direction,
129129
hasBackdrop: config.hasBackdrop,
130+
disposeOnNavigation: config.closeOnNavigation,
130131
maxWidth: '100%',
131132
scrollStrategy: this._overlay.scrollStrategies.block(),
132133
positionStrategy: this._overlay.position()

src/lib/dialog/dialog-config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,11 @@ export class MatDialogConfig<D = any> {
104104
/** Scroll strategy to be used for the dialog. */
105105
scrollStrategy?: ScrollStrategy;
106106

107-
/** Whether the dialog should close when the user goes backwards/forwards in history. */
107+
/**
108+
* Whether the dialog should close when the user goes backwards/forwards in history.
109+
* Note that this usually doesn't include clicking on links (unless the user is using
110+
* the `HashLocationStrategy`).
111+
*/
108112
closeOnNavigation?: boolean = true;
109113

110114
// TODO(jelbourn): add configuration for lifecycle hooks, ARIA labelling.

src/lib/dialog/dialog-ref.ts

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import {ESCAPE} from '@angular/cdk/keycodes';
1010
import {GlobalPositionStrategy, OverlayRef} from '@angular/cdk/overlay';
1111
import {Location} from '@angular/common';
12-
import {Observable, Subject, Subscription, SubscriptionLike} from 'rxjs';
12+
import {Observable, Subject} from 'rxjs';
1313
import {filter, take} from 'rxjs/operators';
1414
import {DialogPosition} from './dialog-config';
1515
import {MatDialogContainer} from './dialog-container';
@@ -42,13 +42,11 @@ export class MatDialogRef<T, R = any> {
4242
/** Result to be passed to afterClosed. */
4343
private _result: R | undefined;
4444

45-
/** Subscription to changes in the user's location. */
46-
private _locationChanges: SubscriptionLike = Subscription.EMPTY;
47-
4845
constructor(
4946
private _overlayRef: OverlayRef,
5047
public _containerInstance: MatDialogContainer,
51-
location?: Location,
48+
// @breaking-change 8.0.0 `_location` parameter to be removed.
49+
_location?: Location,
5250
readonly id: string = `mat-dialog-${uniqueId++}`) {
5351

5452
// Pass the id along to the container.
@@ -73,7 +71,6 @@ export class MatDialogRef<T, R = any> {
7371
_overlayRef.detachments().subscribe(() => {
7472
this._beforeClosed.next(this._result);
7573
this._beforeClosed.complete();
76-
this._locationChanges.unsubscribe();
7774
this._afterClosed.next(this._result);
7875
this._afterClosed.complete();
7976
this.componentInstance = null!;
@@ -83,17 +80,6 @@ export class MatDialogRef<T, R = any> {
8380
_overlayRef.keydownEvents()
8481
.pipe(filter(event => event.keyCode === ESCAPE && !this.disableClose))
8582
.subscribe(() => this.close());
86-
87-
if (location) {
88-
// Close the dialog when the user goes forwards/backwards in history or when the location
89-
// hash changes. Note that this usually doesn't include clicking on links (unless the user
90-
// is using the `HashLocationStrategy`).
91-
this._locationChanges = location.subscribe(() => {
92-
if (this._containerInstance._config.closeOnNavigation) {
93-
this.close();
94-
}
95-
});
96-
}
9783
}
9884

9985
/**

src/lib/dialog/dialog.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,8 @@ export class MatDialog implements OnDestroy {
195195
minWidth: dialogConfig.minWidth,
196196
minHeight: dialogConfig.minHeight,
197197
maxWidth: dialogConfig.maxWidth,
198-
maxHeight: dialogConfig.maxHeight
198+
maxHeight: dialogConfig.maxHeight,
199+
disposeOnNavigation: dialogConfig.closeOnNavigation
199200
});
200201

201202
if (dialogConfig.backdropClass) {

0 commit comments

Comments
 (0)