Skip to content

Commit de963dd

Browse files
committed
fix(dialog): aria-label not being removed if text content changes after init
Currently we set an `aria-label` on the `mat-dialog-close` button if it's an icon button or it doesn't have text, however it wasn't accounting for the case where the text comes in at a later point. Fixes #15542.
1 parent 177a433 commit de963dd

File tree

4 files changed

+76
-7
lines changed

4 files changed

+76
-7
lines changed

src/lib/dialog/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ ng_module(
1919
"//src/cdk/keycodes",
2020
"//src/cdk/overlay",
2121
"//src/cdk/portal",
22+
"//src/cdk/observers",
2223
"//src/lib/core",
2324
],
2425
)
@@ -50,6 +51,7 @@ ng_test_library(
5051
"//src/cdk/overlay",
5152
"//src/cdk/scrolling",
5253
"//src/cdk/testing",
54+
"//src/cdk/observers",
5355
":dialog",
5456
]
5557
)

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

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ import {
1414
Optional,
1515
SimpleChanges,
1616
ElementRef,
17+
ChangeDetectorRef,
18+
NgZone,
19+
OnDestroy,
1720
} from '@angular/core';
21+
import {ContentObserver} from '@angular/cdk/observers';
22+
import {of as observableOf, Subscription} from 'rxjs';
23+
import {startWith} from 'rxjs/operators';
1824
import {MatDialog} from './dialog';
1925
import {MatDialogRef} from './dialog-ref';
2026

@@ -33,7 +39,7 @@ let dialogElementUid = 0;
3339
'type': 'button', // Prevents accidental form submits.
3440
}
3541
})
36-
export class MatDialogClose implements OnInit, OnChanges {
42+
export class MatDialogClose implements OnInit, OnChanges, OnDestroy {
3743
/** Screenreader label for the button. */
3844
@Input('aria-label') ariaLabel: string = 'Close dialog';
3945

@@ -48,10 +54,21 @@ export class MatDialogClose implements OnInit, OnChanges {
4854
*/
4955
_hasAriaLabel?: boolean;
5056

57+
/** Subscription to changes in the button's content. */
58+
private _contentChanges = Subscription.EMPTY;
59+
5160
constructor(
5261
@Optional() public dialogRef: MatDialogRef<any>,
5362
private _elementRef: ElementRef<HTMLElement>,
54-
private _dialog: MatDialog) {}
63+
private _dialog: MatDialog,
64+
65+
/**
66+
* @deprecated @breaking-change 9.0.0
67+
* _contentObserver, _ngZone and _changeDetectorRef parameters to be made required.
68+
*/
69+
private _contentObserver?: ContentObserver,
70+
private _ngZone?: NgZone,
71+
private _changeDetectorRef?: ChangeDetectorRef) {}
5572

5673
ngOnInit() {
5774
if (!this.dialogRef) {
@@ -69,8 +86,23 @@ export class MatDialogClose implements OnInit, OnChanges {
6986
if (element.hasAttribute('mat-icon-button')) {
7087
this._hasAriaLabel = true;
7188
} else {
72-
const buttonTextContent = element.textContent;
73-
this._hasAriaLabel = !buttonTextContent || buttonTextContent.trim().length === 0;
89+
// @breaking-change 9.0.0 Remove null checks for _contentObserver, _ngZone and
90+
// _changeDetectorRef once they are made into required parameters.
91+
const contentChangesStream = this._contentObserver ?
92+
this._contentObserver.observe(this._elementRef) : observableOf<MutationRecord[]>();
93+
94+
// Toggle whether the button should have an aria-label, based on its content.
95+
this._contentChanges = contentChangesStream.pipe(startWith(null)).subscribe(() => {
96+
const buttonTextContent = element.textContent;
97+
this._hasAriaLabel = !buttonTextContent || buttonTextContent.trim().length === 0;
98+
99+
// The content observer runs outside the `NgZone` so we need to bring it back in.
100+
if (this._ngZone && this._changeDetectorRef) {
101+
this._ngZone.run(() => {
102+
this._changeDetectorRef!.markForCheck();
103+
});
104+
}
105+
});
74106
}
75107
}
76108
}
@@ -87,6 +119,10 @@ export class MatDialogClose implements OnInit, OnChanges {
87119
this._hasAriaLabel = !!changes['ariaLabel'].currentValue;
88120
}
89121
}
122+
123+
ngOnDestroy() {
124+
this._contentChanges.unsubscribe();
125+
}
90126
}
91127

92128
/**

src/lib/dialog/dialog.spec.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
MAT_DIALOG_DEFAULT_OPTIONS
3737
} from './index';
3838
import {Subject} from 'rxjs';
39+
import {MutationObserverFactory} from '@angular/cdk/observers';
3940

4041

4142
describe('MatDialog', () => {
@@ -47,15 +48,30 @@ describe('MatDialog', () => {
4748
let testViewContainerRef: ViewContainerRef;
4849
let viewContainerFixture: ComponentFixture<ComponentWithChildViewContainer>;
4950
let mockLocation: SpyLocation;
51+
let mutationCallbacks: Function[];
5052

5153
beforeEach(fakeAsync(() => {
54+
mutationCallbacks = [];
5255
TestBed.configureTestingModule({
5356
imports: [MatDialogModule, DialogTestModule],
5457
providers: [
5558
{provide: Location, useClass: SpyLocation},
5659
{provide: ScrollDispatcher, useFactory: () => ({
5760
scrolled: () => scrolledSubject.asObservable()
5861
})},
62+
{
63+
provide: MutationObserverFactory,
64+
useValue: {
65+
create: (callback: Function) => {
66+
mutationCallbacks.push(callback);
67+
68+
return {
69+
observe: () => {},
70+
disconnect: () => {}
71+
};
72+
}
73+
}
74+
}
5975
],
6076
});
6177

@@ -1148,6 +1164,14 @@ describe('MatDialog', () => {
11481164
expect(button.getAttribute('aria-label')).toBeTruthy();
11491165
}));
11501166

1167+
it('should not have an aria-label if a button has bound text', fakeAsync(() => {
1168+
let button = overlayContainerElement.querySelector('.close-with-text-binding')!;
1169+
mutationCallbacks.forEach(callback => callback());
1170+
viewContainerFixture.detectChanges();
1171+
1172+
expect(button.getAttribute('aria-label')).toBeFalsy();
1173+
}));
1174+
11511175
it('should not have an aria-label if a button has text', fakeAsync(() => {
11521176
let button = overlayContainerElement.querySelector('[mat-dialog-close]')!;
11531177
expect(button.getAttribute('aria-label')).toBeFalsy();
@@ -1511,6 +1535,7 @@ class PizzaMsg {
15111535
<button class="close-without-text" mat-dialog-close></button>
15121536
<button class="close-icon-button" mat-icon-button mat-dialog-close>exit</button>
15131537
<button class="close-with-true" [mat-dialog-close]="true">Close and return true</button>
1538+
<button class="close-with-text-binding" mat-dialog-close>{{closeButtonText}}</button>
15141539
<button
15151540
class="close-with-aria-label"
15161541
aria-label="Best close button ever"
@@ -1519,7 +1544,9 @@ class PizzaMsg {
15191544
</mat-dialog-actions>
15201545
`
15211546
})
1522-
class ContentElementDialog {}
1547+
class ContentElementDialog {
1548+
closeButtonText = 'Close';
1549+
}
15231550

15241551
@Component({
15251552
template: `
@@ -1531,6 +1558,7 @@ class ContentElementDialog {}
15311558
<button class="close-without-text" mat-dialog-close></button>
15321559
<button class="close-icon-button" mat-icon-button mat-dialog-close>exit</button>
15331560
<button class="close-with-true" [mat-dialog-close]="true">Close and return true</button>
1561+
<button class="close-with-text-binding" mat-dialog-close>{{closeButtonText}}</button>
15341562
<button
15351563
class="close-with-aria-label"
15361564
aria-label="Best close button ever"
@@ -1542,6 +1570,7 @@ class ContentElementDialog {}
15421570
})
15431571
class ComponentWithContentElementTemplateRef {
15441572
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
1573+
closeButtonText = 'Close';
15451574
}
15461575

15471576
@Component({

tools/public_api_guard/lib/dialog.d.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,16 @@ export declare const matDialogAnimations: {
4444
readonly slideDialog: AnimationTriggerMetadata;
4545
};
4646

47-
export declare class MatDialogClose implements OnInit, OnChanges {
47+
export declare class MatDialogClose implements OnInit, OnChanges, OnDestroy {
4848
_hasAriaLabel?: boolean;
4949
_matDialogClose: any;
5050
ariaLabel: string;
5151
dialogRef: MatDialogRef<any>;
5252
dialogResult: any;
53-
constructor(dialogRef: MatDialogRef<any>, _elementRef: ElementRef<HTMLElement>, _dialog: MatDialog);
53+
constructor(dialogRef: MatDialogRef<any>, _elementRef: ElementRef<HTMLElement>, _dialog: MatDialog,
54+
_contentObserver?: ContentObserver | undefined, _ngZone?: NgZone | undefined, _changeDetectorRef?: ChangeDetectorRef | undefined);
5455
ngOnChanges(changes: SimpleChanges): void;
56+
ngOnDestroy(): void;
5557
ngOnInit(): void;
5658
}
5759

0 commit comments

Comments
 (0)