Skip to content

Commit b8ade2c

Browse files
committed
feat(sidenav): close via escape key and restore focus to trigger element
* Adds the ability to close a sidenav by pressing escape. * Restores focus to the trigger element after a sidenav is closed.
1 parent cf1b4b9 commit b8ade2c

File tree

3 files changed

+76
-1
lines changed

3 files changed

+76
-1
lines changed

src/lib/sidenav/sidenav.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ md-sidenav {
102102
bottom: 0;
103103
z-index: 3;
104104
min-width: 5%;
105+
outline: 0;
105106

106107
// TODO(kara): revisit scrolling behavior for sidenavs
107108
overflow-y: auto;

src/lib/sidenav/sidenav.spec.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,60 @@ describe('MdSidenav', () => {
194194
tick();
195195
}).not.toThrow();
196196
}));
197+
198+
it('should close when pressing escape', fakeAsync(() => {
199+
let fixture = TestBed.createComponent(BasicTestApp);
200+
let testComponent: BasicTestApp = fixture.debugElement.componentInstance;
201+
let sidenav: MdSidenav = fixture.debugElement
202+
.query(By.directive(MdSidenav)).componentInstance;
203+
204+
sidenav.open();
205+
206+
fixture.detectChanges();
207+
endSidenavTransition(fixture);
208+
tick();
209+
210+
expect(testComponent.openCount).toBe(1);
211+
expect(testComponent.closeCount).toBe(0);
212+
213+
// Simulate pressing the escape key.
214+
sidenav.handleEscapeKey();
215+
216+
fixture.detectChanges();
217+
endSidenavTransition(fixture);
218+
tick();
219+
220+
expect(testComponent.closeCount).toBe(1);
221+
}));
222+
223+
it('should restore focus to the trigger element on close', fakeAsync(() => {
224+
let fixture = TestBed.createComponent(BasicTestApp);
225+
let sidenav: MdSidenav = fixture.debugElement
226+
.query(By.directive(MdSidenav)).componentInstance;
227+
let trigger = document.createElement('button');
228+
229+
document.body.appendChild(trigger);
230+
trigger.focus();
231+
sidenav.open();
232+
233+
fixture.detectChanges();
234+
endSidenavTransition(fixture);
235+
tick();
236+
237+
expect(document.activeElement)
238+
.not.toBe(trigger, 'Expected focus to change when the sidenav was opened.');
239+
240+
sidenav.close();
241+
242+
fixture.detectChanges();
243+
endSidenavTransition(fixture);
244+
tick();
245+
246+
expect(document.activeElement)
247+
.toBe(trigger, 'Expected focus to be restored to the trigger on close.');
248+
249+
trigger.parentNode.removeChild(trigger);
250+
}));
197251
});
198252

199253
describe('attributes', () => {

src/lib/sidenav/sidenav.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export class MdDuplicatedSidenavError extends MdError {
3838
template: '<ng-content></ng-content>',
3939
host: {
4040
'(transitionend)': '_onTransitionEnd($event)',
41+
'(keydown.escape)': 'handleEscapeKey()',
4142
// must prevent the browser from aligning text based on value
4243
'[attr.align]': 'null',
4344
'[class.md-sidenav-closed]': '_isClosed',
@@ -49,6 +50,7 @@ export class MdDuplicatedSidenavError extends MdError {
4950
'[class.md-sidenav-push]': '_modePush',
5051
'[class.md-sidenav-side]': '_modeSide',
5152
'[class.md-sidenav-invalid]': '!valid',
53+
'tabIndex': '-1'
5254
},
5355
changeDetection: ChangeDetectionStrategy.OnPush,
5456
encapsulation: ViewEncapsulation.None,
@@ -110,7 +112,18 @@ export class MdSidenav implements AfterContentInit {
110112
* @param _elementRef The DOM element reference. Used for transition and width calculation.
111113
* If not available we do not hook on transitions.
112114
*/
113-
constructor(private _elementRef: ElementRef) {}
115+
constructor(private _elementRef: ElementRef) {
116+
this.onOpen.subscribe(() => {
117+
this._elementFocusedBeforeSidenavWasOpened = document.activeElement as HTMLElement;
118+
this._elementRef.nativeElement.focus();
119+
});
120+
121+
this.onClose.subscribe(() => {
122+
if (this._elementFocusedBeforeSidenavWasOpened) {
123+
this._elementFocusedBeforeSidenavWasOpened.focus();
124+
}
125+
});
126+
}
114127

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

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

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

271291
/**

0 commit comments

Comments
 (0)