mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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".
This commit is contained in:
committed by
Jeremy Elbourn
parent
61d979e66e
commit
aee984a5e5
@@ -66,6 +66,7 @@ export function throwMdDialogContentAlreadyAttachedError() {
|
||||
host: {
|
||||
'class': 'mat-dialog-container',
|
||||
'[attr.role]': '_config?.role',
|
||||
'[attr.aria-labelledby]': '_ariaLabelledBy',
|
||||
'[@slideDialog]': '_state',
|
||||
'(@slideDialog.done)': '_onAnimationDone($event)',
|
||||
},
|
||||
@@ -92,6 +93,9 @@ export class MdDialogContainer extends BasePortalHost {
|
||||
/** Emits the current animation state whenever it changes. */
|
||||
_onAnimationStateChange = new EventEmitter<AnimationEvent>();
|
||||
|
||||
/** ID of the element that should be considered as the dialog's label. */
|
||||
_ariaLabelledBy: string | null = null;
|
||||
|
||||
constructor(
|
||||
private _ngZone: NgZone,
|
||||
private _elementRef: ElementRef,
|
||||
|
||||
@@ -6,9 +6,12 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Directive, Input} from '@angular/core';
|
||||
import {Directive, Input, Optional, OnInit} from '@angular/core';
|
||||
import {MdDialogRef} from './dialog-ref';
|
||||
import {MdDialogContainer} from './dialog-container';
|
||||
|
||||
/** Counter used to generate unique IDs for dialog elements. */
|
||||
let dialogElementUid = 0;
|
||||
|
||||
/**
|
||||
* Button that will close the current dialog.
|
||||
@@ -40,9 +43,22 @@ export class MdDialogClose {
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[md-dialog-title], [mat-dialog-title], [mdDialogTitle], [matDialogTitle]',
|
||||
host: {'class': 'mat-dialog-title'},
|
||||
host: {
|
||||
'class': 'mat-dialog-title',
|
||||
'[id]': 'id',
|
||||
},
|
||||
})
|
||||
export class MdDialogTitle { }
|
||||
export class MdDialogTitle implements OnInit {
|
||||
@Input() id = `md-dialog-title-${dialogElementUid++}`;
|
||||
|
||||
constructor(@Optional() private _container: MdDialogContainer) { }
|
||||
|
||||
ngOnInit() {
|
||||
if (this._container && !this._container._ariaLabelledBy) {
|
||||
Promise.resolve().then(() => this._container._ariaLabelledBy = this.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,25 +6,21 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Injector, InjectionToken} from '@angular/core';
|
||||
import {Injector} from '@angular/core';
|
||||
import {MdDialogRef} from './dialog-ref';
|
||||
|
||||
export const MD_DIALOG_DATA = new InjectionToken<any>('MdDialogData');
|
||||
import {MdDialogContainer} from './dialog-container';
|
||||
|
||||
/** Custom injector type specifically for instantiating components with a dialog. */
|
||||
export class DialogInjector implements Injector {
|
||||
constructor(
|
||||
private _parentInjector: Injector,
|
||||
private _dialogRef: MdDialogRef<any>,
|
||||
private _data: any) { }
|
||||
private _customTokens: WeakMap<any, any>) { }
|
||||
|
||||
get(token: any, notFoundValue?: any): any {
|
||||
if (token === MdDialogRef) {
|
||||
return this._dialogRef;
|
||||
}
|
||||
const value = this._customTokens.get(token);
|
||||
|
||||
if (token === MD_DIALOG_DATA) {
|
||||
return this._data;
|
||||
if (typeof value !== 'undefined') {
|
||||
return value;
|
||||
}
|
||||
|
||||
return this._parentInjector.get<any>(token, notFoundValue);
|
||||
|
||||
@@ -21,11 +21,10 @@ import {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {Location} from '@angular/common';
|
||||
import {SpyLocation} from '@angular/common/testing';
|
||||
import {MdDialogModule} from './index';
|
||||
import {MdDialog} from './dialog';
|
||||
import {MdDialog, MD_DIALOG_DATA} from './dialog';
|
||||
import {MdDialogContainer} from './dialog-container';
|
||||
import {OverlayContainer, ESCAPE} from '../core';
|
||||
import {MdDialogRef} from './dialog-ref';
|
||||
import {MD_DIALOG_DATA} from './dialog-injector';
|
||||
import {dispatchKeyboardEvent} from '../core/testing/dispatch-events';
|
||||
|
||||
|
||||
@@ -669,6 +668,17 @@ describe('MdDialog', () => {
|
||||
});
|
||||
}));
|
||||
|
||||
it('should set the aria-labelled by attribute to the id of the title', async(() => {
|
||||
let title = overlayContainerElement.querySelector('[md-dialog-title]');
|
||||
let container = overlayContainerElement.querySelector('md-dialog-container');
|
||||
|
||||
viewContainerFixture.whenStable().then(() => {
|
||||
expect(title.id).toBeTruthy('Expected title element to have an id.');
|
||||
expect(container.getAttribute('aria-labelledby'))
|
||||
.toBe(title.id, 'Expected the aria-labelledby to match the title id.');
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,15 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Injector, ComponentRef, Injectable, Optional, SkipSelf, TemplateRef} from '@angular/core';
|
||||
import {
|
||||
Injector,
|
||||
InjectionToken,
|
||||
ComponentRef,
|
||||
Injectable,
|
||||
Optional,
|
||||
SkipSelf,
|
||||
TemplateRef,
|
||||
} from '@angular/core';
|
||||
import {Location} from '@angular/common';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
@@ -25,6 +33,8 @@ import {MdDialogRef} from './dialog-ref';
|
||||
import {MdDialogContainer} from './dialog-container';
|
||||
import {TemplatePortal} from '../core/portal/portal';
|
||||
|
||||
export const MD_DIALOG_DATA = new InjectionToken<any>('MdDialogData');
|
||||
|
||||
|
||||
/**
|
||||
* Service to open Material Design modal dialogs.
|
||||
@@ -187,17 +197,12 @@ export class MdDialog {
|
||||
});
|
||||
}
|
||||
|
||||
// We create an injector specifically for the component we're instantiating so that it can
|
||||
// inject the MdDialogRef. This allows a component loaded inside of a dialog to close itself
|
||||
// and, optionally, to return a value.
|
||||
let userInjector = config && config.viewContainerRef && config.viewContainerRef.injector;
|
||||
let dialogInjector = new DialogInjector(userInjector || this._injector, dialogRef, config.data);
|
||||
|
||||
if (componentOrTemplateRef instanceof TemplateRef) {
|
||||
dialogContainer.attachTemplatePortal(new TemplatePortal(componentOrTemplateRef, null));
|
||||
} else {
|
||||
let injector = this._createInjector<T>(config, dialogRef, dialogContainer);
|
||||
let contentRef = dialogContainer.attachComponentPortal(
|
||||
new ComponentPortal(componentOrTemplateRef, null, dialogInjector));
|
||||
new ComponentPortal(componentOrTemplateRef, null, injector));
|
||||
dialogRef.componentInstance = contentRef.instance;
|
||||
}
|
||||
|
||||
@@ -208,6 +213,29 @@ export class MdDialog {
|
||||
return dialogRef;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a custom injector to be used inside the dialog. This allows a component loaded inside
|
||||
* of a dialog to close itself and, optionally, to return a value.
|
||||
* @param config Config object that is used to construct the dialog.
|
||||
* @param dialogRef Reference to the dialog.
|
||||
* @param container Dialog container element that wraps all of the contents.
|
||||
* @returns The custom injector that can be used inside the dialog.
|
||||
*/
|
||||
private _createInjector<T>(
|
||||
config: MdDialogConfig,
|
||||
dialogRef: MdDialogRef<T>,
|
||||
dialogContainer: MdDialogContainer): DialogInjector {
|
||||
|
||||
let userInjector = config && config.viewContainerRef && config.viewContainerRef.injector;
|
||||
let injectionTokens = new WeakMap();
|
||||
|
||||
injectionTokens.set(MdDialogRef, dialogRef);
|
||||
injectionTokens.set(MdDialogContainer, dialogContainer);
|
||||
injectionTokens.set(MD_DIALOG_DATA, config.data);
|
||||
|
||||
return new DialogInjector(userInjector || this._injector, injectionTokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a dialog from the array of open dialogs.
|
||||
* @param dialogRef Dialog to be removed.
|
||||
|
||||
@@ -59,4 +59,3 @@ export * from './dialog-container';
|
||||
export * from './dialog-content-directives';
|
||||
export * from './dialog-config';
|
||||
export * from './dialog-ref';
|
||||
export {MD_DIALOG_DATA} from './dialog-injector';
|
||||
|
||||
Reference in New Issue
Block a user