fix(sidenav): don't restore focus if focus isn't inside sidenav (#4578)

* fix(sidenav): don't restore focus if focus isn't inside sidenav

* inject document
This commit is contained in:
mmalerba
2017-06-07 14:58:17 -07:00
committed by Andrew Seguin
parent b489fdd885
commit 3bc82f6dc8
2 changed files with 58 additions and 16 deletions
+38 -10
View File
@@ -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';
@@ -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();
@@ -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.');
}));
trigger.parentNode.removeChild(trigger);
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();
fixture.detectChanges();
endSidenavTransition(fixture);
tick();
expect(document.activeElement)
.toBe(closeButton, 'Expected focus not to be restored to the open button on close.');
}));
});
@@ -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 {
@@ -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++;
}
+20 -6
View File
@@ -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. */
@@ -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() {