Skip to content

Commit c7d4038

Browse files
committed
fix(select): close the panel when pressing escape
* Closes the `md-select` panel when pressing escape in the same way as the native select. * Adds an `escape` observable to the `ListKeyManager`, because we'll likely need this on other components. * Adds a missing test to ensure that the select closes when tabbing out.
1 parent 8d0cd04 commit c7d4038

File tree

4 files changed

+65
-13
lines changed

4 files changed

+65
-13
lines changed

src/lib/core/a11y/list-key-manager.spec.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {QueryList} from '@angular/core';
22
import {fakeAsync, tick} from '@angular/core/testing';
33
import {FocusKeyManager} from './focus-key-manager';
4-
import {DOWN_ARROW, UP_ARROW, TAB, HOME, END} from '../keyboard/keycodes';
4+
import {DOWN_ARROW, UP_ARROW, TAB, HOME, END, ESCAPE} from '../keyboard/keycodes';
55
import {ListKeyManager} from './list-key-manager';
66
import {ActiveDescendantKeyManager} from './activedescendant-key-manager';
77

@@ -39,6 +39,7 @@ describe('Key managers', () => {
3939
let TAB_EVENT: KeyboardEvent;
4040
let HOME_EVENT: KeyboardEvent;
4141
let END_EVENT: KeyboardEvent;
42+
let ESCAPE_EVENT: KeyboardEvent;
4243

4344
beforeEach(() => {
4445
itemList = new FakeQueryList<any>();
@@ -48,7 +49,7 @@ describe('Key managers', () => {
4849
TAB_EVENT = new FakeEvent(TAB) as KeyboardEvent;
4950
HOME_EVENT = new FakeEvent(HOME) as KeyboardEvent;
5051
END_EVENT = new FakeEvent(END) as KeyboardEvent;
51-
52+
ESCAPE_EVENT = new FakeEvent(ESCAPE) as KeyboardEvent;
5253
});
5354

5455

@@ -218,11 +219,11 @@ describe('Key managers', () => {
218219
});
219220

220221
it('should emit tabOut when the tab key is pressed', () => {
221-
let tabOutEmitted = false;
222-
keyManager.tabOut.first().subscribe(() => tabOutEmitted = true);
222+
let spy = jasmine.createSpy('tabOut spy');
223+
keyManager.tabOut.first().subscribe(spy);
223224
keyManager.onKeydown(TAB_EVENT);
224225

225-
expect(tabOutEmitted).toBe(true);
226+
expect(spy).toHaveBeenCalled();
226227
});
227228

228229
it('should prevent the default keyboard action', () => {
@@ -241,6 +242,14 @@ describe('Key managers', () => {
241242
expect(TAB_EVENT.defaultPrevented).toBe(false);
242243
});
243244

245+
it('should emit an event when escape is pressed', () => {
246+
let spy = jasmine.createSpy('escape spy');
247+
keyManager.escape.first().subscribe(spy);
248+
keyManager.onKeydown(ESCAPE_EVENT);
249+
250+
expect(spy).toHaveBeenCalled();
251+
});
252+
244253
it('should activate the first item when pressing down on a clean key manager', () => {
245254
keyManager = new ListKeyManager<FakeFocusable>(itemList);
246255

src/lib/core/a11y/list-key-manager.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {QueryList} from '@angular/core';
2-
import {UP_ARROW, DOWN_ARROW, TAB, HOME, END} from '../core';
2+
import {UP_ARROW, DOWN_ARROW, TAB, HOME, END, ESCAPE} from '../core';
33
import {Observable} from 'rxjs/Observable';
44
import {Subject} from 'rxjs/Subject';
55

@@ -18,7 +18,8 @@ export interface CanDisable {
1818
export class ListKeyManager<T extends CanDisable> {
1919
private _activeItemIndex: number = null;
2020
private _activeItem: T;
21-
private _tabOut: Subject<any> = new Subject();
21+
private _tabOut = new Subject<void>();
22+
private _escape = new Subject<void>();
2223
private _wrap: boolean = false;
2324

2425
constructor(private _items: QueryList<T>) {
@@ -63,6 +64,9 @@ export class ListKeyManager<T extends CanDisable> {
6364
case END:
6465
this.setLastItemActive();
6566
break;
67+
case ESCAPE:
68+
this._escape.next(null);
69+
break;
6670
case TAB:
6771
// Note that we shouldn't prevent the default action on tab.
6872
this._tabOut.next(null);
@@ -121,6 +125,14 @@ export class ListKeyManager<T extends CanDisable> {
121125
return this._tabOut.asObservable();
122126
}
123127

128+
/**
129+
* Observable that emits whenever the escape key is pressed, which usually indicates
130+
* that the component should be closed.
131+
*/
132+
get escape(): Observable<void> {
133+
return this._escape.asObservable();
134+
}
135+
124136
/**
125137
* This method sets the active item, given a list of items and the delta between the
126138
* currently active item and the new active item. It will calculate differently

src/lib/select/select.spec.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ import {
2020
ControlValueAccessor, FormControl, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule
2121
} from '@angular/forms';
2222
import {ViewportRuler} from '../core/overlay/position/viewport-ruler';
23-
import {dispatchFakeEvent} from '../core/testing/dispatch-events';
23+
import {dispatchFakeEvent, dispatchKeyboardEvent} from '../core/testing/dispatch-events';
2424
import {wrappedErrorMessage} from '../core/testing/wrapped-error-message';
25+
import {TAB, ESCAPE} from '../core/keyboard/keycodes';
2526

2627

2728
describe('MdSelect', () => {
@@ -205,6 +206,34 @@ describe('MdSelect', () => {
205206
});
206207
}));
207208

209+
it('should close the panel when tabbing out', async(() => {
210+
trigger.click();
211+
fixture.detectChanges();
212+
expect(fixture.componentInstance.select.panelOpen).toBe(true);
213+
214+
const panel = overlayContainerElement.querySelector('.mat-select-panel');
215+
dispatchKeyboardEvent(panel, 'keydown', TAB);
216+
fixture.detectChanges();
217+
218+
fixture.whenStable().then(() => {
219+
expect(fixture.componentInstance.select.panelOpen).toBe(false);
220+
});
221+
}));
222+
223+
it('should close the panel when pressing escape', async(() => {
224+
trigger.click();
225+
fixture.detectChanges();
226+
expect(fixture.componentInstance.select.panelOpen).toBe(true);
227+
228+
const panel = overlayContainerElement.querySelector('.mat-select-panel');
229+
dispatchKeyboardEvent(panel, 'keydown', ESCAPE);
230+
fixture.detectChanges();
231+
232+
fixture.whenStable().then(() => {
233+
expect(fixture.componentInstance.select.panelOpen).toBe(false);
234+
});
235+
}));
236+
208237
});
209238

210239
describe('selection logic', () => {

src/lib/select/select.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,8 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal
130130
/** Subscription to changes in the option list. */
131131
private _changeSubscription: Subscription;
132132

133-
/** Subscription to tab events while overlay is focused. */
134-
private _tabSubscription: Subscription;
133+
/** Subscription to tab and escape presses while overlay is focused. */
134+
private _closeKeySubscription: Subscription;
135135

136136
/** Whether filling out the select is required in the form. */
137137
private _required: boolean = false;
@@ -337,8 +337,8 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal
337337
this._changeSubscription.unsubscribe();
338338
}
339339

340-
if (this._tabSubscription) {
341-
this._tabSubscription.unsubscribe();
340+
if (this._closeKeySubscription) {
341+
this._closeKeySubscription.unsubscribe();
342342
}
343343
}
344344

@@ -569,7 +569,9 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal
569569
/** Sets up a key manager to listen to keyboard events on the overlay panel. */
570570
private _initKeyManager() {
571571
this._keyManager = new FocusKeyManager(this.options);
572-
this._tabSubscription = this._keyManager.tabOut.subscribe(() => this.close());
572+
this._closeKeySubscription = Observable
573+
.merge(this._keyManager.tabOut, this._keyManager.escape)
574+
.subscribe(() => this.close());
573575
}
574576

575577
/** Drops current option subscriptions and IDs and resets from scratch. */

0 commit comments

Comments
 (0)