Skip to content

Commit de49d3e

Browse files
committed
fix(datepicker): restore focus to trigger element
* The datepicker now restores focus to whatever element was focused before it was open. * Adds a test for the functionality that closes the datepicker when pressing escape.
1 parent 0e24345 commit de49d3e

File tree

2 files changed

+75
-20
lines changed

2 files changed

+75
-20
lines changed

src/lib/datepicker/datepicker.spec.ts

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1+
import {Component, ViewChild} from '@angular/core';
12
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
3+
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
4+
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
5+
import {By} from '@angular/platform-browser';
26
import {MdDatepickerModule} from './index';
3-
import {Component, ViewChild} from '@angular/core';
47
import {MdDatepicker} from './datepicker';
58
import {MdDatepickerInput} from './datepicker-input';
6-
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
7-
import {By} from '@angular/platform-browser';
8-
import {dispatchFakeEvent, dispatchMouseEvent} from '../core/testing/dispatch-events';
99
import {MdInputModule} from '../input/index';
10-
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
1110
import {MdNativeDateModule} from '../core/datetime/index';
11+
import {ESCAPE} from '../core';
12+
import {
13+
dispatchFakeEvent,
14+
dispatchMouseEvent,
15+
dispatchKeyboardEvent,
16+
} from '../core/testing/dispatch-events';
1217

1318

1419
// When constructing a Date, the month is zero-based. This can be confusing, since people are
@@ -17,7 +22,7 @@ const JAN = 0, FEB = 1, MAR = 2, APR = 3, MAY = 4, JUN = 5, JUL = 6, AUG = 7, SE
1722
NOV = 10, DEC = 11;
1823

1924

20-
describe('MdDatepicker', () => {
25+
fdescribe('MdDatepicker', () => {
2126
describe('with MdNativeDateModule', () => {
2227
beforeEach(async(() => {
2328
TestBed.configureTestingModule({
@@ -99,6 +104,23 @@ describe('MdDatepicker', () => {
99104
});
100105
}));
101106

107+
it('should close the popup when pressing ESCAPE', () => {
108+
testComponent.datepicker.open();
109+
fixture.detectChanges();
110+
111+
let content = document.querySelector('.cdk-overlay-pane md-datepicker-content');
112+
expect(content).toBeTruthy('Expected datepicker to be open.');
113+
114+
let keyboadEvent = dispatchKeyboardEvent(content, 'keydown', ESCAPE);
115+
fixture.detectChanges();
116+
117+
content = document.querySelector('.cdk-overlay-pane md-datepicker-content');
118+
119+
expect(content).toBeFalsy('Expected datepicker to be closed.');
120+
expect(keyboadEvent.defaultPrevented)
121+
.toBe(true, 'Expected default ESCAPE action to be prevented.');
122+
});
123+
102124
it('close should close dialog', async(() => {
103125
testComponent.touch = true;
104126
fixture.detectChanges();
@@ -388,6 +410,30 @@ describe('MdDatepicker', () => {
388410
let toggle = fixture.debugElement.query(By.css('button')).nativeElement;
389411
expect(toggle.getAttribute('type')).toBe('button');
390412
});
413+
414+
it('should restore focus to the toggle after the calendar is closed', () => {
415+
let toggle = fixture.debugElement.query(By.css('button')).nativeElement;
416+
417+
fixture.componentInstance.touchUI = false;
418+
fixture.detectChanges();
419+
420+
toggle.focus();
421+
expect(document.activeElement).toBe(toggle, 'Expected toggle to be focused.');
422+
423+
fixture.componentInstance.datepicker.open();
424+
fixture.detectChanges();
425+
426+
let pane = document.querySelector('.cdk-overlay-pane');
427+
428+
expect(pane).toBeTruthy('Expected calendar to be open.');
429+
expect(pane.contains(document.activeElement))
430+
.toBe(true, 'Expected focus to be inside the calendar.');
431+
432+
fixture.componentInstance.datepicker.close();
433+
fixture.detectChanges();
434+
435+
expect(document.activeElement).toBe(toggle, 'Expected focus to be restored to toggle.');
436+
});
391437
});
392438

393439
describe('datepicker inside input-container', () => {
@@ -582,11 +628,12 @@ class DatepickerWithFormControl {
582628
template: `
583629
<input [mdDatepicker]="d">
584630
<button [mdDatepickerToggle]="d"></button>
585-
<md-datepicker #d [touchUi]="true"></md-datepicker>
631+
<md-datepicker #d [touchUi]="touchUI"></md-datepicker>
586632
`,
587633
})
588634
class DatepickerWithToggle {
589635
@ViewChild('d') datepicker: MdDatepicker<Date>;
636+
touchUI = true;
590637
}
591638

592639

src/lib/datepicker/datepicker.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import {
1010
Output,
1111
ViewChild,
1212
ViewContainerRef,
13-
ViewEncapsulation
13+
ViewEncapsulation,
14+
Inject,
1415
} from '@angular/core';
16+
import {DOCUMENT} from '@angular/platform-browser';
1517
import {Overlay} from '../core/overlay/overlay';
1618
import {OverlayRef} from '../core/overlay/overlay-ref';
1719
import {ComponentPortal} from '../core/portal/portal';
@@ -30,7 +32,7 @@ import {Subscription} from 'rxjs/Subscription';
3032
import {MdDialogConfig} from '../dialog/dialog-config';
3133
import {DateAdapter} from '../core/datetime/index';
3234
import {createMissingDateImplError} from './datepicker-errors';
33-
import {ESCAPE} from '../core/keyboard/keycodes';
35+
import {ESCAPE, TAB} from '../core/keyboard/keycodes';
3436
import {MdCalendar} from './calendar';
3537

3638

@@ -72,16 +74,10 @@ export class MdDatepickerContent<D> implements AfterContentInit {
7274
* @param event The event.
7375
*/
7476
_handleKeydown(event: KeyboardEvent): void {
75-
switch (event.keyCode) {
76-
case ESCAPE:
77-
this.datepicker.close();
78-
break;
79-
default:
80-
// Return so that we don't preventDefault on keys that are not explicitly handled.
81-
return;
77+
if (event.keyCode === ESCAPE) {
78+
this.datepicker.close();
79+
event.preventDefault();
8280
}
83-
84-
event.preventDefault();
8581
}
8682
}
8783

@@ -153,16 +149,20 @@ export class MdDatepicker<D> implements OnDestroy {
153149
/** The input element this datepicker is associated with. */
154150
private _datepickerInput: MdDatepickerInput<D>;
155151

152+
/** The element that was focused before the datepicker was opened. */
153+
private _focusedElementBeforeOpen: HTMLElement;
154+
156155
private _inputSubscription: Subscription;
157156

158157
constructor(private _dialog: MdDialog, private _overlay: Overlay,
159158
private _viewContainerRef: ViewContainerRef,
160159
@Optional() private _dateAdapter: DateAdapter<D>,
161-
@Optional() private _dir: Dir) {
160+
@Optional() private _dir: Dir,
161+
@Optional() @Inject(DOCUMENT) private _document: any) {
162+
162163
if (!this._dateAdapter) {
163164
throw createMissingDateImplError('DateAdapter');
164165
}
165-
166166
}
167167

168168
ngOnDestroy() {
@@ -206,6 +206,9 @@ export class MdDatepicker<D> implements OnDestroy {
206206
if (!this._datepickerInput) {
207207
throw new Error('Attempted to open an MdDatepicker with no associated input.');
208208
}
209+
if (this._document) {
210+
this._focusedElementBeforeOpen = this._document.activeElement;
211+
}
209212

210213
this.touchUi ? this._openAsDialog() : this._openAsPopup();
211214
this.opened = true;
@@ -226,6 +229,11 @@ export class MdDatepicker<D> implements OnDestroy {
226229
if (this._calendarPortal && this._calendarPortal.isAttached) {
227230
this._calendarPortal.detach();
228231
}
232+
if (this._focusedElementBeforeOpen && 'focus' in this._focusedElementBeforeOpen) {
233+
this._focusedElementBeforeOpen.focus();
234+
this._focusedElementBeforeOpen = null;
235+
}
236+
229237
this.opened = false;
230238
}
231239

0 commit comments

Comments
 (0)