Skip to content

Commit d94efbe

Browse files
committed
feat(dialog): support using dialog content directives with template dialogs
Previously the `matDialogClose`, `matDialogTitle` etc. directives would only work correctly inside component dialogs, because using DI to get the dialog ref doesn't work inside template dialogs. These changes add a fallback that finds the dialog ref based on the id of the closest dialog container. Fixes #5412.
1 parent 60b0625 commit d94efbe

File tree

4 files changed

+153
-61
lines changed

4 files changed

+153
-61
lines changed

src/lib/dialog/dialog-container.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export function throwMatDialogContentAlreadyAttachedError() {
6060
host: {
6161
'class': 'mat-dialog-container',
6262
'tabindex': '-1',
63+
'[attr.id]': '_id',
6364
'[attr.role]': '_config?.role',
6465
'[attr.aria-labelledby]': '_config?.ariaLabel ? null : _ariaLabelledBy',
6566
'[attr.aria-label]': '_config?.ariaLabel',
@@ -91,6 +92,9 @@ export class MatDialogContainer extends BasePortalOutlet {
9192
/** ID of the element that should be considered as the dialog's label. */
9293
_ariaLabelledBy: string | null = null;
9394

95+
/** ID for the container DOM element. */
96+
_id: string;
97+
9498
constructor(
9599
private _elementRef: ElementRef,
96100
private _focusTrapFactory: FocusTrapFactory,

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

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,17 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Directive, Input, OnChanges, OnInit, Optional, SimpleChanges} from '@angular/core';
9+
import {
10+
Directive,
11+
Input,
12+
OnChanges,
13+
OnInit,
14+
Optional,
15+
SimpleChanges,
16+
ElementRef,
17+
} from '@angular/core';
18+
import {MatDialog} from './dialog';
1019
import {MatDialogRef} from './dialog-ref';
11-
import {MatDialogContainer} from './dialog-container';
1220

1321
/** Counter used to generate unique IDs for dialog elements. */
1422
let dialogElementUid = 0;
@@ -25,7 +33,7 @@ let dialogElementUid = 0;
2533
'type': 'button', // Prevents accidental form submits.
2634
}
2735
})
28-
export class MatDialogClose implements OnChanges {
36+
export class MatDialogClose implements OnInit, OnChanges {
2937
/** Screenreader label for the button. */
3038
@Input('aria-label') ariaLabel: string = 'Close dialog';
3139

@@ -34,7 +42,21 @@ export class MatDialogClose implements OnChanges {
3442

3543
@Input('matDialogClose') _matDialogClose: any;
3644

37-
constructor(public dialogRef: MatDialogRef<any>) { }
45+
constructor(
46+
@Optional() public dialogRef: MatDialogRef<any>,
47+
private _elementRef: ElementRef,
48+
private _dialog: MatDialog) {}
49+
50+
ngOnInit() {
51+
if (!this.dialogRef) {
52+
// When this directive is included in a dialog via TemplateRef (rather than being
53+
// in a Component), the DialogRef isn't available via injection because embedded
54+
// views cannot be given a custom injector. Instead, we look up the DialogRef by
55+
// ID. This must occur in `onInit`, as the ID binding for the dialog container won't
56+
// be resolved at constructor time.
57+
this.dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs)!;
58+
}
59+
}
3860

3961
ngOnChanges(changes: SimpleChanges) {
4062
const proxiedChange = changes._matDialogClose || changes._matDialogCloseResult;
@@ -59,11 +81,18 @@ export class MatDialogClose implements OnChanges {
5981
export class MatDialogTitle implements OnInit {
6082
@Input() id = `mat-dialog-title-${dialogElementUid++}`;
6183

62-
constructor(@Optional() private _container: MatDialogContainer) { }
84+
constructor(
85+
@Optional() private _dialogRef: MatDialogRef<any>,
86+
private _elementRef: ElementRef,
87+
private _dialog: MatDialog) {}
6388

6489
ngOnInit() {
65-
if (this._container && !this._container._ariaLabelledBy) {
66-
Promise.resolve().then(() => this._container._ariaLabelledBy = this.id);
90+
if (!this._dialogRef) {
91+
this._dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs)!;
92+
}
93+
94+
if (this._dialogRef && !this._dialogRef._containerInstance._ariaLabelledBy) {
95+
Promise.resolve().then(() => this._dialogRef._containerInstance._ariaLabelledBy = this.id);
6796
}
6897
}
6998
}
@@ -76,7 +105,7 @@ export class MatDialogTitle implements OnInit {
76105
selector: `[mat-dialog-content], mat-dialog-content, [matDialogContent]`,
77106
host: {'class': 'mat-dialog-content'}
78107
})
79-
export class MatDialogContent { }
108+
export class MatDialogContent {}
80109

81110

82111
/**
@@ -87,4 +116,20 @@ export class MatDialogContent { }
87116
selector: `[mat-dialog-actions], mat-dialog-actions, [matDialogActions]`,
88117
host: {'class': 'mat-dialog-actions'}
89118
})
90-
export class MatDialogActions { }
119+
export class MatDialogActions {}
120+
121+
122+
/**
123+
* Finds the closest MatDialogRef to an element by looking at the DOM.
124+
* @param element Element relative to which to look for a dialog.
125+
* @param openDialogs References to the currently-open dialogs.
126+
*/
127+
function getClosestDialog(element: ElementRef, openDialogs: MatDialogRef<any>[]) {
128+
let parent: HTMLElement | null = element.nativeElement.parentElement;
129+
130+
while (parent && !parent.classList.contains('mat-dialog-container')) {
131+
parent = parent.parentElement;
132+
}
133+
134+
return parent ? openDialogs.find(dialog => dialog.id === parent!.id) : null;
135+
}

src/lib/dialog/dialog-ref.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,13 @@ export class MatDialogRef<T, R = any> {
5050

5151
constructor(
5252
private _overlayRef: OverlayRef,
53-
private _containerInstance: MatDialogContainer,
53+
public _containerInstance: MatDialogContainer,
5454
location?: Location,
5555
readonly id: string = `mat-dialog-${uniqueId++}`) {
5656

57+
// Pass the id along to the container.
58+
_containerInstance._id = id;
59+
5760
// Emit when opening animation completes
5861
_containerInstance._animationStateChanged.pipe(
5962
filter(event => event.phaseName === 'done' && event.toState === 'enter'),

src/lib/dialog/dialog.spec.ts

Lines changed: 91 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -958,72 +958,88 @@ describe('MatDialog', () => {
958958
});
959959

960960
describe('dialog content elements', () => {
961-
let dialogRef: MatDialogRef<ContentElementDialog>;
961+
let dialogRef: MatDialogRef<any>;
962962

963-
beforeEach(fakeAsync(() => {
964-
dialogRef = dialog.open(ContentElementDialog, {viewContainerRef: testViewContainerRef});
965-
viewContainerFixture.detectChanges();
966-
flush();
967-
}));
968-
969-
it('should close the dialog when clicking on the close button', fakeAsync(() => {
970-
expect(overlayContainerElement.querySelectorAll('.mat-dialog-container').length).toBe(1);
963+
describe('inside component dialog', () => {
964+
beforeEach(fakeAsync(() => {
965+
dialogRef = dialog.open(ContentElementDialog, {viewContainerRef: testViewContainerRef});
966+
viewContainerFixture.detectChanges();
967+
flush();
968+
}));
971969

972-
(overlayContainerElement.querySelector('button[mat-dialog-close]') as HTMLElement).click();
973-
viewContainerFixture.detectChanges();
974-
flush();
970+
runContentElementTests();
971+
});
975972

976-
expect(overlayContainerElement.querySelectorAll('.mat-dialog-container').length).toBe(0);
977-
}));
973+
describe('inside template portal', () => {
974+
beforeEach(fakeAsync(() => {
975+
const fixture = TestBed.createComponent(ComponentWithContentElementTemplateRef);
976+
fixture.detectChanges();
978977

979-
it('should not close the dialog if [mat-dialog-close] is applied on a non-button node', () => {
980-
expect(overlayContainerElement.querySelectorAll('.mat-dialog-container').length).toBe(1);
978+
dialogRef = dialog.open(fixture.componentInstance.templateRef, {
979+
viewContainerRef: testViewContainerRef
980+
});
981981

982-
(overlayContainerElement.querySelector('div[mat-dialog-close]') as HTMLElement).click();
982+
viewContainerFixture.detectChanges();
983+
flush();
984+
}));
983985

984-
expect(overlayContainerElement.querySelectorAll('.mat-dialog-container').length).toBe(1);
986+
runContentElementTests();
985987
});
986988

987-
it('should allow for a user-specified aria-label on the close button', fakeAsync(() => {
988-
let button = overlayContainerElement.querySelector('button[mat-dialog-close]')!;
989+
function runContentElementTests() {
990+
it('should close the dialog when clicking on the close button', fakeAsync(() => {
991+
expect(overlayContainerElement.querySelectorAll('.mat-dialog-container').length).toBe(1);
989992

990-
dialogRef.componentInstance.closeButtonAriaLabel = 'Best close button ever';
991-
viewContainerFixture.detectChanges();
992-
flush();
993+
(overlayContainerElement.querySelector('button[mat-dialog-close]') as HTMLElement).click();
994+
viewContainerFixture.detectChanges();
995+
flush();
993996

994-
expect(button.getAttribute('aria-label')).toBe('Best close button ever');
995-
}));
997+
expect(overlayContainerElement.querySelectorAll('.mat-dialog-container').length).toBe(0);
998+
}));
996999

997-
it('should override the "type" attribute of the close button', () => {
998-
let button = overlayContainerElement.querySelector('button[mat-dialog-close]')!;
1000+
it('should not close if [mat-dialog-close] is applied on a non-button node', () => {
1001+
expect(overlayContainerElement.querySelectorAll('.mat-dialog-container').length).toBe(1);
9991002

1000-
expect(button.getAttribute('type')).toBe('button');
1001-
});
1003+
(overlayContainerElement.querySelector('div[mat-dialog-close]') as HTMLElement).click();
10021004

1003-
it('should return the [mat-dialog-close] result when clicking the close button',
1004-
fakeAsync(() => {
1005-
let afterCloseCallback = jasmine.createSpy('afterClose callback');
1006-
dialogRef.afterClosed().subscribe(afterCloseCallback);
1007-
1008-
(overlayContainerElement.querySelector('button.close-with-true') as HTMLElement).click();
1009-
viewContainerFixture.detectChanges();
1010-
flush();
1005+
expect(overlayContainerElement.querySelectorAll('.mat-dialog-container').length).toBe(1);
1006+
});
10111007

1012-
expect(afterCloseCallback).toHaveBeenCalledWith(true);
1008+
it('should allow for a user-specified aria-label on the close button', fakeAsync(() => {
1009+
let button = overlayContainerElement.querySelector('.close-with-aria-label')!;
1010+
expect(button.getAttribute('aria-label')).toBe('Best close button ever');
10131011
}));
10141012

1015-
it('should set the aria-labelledby attribute to the id of the title', fakeAsync(() => {
1016-
let title = overlayContainerElement.querySelector('[mat-dialog-title]')!;
1017-
let container = overlayContainerElement.querySelector('mat-dialog-container')!;
1013+
it('should override the "type" attribute of the close button', () => {
1014+
let button = overlayContainerElement.querySelector('button[mat-dialog-close]')!;
10181015

1019-
flush();
1020-
viewContainerFixture.detectChanges();
1016+
expect(button.getAttribute('type')).toBe('button');
1017+
});
10211018

1022-
expect(title.id).toBeTruthy('Expected title element to have an id.');
1023-
expect(container.getAttribute('aria-labelledby'))
1024-
.toBe(title.id, 'Expected the aria-labelledby to match the title id.');
1025-
}));
1019+
it('should return the [mat-dialog-close] result when clicking the close button',
1020+
fakeAsync(() => {
1021+
let afterCloseCallback = jasmine.createSpy('afterClose callback');
1022+
dialogRef.afterClosed().subscribe(afterCloseCallback);
1023+
1024+
(overlayContainerElement.querySelector('button.close-with-true') as HTMLElement).click();
1025+
viewContainerFixture.detectChanges();
1026+
flush();
1027+
1028+
expect(afterCloseCallback).toHaveBeenCalledWith(true);
1029+
}));
1030+
1031+
it('should set the aria-labelledby attribute to the id of the title', fakeAsync(() => {
1032+
let title = overlayContainerElement.querySelector('[mat-dialog-title]')!;
1033+
let container = overlayContainerElement.querySelector('mat-dialog-container')!;
10261034

1035+
flush();
1036+
viewContainerFixture.detectChanges();
1037+
1038+
expect(title.id).toBeTruthy('Expected title element to have an id.');
1039+
expect(container.getAttribute('aria-labelledby'))
1040+
.toBe(title.id, 'Expected the aria-labelledby to match the title id.');
1041+
}));
1042+
}
10271043
});
10281044

10291045
describe('aria-label', () => {
@@ -1277,14 +1293,37 @@ class PizzaMsg {
12771293
<h1 mat-dialog-title>This is the title</h1>
12781294
<mat-dialog-content>Lorem ipsum dolor sit amet.</mat-dialog-content>
12791295
<mat-dialog-actions>
1280-
<button mat-dialog-close [aria-label]="closeButtonAriaLabel">Close</button>
1296+
<button mat-dialog-close>Close</button>
12811297
<button class="close-with-true" [mat-dialog-close]="true">Close and return true</button>
1298+
<button
1299+
class="close-with-aria-label"
1300+
aria-label="Best close button ever"
1301+
[mat-dialog-close]="true">Close</button>
12821302
<div mat-dialog-close>Should not close</div>
12831303
</mat-dialog-actions>
12841304
`
12851305
})
1286-
class ContentElementDialog {
1287-
closeButtonAriaLabel: string;
1306+
class ContentElementDialog {}
1307+
1308+
@Component({
1309+
template: `
1310+
<ng-template>
1311+
<h1 mat-dialog-title>This is the title</h1>
1312+
<mat-dialog-content>Lorem ipsum dolor sit amet.</mat-dialog-content>
1313+
<mat-dialog-actions>
1314+
<button mat-dialog-close>Close</button>
1315+
<button class="close-with-true" [mat-dialog-close]="true">Close and return true</button>
1316+
<button
1317+
class="close-with-aria-label"
1318+
aria-label="Best close button ever"
1319+
[mat-dialog-close]="true">Close</button>
1320+
<div mat-dialog-close>Should not close</div>
1321+
</mat-dialog-actions>
1322+
</ng-template>
1323+
`
1324+
})
1325+
class ComponentWithContentElementTemplateRef {
1326+
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
12881327
}
12891328

12901329
@Component({
@@ -1314,7 +1353,8 @@ const TEST_DIRECTIVES = [
13141353
ComponentWithOnPushViewContainer,
13151354
ContentElementDialog,
13161355
DialogWithInjectedData,
1317-
DialogWithoutFocusableElements
1356+
DialogWithoutFocusableElements,
1357+
ComponentWithContentElementTemplateRef,
13181358
];
13191359

13201360
@NgModule({

0 commit comments

Comments
 (0)