Skip to content

Commit

Permalink
fix(sidenav): don't restore focus if focus isn't inside sidenav (#4578)
Browse files Browse the repository at this point in the history
* fix(sidenav): don't restore focus if focus isn't inside sidenav

* inject document
  • Loading branch information
mmalerba authored and andrewseguin committed Jun 7, 2017
1 parent b489fdd commit 3bc82f6
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 16 deletions.
48 changes: 38 additions & 10 deletions src/lib/sidenav/sidenav.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {fakeAsync, async, tick, ComponentFixture, TestBed} from '@angular/core/testing';
import {Component, ViewChild} from '@angular/core';
import {Component, ElementRef, ViewChild} from '@angular/core';
import {By} from '@angular/platform-browser';
import {MdSidenav, MdSidenavModule, MdSidenavToggleResult, MdSidenavContainer} from './index';
import {A11yModule} from '../core/a11y/index';
Expand Down Expand Up @@ -310,19 +310,20 @@ describe('MdSidenav', () => {
expect(testComponent.closeCount).toBe(0);
}));

it('should restore focus to the trigger element on close', fakeAsync(() => {
it('should restore focus on close if focus is inside sidenav', fakeAsync(() => {
let fixture = TestBed.createComponent(BasicTestApp);
let sidenav: MdSidenav = fixture.debugElement
.query(By.directive(MdSidenav)).componentInstance;
let trigger = document.createElement('button');
let openButton = fixture.componentInstance.openButton.nativeElement;
let sidenavButton = fixture.componentInstance.sidenavButton.nativeElement;

document.body.appendChild(trigger);
trigger.focus();
openButton.focus();
sidenav.open();

fixture.detectChanges();
endSidenavTransition(fixture);
tick();
sidenavButton.focus();

sidenav.close();

Expand All @@ -331,9 +332,32 @@ describe('MdSidenav', () => {
tick();

expect(document.activeElement)
.toBe(trigger, 'Expected focus to be restored to the trigger on close.');
.toBe(openButton, 'Expected focus to be restored to the open button on close.');
}));

it('should not restore focus on close if focus is outside sidenav', fakeAsync(() => {
let fixture = TestBed.createComponent(BasicTestApp);
let sidenav: MdSidenav = fixture.debugElement
.query(By.directive(MdSidenav)).componentInstance;
let openButton = fixture.componentInstance.openButton.nativeElement;
let closeButton = fixture.componentInstance.closeButton.nativeElement;

openButton.focus();
sidenav.open();

fixture.detectChanges();
endSidenavTransition(fixture);
tick();
closeButton.focus();

sidenav.close();

trigger.parentNode.removeChild(trigger);
fixture.detectChanges();
endSidenavTransition(fixture);
tick();

expect(document.activeElement)
.toBe(closeButton, 'Expected focus not to be restored to the open button on close.');
}));
});

Expand Down Expand Up @@ -508,10 +532,10 @@ class SidenavContainerTwoSidenavTestApp {
(open)="open()"
(close-start)="closeStart()"
(close)="close()">
Content.
<button #sidenavButton>Content.</button>
</md-sidenav>
<button (click)="sidenav.open()" class="open"></button>
<button (click)="sidenav.close()" class="close"></button>
<button (click)="sidenav.open()" class="open" #openButton></button>
<button (click)="sidenav.close()" class="close" #closeButton></button>
</md-sidenav-container>`,
})
class BasicTestApp {
Expand All @@ -521,6 +545,10 @@ class BasicTestApp {
closeCount: number = 0;
backdropClickedCount: number = 0;

@ViewChild('sidenavButton') sidenavButton: ElementRef;
@ViewChild('openButton') openButton: ElementRef;
@ViewChild('closeButton') closeButton: ElementRef;

openStart() {
this.openStartCount++;
}
Expand Down
26 changes: 20 additions & 6 deletions src/lib/sidenav/sidenav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ import {
Renderer2,
ViewEncapsulation,
NgZone,
OnDestroy,
OnDestroy, Inject,
} from '@angular/core';
import {Dir, coerceBooleanProperty} from '../core';
import {FocusTrapFactory, FocusTrap} from '../core/a11y/focus-trap';
import {ESCAPE} from '../core/keyboard/keycodes';
import 'rxjs/add/operator/first';
import {DOCUMENT} from '@angular/platform-browser';


/** Throws an exception when two MdSidenav are matching the same side. */
Expand Down Expand Up @@ -126,24 +127,37 @@ export class MdSidenav implements AfterContentInit, OnDestroy {
* @param _elementRef The DOM element reference. Used for transition and width calculation.
* If not available we do not hook on transitions.
*/
constructor(private _elementRef: ElementRef, private _focusTrapFactory: FocusTrapFactory) {
constructor(private _elementRef: ElementRef,
private _focusTrapFactory: FocusTrapFactory,
@Optional() @Inject(DOCUMENT) private _doc: any) {
this.onOpen.subscribe(() => {
this._elementFocusedBeforeSidenavWasOpened = document.activeElement as HTMLElement;
if (this._doc) {
this._elementFocusedBeforeSidenavWasOpened = this._doc.activeElement as HTMLElement;
}

if (this.isFocusTrapEnabled && this._focusTrap) {
this._focusTrap.focusInitialElementWhenReady();
}
});

this.onClose.subscribe(() => {
this.onClose.subscribe(() => this._restoreFocus());
}

/**
* If focus is currently inside the sidenav, restores it to where it was before the sidenav
* opened.
*/
private _restoreFocus() {
let activeEl = this._doc && this._doc.activeElement;
if (activeEl && this._elementRef.nativeElement.contains(activeEl)) {
if (this._elementFocusedBeforeSidenavWasOpened instanceof HTMLElement) {
this._elementFocusedBeforeSidenavWasOpened.focus();
} else {
this._elementRef.nativeElement.blur();
}
}

this._elementFocusedBeforeSidenavWasOpened = null;
});
this._elementFocusedBeforeSidenavWasOpened = null;
}

ngAfterContentInit() {
Expand Down

0 comments on commit 3bc82f6

Please sign in to comment.