Skip to content

Commit f64c59c

Browse files
committed
address comments
1 parent 7179b75 commit f64c59c

File tree

4 files changed

+44
-20
lines changed

4 files changed

+44
-20
lines changed

src/lib/core/a11y/focus-trap.ts

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
Component, ViewEncapsulation, ViewChild, ElementRef, Input, AfterContentInit, NgZone
33
} from '@angular/core';
44
import {InteractivityChecker} from './interactivity-checker';
5+
import {coerceBooleanProperty} from '../coersion/boolean-property';
56

67

78
/**
@@ -26,17 +27,8 @@ export class FocusTrap implements AfterContentInit {
2627
@ViewChild('trappedContent') trappedContent: ElementRef;
2728

2829
@Input()
29-
get active(): boolean {
30-
return this._active;
31-
}
32-
set active(val: boolean) {
33-
this._active = val;
34-
if (val && this._contentReady) {
35-
this._ngZone.onMicrotaskEmpty.first().subscribe(() => {
36-
this.focusFirstTabbableElement();
37-
});
38-
}
39-
}
30+
get active(): boolean { return this._active; }
31+
set active(val: boolean) { this._active = coerceBooleanProperty(val); }
4032

4133
/** Whether the DOM content is ready. */
4234
private _contentReady: boolean = false;
@@ -50,12 +42,30 @@ export class FocusTrap implements AfterContentInit {
5042
this._contentReady = true;
5143
// Trigger setter behavior.
5244
if (this.active) {
53-
this._ngZone.onMicrotaskEmpty.first().subscribe(() => {
54-
this.focusFirstTabbableElement();
55-
});
45+
5646
}
5747
}
5848

49+
/**
50+
* Waits for microtask queue to empty, then focuses the first tabbable element within the focus
51+
* trap region.
52+
*/
53+
focusFirstTabbableElementWhenReady() {
54+
this._ngZone.onMicrotaskEmpty.first().subscribe(() => {
55+
this.focusFirstTabbableElement();
56+
});
57+
}
58+
59+
/**
60+
* Waits for microtask queue to empty, then focuses the last tabbable element within the focus
61+
* trap region.
62+
*/
63+
focusLastTabbableElementWhenReady() {
64+
this._ngZone.onMicrotaskEmpty.first().subscribe(() => {
65+
this.focusLastTabbableElement();
66+
});
67+
}
68+
5969
/** Focuses the first tabbable element within the focus trap region. */
6070
focusFirstTabbableElement() {
6171
let redirectToElement = this._getFirstTabbableElement(this.trappedContent.nativeElement);
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
<focus-trap [active]="focusTrapActive">
1+
<focus-trap>
22
<template portalHost></template>
33
</focus-trap>

src/lib/dialog/dialog-container.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {BasePortalHost, ComponentPortal, PortalHostDirective, TemplatePortal} fr
1010
import {MdDialogConfig} from './dialog-config';
1111
import {MdDialogRef} from './dialog-ref';
1212
import {MdDialogContentAlreadyAttachedError} from './dialog-errors';
13+
import {FocusTrap} from '../core/a11y/focus-trap';
1314
import 'rxjs/add/operator/first';
1415

1516

@@ -32,6 +33,9 @@ export class MdDialogContainer extends BasePortalHost implements OnDestroy {
3233
/** The portal host inside of this container into which the dialog content will be loaded. */
3334
@ViewChild(PortalHostDirective) _portalHost: PortalHostDirective;
3435

36+
/** The directive that traps and manages focus within the dialog. */
37+
@ViewChild(FocusTrap) _focusTrap: FocusTrap;
38+
3539
/** Element that was focused before the dialog was opened. Save this to restore upon close. */
3640
private _elementFocusedBeforeDialogWasOpened: Element = null;
3741

@@ -41,9 +45,6 @@ export class MdDialogContainer extends BasePortalHost implements OnDestroy {
4145
/** Reference to the open dialog. */
4246
dialogRef: MdDialogRef<any>;
4347

44-
/** Whether the focus trap is active. */
45-
focusTrapActive: boolean = false;
46-
4748
constructor(private _ngZone: NgZone) {
4849
super();
4950
}
@@ -56,8 +57,13 @@ export class MdDialogContainer extends BasePortalHost implements OnDestroy {
5657

5758
let attachResult = this._portalHost.attachComponentPortal(portal);
5859

59-
this._elementFocusedBeforeDialogWasOpened = document.activeElement;
60-
this.focusTrapActive = true;
60+
// If were to attempt to focus immediately, then the content of the dialog would not yet be
61+
// ready in instances where change detection has to run first. To deal with this, we simply
62+
// wait for the microtask queue to be empty.
63+
this._ngZone.onMicrotaskEmpty.first().subscribe(() => {
64+
this._elementFocusedBeforeDialogWasOpened = document.activeElement;
65+
this._focusTrap.focusFirstTabbableElement();
66+
});
6167

6268
return attachResult;
6369
}

src/lib/sidenav/sidenav.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ import {
1313
EventEmitter,
1414
Renderer,
1515
ViewEncapsulation,
16+
ViewChild,
1617
} from '@angular/core';
1718
import {CommonModule} from '@angular/common';
1819
import {Dir, MdError, coerceBooleanProperty} from '../core';
1920
import {A11yModule} from '../core/a11y/index';
21+
import {FocusTrap} from '../core/a11y/focus-trap';
2022

2123

2224
/** Exception thrown when two MdSidenav are matching the same side. */
@@ -55,6 +57,8 @@ export class MdDuplicatedSidenavError extends MdError {
5557
encapsulation: ViewEncapsulation.None,
5658
})
5759
export class MdSidenav implements AfterContentInit {
60+
@ViewChild(FocusTrap) private _focusTrap: FocusTrap;
61+
5862
/** Alignment of the sidenav (direction neutral); whether 'start' or 'end'. */
5963
private _align: 'start' | 'end' = 'start';
6064

@@ -177,6 +181,10 @@ export class MdSidenav implements AfterContentInit {
177181
this.onCloseStart.emit();
178182
}
179183

184+
if (this.focusTrapActive) {
185+
this._focusTrap.focusFirstTabbableElementWhenReady();
186+
}
187+
180188
if (isOpen) {
181189
if (this._openPromise == null) {
182190
this._openPromise = new Promise<void>((resolve, reject) => {

0 commit comments

Comments
 (0)