Skip to content

Commit a110485

Browse files
committed
feat(dialog): add ariaLabel and focusOnOpen config options
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.
1 parent 372436c commit a110485

File tree

4 files changed

+76
-2
lines changed

4 files changed

+76
-2
lines changed

src/lib/dialog/dialog-config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,14 @@ export class MdDialogConfig {
6969
/** ID of the element that describes the dialog. */
7070
ariaDescribedBy?: string | null = null;
7171

72+
/** Aria label to assign to the dialog element */
73+
ariaLabel?: string | null = null;
74+
75+
/**
76+
* Selector for an element to be focused when the dialog is opened. If omitted or the
77+
* element is not found, the dialog will focus the first focusable element.
78+
*/
79+
focusOnOpen?: string | null = null;
80+
7281
// TODO(jelbourn): add configuration for lifecycle hooks, ARIA labelling.
7382
}

src/lib/dialog/dialog-container.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
} from '@angular/cdk/portal';
2929
import {FocusTrap, FocusTrapFactory} from '@angular/cdk/a11y';
3030
import {MdDialogConfig} from './dialog-config';
31+
import {first} from '@angular/cdk/rxjs';
3132

3233

3334
/**
@@ -64,8 +65,10 @@ export function throwMdDialogContentAlreadyAttachedError() {
6465
],
6566
host: {
6667
'class': 'mat-dialog-container',
68+
'tabindex': '-1',
6769
'[attr.role]': '_config?.role',
68-
'[attr.aria-labelledby]': '_ariaLabelledBy',
70+
'[attr.aria-labelledby]': '_config?.ariaLabel ? null : _ariaLabelledBy',
71+
'[attr.aria-label]': '_config?.ariaLabel',
6972
'[attr.aria-describedby]': '_config?.ariaDescribedBy || null',
7073
'[@slideDialog]': '_state',
7174
'(@slideDialog.start)': '_onAnimationStart($event)',
@@ -142,7 +145,14 @@ export class MdDialogContainer extends BasePortalHost {
142145
// If were to attempt to focus immediately, then the content of the dialog would not yet be
143146
// ready in instances where change detection has to run first. To deal with this, we simply
144147
// wait for the microtask queue to be empty.
145-
this._focusTrap.focusInitialElementWhenReady();
148+
first.call(this._ngZone.onStable).subscribe(() => {
149+
const toFocus = this._config.focusOnOpen ?
150+
// Start from the parent to allow for the dialog container itself to be focused as well.
151+
this._elementRef.nativeElement.parentNode.querySelector(this._config.focusOnOpen) :
152+
null;
153+
154+
toFocus ? toFocus.focus() : this._focusTrap.focusInitialElement();
155+
});
146156
}
147157

148158
/** Restores focus to the element that was focused before the dialog opened. */

src/lib/dialog/dialog.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ $mat-dialog-button-margin: 8px !default;
1717
box-sizing: border-box;
1818
overflow: auto;
1919
max-width: $mat-dialog-max-width;
20+
outline: 0;
2021

2122
// The dialog container should completely fill its parent overlay element.
2223
width: 100%;

src/lib/dialog/dialog.spec.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,34 @@ describe('MdDialog', () => {
618618
.toBe('INPUT', 'Expected first tabbable element (input) in the dialog to be focused.');
619619
}));
620620

621+
it('should be able to specify what element should be focused on open', fakeAsync(() => {
622+
dialog.open(PizzaMsg, { focusOnOpen: 'button' });
623+
624+
viewContainerFixture.detectChanges();
625+
flushMicrotasks();
626+
627+
expect(document.activeElement.tagName).toBe('BUTTON');
628+
}));
629+
630+
it('should fall back to the first focusable element, if the override is not found',
631+
fakeAsync(() => {
632+
dialog.open(PizzaMsg, { focusOnOpen: 'unicorn' });
633+
634+
viewContainerFixture.detectChanges();
635+
flushMicrotasks();
636+
637+
expect(document.activeElement.tagName).toBe('INPUT');
638+
}));
639+
640+
it('should be able to focus the dialog container', fakeAsync(() => {
641+
dialog.open(PizzaMsg, { focusOnOpen: '[role="dialog"]' });
642+
643+
viewContainerFixture.detectChanges();
644+
flushMicrotasks();
645+
646+
expect(document.activeElement.tagName).toBe('MD-DIALOG-CONTAINER');
647+
}));
648+
621649
it('should re-focus trigger element when dialog closes', fakeAsync(() => {
622650
// Create a element that has focus before the dialog is opened.
623651
let button = document.createElement('button');
@@ -749,6 +777,32 @@ describe('MdDialog', () => {
749777
}));
750778

751779
});
780+
781+
describe('aria-label', () => {
782+
it('should be able to set a custom aria-label', () => {
783+
dialog.open(PizzaMsg, {
784+
ariaLabel: 'Hello there',
785+
viewContainerRef: testViewContainerRef
786+
});
787+
viewContainerFixture.detectChanges();
788+
789+
const container = overlayContainerElement.querySelector('md-dialog-container')!;
790+
expect(container.getAttribute('aria-label')).toBe('Hello there');
791+
});
792+
793+
it('should not set the aria-labelledby automatically if it has an aria-label', fakeAsync(() => {
794+
dialog.open(ContentElementDialog, {
795+
ariaLabel: 'Hello there',
796+
viewContainerRef: testViewContainerRef
797+
});
798+
viewContainerFixture.detectChanges();
799+
tick();
800+
viewContainerFixture.detectChanges();
801+
802+
const container = overlayContainerElement.querySelector('md-dialog-container')!;
803+
expect(container.hasAttribute('aria-labelledby')).toBe(false);
804+
}));
805+
});
752806
});
753807

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

0 commit comments

Comments
 (0)