diff --git a/src/lib/dialog/dialog-container.ts b/src/lib/dialog/dialog-container.ts index e47929d91b06..a65ed9da04f7 100644 --- a/src/lib/dialog/dialog-container.ts +++ b/src/lib/dialog/dialog-container.ts @@ -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(); + /** ID of the element that should be considered as the dialog's label. */ + _ariaLabelledBy: string | null = null; + constructor( private _ngZone: NgZone, private _elementRef: ElementRef, diff --git a/src/lib/dialog/dialog-content-directives.ts b/src/lib/dialog/dialog-content-directives.ts index 1f1487efc034..6f62aa5a4305 100644 --- a/src/lib/dialog/dialog-content-directives.ts +++ b/src/lib/dialog/dialog-content-directives.ts @@ -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); + } + } +} /** diff --git a/src/lib/dialog/dialog-injector.ts b/src/lib/dialog/dialog-injector.ts index f8bedc1960f7..a29299143a91 100644 --- a/src/lib/dialog/dialog-injector.ts +++ b/src/lib/dialog/dialog-injector.ts @@ -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('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, - private _data: any) { } + private _customTokens: WeakMap) { } 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(token, notFoundValue); diff --git a/src/lib/dialog/dialog.spec.ts b/src/lib/dialog/dialog.spec.ts index c33c7bf70bde..f3673ed0b17d 100644 --- a/src/lib/dialog/dialog.spec.ts +++ b/src/lib/dialog/dialog.spec.ts @@ -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.'); + }); + })); + }); }); diff --git a/src/lib/dialog/dialog.ts b/src/lib/dialog/dialog.ts index 46dd99d807d8..fe6b4c0f3eb8 100644 --- a/src/lib/dialog/dialog.ts +++ b/src/lib/dialog/dialog.ts @@ -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('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(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( + config: MdDialogConfig, + dialogRef: MdDialogRef, + 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. diff --git a/src/lib/dialog/index.ts b/src/lib/dialog/index.ts index 4cf53ae2c605..7dd3b29dd8db 100644 --- a/src/lib/dialog/index.ts +++ b/src/lib/dialog/index.ts @@ -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';