Skip to content

Commit 8860090

Browse files
crisbetokara
authored andcommitted
fix(datepicker): restore focus to trigger element (#4804)
1 parent 223b237 commit 8860090

File tree

2 files changed

+72
-17
lines changed

2 files changed

+72
-17
lines changed

src/lib/datepicker/datepicker.spec.ts

Lines changed: 53 additions & 6 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, DateAdapter, NativeDateAdapter} 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
@@ -107,6 +112,23 @@ describe('MdDatepicker', () => {
107112
});
108113
}));
109114

115+
it('should close the popup when pressing ESCAPE', () => {
116+
testComponent.datepicker.open();
117+
fixture.detectChanges();
118+
119+
let content = document.querySelector('.cdk-overlay-pane md-datepicker-content');
120+
expect(content).toBeTruthy('Expected datepicker to be open.');
121+
122+
let keyboadEvent = dispatchKeyboardEvent(content, 'keydown', ESCAPE);
123+
fixture.detectChanges();
124+
125+
content = document.querySelector('.cdk-overlay-pane md-datepicker-content');
126+
127+
expect(content).toBeFalsy('Expected datepicker to be closed.');
128+
expect(keyboadEvent.defaultPrevented)
129+
.toBe(true, 'Expected default ESCAPE action to be prevented.');
130+
});
131+
110132
it('close should close dialog', async(() => {
111133
testComponent.touch = true;
112134
fixture.detectChanges();
@@ -425,6 +447,30 @@ describe('MdDatepicker', () => {
425447
let toggle = fixture.debugElement.query(By.css('button')).nativeElement;
426448
expect(toggle.getAttribute('type')).toBe('button');
427449
});
450+
451+
it('should restore focus to the toggle after the calendar is closed', () => {
452+
let toggle = fixture.debugElement.query(By.css('button')).nativeElement;
453+
454+
fixture.componentInstance.touchUI = false;
455+
fixture.detectChanges();
456+
457+
toggle.focus();
458+
expect(document.activeElement).toBe(toggle, 'Expected toggle to be focused.');
459+
460+
fixture.componentInstance.datepicker.open();
461+
fixture.detectChanges();
462+
463+
let pane = document.querySelector('.cdk-overlay-pane');
464+
465+
expect(pane).toBeTruthy('Expected calendar to be open.');
466+
expect(pane.contains(document.activeElement))
467+
.toBe(true, 'Expected focus to be inside the calendar.');
468+
469+
fixture.componentInstance.datepicker.close();
470+
fixture.detectChanges();
471+
472+
expect(document.activeElement).toBe(toggle, 'Expected focus to be restored to toggle.');
473+
});
428474
});
429475

430476
describe('datepicker inside input-container', () => {
@@ -767,11 +813,12 @@ class DatepickerWithFormControl {
767813
template: `
768814
<input [mdDatepicker]="d">
769815
<button [mdDatepickerToggle]="d"></button>
770-
<md-datepicker #d [touchUi]="true"></md-datepicker>
816+
<md-datepicker #d [touchUi]="touchUI"></md-datepicker>
771817
`,
772818
})
773819
class DatepickerWithToggle {
774820
@ViewChild('d') datepicker: MdDatepicker<Date>;
821+
touchUI = true;
775822
}
776823

777824

src/lib/datepicker/datepicker.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import {
2020
ViewContainerRef,
2121
ViewEncapsulation,
2222
NgZone,
23+
Inject,
2324
} from '@angular/core';
25+
import {DOCUMENT} from '@angular/platform-browser';
2426
import {Overlay} from '../core/overlay/overlay';
2527
import {OverlayRef} from '../core/overlay/overlay-ref';
2628
import {ComponentPortal} from '../core/portal/portal';
@@ -77,16 +79,10 @@ export class MdDatepickerContent<D> implements AfterContentInit {
7779
* @param event The event.
7880
*/
7981
_handleKeydown(event: KeyboardEvent): void {
80-
switch (event.keyCode) {
81-
case ESCAPE:
82-
this.datepicker.close();
83-
break;
84-
default:
85-
// Return so that we don't preventDefault on keys that are not explicitly handled.
86-
return;
82+
if (event.keyCode === ESCAPE) {
83+
this.datepicker.close();
84+
event.preventDefault();
8785
}
88-
89-
event.preventDefault();
9086
}
9187
}
9288

@@ -158,18 +154,22 @@ export class MdDatepicker<D> implements OnDestroy {
158154
/** The input element this datepicker is associated with. */
159155
private _datepickerInput: MdDatepickerInput<D>;
160156

157+
/** The element that was focused before the datepicker was opened. */
158+
private _focusedElementBeforeOpen: HTMLElement;
159+
161160
private _inputSubscription: Subscription;
162161

163162
constructor(private _dialog: MdDialog,
164163
private _overlay: Overlay,
165164
private _ngZone: NgZone,
166165
private _viewContainerRef: ViewContainerRef,
167166
@Optional() private _dateAdapter: DateAdapter<D>,
168-
@Optional() private _dir: Dir) {
167+
@Optional() private _dir: Dir,
168+
@Optional() @Inject(DOCUMENT) private _document: any) {
169+
169170
if (!this._dateAdapter) {
170171
throw createMissingDateImplError('DateAdapter');
171172
}
172-
173173
}
174174

175175
ngOnDestroy() {
@@ -213,6 +213,9 @@ export class MdDatepicker<D> implements OnDestroy {
213213
if (!this._datepickerInput) {
214214
throw Error('Attempted to open an MdDatepicker with no associated input.');
215215
}
216+
if (this._document) {
217+
this._focusedElementBeforeOpen = this._document.activeElement;
218+
}
216219

217220
this.touchUi ? this._openAsDialog() : this._openAsPopup();
218221
this.opened = true;
@@ -233,6 +236,11 @@ export class MdDatepicker<D> implements OnDestroy {
233236
if (this._calendarPortal && this._calendarPortal.isAttached) {
234237
this._calendarPortal.detach();
235238
}
239+
if (this._focusedElementBeforeOpen && 'focus' in this._focusedElementBeforeOpen) {
240+
this._focusedElementBeforeOpen.focus();
241+
this._focusedElementBeforeOpen = null;
242+
}
243+
236244
this.opened = false;
237245
}
238246

0 commit comments

Comments
 (0)