Skip to content

Commit

Permalink
feat(dialog): add ariaLabel and focusOnOpen config options
Browse files Browse the repository at this point in the history
Based on the discussion on #6360 (comment), these changes add the ability to set the `aria-label` of a dialog, as well as the element that should be focus when the dialog is opened.
  • Loading branch information
crisbeto committed Aug 20, 2017
1 parent 372436c commit a110485
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 2 deletions.
9 changes: 9 additions & 0 deletions src/lib/dialog/dialog-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,14 @@ export class MdDialogConfig {
/** ID of the element that describes the dialog. */
ariaDescribedBy?: string | null = null;

/** Aria label to assign to the dialog element */
ariaLabel?: string | null = null;

/**
* Selector for an element to be focused when the dialog is opened. If omitted or the
* element is not found, the dialog will focus the first focusable element.
*/
focusOnOpen?: string | null = null;

// TODO(jelbourn): add configuration for lifecycle hooks, ARIA labelling.
}
14 changes: 12 additions & 2 deletions src/lib/dialog/dialog-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
} from '@angular/cdk/portal';
import {FocusTrap, FocusTrapFactory} from '@angular/cdk/a11y';
import {MdDialogConfig} from './dialog-config';
import {first} from '@angular/cdk/rxjs';


/**
Expand Down Expand Up @@ -64,8 +65,10 @@ export function throwMdDialogContentAlreadyAttachedError() {
],
host: {
'class': 'mat-dialog-container',
'tabindex': '-1',
'[attr.role]': '_config?.role',
'[attr.aria-labelledby]': '_ariaLabelledBy',
'[attr.aria-labelledby]': '_config?.ariaLabel ? null : _ariaLabelledBy',
'[attr.aria-label]': '_config?.ariaLabel',
'[attr.aria-describedby]': '_config?.ariaDescribedBy || null',
'[@slideDialog]': '_state',
'(@slideDialog.start)': '_onAnimationStart($event)',
Expand Down Expand Up @@ -142,7 +145,14 @@ export class MdDialogContainer extends BasePortalHost {
// If were to attempt to focus immediately, then the content of the dialog would not yet be
// ready in instances where change detection has to run first. To deal with this, we simply
// wait for the microtask queue to be empty.
this._focusTrap.focusInitialElementWhenReady();
first.call(this._ngZone.onStable).subscribe(() => {
const toFocus = this._config.focusOnOpen ?
// Start from the parent to allow for the dialog container itself to be focused as well.
this._elementRef.nativeElement.parentNode.querySelector(this._config.focusOnOpen) :
null;

toFocus ? toFocus.focus() : this._focusTrap.focusInitialElement();
});
}

/** Restores focus to the element that was focused before the dialog opened. */
Expand Down
1 change: 1 addition & 0 deletions src/lib/dialog/dialog.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ $mat-dialog-button-margin: 8px !default;
box-sizing: border-box;
overflow: auto;
max-width: $mat-dialog-max-width;
outline: 0;

// The dialog container should completely fill its parent overlay element.
width: 100%;
Expand Down
54 changes: 54 additions & 0 deletions src/lib/dialog/dialog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,34 @@ describe('MdDialog', () => {
.toBe('INPUT', 'Expected first tabbable element (input) in the dialog to be focused.');
}));

it('should be able to specify what element should be focused on open', fakeAsync(() => {
dialog.open(PizzaMsg, { focusOnOpen: 'button' });

viewContainerFixture.detectChanges();
flushMicrotasks();

expect(document.activeElement.tagName).toBe('BUTTON');
}));

it('should fall back to the first focusable element, if the override is not found',
fakeAsync(() => {
dialog.open(PizzaMsg, { focusOnOpen: 'unicorn' });

viewContainerFixture.detectChanges();
flushMicrotasks();

expect(document.activeElement.tagName).toBe('INPUT');
}));

it('should be able to focus the dialog container', fakeAsync(() => {
dialog.open(PizzaMsg, { focusOnOpen: '[role="dialog"]' });

viewContainerFixture.detectChanges();
flushMicrotasks();

expect(document.activeElement.tagName).toBe('MD-DIALOG-CONTAINER');
}));

it('should re-focus trigger element when dialog closes', fakeAsync(() => {
// Create a element that has focus before the dialog is opened.
let button = document.createElement('button');
Expand Down Expand Up @@ -749,6 +777,32 @@ describe('MdDialog', () => {
}));

});

describe('aria-label', () => {
it('should be able to set a custom aria-label', () => {
dialog.open(PizzaMsg, {
ariaLabel: 'Hello there',
viewContainerRef: testViewContainerRef
});
viewContainerFixture.detectChanges();

const container = overlayContainerElement.querySelector('md-dialog-container')!;
expect(container.getAttribute('aria-label')).toBe('Hello there');
});

it('should not set the aria-labelledby automatically if it has an aria-label', fakeAsync(() => {
dialog.open(ContentElementDialog, {
ariaLabel: 'Hello there',
viewContainerRef: testViewContainerRef
});
viewContainerFixture.detectChanges();
tick();
viewContainerFixture.detectChanges();

const container = overlayContainerElement.querySelector('md-dialog-container')!;
expect(container.hasAttribute('aria-labelledby')).toBe(false);
}));
});
});

describe('MdDialog with a parent MdDialog', () => {
Expand Down

0 comments on commit a110485

Please sign in to comment.