Skip to content

Commit bf95d4a

Browse files
committed
fix(material/dialog): content directives picking up wrong ref for stacked mixed type dialogs
The dialog content directives try to resolve their closest dialog using DI, and if it fails (e.g. inside a template dialog), they fall back to resolving it through the DOM. This becomes problematic if the `ng-template` was declared inside another dialog component, because it'll pick up the declaration dialog, not the one that the button exists in. These changes remove the resolution using DI and switch to only going through the DOM. Fixes #21554.
1 parent 03485cd commit bf95d4a

File tree

5 files changed

+125
-43
lines changed

5 files changed

+125
-43
lines changed

src/material-experimental/mdc-dialog/dialog-content-directives.ts

+20-18
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
import {
1010
Directive,
1111
ElementRef,
12+
Inject,
1213
Input,
1314
OnChanges,
1415
OnInit,
15-
Optional,
1616
SimpleChanges,
1717
} from '@angular/core';
1818
import {_closeDialogVia} from '@angular/material/dialog';
@@ -47,23 +47,23 @@ export class MatDialogClose implements OnInit, OnChanges {
4747

4848
@Input('matDialogClose') _matDialogClose: any;
4949

50+
/** Reference to the dialog that the close button is placed inside of. */
51+
dialogRef: MatDialogRef<any>;
52+
5053
constructor(
51-
// The dialog title directive is always used in combination with a `MatDialogRef`.
52-
// tslint:disable-next-line: lightweight-tokens
53-
@Optional() public dialogRef: MatDialogRef<any>,
54+
/**
55+
* @deprecated `_dialogRef` parameter to be removed.
56+
* @breaking-change 12.0.0
57+
*/
58+
@Inject(ElementRef) _dialogRef: any,
5459
private _elementRef: ElementRef<HTMLElement>,
5560
private _dialog: MatDialog,
5661
) {}
5762

5863
ngOnInit() {
59-
if (!this.dialogRef) {
60-
// When this directive is included in a dialog via TemplateRef (rather than being
61-
// in a Component), the DialogRef isn't available via injection because embedded
62-
// views cannot be given a custom injector. Instead, we look up the DialogRef by
63-
// ID. This must occur in `onInit`, as the ID binding for the dialog container won't
64-
// be resolved at constructor time.
65-
this.dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs)!;
66-
}
64+
// Always resolve the closest dialog using the DOM, rather than DI, because DI won't work for
65+
// `TemplateRef`-based dialogs and it may give us wrong results for stacked ones (see #21554).
66+
this.dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs)!;
6767
}
6868

6969
ngOnChanges(changes: SimpleChanges) {
@@ -101,18 +101,20 @@ export class MatDialogClose implements OnInit, OnChanges {
101101
export class MatDialogTitle implements OnInit {
102102
@Input() id: string = `mat-mdc-dialog-title-${dialogElementUid++}`;
103103

104+
private _dialogRef: MatDialogRef<unknown>;
105+
104106
constructor(
105-
// The dialog title directive is always used in combination with a `MatDialogRef`.
106-
// tslint:disable-next-line: lightweight-tokens
107-
@Optional() private _dialogRef: MatDialogRef<any>,
107+
/**
108+
* @deprecated `_dialogRef` parameter to be removed.
109+
* @breaking-change 12.0.0
110+
*/
111+
@Inject(ElementRef) _dialogRef: any,
108112
private _elementRef: ElementRef<HTMLElement>,
109113
private _dialog: MatDialog,
110114
) {}
111115

112116
ngOnInit() {
113-
if (!this._dialogRef) {
114-
this._dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs)!;
115-
}
117+
this._dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs)!;
116118

117119
if (this._dialogRef) {
118120
Promise.resolve().then(() => {

src/material-experimental/mdc-dialog/dialog.spec.ts

+39
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ describe('MDC-based MatDialog', () => {
7070
DialogWithoutFocusableElements,
7171
DirectiveWithViewContainer,
7272
ComponentWithContentElementTemplateRef,
73+
MixedTypeStackedDialog,
7374
],
7475
providers: [
7576
{provide: Location, useClass: SpyLocation},
@@ -764,6 +765,25 @@ describe('MDC-based MatDialog', () => {
764765
},
765766
));
766767

768+
it('should close the correct dialog when stacked and using a template from another dialog', fakeAsync(() => {
769+
const dialogRef = dialog.open(MixedTypeStackedDialog);
770+
viewContainerFixture.detectChanges();
771+
772+
dialogRef.componentInstance.open();
773+
viewContainerFixture.detectChanges();
774+
775+
expect(overlayContainerElement.textContent).toContain('Bottom');
776+
expect(overlayContainerElement.textContent).toContain('Top');
777+
778+
(overlayContainerElement.querySelector('.close') as HTMLButtonElement).click();
779+
flushMicrotasks();
780+
viewContainerFixture.detectChanges();
781+
tick(500);
782+
783+
expect(overlayContainerElement.textContent).toContain('Bottom');
784+
expect(overlayContainerElement.textContent).not.toContain('Top');
785+
}));
786+
767787
describe('passing in data', () => {
768788
it('should be able to pass in data', () => {
769789
let config = {data: {stringParam: 'hello', dateParam: new Date()}};
@@ -2127,3 +2147,22 @@ class DialogWithoutFocusableElements {}
21272147
encapsulation: ViewEncapsulation.ShadowDom,
21282148
})
21292149
class ShadowDomComponent {}
2150+
2151+
@Component({
2152+
template: `
2153+
Bottom
2154+
<ng-template>
2155+
Top
2156+
<button class="close" mat-dialog-close>Close</button>
2157+
</ng-template>
2158+
`,
2159+
})
2160+
class MixedTypeStackedDialog {
2161+
@ViewChild(TemplateRef) template: TemplateRef<any>;
2162+
2163+
constructor(private _dialog: MatDialog) {}
2164+
2165+
open() {
2166+
this._dialog.open(this.template);
2167+
}
2168+
}

src/material/dialog/dialog-content-directives.ts

+22-21
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import {
1111
Input,
1212
OnChanges,
1313
OnInit,
14-
Optional,
1514
SimpleChanges,
1615
ElementRef,
16+
Inject,
1717
} from '@angular/core';
1818
import {MatDialog} from './dialog';
1919
import {_closeDialogVia, MatDialogRef} from './dialog-ref';
@@ -45,28 +45,27 @@ export class MatDialogClose implements OnInit, OnChanges {
4545

4646
@Input('matDialogClose') _matDialogClose: any;
4747

48+
/**
49+
* Reference to the containing dialog.
50+
* @deprecated `dialogRef` property to become private.
51+
* @breaking-change 13.0.0
52+
*/
53+
dialogRef: MatDialogRef<any>;
54+
4855
constructor(
4956
/**
50-
* Reference to the containing dialog.
51-
* @deprecated `dialogRef` property to become private.
52-
* @breaking-change 13.0.0
57+
* @deprecated `_dialogRef` parameter to be removed.
58+
* @breaking-change 12.0.0
5359
*/
54-
// The dialog title directive is always used in combination with a `MatDialogRef`.
55-
// tslint:disable-next-line: lightweight-tokens
56-
@Optional() public dialogRef: MatDialogRef<any>,
60+
@Inject(ElementRef) _dialogRef: any,
5761
private _elementRef: ElementRef<HTMLElement>,
5862
private _dialog: MatDialog,
5963
) {}
6064

6165
ngOnInit() {
62-
if (!this.dialogRef) {
63-
// When this directive is included in a dialog via TemplateRef (rather than being
64-
// in a Component), the DialogRef isn't available via injection because embedded
65-
// views cannot be given a custom injector. Instead, we look up the DialogRef by
66-
// ID. This must occur in `onInit`, as the ID binding for the dialog container won't
67-
// be resolved at constructor time.
68-
this.dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs)!;
69-
}
66+
// Always resolve the closest dialog using the DOM, rather than DI, because DI won't work for
67+
// `TemplateRef`-based dialogs and it may give us wrong results for stacked ones (see #21554).
68+
this.dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs)!;
7069
}
7170

7271
ngOnChanges(changes: SimpleChanges) {
@@ -105,18 +104,20 @@ export class MatDialogTitle implements OnInit {
105104
/** Unique id for the dialog title. If none is supplied, it will be auto-generated. */
106105
@Input() id: string = `mat-dialog-title-${dialogElementUid++}`;
107106

107+
private _dialogRef: MatDialogRef<unknown>;
108+
108109
constructor(
109-
// The dialog title directive is always used in combination with a `MatDialogRef`.
110-
// tslint:disable-next-line: lightweight-tokens
111-
@Optional() private _dialogRef: MatDialogRef<any>,
110+
/**
111+
* @deprecated `_dialogRef` parameter to be removed.
112+
* @breaking-change 12.0.0
113+
*/
114+
@Inject(ElementRef) _dialogRef: any,
112115
private _elementRef: ElementRef<HTMLElement>,
113116
private _dialog: MatDialog,
114117
) {}
115118

116119
ngOnInit() {
117-
if (!this._dialogRef) {
118-
this._dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs)!;
119-
}
120+
this._dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs)!;
120121

121122
if (this._dialogRef) {
122123
Promise.resolve().then(() => {

src/material/dialog/dialog.spec.ts

+39
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ describe('MatDialog', () => {
7070
DialogWithoutFocusableElements,
7171
DirectiveWithViewContainer,
7272
ComponentWithContentElementTemplateRef,
73+
MixedTypeStackedDialog,
7374
],
7475
providers: [
7576
{provide: Location, useClass: SpyLocation},
@@ -824,6 +825,25 @@ describe('MatDialog', () => {
824825
},
825826
));
826827

828+
it('should close the correct dialog when stacked and using a template from another dialog', fakeAsync(() => {
829+
const dialogRef = dialog.open(MixedTypeStackedDialog);
830+
viewContainerFixture.detectChanges();
831+
832+
dialogRef.componentInstance.open();
833+
viewContainerFixture.detectChanges();
834+
835+
expect(overlayContainerElement.textContent).toContain('Bottom');
836+
expect(overlayContainerElement.textContent).toContain('Top');
837+
838+
(overlayContainerElement.querySelector('.close') as HTMLButtonElement).click();
839+
flushMicrotasks();
840+
viewContainerFixture.detectChanges();
841+
tick(500);
842+
843+
expect(overlayContainerElement.textContent).toContain('Bottom');
844+
expect(overlayContainerElement.textContent).not.toContain('Top');
845+
}));
846+
827847
describe('passing in data', () => {
828848
it('should be able to pass in data', () => {
829849
const config = {
@@ -2174,3 +2194,22 @@ class DialogWithoutFocusableElements {}
21742194
encapsulation: ViewEncapsulation.ShadowDom,
21752195
})
21762196
class ShadowDomComponent {}
2197+
2198+
@Component({
2199+
template: `
2200+
Bottom
2201+
<ng-template>
2202+
Top
2203+
<button class="close" mat-dialog-close>Close</button>
2204+
</ng-template>
2205+
`,
2206+
})
2207+
class MixedTypeStackedDialog {
2208+
@ViewChild(TemplateRef) template: TemplateRef<any>;
2209+
2210+
constructor(private _dialog: MatDialog) {}
2211+
2212+
open() {
2213+
this._dialog.open(this.template);
2214+
}
2215+
}

tools/public_api_guard/material/dialog.md

+5-4
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export abstract class _MatDialogBase<C extends _MatDialogContainerBase> implemen
132132
// @public
133133
export class MatDialogClose implements OnInit, OnChanges {
134134
constructor(
135-
dialogRef: MatDialogRef<any>, _elementRef: ElementRef<HTMLElement>, _dialog: MatDialog);
135+
_dialogRef: any, _elementRef: ElementRef<HTMLElement>, _dialog: MatDialog);
136136
ariaLabel: string;
137137
// @deprecated
138138
dialogRef: MatDialogRef<any>;
@@ -149,7 +149,7 @@ export class MatDialogClose implements OnInit, OnChanges {
149149
// (undocumented)
150150
static ɵdir: i0.ɵɵDirectiveDeclaration<MatDialogClose, "[mat-dialog-close], [matDialogClose]", ["matDialogClose"], { "ariaLabel": "aria-label"; "type": "type"; "dialogResult": "mat-dialog-close"; "_matDialogClose": "matDialogClose"; }, {}, never>;
151151
// (undocumented)
152-
static ɵfac: i0.ɵɵFactoryDeclaration<MatDialogClose, [{ optional: true; }, null, null]>;
152+
static ɵfac: i0.ɵɵFactoryDeclaration<MatDialogClose, never>;
153153
}
154154

155155
// @public
@@ -277,14 +277,15 @@ export const enum MatDialogState {
277277

278278
// @public
279279
export class MatDialogTitle implements OnInit {
280-
constructor(_dialogRef: MatDialogRef<any>, _elementRef: ElementRef<HTMLElement>, _dialog: MatDialog);
280+
constructor(
281+
_dialogRef: any, _elementRef: ElementRef<HTMLElement>, _dialog: MatDialog);
281282
id: string;
282283
// (undocumented)
283284
ngOnInit(): void;
284285
// (undocumented)
285286
static ɵdir: i0.ɵɵDirectiveDeclaration<MatDialogTitle, "[mat-dialog-title], [matDialogTitle]", ["matDialogTitle"], { "id": "id"; }, {}, never>;
286287
// (undocumented)
287-
static ɵfac: i0.ɵɵFactoryDeclaration<MatDialogTitle, [{ optional: true; }, null, null]>;
288+
static ɵfac: i0.ɵɵFactoryDeclaration<MatDialogTitle, never>;
288289
}
289290

290291
// @public

0 commit comments

Comments
 (0)