From a639109d2362c6644a0eabaa4c515eec61e8b41e Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Wed, 2 Nov 2016 12:11:42 -0700 Subject: [PATCH 1/8] Focus capturing for sidenav. Captures focus when sidenav is open in "over" or "push" mode, but not when opened in "side" mode. --- src/lib/core/a11y/focus-trap.html | 4 +- src/lib/core/a11y/focus-trap.ts | 43 ++++++++++++++++-- src/lib/core/a11y/index.ts | 3 +- src/lib/dialog/dialog-container.html | 2 +- src/lib/dialog/dialog-container.ts | 16 +++---- src/lib/sidenav/sidenav.spec.ts | 67 +++++++++++++++++++++++++++- src/lib/sidenav/sidenav.ts | 9 +++- 7 files changed, 122 insertions(+), 22 deletions(-) diff --git a/src/lib/core/a11y/focus-trap.html b/src/lib/core/a11y/focus-trap.html index 577b68bfa8d1..0b62e1e4e744 100644 --- a/src/lib/core/a11y/focus-trap.html +++ b/src/lib/core/a11y/focus-trap.html @@ -1,3 +1,3 @@ -
+
-
+
diff --git a/src/lib/core/a11y/focus-trap.ts b/src/lib/core/a11y/focus-trap.ts index 56368bb41e8d..f3c9ea8a464a 100644 --- a/src/lib/core/a11y/focus-trap.ts +++ b/src/lib/core/a11y/focus-trap.ts @@ -1,4 +1,12 @@ -import {Component, ViewEncapsulation, ViewChild, ElementRef} from '@angular/core'; +import { + Component, + ViewEncapsulation, + ViewChild, + ElementRef, + Input, + AfterContentInit, + NgZone +} from '@angular/core'; import {InteractivityChecker} from './interactivity-checker'; @@ -16,10 +24,39 @@ import {InteractivityChecker} from './interactivity-checker'; templateUrl: 'focus-trap.html', encapsulation: ViewEncapsulation.None, }) -export class FocusTrap { +export class FocusTrap implements AfterContentInit { @ViewChild('trappedContent') trappedContent: ElementRef; - constructor(private _checker: InteractivityChecker) { } + @Input() + get active(): boolean { + return this._active; + } + set active(val : boolean) { + this._active = val; + if (val && this._contentReady) { + this._ngZone.onMicrotaskEmpty.first().subscribe(() => { + this.focusFirstTabbableElement(); + }); + } + } + + /** Whether the DOM content is ready. */ + private _contentReady : boolean = false; + + /** Whether the focus trap is active. */ + private _active : boolean = true; + + constructor(private _checker: InteractivityChecker, private _ngZone: NgZone) { } + + ngAfterContentInit() { + this._contentReady = true; + // Trigger setter behavior. + if (this.active) { + this._ngZone.onMicrotaskEmpty.first().subscribe(() => { + this.focusFirstTabbableElement(); + }); + } + } /** Focuses the first tabbable element within the focus trap region. */ focusFirstTabbableElement() { diff --git a/src/lib/core/a11y/index.ts b/src/lib/core/a11y/index.ts index 0e285f77d60c..85a673ae3555 100644 --- a/src/lib/core/a11y/index.ts +++ b/src/lib/core/a11y/index.ts @@ -2,12 +2,13 @@ import {NgModule, ModuleWithProviders} from '@angular/core'; import {FocusTrap} from './focus-trap'; import {MdLiveAnnouncer} from './live-announcer'; import {InteractivityChecker} from './interactivity-checker'; +import {CommonModule} from "@angular/common"; import {PlatformModule} from '../platform/platform'; export const A11Y_PROVIDERS = [MdLiveAnnouncer, InteractivityChecker]; @NgModule({ - imports: [PlatformModule], + imports: [CommonModule, PlatformModule], declarations: [FocusTrap], exports: [FocusTrap], }) diff --git a/src/lib/dialog/dialog-container.html b/src/lib/dialog/dialog-container.html index f1c4963b9627..c764c6d2af46 100644 --- a/src/lib/dialog/dialog-container.html +++ b/src/lib/dialog/dialog-container.html @@ -1,3 +1,3 @@ - + diff --git a/src/lib/dialog/dialog-container.ts b/src/lib/dialog/dialog-container.ts index aaaffb98436f..89fbab58f772 100644 --- a/src/lib/dialog/dialog-container.ts +++ b/src/lib/dialog/dialog-container.ts @@ -10,7 +10,6 @@ import {BasePortalHost, ComponentPortal, PortalHostDirective, TemplatePortal} fr import {MdDialogConfig} from './dialog-config'; import {MdDialogRef} from './dialog-ref'; import {MdDialogContentAlreadyAttachedError} from './dialog-errors'; -import {FocusTrap} from '../core/a11y/focus-trap'; import 'rxjs/add/operator/first'; @@ -33,9 +32,6 @@ export class MdDialogContainer extends BasePortalHost implements OnDestroy { /** The portal host inside of this container into which the dialog content will be loaded. */ @ViewChild(PortalHostDirective) _portalHost: PortalHostDirective; - /** The directive that traps and manages focus within the dialog. */ - @ViewChild(FocusTrap) _focusTrap: FocusTrap; - /** Element that was focused before the dialog was opened. Save this to restore upon close. */ private _elementFocusedBeforeDialogWasOpened: Element = null; @@ -45,6 +41,9 @@ export class MdDialogContainer extends BasePortalHost implements OnDestroy { /** Reference to the open dialog. */ dialogRef: MdDialogRef; + /** Whether the focus trap is active. */ + focusTrapActive: boolean = false; + constructor(private _ngZone: NgZone) { super(); } @@ -57,13 +56,8 @@ export class MdDialogContainer extends BasePortalHost implements OnDestroy { let attachResult = this._portalHost.attachComponentPortal(portal); - // 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._ngZone.onMicrotaskEmpty.first().subscribe(() => { - this._elementFocusedBeforeDialogWasOpened = document.activeElement; - this._focusTrap.focusFirstTabbableElement(); - }); + this._elementFocusedBeforeDialogWasOpened = document.activeElement; + this.focusTrapActive = true; return attachResult; } diff --git a/src/lib/sidenav/sidenav.spec.ts b/src/lib/sidenav/sidenav.spec.ts index 206519434115..8d666bf252d6 100644 --- a/src/lib/sidenav/sidenav.spec.ts +++ b/src/lib/sidenav/sidenav.spec.ts @@ -2,6 +2,7 @@ import {fakeAsync, async, tick, ComponentFixture, TestBed} from '@angular/core/t import {Component} from '@angular/core'; import {By} from '@angular/platform-browser'; import {MdSidenav, MdSidenavModule, MdSidenavToggleResult} from './sidenav'; +import {A11yModule} from "../core/a11y/index"; function endSidenavTransition(fixture: ComponentFixture) { @@ -18,7 +19,7 @@ describe('MdSidenav', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [MdSidenavModule.forRoot()], + imports: [MdSidenavModule.forRoot(), A11yModule.forRoot()], declarations: [ BasicTestApp, SidenavLayoutTwoSidenavTestApp, @@ -26,6 +27,7 @@ describe('MdSidenav', () => { SidenavSetToOpenedFalse, SidenavSetToOpenedTrue, SidenavDynamicAlign, + SidenavWitFocusableElements, ], }); @@ -236,7 +238,6 @@ describe('MdSidenav', () => { }); describe('attributes', () => { - it('should correctly parse opened="false"', () => { let fixture = TestBed.createComponent(SidenavSetToOpenedFalse); fixture.detectChanges(); @@ -290,6 +291,55 @@ describe('MdSidenav', () => { }); }); + describe('focus trapping behavior', () => { + let fixture: ComponentFixture; + let testComponent: SidenavWitFocusableElements; + let sidenav: MdSidenav; + let link1Element: HTMLElement; + let link2Element: HTMLElement; + + beforeEach(() => { + fixture = TestBed.createComponent(SidenavWitFocusableElements); + testComponent = fixture.debugElement.componentInstance; + sidenav = fixture.debugElement.query(By.directive(MdSidenav)).componentInstance; + link1Element = fixture.debugElement.query(By.css('.link1')).nativeElement; + link2Element = fixture.debugElement.query(By.css('.link1')).nativeElement; + link2Element.focus(); + }); + + it('should trp focus when opened in "over" mode', fakeAsync(() => { + testComponent.mode = 'over'; + link2Element.focus(); + + sidenav.open(); + endSidenavTransition(fixture); + tick(); + + expect(document.activeElement).toBe(link1Element); + })); + + it('should trap tabs when opened in "push" mode', fakeAsync(() => { + testComponent.mode = 'push'; + link2Element.focus(); + + sidenav.open(); + endSidenavTransition(fixture); + tick(); + + expect(document.activeElement).toBe(link1Element); + })); + + it('should not trap tabs when opened in "side" mode', fakeAsync(() => { + testComponent.mode = 'side'; + link2Element.focus(); + + sidenav.open(); + endSidenavTransition(fixture); + tick(); + + expect(document.activeElement).toBe(link2Element); + })); + }); }); @@ -381,3 +431,16 @@ class SidenavDynamicAlign { sidenav1Align = 'start'; sidenav2Align = 'end'; } + +@Component({ + template: ` + + + link1 + + link2 + `, +}) +class SidenavWitFocusableElements { + mode: string = 'over'; +} diff --git a/src/lib/sidenav/sidenav.ts b/src/lib/sidenav/sidenav.ts index 8398f6892320..ce676029ae43 100644 --- a/src/lib/sidenav/sidenav.ts +++ b/src/lib/sidenav/sidenav.ts @@ -16,6 +16,7 @@ import { } from '@angular/core'; import {CommonModule} from '@angular/common'; import {Dir, MdError, coerceBooleanProperty, DefaultStyleCompatibilityModeModule} from '../core'; +import {A11yModule} from "../core/a11y/index"; /** Exception thrown when two MdSidenav are matching the same side. */ @@ -42,7 +43,7 @@ export class MdSidenavToggleResult { @Component({ moduleId: module.id, selector: 'md-sidenav, mat-sidenav', - template: '', + template: '', host: { '(transitionend)': '_onTransitionEnd($event)', // must prevent the browser from aligning text based on value @@ -122,6 +123,10 @@ export class MdSidenav implements AfterContentInit { */ private _resolveToggleAnimationPromise: (animationFinished: boolean) => void = null; + get focusTrapActive() { + return this.opened && this.mode != 'side'; + } + /** * @param _elementRef The DOM element reference. Used for transition and width calculation. * If not available we do not hook on transitions. @@ -456,7 +461,7 @@ export class MdSidenavLayout implements AfterContentInit { @NgModule({ - imports: [CommonModule, DefaultStyleCompatibilityModeModule], + imports: [CommonModule, DefaultStyleCompatibilityModeModule, A11yModule], exports: [MdSidenavLayout, MdSidenav, DefaultStyleCompatibilityModeModule], declarations: [MdSidenavLayout, MdSidenav], }) From c56579a61c6e65f1044480e5e67c967671fc6c39 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Wed, 2 Nov 2016 14:37:43 -0700 Subject: [PATCH 2/8] lint fixes. --- src/lib/core/a11y/focus-trap.ts | 6 +++--- src/lib/core/a11y/index.ts | 2 +- src/lib/sidenav/sidenav.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib/core/a11y/focus-trap.ts b/src/lib/core/a11y/focus-trap.ts index f3c9ea8a464a..8fd86769c5b9 100644 --- a/src/lib/core/a11y/focus-trap.ts +++ b/src/lib/core/a11y/focus-trap.ts @@ -31,7 +31,7 @@ export class FocusTrap implements AfterContentInit { get active(): boolean { return this._active; } - set active(val : boolean) { + set active(val: boolean) { this._active = val; if (val && this._contentReady) { this._ngZone.onMicrotaskEmpty.first().subscribe(() => { @@ -41,10 +41,10 @@ export class FocusTrap implements AfterContentInit { } /** Whether the DOM content is ready. */ - private _contentReady : boolean = false; + private _contentReady: boolean = false; /** Whether the focus trap is active. */ - private _active : boolean = true; + private _active: boolean = true; constructor(private _checker: InteractivityChecker, private _ngZone: NgZone) { } diff --git a/src/lib/core/a11y/index.ts b/src/lib/core/a11y/index.ts index 85a673ae3555..0be8e05abeb6 100644 --- a/src/lib/core/a11y/index.ts +++ b/src/lib/core/a11y/index.ts @@ -2,7 +2,7 @@ import {NgModule, ModuleWithProviders} from '@angular/core'; import {FocusTrap} from './focus-trap'; import {MdLiveAnnouncer} from './live-announcer'; import {InteractivityChecker} from './interactivity-checker'; -import {CommonModule} from "@angular/common"; +import {CommonModule} from '@angular/common'; import {PlatformModule} from '../platform/platform'; export const A11Y_PROVIDERS = [MdLiveAnnouncer, InteractivityChecker]; diff --git a/src/lib/sidenav/sidenav.ts b/src/lib/sidenav/sidenav.ts index ce676029ae43..db597eeaa3c2 100644 --- a/src/lib/sidenav/sidenav.ts +++ b/src/lib/sidenav/sidenav.ts @@ -16,7 +16,7 @@ import { } from '@angular/core'; import {CommonModule} from '@angular/common'; import {Dir, MdError, coerceBooleanProperty, DefaultStyleCompatibilityModeModule} from '../core'; -import {A11yModule} from "../core/a11y/index"; +import {A11yModule} from '../core/a11y/index'; /** Exception thrown when two MdSidenav are matching the same side. */ From ce936103322dcafad2660edb7f08a44bb1f6c5df Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Wed, 9 Nov 2016 11:44:35 -0800 Subject: [PATCH 3/8] address comments --- src/lib/core/a11y/focus-trap.ts | 38 ++++++++++++++++++---------- src/lib/dialog/dialog-container.html | 2 +- src/lib/dialog/dialog-container.ts | 16 ++++++++---- src/lib/sidenav/sidenav.ts | 8 ++++++ 4 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/lib/core/a11y/focus-trap.ts b/src/lib/core/a11y/focus-trap.ts index 8fd86769c5b9..3880d6d6ba5e 100644 --- a/src/lib/core/a11y/focus-trap.ts +++ b/src/lib/core/a11y/focus-trap.ts @@ -8,6 +8,7 @@ import { NgZone } from '@angular/core'; import {InteractivityChecker} from './interactivity-checker'; +import {coerceBooleanProperty} from '../coersion/boolean-property'; /** @@ -28,17 +29,8 @@ export class FocusTrap implements AfterContentInit { @ViewChild('trappedContent') trappedContent: ElementRef; @Input() - get active(): boolean { - return this._active; - } - set active(val: boolean) { - this._active = val; - if (val && this._contentReady) { - this._ngZone.onMicrotaskEmpty.first().subscribe(() => { - this.focusFirstTabbableElement(); - }); - } - } + get active(): boolean { return this._active; } + set active(val: boolean) { this._active = coerceBooleanProperty(val); } /** Whether the DOM content is ready. */ private _contentReady: boolean = false; @@ -52,12 +44,30 @@ export class FocusTrap implements AfterContentInit { this._contentReady = true; // Trigger setter behavior. if (this.active) { - this._ngZone.onMicrotaskEmpty.first().subscribe(() => { - this.focusFirstTabbableElement(); - }); + } } + /** + * Waits for microtask queue to empty, then focuses the first tabbable element within the focus + * trap region. + */ + focusFirstTabbableElementWhenReady() { + this._ngZone.onMicrotaskEmpty.first().subscribe(() => { + this.focusFirstTabbableElement(); + }); + } + + /** + * Waits for microtask queue to empty, then focuses the last tabbable element within the focus + * trap region. + */ + focusLastTabbableElementWhenReady() { + this._ngZone.onMicrotaskEmpty.first().subscribe(() => { + this.focusLastTabbableElement(); + }); + } + /** Focuses the first tabbable element within the focus trap region. */ focusFirstTabbableElement() { let rootElement = this.trappedContent.nativeElement; diff --git a/src/lib/dialog/dialog-container.html b/src/lib/dialog/dialog-container.html index c764c6d2af46..f1c4963b9627 100644 --- a/src/lib/dialog/dialog-container.html +++ b/src/lib/dialog/dialog-container.html @@ -1,3 +1,3 @@ - + diff --git a/src/lib/dialog/dialog-container.ts b/src/lib/dialog/dialog-container.ts index 89fbab58f772..aaaffb98436f 100644 --- a/src/lib/dialog/dialog-container.ts +++ b/src/lib/dialog/dialog-container.ts @@ -10,6 +10,7 @@ import {BasePortalHost, ComponentPortal, PortalHostDirective, TemplatePortal} fr import {MdDialogConfig} from './dialog-config'; import {MdDialogRef} from './dialog-ref'; import {MdDialogContentAlreadyAttachedError} from './dialog-errors'; +import {FocusTrap} from '../core/a11y/focus-trap'; import 'rxjs/add/operator/first'; @@ -32,6 +33,9 @@ export class MdDialogContainer extends BasePortalHost implements OnDestroy { /** The portal host inside of this container into which the dialog content will be loaded. */ @ViewChild(PortalHostDirective) _portalHost: PortalHostDirective; + /** The directive that traps and manages focus within the dialog. */ + @ViewChild(FocusTrap) _focusTrap: FocusTrap; + /** Element that was focused before the dialog was opened. Save this to restore upon close. */ private _elementFocusedBeforeDialogWasOpened: Element = null; @@ -41,9 +45,6 @@ export class MdDialogContainer extends BasePortalHost implements OnDestroy { /** Reference to the open dialog. */ dialogRef: MdDialogRef; - /** Whether the focus trap is active. */ - focusTrapActive: boolean = false; - constructor(private _ngZone: NgZone) { super(); } @@ -56,8 +57,13 @@ export class MdDialogContainer extends BasePortalHost implements OnDestroy { let attachResult = this._portalHost.attachComponentPortal(portal); - this._elementFocusedBeforeDialogWasOpened = document.activeElement; - this.focusTrapActive = true; + // 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._ngZone.onMicrotaskEmpty.first().subscribe(() => { + this._elementFocusedBeforeDialogWasOpened = document.activeElement; + this._focusTrap.focusFirstTabbableElement(); + }); return attachResult; } diff --git a/src/lib/sidenav/sidenav.ts b/src/lib/sidenav/sidenav.ts index db597eeaa3c2..68059ff27ca1 100644 --- a/src/lib/sidenav/sidenav.ts +++ b/src/lib/sidenav/sidenav.ts @@ -13,10 +13,12 @@ import { EventEmitter, Renderer, ViewEncapsulation, + ViewChild, } from '@angular/core'; import {CommonModule} from '@angular/common'; import {Dir, MdError, coerceBooleanProperty, DefaultStyleCompatibilityModeModule} from '../core'; import {A11yModule} from '../core/a11y/index'; +import {FocusTrap} from '../core/a11y/focus-trap'; /** Exception thrown when two MdSidenav are matching the same side. */ @@ -62,6 +64,8 @@ export class MdSidenavToggleResult { encapsulation: ViewEncapsulation.None, }) export class MdSidenav implements AfterContentInit { + @ViewChild(FocusTrap) private _focusTrap: FocusTrap; + /** Alignment of the sidenav (direction neutral); whether 'start' or 'end'. */ private _align: 'start' | 'end' = 'start'; @@ -191,6 +195,10 @@ export class MdSidenav implements AfterContentInit { this.onCloseStart.emit(); } + if (this.focusTrapActive) { + this._focusTrap.focusFirstTabbableElementWhenReady(); + } + if (this._toggleAnimationPromise) { this._resolveToggleAnimationPromise(false); } From e316dcaecfb6c2d661e8dcc0c6429e3b76021eb8 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Thu, 10 Nov 2016 10:25:56 -0800 Subject: [PATCH 4/8] addressed comments --- src/lib/core/a11y/focus-trap.ts | 32 ++++++-------------------------- src/lib/sidenav/sidenav.spec.ts | 28 ++++++++++++++-------------- src/lib/sidenav/sidenav.ts | 9 +++++---- 3 files changed, 25 insertions(+), 44 deletions(-) diff --git a/src/lib/core/a11y/focus-trap.ts b/src/lib/core/a11y/focus-trap.ts index 3880d6d6ba5e..bda63179dcbe 100644 --- a/src/lib/core/a11y/focus-trap.ts +++ b/src/lib/core/a11y/focus-trap.ts @@ -1,12 +1,4 @@ -import { - Component, - ViewEncapsulation, - ViewChild, - ElementRef, - Input, - AfterContentInit, - NgZone -} from '@angular/core'; +import {Component, ViewEncapsulation, ViewChild, ElementRef, Input, NgZone} from '@angular/core'; import {InteractivityChecker} from './interactivity-checker'; import {coerceBooleanProperty} from '../coersion/boolean-property'; @@ -25,29 +17,17 @@ import {coerceBooleanProperty} from '../coersion/boolean-property'; templateUrl: 'focus-trap.html', encapsulation: ViewEncapsulation.None, }) -export class FocusTrap implements AfterContentInit { +export class FocusTrap { @ViewChild('trappedContent') trappedContent: ElementRef; - @Input() - get active(): boolean { return this._active; } - set active(val: boolean) { this._active = coerceBooleanProperty(val); } - - /** Whether the DOM content is ready. */ - private _contentReady: boolean = false; - /** Whether the focus trap is active. */ - private _active: boolean = true; + @Input() + get disabled(): boolean { return this._disabled; } + set disabled(val: boolean) { this._disabled = coerceBooleanProperty(val); } + private _disabled: boolean = false; constructor(private _checker: InteractivityChecker, private _ngZone: NgZone) { } - ngAfterContentInit() { - this._contentReady = true; - // Trigger setter behavior. - if (this.active) { - - } - } - /** * Waits for microtask queue to empty, then focuses the first tabbable element within the focus * trap region. diff --git a/src/lib/sidenav/sidenav.spec.ts b/src/lib/sidenav/sidenav.spec.ts index 8d666bf252d6..204113923781 100644 --- a/src/lib/sidenav/sidenav.spec.ts +++ b/src/lib/sidenav/sidenav.spec.ts @@ -295,49 +295,49 @@ describe('MdSidenav', () => { let fixture: ComponentFixture; let testComponent: SidenavWitFocusableElements; let sidenav: MdSidenav; - let link1Element: HTMLElement; - let link2Element: HTMLElement; + let firstFocusableElement: HTMLElement; + let lastFocusableElement: HTMLElement; beforeEach(() => { fixture = TestBed.createComponent(SidenavWitFocusableElements); testComponent = fixture.debugElement.componentInstance; sidenav = fixture.debugElement.query(By.directive(MdSidenav)).componentInstance; - link1Element = fixture.debugElement.query(By.css('.link1')).nativeElement; - link2Element = fixture.debugElement.query(By.css('.link1')).nativeElement; - link2Element.focus(); + firstFocusableElement = fixture.debugElement.query(By.css('.link1')).nativeElement; + lastFocusableElement = fixture.debugElement.query(By.css('.link1')).nativeElement; + lastFocusableElement.focus(); }); - it('should trp focus when opened in "over" mode', fakeAsync(() => { + it('should trap focus when opened in "over" mode', fakeAsync(() => { testComponent.mode = 'over'; - link2Element.focus(); + lastFocusableElement.focus(); sidenav.open(); endSidenavTransition(fixture); tick(); - expect(document.activeElement).toBe(link1Element); + expect(document.activeElement).toBe(firstFocusableElement); })); - it('should trap tabs when opened in "push" mode', fakeAsync(() => { + it('should trap focus when opened in "push" mode', fakeAsync(() => { testComponent.mode = 'push'; - link2Element.focus(); + lastFocusableElement.focus(); sidenav.open(); endSidenavTransition(fixture); tick(); - expect(document.activeElement).toBe(link1Element); + expect(document.activeElement).toBe(firstFocusableElement); })); - it('should not trap tabs when opened in "side" mode', fakeAsync(() => { + it('should not trap focus when opened in "side" mode', fakeAsync(() => { testComponent.mode = 'side'; - link2Element.focus(); + lastFocusableElement.focus(); sidenav.open(); endSidenavTransition(fixture); tick(); - expect(document.activeElement).toBe(link2Element); + expect(document.activeElement).toBe(lastFocusableElement); })); }); }); diff --git a/src/lib/sidenav/sidenav.ts b/src/lib/sidenav/sidenav.ts index 68059ff27ca1..8687380c755b 100644 --- a/src/lib/sidenav/sidenav.ts +++ b/src/lib/sidenav/sidenav.ts @@ -45,7 +45,7 @@ export class MdSidenavToggleResult { @Component({ moduleId: module.id, selector: 'md-sidenav, mat-sidenav', - template: '', + template: '', host: { '(transitionend)': '_onTransitionEnd($event)', // must prevent the browser from aligning text based on value @@ -127,8 +127,9 @@ export class MdSidenav implements AfterContentInit { */ private _resolveToggleAnimationPromise: (animationFinished: boolean) => void = null; - get focusTrapActive() { - return this.opened && this.mode != 'side'; + get isFocusTrapDisabled() { + // The focus trap is only enabled when the sidenav is open in any mode other than side. + return !this.opened || this.mode == 'side'; } /** @@ -195,7 +196,7 @@ export class MdSidenav implements AfterContentInit { this.onCloseStart.emit(); } - if (this.focusTrapActive) { + if (!this.isFocusTrapDisabled) { this._focusTrap.focusFirstTabbableElementWhenReady(); } From b44b0fb5359bec2ed51b07fa7e060c1cd967bf52 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 15 Nov 2016 07:54:53 -0800 Subject: [PATCH 5/8] s/active/!disabled --- src/lib/core/a11y/focus-trap.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/core/a11y/focus-trap.html b/src/lib/core/a11y/focus-trap.html index 0b62e1e4e744..077fc134b42a 100644 --- a/src/lib/core/a11y/focus-trap.html +++ b/src/lib/core/a11y/focus-trap.html @@ -1,3 +1,3 @@ -
+
-
+
From aaf4aa4c042887c1fc85666ee31612958de41d1d Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Thu, 1 Dec 2016 14:01:01 -0800 Subject: [PATCH 6/8] fix tests --- src/lib/sidenav/sidenav.spec.ts | 6 +++--- src/lib/sidenav/sidenav.ts | 8 -------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/lib/sidenav/sidenav.spec.ts b/src/lib/sidenav/sidenav.spec.ts index 204113923781..ad24acffd9ee 100644 --- a/src/lib/sidenav/sidenav.spec.ts +++ b/src/lib/sidenav/sidenav.spec.ts @@ -2,7 +2,8 @@ import {fakeAsync, async, tick, ComponentFixture, TestBed} from '@angular/core/t import {Component} from '@angular/core'; import {By} from '@angular/platform-browser'; import {MdSidenav, MdSidenavModule, MdSidenavToggleResult} from './sidenav'; -import {A11yModule} from "../core/a11y/index"; +import {A11yModule} from '../core/a11y/index'; +import {PlatformModule} from '../core/platform/platform'; function endSidenavTransition(fixture: ComponentFixture) { @@ -16,10 +17,9 @@ function endSidenavTransition(fixture: ComponentFixture) { describe('MdSidenav', () => { - beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [MdSidenavModule.forRoot(), A11yModule.forRoot()], + imports: [MdSidenavModule.forRoot(), A11yModule.forRoot(), PlatformModule.forRoot()], declarations: [ BasicTestApp, SidenavLayoutTwoSidenavTestApp, diff --git a/src/lib/sidenav/sidenav.ts b/src/lib/sidenav/sidenav.ts index 8687380c755b..58be88947b70 100644 --- a/src/lib/sidenav/sidenav.ts +++ b/src/lib/sidenav/sidenav.ts @@ -21,14 +21,6 @@ import {A11yModule} from '../core/a11y/index'; import {FocusTrap} from '../core/a11y/focus-trap'; -/** Exception thrown when two MdSidenav are matching the same side. */ -export class MdDuplicatedSidenavError extends MdError { - constructor(align: string) { - super(`A sidenav was already declared for 'align="${align}"'`); - } -} - - /** Sidenav toggle promise result. */ export class MdSidenavToggleResult { constructor(public type: 'open' | 'close', public animationFinished: boolean) {} From 7b123c9c8331f98324263182b5103253bbee2960 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Mon, 5 Dec 2016 11:19:00 -0800 Subject: [PATCH 7/8] fix lint --- src/lib/sidenav/sidenav.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/sidenav/sidenav.ts b/src/lib/sidenav/sidenav.ts index 58be88947b70..f7e1d144b94f 100644 --- a/src/lib/sidenav/sidenav.ts +++ b/src/lib/sidenav/sidenav.ts @@ -13,10 +13,10 @@ import { EventEmitter, Renderer, ViewEncapsulation, - ViewChild, + ViewChild } from '@angular/core'; import {CommonModule} from '@angular/common'; -import {Dir, MdError, coerceBooleanProperty, DefaultStyleCompatibilityModeModule} from '../core'; +import {Dir, coerceBooleanProperty, DefaultStyleCompatibilityModeModule} from '../core'; import {A11yModule} from '../core/a11y/index'; import {FocusTrap} from '../core/a11y/focus-trap'; From 7b86418419b7ef72afe5ddbf4b45fe99b2093437 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Mon, 5 Dec 2016 13:53:58 -0800 Subject: [PATCH 8/8] fix visibility issue --- src/lib/sidenav/sidenav.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/sidenav/sidenav.ts b/src/lib/sidenav/sidenav.ts index f7e1d144b94f..a3083de1bacf 100644 --- a/src/lib/sidenav/sidenav.ts +++ b/src/lib/sidenav/sidenav.ts @@ -56,7 +56,7 @@ export class MdSidenavToggleResult { encapsulation: ViewEncapsulation.None, }) export class MdSidenav implements AfterContentInit { - @ViewChild(FocusTrap) private _focusTrap: FocusTrap; + @ViewChild(FocusTrap) _focusTrap: FocusTrap; /** Alignment of the sidenav (direction neutral); whether 'start' or 'end'. */ private _align: 'start' | 'end' = 'start';