Skip to content

Commit 94a2855

Browse files
crisbetokara
authored andcommitted
feat(select): close the panel when pressing escape (#3879)
1 parent 81a6f8d commit 94a2855

File tree

6 files changed

+60
-9
lines changed

6 files changed

+60
-9
lines changed

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ describe('Key managers', () => {
4848
TAB_EVENT = new FakeEvent(TAB) as KeyboardEvent;
4949
HOME_EVENT = new FakeEvent(HOME) as KeyboardEvent;
5050
END_EVENT = new FakeEvent(END) as KeyboardEvent;
51-
5251
});
5352

5453

@@ -218,11 +217,11 @@ describe('Key managers', () => {
218217
});
219218

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

225-
expect(tabOutEmitted).toBe(true);
224+
expect(spy).toHaveBeenCalled();
226225
});
227226

228227
it('should prevent the default keyboard action', () => {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ 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>();
2222
private _wrap: boolean = false;
2323

2424
constructor(private _items: QueryList<T>) {

src/lib/core/overlay/overlay-directives.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {OverlayContainer} from './overlay-container';
66
import {ConnectedPositionStrategy} from './position/connected-position-strategy';
77
import {ConnectedOverlayPositionChange} from './position/connected-position';
88
import {Dir} from '../rtl/dir';
9+
import {dispatchKeyboardEvent} from '../testing/dispatch-events';
10+
import {ESCAPE} from '../keyboard/keycodes';
911

1012

1113
describe('Overlay directives', () => {
@@ -98,6 +100,17 @@ describe('Overlay directives', () => {
98100
expect(getPaneElement().getAttribute('dir')).toBe('ltr');
99101
});
100102

103+
it('should close when pressing escape', () => {
104+
fixture.componentInstance.isOpen = true;
105+
fixture.detectChanges();
106+
107+
dispatchKeyboardEvent(document, 'keydown', ESCAPE);
108+
fixture.detectChanges();
109+
110+
expect(overlayContainerElement.textContent.trim()).toBe('',
111+
'Expected overlay to have been detached.');
112+
});
113+
101114
describe('inputs', () => {
102115

103116
it('should set the width', () => {

src/lib/core/overlay/overlay-directives.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
Input,
1010
OnDestroy,
1111
Output,
12-
ElementRef
12+
ElementRef,
13+
Renderer2,
1314
} from '@angular/core';
1415
import {Overlay, OVERLAY_PROVIDERS} from './overlay';
1516
import {OverlayRef} from './overlay-ref';
@@ -21,10 +22,12 @@ import {
2122
} from './position/connected-position';
2223
import {PortalModule} from '../portal/portal-directives';
2324
import {ConnectedPositionStrategy} from './position/connected-position-strategy';
24-
import {Subscription} from 'rxjs/Subscription';
2525
import {Dir, LayoutDirection} from '../rtl/dir';
2626
import {Scrollable} from './scroll/scrollable';
2727
import {coerceBooleanProperty} from '../coercion/boolean-property';
28+
import {ESCAPE} from '../keyboard/keycodes';
29+
import {Subscription} from 'rxjs/Subscription';
30+
2831

2932
/** Default set of positions for the overlay. Follows the behavior of a dropdown. */
3033
let defaultPositionList = [
@@ -68,6 +71,7 @@ export class ConnectedOverlayDirective implements OnDestroy {
6871
private _offsetX: number = 0;
6972
private _offsetY: number = 0;
7073
private _position: ConnectedPositionStrategy;
74+
private _escapeListener: Function;
7175

7276
/** Origin for the connected overlay. */
7377
@Input() origin: OverlayOrigin;
@@ -152,6 +156,7 @@ export class ConnectedOverlayDirective implements OnDestroy {
152156

153157
constructor(
154158
private _overlay: Overlay,
159+
private _renderer: Renderer2,
155160
templateRef: TemplateRef<any>,
156161
viewContainerRef: ViewContainerRef,
157162
@Optional() private _dir: Dir) {
@@ -249,6 +254,7 @@ export class ConnectedOverlayDirective implements OnDestroy {
249254

250255
this._position.withDirection(this.dir);
251256
this._overlayRef.getState().direction = this.dir;
257+
this._initEscapeListener();
252258

253259
if (!this._overlayRef.hasAttached()) {
254260
this._overlayRef.attach(this._templatePortal);
@@ -273,6 +279,10 @@ export class ConnectedOverlayDirective implements OnDestroy {
273279
this._backdropSubscription.unsubscribe();
274280
this._backdropSubscription = null;
275281
}
282+
283+
if (this._escapeListener) {
284+
this._escapeListener();
285+
}
276286
}
277287

278288
/** Destroys the overlay created by this directive. */
@@ -284,9 +294,23 @@ export class ConnectedOverlayDirective implements OnDestroy {
284294
if (this._backdropSubscription) {
285295
this._backdropSubscription.unsubscribe();
286296
}
297+
287298
if (this._positionSubscription) {
288299
this._positionSubscription.unsubscribe();
289300
}
301+
302+
if (this._escapeListener) {
303+
this._escapeListener();
304+
}
305+
}
306+
307+
/** Sets the event listener that closes the overlay when pressing Escape. */
308+
private _initEscapeListener() {
309+
this._escapeListener = this._renderer.listen('document', 'keydown', (event: KeyboardEvent) => {
310+
if (event.keyCode === ESCAPE) {
311+
this._detachOverlay();
312+
}
313+
});
290314
}
291315
}
292316

src/lib/select/select.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
<ng-template cdk-connected-overlay [origin]="origin" [open]="panelOpen" hasBackdrop (backdropClick)="close()"
1717
backdropClass="cdk-overlay-transparent-backdrop" [positions]="_positions" [minWidth]="_triggerWidth"
18-
[offsetY]="_offsetY" [offsetX]="_offsetX" (attach)="_setScrollTop()">
18+
[offsetY]="_offsetY" [offsetX]="_offsetX" (attach)="_setScrollTop()" (detach)="close()">
1919
<div class="mat-select-panel" [@transformPanel]="'showing'" (@transformPanel.done)="_onPanelDone()"
2020
(keydown)="_keyManager.onKeydown($event)" [style.transformOrigin]="_transformOrigin"
2121
[class.mat-select-panel-done-animating]="_panelDoneAnimating">

src/lib/select/select.spec.ts

Lines changed: 16 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} from '../core/keyboard/keycodes';
2526

2627

2728
describe('MdSelect', () => {
@@ -205,6 +206,20 @@ 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+
208223
});
209224

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

0 commit comments

Comments
 (0)