Skip to content

Commit 34c529d

Browse files
committed
fix(cdk/dialog): not emitting closed event on external detachments
Fixes that the CDK dialog wasn't emitting to the `closed` event when it is detached externally, e.g. by a scroll strategy or a navigation. We had unit tests for this on the Material side, but we had special logic to handle it there. Fixes #26581.
1 parent e6536b7 commit 34c529d

File tree

7 files changed

+71
-15
lines changed

7 files changed

+71
-15
lines changed

src/cdk/dialog/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ ng_test_library(
4545
"//src/cdk/testing/private",
4646
"@npm//@angular/common",
4747
"@npm//@angular/platform-browser",
48+
"@npm//rxjs",
4849
],
4950
)
5051

src/cdk/dialog/dialog-config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,12 @@ export class DialogConfig<D = unknown, R = unknown, C extends BasePortalOutlet =
132132
*/
133133
closeOnDestroy?: boolean = true;
134134

135+
/**
136+
* Whether the dialog should close when the underlying overlay is disposed. This is useful if
137+
* another service is wrapping the dialog and is managing the destruction instead.
138+
*/
139+
closeOnOverlayDetachments?: boolean = true;
140+
135141
/** Alternate `ComponentFactoryResolver` to use when resolving the associated component. */
136142
componentFactoryResolver?: ComponentFactoryResolver;
137143

src/cdk/dialog/dialog-ref.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ export class DialogRef<R = unknown, C = unknown> {
7272
this.close(undefined, {focusOrigin: 'mouse'});
7373
}
7474
});
75+
76+
overlayRef.detachments().subscribe(() => {
77+
// Check specifically for `false`, because we want `undefined` to be treated like `true`.
78+
if (config.closeOnOverlayDetachments !== false) {
79+
this._finishClose();
80+
}
81+
});
7582
}
7683

7784
/**
@@ -80,16 +87,8 @@ export class DialogRef<R = unknown, C = unknown> {
8087
* @param options Additional options to customize the closing behavior.
8188
*/
8289
close(result?: R, options?: DialogCloseOptions): void {
83-
if (this.containerInstance) {
84-
const closedSubject = this.closed as Subject<R | undefined>;
85-
this.containerInstance._closeInteractionType = options?.focusOrigin || 'program';
86-
this.overlayRef.dispose();
87-
closedSubject.next(result);
88-
closedSubject.complete();
89-
(this as {componentInstance: C}).componentInstance = (
90-
this as {containerInstance: BasePortalOutlet}
91-
).containerInstance = null!;
92-
}
90+
this._finishClose(result, options);
91+
this.overlayRef.dispose();
9392
}
9493

9594
/** Updates the position of the dialog based on the current position strategy. */
@@ -119,4 +118,17 @@ export class DialogRef<R = unknown, C = unknown> {
119118
this.overlayRef.removePanelClass(classes);
120119
return this;
121120
}
121+
122+
/** Finalizes the closing sequence once the overlay is detached. */
123+
private _finishClose(result?: R, options?: DialogCloseOptions) {
124+
if (this.containerInstance) {
125+
const closedSubject = this.closed as Subject<R | undefined>;
126+
this.containerInstance._closeInteractionType = options?.focusOrigin || 'program';
127+
closedSubject.next(result);
128+
closedSubject.complete();
129+
(this as {componentInstance: C}).componentInstance = (
130+
this as {containerInstance: BasePortalOutlet}
131+
).containerInstance = null!;
132+
}
133+
}
122134
}

src/cdk/dialog/dialog.spec.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,15 @@ import {By} from '@angular/platform-browser';
2323
import {Location} from '@angular/common';
2424
import {SpyLocation} from '@angular/common/testing';
2525
import {Directionality} from '@angular/cdk/bidi';
26-
import {Overlay, OverlayContainer} from '@angular/cdk/overlay';
26+
import {Overlay, OverlayContainer, ScrollDispatcher} from '@angular/cdk/overlay';
2727
import {A, ESCAPE} from '@angular/cdk/keycodes';
2828
import {_supportsShadowDom} from '@angular/cdk/platform';
2929
import {
3030
dispatchKeyboardEvent,
3131
createKeyboardEvent,
3232
dispatchEvent,
3333
} from '@angular/cdk/testing/private';
34+
import {Subject} from 'rxjs';
3435
import {DIALOG_DATA, Dialog, DialogModule, DialogRef} from './index';
3536

3637
describe('Dialog', () => {
@@ -41,6 +42,7 @@ describe('Dialog', () => {
4142
let viewContainerFixture: ComponentFixture<ComponentWithChildViewContainer>;
4243
let mockLocation: SpyLocation;
4344
let overlay: Overlay;
45+
let scrolledSubject = new Subject();
4446

4547
beforeEach(fakeAsync(() => {
4648
TestBed.configureTestingModule({
@@ -59,6 +61,10 @@ describe('Dialog', () => {
5961
providers: [
6062
{provide: Location, useClass: SpyLocation},
6163
{provide: TEMPLATE_INJECTOR_TEST_TOKEN, useValue: 'hello from test module'},
64+
{
65+
provide: ScrollDispatcher,
66+
useFactory: () => ({scrolled: () => scrolledSubject}),
67+
},
6268
],
6369
});
6470

@@ -504,24 +510,28 @@ describe('Dialog', () => {
504510
}));
505511

506512
it('should close all dialogs when the user goes forwards/backwards in history', fakeAsync(() => {
507-
dialog.open(PizzaMsg);
513+
const closeSpy = jasmine.createSpy('closed');
514+
dialog.open(PizzaMsg).closed.subscribe(closeSpy);
508515
viewContainerFixture.detectChanges();
509-
dialog.open(PizzaMsg);
516+
dialog.open(PizzaMsg).closed.subscribe(closeSpy);
510517
viewContainerFixture.detectChanges();
511518

512519
expect(overlayContainerElement.querySelectorAll('cdk-dialog-container').length).toBe(2);
520+
expect(closeSpy).not.toHaveBeenCalled();
513521

514522
mockLocation.simulateUrlPop('');
515523
viewContainerFixture.detectChanges();
516524
flush();
517525

518526
expect(overlayContainerElement.querySelectorAll('cdk-dialog-container').length).toBe(0);
527+
expect(closeSpy).toHaveBeenCalledTimes(2);
519528
}));
520529

521530
it('should close all open dialogs when the location hash changes', fakeAsync(() => {
522-
dialog.open(PizzaMsg);
531+
const closeSpy = jasmine.createSpy('closed');
532+
dialog.open(PizzaMsg).closed.subscribe(closeSpy);
523533
viewContainerFixture.detectChanges();
524-
dialog.open(PizzaMsg);
534+
dialog.open(PizzaMsg).closed.subscribe(closeSpy);
525535
viewContainerFixture.detectChanges();
526536

527537
expect(overlayContainerElement.querySelectorAll('cdk-dialog-container').length).toBe(2);
@@ -533,6 +543,28 @@ describe('Dialog', () => {
533543
expect(overlayContainerElement.querySelectorAll('cdk-dialog-container').length).toBe(0);
534544
}));
535545

546+
it('should close the dialog when detached externally', fakeAsync(() => {
547+
const closeSpy = jasmine.createSpy('closed');
548+
dialog
549+
.open(PizzaMsg, {scrollStrategy: overlay.scrollStrategies.close()})
550+
.closed.subscribe(closeSpy);
551+
viewContainerFixture.detectChanges();
552+
dialog
553+
.open(PizzaMsg, {scrollStrategy: overlay.scrollStrategies.close()})
554+
.closed.subscribe(closeSpy);
555+
viewContainerFixture.detectChanges();
556+
557+
expect(overlayContainerElement.querySelectorAll('cdk-dialog-container').length).toBe(2);
558+
expect(closeSpy).not.toHaveBeenCalled();
559+
560+
scrolledSubject.next();
561+
viewContainerFixture.detectChanges();
562+
flush();
563+
564+
expect(overlayContainerElement.querySelectorAll('cdk-dialog-container').length).toBe(0);
565+
expect(closeSpy).toHaveBeenCalledTimes(2);
566+
}));
567+
536568
it('should have the componentInstance available in the afterClosed callback', fakeAsync(() => {
537569
let dialogRef = dialog.open(PizzaMsg);
538570
let spy = jasmine.createSpy('afterClosed spy');

src/material/bottom-sheet/bottom-sheet.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ export class MatBottomSheet implements OnDestroy {
9595
..._config,
9696
// Disable closing since we need to sync it up to the animation ourselves.
9797
disableClose: true,
98+
// Disable closing on detachments so that we can sync up the animation.
99+
closeOnOverlayDetachments: false,
98100
maxWidth: '100%',
99101
container: MatBottomSheetContainer,
100102
scrollStrategy: _config.scrollStrategy || this._overlay.scrollStrategies.block(),

src/material/dialog/dialog.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ export abstract class _MatDialogBase<C extends _MatDialogContainerBase> implemen
170170
// We want to do the cleanup here, rather than the CDK service, because the CDK destroys
171171
// the dialogs immediately whereas we want it to wait for the animations to finish.
172172
closeOnDestroy: false,
173+
// Disable closing on detachments so that we can sync up the animation.
174+
closeOnOverlayDetachments: false,
173175
container: {
174176
type: this._dialogContainerType,
175177
providers: () => [

tools/public_api_guard/cdk/dialog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export class DialogConfig<D = unknown, R = unknown, C extends BasePortalOutlet =
127127
backdropClass?: string | string[];
128128
closeOnDestroy?: boolean;
129129
closeOnNavigation?: boolean;
130+
closeOnOverlayDetachments?: boolean;
130131
componentFactoryResolver?: ComponentFactoryResolver;
131132
container?: Type<C> | {
132133
type: Type<C>;

0 commit comments

Comments
 (0)