Skip to content

Commit

Permalink
feat(sidenav): close via escape key and restore focus to trigger element
Browse files Browse the repository at this point in the history
* Adds the ability to close a sidenav by pressing escape.
* Restores focus to the trigger element after a sidenav is closed.
  • Loading branch information
crisbeto committed Nov 26, 2016
1 parent cf1b4b9 commit b8ade2c
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/lib/sidenav/sidenav.scss
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ md-sidenav {
bottom: 0;
z-index: 3;
min-width: 5%;
outline: 0;

// TODO(kara): revisit scrolling behavior for sidenavs
overflow-y: auto;
Expand Down
54 changes: 54 additions & 0 deletions src/lib/sidenav/sidenav.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,60 @@ describe('MdSidenav', () => {
tick();
}).not.toThrow();
}));

it('should close when pressing escape', fakeAsync(() => {
let fixture = TestBed.createComponent(BasicTestApp);
let testComponent: BasicTestApp = fixture.debugElement.componentInstance;
let sidenav: MdSidenav = fixture.debugElement
.query(By.directive(MdSidenav)).componentInstance;

sidenav.open();

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

expect(testComponent.openCount).toBe(1);
expect(testComponent.closeCount).toBe(0);

// Simulate pressing the escape key.
sidenav.handleEscapeKey();

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

expect(testComponent.closeCount).toBe(1);
}));

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

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

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

expect(document.activeElement)
.not.toBe(trigger, 'Expected focus to change when the sidenav was opened.');

sidenav.close();

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

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

trigger.parentNode.removeChild(trigger);
}));
});

describe('attributes', () => {
Expand Down
22 changes: 21 additions & 1 deletion src/lib/sidenav/sidenav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export class MdDuplicatedSidenavError extends MdError {
template: '<ng-content></ng-content>',
host: {
'(transitionend)': '_onTransitionEnd($event)',
'(keydown.escape)': 'handleEscapeKey()',
// must prevent the browser from aligning text based on value
'[attr.align]': 'null',
'[class.md-sidenav-closed]': '_isClosed',
Expand All @@ -49,6 +50,7 @@ export class MdDuplicatedSidenavError extends MdError {
'[class.md-sidenav-push]': '_modePush',
'[class.md-sidenav-side]': '_modeSide',
'[class.md-sidenav-invalid]': '!valid',
'tabIndex': '-1'
},
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
Expand Down Expand Up @@ -110,7 +112,18 @@ export class MdSidenav implements AfterContentInit {
* @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) {}
constructor(private _elementRef: ElementRef) {
this.onOpen.subscribe(() => {
this._elementFocusedBeforeSidenavWasOpened = document.activeElement as HTMLElement;
this._elementRef.nativeElement.focus();
});

this.onClose.subscribe(() => {
if (this._elementFocusedBeforeSidenavWasOpened) {
this._elementFocusedBeforeSidenavWasOpened.focus();
}
});
}

ngAfterContentInit() {
// This can happen when the sidenav is set to opened in the template and the transition
Expand Down Expand Up @@ -191,6 +204,12 @@ export class MdSidenav implements AfterContentInit {
}
}

/** Handles the user pressing the escape key. */
handleEscapeKey() {
// TODO(crisbeto): this is in a separate method in order to
// allow for disabling the behavior later.
this.close();
}

/**
* When transition has finished, set the internal state for classes and emit the proper event.
Expand Down Expand Up @@ -266,6 +285,7 @@ export class MdSidenav implements AfterContentInit {
private _closePromise: Promise<void>;
private _closePromiseResolve: () => void;
private _closePromiseReject: () => void;
private _elementFocusedBeforeSidenavWasOpened: HTMLElement = null;
}

/**
Expand Down

0 comments on commit b8ade2c

Please sign in to comment.