Skip to content

Commit aee984a

Browse files
crisbetojelbourn
authored andcommitted
fix(dialog): set aria-labelledby based on the md-dialog-title (#5178)
* [Based on the accessibility guidelines](https://www.w3.org/TR/wai-aria-practices-1.1/#dialog_modal), these changes add the `aria-labelledby` to dialog that use `md-dialog-title`, which causes the screen reader to read out the title. E.g. before NVDA would read out "Dialog", but now it reads out "Neptune dialog".
1 parent 61d979e commit aee984a

File tree

6 files changed

+77
-24
lines changed

6 files changed

+77
-24
lines changed

src/lib/dialog/dialog-container.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export function throwMdDialogContentAlreadyAttachedError() {
6666
host: {
6767
'class': 'mat-dialog-container',
6868
'[attr.role]': '_config?.role',
69+
'[attr.aria-labelledby]': '_ariaLabelledBy',
6970
'[@slideDialog]': '_state',
7071
'(@slideDialog.done)': '_onAnimationDone($event)',
7172
},
@@ -92,6 +93,9 @@ export class MdDialogContainer extends BasePortalHost {
9293
/** Emits the current animation state whenever it changes. */
9394
_onAnimationStateChange = new EventEmitter<AnimationEvent>();
9495

96+
/** ID of the element that should be considered as the dialog's label. */
97+
_ariaLabelledBy: string | null = null;
98+
9599
constructor(
96100
private _ngZone: NgZone,
97101
private _elementRef: ElementRef,

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

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

9-
import {Directive, Input} from '@angular/core';
9+
import {Directive, Input, Optional, OnInit} from '@angular/core';
1010
import {MdDialogRef} from './dialog-ref';
11+
import {MdDialogContainer} from './dialog-container';
1112

13+
/** Counter used to generate unique IDs for dialog elements. */
14+
let dialogElementUid = 0;
1215

1316
/**
1417
* Button that will close the current dialog.
@@ -40,9 +43,22 @@ export class MdDialogClose {
4043
*/
4144
@Directive({
4245
selector: '[md-dialog-title], [mat-dialog-title], [mdDialogTitle], [matDialogTitle]',
43-
host: {'class': 'mat-dialog-title'},
46+
host: {
47+
'class': 'mat-dialog-title',
48+
'[id]': 'id',
49+
},
4450
})
45-
export class MdDialogTitle { }
51+
export class MdDialogTitle implements OnInit {
52+
@Input() id = `md-dialog-title-${dialogElementUid++}`;
53+
54+
constructor(@Optional() private _container: MdDialogContainer) { }
55+
56+
ngOnInit() {
57+
if (this._container && !this._container._ariaLabelledBy) {
58+
Promise.resolve().then(() => this._container._ariaLabelledBy = this.id);
59+
}
60+
}
61+
}
4662

4763

4864
/**

src/lib/dialog/dialog-injector.ts

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

9-
import {Injector, InjectionToken} from '@angular/core';
9+
import {Injector} from '@angular/core';
1010
import {MdDialogRef} from './dialog-ref';
11-
12-
export const MD_DIALOG_DATA = new InjectionToken<any>('MdDialogData');
11+
import {MdDialogContainer} from './dialog-container';
1312

1413
/** Custom injector type specifically for instantiating components with a dialog. */
1514
export class DialogInjector implements Injector {
1615
constructor(
1716
private _parentInjector: Injector,
18-
private _dialogRef: MdDialogRef<any>,
19-
private _data: any) { }
17+
private _customTokens: WeakMap<any, any>) { }
2018

2119
get(token: any, notFoundValue?: any): any {
22-
if (token === MdDialogRef) {
23-
return this._dialogRef;
24-
}
20+
const value = this._customTokens.get(token);
2521

26-
if (token === MD_DIALOG_DATA) {
27-
return this._data;
22+
if (typeof value !== 'undefined') {
23+
return value;
2824
}
2925

3026
return this._parentInjector.get<any>(token, notFoundValue);

src/lib/dialog/dialog.spec.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,10 @@ import {NoopAnimationsModule} from '@angular/platform-browser/animations';
2121
import {Location} from '@angular/common';
2222
import {SpyLocation} from '@angular/common/testing';
2323
import {MdDialogModule} from './index';
24-
import {MdDialog} from './dialog';
24+
import {MdDialog, MD_DIALOG_DATA} from './dialog';
2525
import {MdDialogContainer} from './dialog-container';
2626
import {OverlayContainer, ESCAPE} from '../core';
2727
import {MdDialogRef} from './dialog-ref';
28-
import {MD_DIALOG_DATA} from './dialog-injector';
2928
import {dispatchKeyboardEvent} from '../core/testing/dispatch-events';
3029

3130

@@ -669,6 +668,17 @@ describe('MdDialog', () => {
669668
});
670669
}));
671670

671+
it('should set the aria-labelled by attribute to the id of the title', async(() => {
672+
let title = overlayContainerElement.querySelector('[md-dialog-title]');
673+
let container = overlayContainerElement.querySelector('md-dialog-container');
674+
675+
viewContainerFixture.whenStable().then(() => {
676+
expect(title.id).toBeTruthy('Expected title element to have an id.');
677+
expect(container.getAttribute('aria-labelledby'))
678+
.toBe(title.id, 'Expected the aria-labelledby to match the title id.');
679+
});
680+
}));
681+
672682
});
673683
});
674684

src/lib/dialog/dialog.ts

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

9-
import {Injector, ComponentRef, Injectable, Optional, SkipSelf, TemplateRef} from '@angular/core';
9+
import {
10+
Injector,
11+
InjectionToken,
12+
ComponentRef,
13+
Injectable,
14+
Optional,
15+
SkipSelf,
16+
TemplateRef,
17+
} from '@angular/core';
1018
import {Location} from '@angular/common';
1119
import {Observable} from 'rxjs/Observable';
1220
import {Subject} from 'rxjs/Subject';
@@ -25,6 +33,8 @@ import {MdDialogRef} from './dialog-ref';
2533
import {MdDialogContainer} from './dialog-container';
2634
import {TemplatePortal} from '../core/portal/portal';
2735

36+
export const MD_DIALOG_DATA = new InjectionToken<any>('MdDialogData');
37+
2838

2939
/**
3040
* Service to open Material Design modal dialogs.
@@ -187,17 +197,12 @@ export class MdDialog {
187197
});
188198
}
189199

190-
// We create an injector specifically for the component we're instantiating so that it can
191-
// inject the MdDialogRef. This allows a component loaded inside of a dialog to close itself
192-
// and, optionally, to return a value.
193-
let userInjector = config && config.viewContainerRef && config.viewContainerRef.injector;
194-
let dialogInjector = new DialogInjector(userInjector || this._injector, dialogRef, config.data);
195-
196200
if (componentOrTemplateRef instanceof TemplateRef) {
197201
dialogContainer.attachTemplatePortal(new TemplatePortal(componentOrTemplateRef, null));
198202
} else {
203+
let injector = this._createInjector<T>(config, dialogRef, dialogContainer);
199204
let contentRef = dialogContainer.attachComponentPortal(
200-
new ComponentPortal(componentOrTemplateRef, null, dialogInjector));
205+
new ComponentPortal(componentOrTemplateRef, null, injector));
201206
dialogRef.componentInstance = contentRef.instance;
202207
}
203208

@@ -208,6 +213,29 @@ export class MdDialog {
208213
return dialogRef;
209214
}
210215

216+
/**
217+
* Creates a custom injector to be used inside the dialog. This allows a component loaded inside
218+
* of a dialog to close itself and, optionally, to return a value.
219+
* @param config Config object that is used to construct the dialog.
220+
* @param dialogRef Reference to the dialog.
221+
* @param container Dialog container element that wraps all of the contents.
222+
* @returns The custom injector that can be used inside the dialog.
223+
*/
224+
private _createInjector<T>(
225+
config: MdDialogConfig,
226+
dialogRef: MdDialogRef<T>,
227+
dialogContainer: MdDialogContainer): DialogInjector {
228+
229+
let userInjector = config && config.viewContainerRef && config.viewContainerRef.injector;
230+
let injectionTokens = new WeakMap();
231+
232+
injectionTokens.set(MdDialogRef, dialogRef);
233+
injectionTokens.set(MdDialogContainer, dialogContainer);
234+
injectionTokens.set(MD_DIALOG_DATA, config.data);
235+
236+
return new DialogInjector(userInjector || this._injector, injectionTokens);
237+
}
238+
211239
/**
212240
* Removes a dialog from the array of open dialogs.
213241
* @param dialogRef Dialog to be removed.

src/lib/dialog/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,3 @@ export * from './dialog-container';
5959
export * from './dialog-content-directives';
6060
export * from './dialog-config';
6161
export * from './dialog-ref';
62-
export {MD_DIALOG_DATA} from './dialog-injector';

0 commit comments

Comments
 (0)