Skip to content

Commit a6502df

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 f5bb3b0 commit a6502df

File tree

5 files changed

+131
-42
lines changed

5 files changed

+131
-42
lines changed

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

Lines changed: 22 additions & 20 deletions
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,22 +47,22 @@ 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-
private _elementRef: ElementRef<HTMLElement>,
55-
private _dialog: MatDialog) {}
54+
/**
55+
* @deprecated `_dialogRef` parameter to be removed.
56+
* @breaking-change 12.0.0
57+
*/
58+
@Inject(ElementRef) _dialogRef: any,
59+
private _elementRef: ElementRef<HTMLElement>,
60+
private _dialog: MatDialog) {}
5661

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

6868
ngOnChanges(changes: SimpleChanges) {
@@ -97,17 +97,19 @@ export class MatDialogClose implements OnInit, OnChanges {
9797
export class MatDialogTitle implements OnInit {
9898
@Input() id: string = `mat-mdc-dialog-title-${dialogElementUid++}`;
9999

100+
private _dialogRef: MatDialogRef<unknown>;
101+
100102
constructor(
101-
// The dialog title directive is always used in combination with a `MatDialogRef`.
102-
// tslint:disable-next-line: lightweight-tokens
103-
@Optional() private _dialogRef: MatDialogRef<any>,
103+
/**
104+
* @deprecated `_dialogRef` parameter to be removed.
105+
* @breaking-change 12.0.0
106+
*/
107+
@Inject(ElementRef) _dialogRef: any,
104108
private _elementRef: ElementRef<HTMLElement>,
105109
private _dialog: MatDialog) {}
106110

107111
ngOnInit() {
108-
if (!this._dialogRef) {
109-
this._dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs)!;
110-
}
112+
this._dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs)!;
111113

112114
if (this._dialogRef) {
113115
Promise.resolve().then(() => {

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,26 @@ describe('MDC-based MatDialog', () => {
721721
expect(resolver.resolveComponentFactory).toHaveBeenCalled();
722722
}));
723723

724+
it('should close the correct dialog when stacked and using a template from another dialog',
725+
fakeAsync(() => {
726+
const dialogRef = dialog.open(MixedTypeStackedDialog);
727+
viewContainerFixture.detectChanges();
728+
729+
dialogRef.componentInstance.open();
730+
viewContainerFixture.detectChanges();
731+
732+
expect(overlayContainerElement.textContent).toContain('Bottom');
733+
expect(overlayContainerElement.textContent).toContain('Top');
734+
735+
(overlayContainerElement.querySelector('.close') as HTMLButtonElement).click();
736+
flushMicrotasks();
737+
viewContainerFixture.detectChanges();
738+
tick(500);
739+
740+
expect(overlayContainerElement.textContent).toContain('Bottom');
741+
expect(overlayContainerElement.textContent).not.toContain('Top');
742+
}));
743+
724744
describe('passing in data', () => {
725745
it('should be able to pass in data', () => {
726746
let config = {data: {stringParam: 'hello', dateParam: new Date()}};
@@ -1852,6 +1872,26 @@ class DialogWithInjectedData {
18521872
class DialogWithoutFocusableElements {
18531873
}
18541874

1875+
@Component({
1876+
template: `
1877+
Bottom
1878+
<ng-template>
1879+
Top
1880+
<button class="close" mat-dialog-close>Close</button>
1881+
</ng-template>
1882+
`,
1883+
})
1884+
class MixedTypeStackedDialog {
1885+
@ViewChild(TemplateRef) template: TemplateRef<any>;
1886+
1887+
constructor(private _dialog: MatDialog) {}
1888+
1889+
open() {
1890+
this._dialog.open(this.template);
1891+
}
1892+
}
1893+
1894+
18551895
// Create a real (non-test) NgModule as a workaround for
18561896
// https://github.com/angular/angular/issues/10760
18571897
const TEST_DIRECTIVES = [
@@ -1864,6 +1904,7 @@ const TEST_DIRECTIVES = [
18641904
DialogWithInjectedData,
18651905
DialogWithoutFocusableElements,
18661906
ComponentWithContentElementTemplateRef,
1907+
MixedTypeStackedDialog,
18671908
];
18681909

18691910
@NgModule({
@@ -1877,6 +1918,7 @@ const TEST_DIRECTIVES = [
18771918
ContentElementDialog,
18781919
DialogWithInjectedData,
18791920
DialogWithoutFocusableElements,
1921+
MixedTypeStackedDialog,
18801922
],
18811923
})
18821924
class DialogTestModule {

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

Lines changed: 20 additions & 18 deletions
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,22 +45,22 @@ export class MatDialogClose implements OnInit, OnChanges {
4545

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

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

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

6666
ngOnChanges(changes: SimpleChanges) {
@@ -95,17 +95,19 @@ export class MatDialogClose implements OnInit, OnChanges {
9595
export class MatDialogTitle implements OnInit {
9696
@Input() id: string = `mat-dialog-title-${dialogElementUid++}`;
9797

98+
private _dialogRef: MatDialogRef<unknown>;
99+
98100
constructor(
99-
// The dialog title directive is always used in combination with a `MatDialogRef`.
100-
// tslint:disable-next-line: lightweight-tokens
101-
@Optional() private _dialogRef: MatDialogRef<any>,
101+
/**
102+
* @deprecated `_dialogRef` parameter to be removed.
103+
* @breaking-change 12.0.0
104+
*/
105+
@Inject(ElementRef) _dialogRef: any,
102106
private _elementRef: ElementRef<HTMLElement>,
103107
private _dialog: MatDialog) {}
104108

105109
ngOnInit() {
106-
if (!this._dialogRef) {
107-
this._dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs)!;
108-
}
110+
this._dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs)!;
109111

110112
if (this._dialogRef) {
111113
Promise.resolve().then(() => {

src/material/dialog/dialog.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,26 @@ describe('MatDialog', () => {
780780
expect(resolver.resolveComponentFactory).toHaveBeenCalled();
781781
}));
782782

783+
it('should close the correct dialog when stacked and using a template from another dialog',
784+
fakeAsync(() => {
785+
const dialogRef = dialog.open(MixedTypeStackedDialog);
786+
viewContainerFixture.detectChanges();
787+
788+
dialogRef.componentInstance.open();
789+
viewContainerFixture.detectChanges();
790+
791+
expect(overlayContainerElement.textContent).toContain('Bottom');
792+
expect(overlayContainerElement.textContent).toContain('Top');
793+
794+
(overlayContainerElement.querySelector('.close') as HTMLButtonElement).click();
795+
flushMicrotasks();
796+
viewContainerFixture.detectChanges();
797+
tick(500);
798+
799+
expect(overlayContainerElement.textContent).toContain('Bottom');
800+
expect(overlayContainerElement.textContent).not.toContain('Top');
801+
}));
802+
783803
describe('passing in data', () => {
784804
it('should be able to pass in data', () => {
785805
let config = {
@@ -1929,6 +1949,25 @@ class DialogWithInjectedData {
19291949
@Component({template: '<p>Pasta</p>'})
19301950
class DialogWithoutFocusableElements {}
19311951

1952+
@Component({
1953+
template: `
1954+
Bottom
1955+
<ng-template>
1956+
Top
1957+
<button class="close" mat-dialog-close>Close</button>
1958+
</ng-template>
1959+
`,
1960+
})
1961+
class MixedTypeStackedDialog {
1962+
@ViewChild(TemplateRef) template: TemplateRef<any>;
1963+
1964+
constructor(private _dialog: MatDialog) {}
1965+
1966+
open() {
1967+
this._dialog.open(this.template);
1968+
}
1969+
}
1970+
19321971
// Create a real (non-test) NgModule as a workaround for
19331972
// https://github.com/angular/angular/issues/10760
19341973
const TEST_DIRECTIVES = [
@@ -1941,6 +1980,7 @@ const TEST_DIRECTIVES = [
19411980
DialogWithInjectedData,
19421981
DialogWithoutFocusableElements,
19431982
ComponentWithContentElementTemplateRef,
1983+
MixedTypeStackedDialog,
19441984
];
19451985

19461986
@NgModule({
@@ -1954,6 +1994,7 @@ const TEST_DIRECTIVES = [
19541994
ContentElementDialog,
19551995
DialogWithInjectedData,
19561996
DialogWithoutFocusableElements,
1997+
MixedTypeStackedDialog,
19571998
],
19581999
})
19592000
class DialogTestModule { }

tools/public_api_guard/material/dialog.d.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,13 @@ export declare class MatDialogClose implements OnInit, OnChanges {
8888
dialogRef: MatDialogRef<any>;
8989
dialogResult: any;
9090
type: 'submit' | 'button' | 'reset';
91-
constructor(dialogRef: MatDialogRef<any>, _elementRef: ElementRef<HTMLElement>, _dialog: MatDialog);
91+
constructor(
92+
_dialogRef: any, _elementRef: ElementRef<HTMLElement>, _dialog: MatDialog);
9293
_onButtonClick(event: MouseEvent): void;
9394
ngOnChanges(changes: SimpleChanges): void;
9495
ngOnInit(): void;
9596
static ɵdir: i0.ɵɵDirectiveDefWithMeta<MatDialogClose, "[mat-dialog-close], [matDialogClose]", ["matDialogClose"], { "ariaLabel": "aria-label"; "type": "type"; "dialogResult": "mat-dialog-close"; "_matDialogClose": "matDialogClose"; }, {}, never>;
96-
static ɵfac: i0.ɵɵFactoryDef<MatDialogClose, [{ optional: true; }, null, null]>;
97+
static ɵfac: i0.ɵɵFactoryDef<MatDialogClose, never>;
9798
}
9899

99100
export declare class MatDialogConfig<D = any> {
@@ -169,10 +170,11 @@ export declare const enum MatDialogState {
169170

170171
export declare class MatDialogTitle implements OnInit {
171172
id: string;
172-
constructor(_dialogRef: MatDialogRef<any>, _elementRef: ElementRef<HTMLElement>, _dialog: MatDialog);
173+
constructor(
174+
_dialogRef: any, _elementRef: ElementRef<HTMLElement>, _dialog: MatDialog);
173175
ngOnInit(): void;
174176
static ɵdir: i0.ɵɵDirectiveDefWithMeta<MatDialogTitle, "[mat-dialog-title], [matDialogTitle]", ["matDialogTitle"], { "id": "id"; }, {}, never>;
175-
static ɵfac: i0.ɵɵFactoryDef<MatDialogTitle, [{ optional: true; }, null, null]>;
177+
static ɵfac: i0.ɵɵFactoryDef<MatDialogTitle, never>;
176178
}
177179

178180
export declare function throwMatDialogContentAlreadyAttachedError(): void;

0 commit comments

Comments
 (0)