Skip to content

Commit a411382

Browse files
crisbetoandrewseguin
authored andcommitted
fix(datepicker): unable to close calendar when opened on focus in IE11 (#8918)
Fixes not being able to close a datepicker's calendar in IE11, if the datepicker's trigger opens it on focus. The issue comes down to the fact that all browsers focus elements synchronously, whereas IE does so asynchronously. Since our logic depends on everything firing in sequence, when IE focuses at a later point, the datepicker is already considered as closed which causes the logic that restores focus to the trigger to reopen the calendar. Fixes #8914.
1 parent 3dcf4cd commit a411382

File tree

2 files changed

+80
-10
lines changed

2 files changed

+80
-10
lines changed

src/lib/datepicker/datepicker.spec.ts

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
dispatchMouseEvent,
99
} from '@angular/cdk/testing';
1010
import {Component, ViewChild} from '@angular/core';
11-
import {async, ComponentFixture, inject, TestBed} from '@angular/core/testing';
11+
import {async, ComponentFixture, inject, TestBed, fakeAsync, flush} from '@angular/core/testing';
1212
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
1313
import {
1414
DEC,
@@ -63,6 +63,7 @@ describe('MatDatepicker', () => {
6363
NoInputDatepicker,
6464
StandardDatepicker,
6565
DatepickerWithEvents,
66+
DatepickerOpeningOnFocus,
6667
],
6768
});
6869

@@ -248,7 +249,7 @@ describe('MatDatepicker', () => {
248249
});
249250

250251
it('clicking the currently selected date should close the calendar ' +
251-
'without firing selectedChanged', () => {
252+
'without firing selectedChanged', fakeAsync(() => {
252253
const selectedChangedSpy =
253254
spyOn(testComponent.datepicker.selectedChanged, 'emit').and.callThrough();
254255

@@ -263,12 +264,13 @@ describe('MatDatepicker', () => {
263264
let cells = document.querySelectorAll('.mat-calendar-body-cell');
264265
dispatchMouseEvent(cells[1], 'click');
265266
fixture.detectChanges();
267+
flush();
266268
}
267269

268270
expect(selectedChangedSpy.calls.count()).toEqual(1);
269271
expect(document.querySelector('mat-dialog-container')).toBeNull();
270272
expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 2));
271-
});
273+
}));
272274

273275
it('pressing enter on the currently selected date should close the calendar without ' +
274276
'firing selectedChanged', () => {
@@ -1020,18 +1022,65 @@ describe('MatDatepicker', () => {
10201022
expect(testComponent.openedSpy).toHaveBeenCalled();
10211023
});
10221024

1023-
it('should dispatch an event when a datepicker is closed', () => {
1025+
it('should dispatch an event when a datepicker is closed', fakeAsync(() => {
10241026
testComponent.datepicker.open();
10251027
fixture.detectChanges();
10261028

10271029
testComponent.datepicker.close();
1030+
flush();
10281031
fixture.detectChanges();
10291032

10301033
expect(testComponent.closedSpy).toHaveBeenCalled();
1031-
});
1034+
}));
10321035

10331036
});
10341037

1038+
describe('datepicker that opens on focus', () => {
1039+
let fixture: ComponentFixture<DatepickerOpeningOnFocus>;
1040+
let testComponent: DatepickerOpeningOnFocus;
1041+
let input: HTMLInputElement;
1042+
1043+
beforeEach(fakeAsync(() => {
1044+
fixture = TestBed.createComponent(DatepickerOpeningOnFocus);
1045+
fixture.detectChanges();
1046+
testComponent = fixture.componentInstance;
1047+
input = fixture.debugElement.query(By.css('input')).nativeElement;
1048+
}));
1049+
1050+
it('should not reopen if the browser fires the focus event asynchronously', fakeAsync(() => {
1051+
// Stub out the real focus method so we can call it reliably.
1052+
spyOn(input, 'focus').and.callFake(() => {
1053+
// Dispatch the event handler async to simulate the IE11 behavior.
1054+
Promise.resolve().then(() => dispatchFakeEvent(input, 'focus'));
1055+
});
1056+
1057+
// Open initially by focusing.
1058+
input.focus();
1059+
fixture.detectChanges();
1060+
flush();
1061+
1062+
// Due to some browser limitations we can't install a stub on `document.activeElement`
1063+
// so instead we have to override the previously-focused element manually.
1064+
(fixture.componentInstance.datepicker as any)._focusedElementBeforeOpen = input;
1065+
1066+
// Ensure that the datepicker is actually open.
1067+
expect(testComponent.datepicker.opened).toBe(true, 'Expected datepicker to be open.');
1068+
1069+
// Close the datepicker.
1070+
testComponent.datepicker.close();
1071+
fixture.detectChanges();
1072+
1073+
// Schedule the input to be focused asynchronously.
1074+
input.focus();
1075+
fixture.detectChanges();
1076+
1077+
// Flush out the scheduled tasks.
1078+
flush();
1079+
1080+
expect(testComponent.datepicker.opened).toBe(false, 'Expected datepicker to be closed.');
1081+
}));
1082+
});
1083+
10351084
});
10361085

10371086
describe('with missing DateAdapter and MAT_DATE_FORMATS', () => {
@@ -1390,3 +1439,14 @@ class DatepickerWithEvents {
13901439
closedSpy = jasmine.createSpy('closed spy');
13911440
@ViewChild('d') datepicker: MatDatepicker<Date>;
13921441
}
1442+
1443+
1444+
@Component({
1445+
template: `
1446+
<input (focus)="d.open()" [matDatepicker]="d">
1447+
<mat-datepicker #d="matDatepicker"></mat-datepicker>
1448+
`,
1449+
})
1450+
class DatepickerOpeningOnFocus {
1451+
@ViewChild(MatDatepicker) datepicker: MatDatepicker<Date>;
1452+
}

src/lib/datepicker/datepicker.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -310,15 +310,25 @@ export class MatDatepicker<D> implements OnDestroy {
310310
if (this._calendarPortal && this._calendarPortal.isAttached) {
311311
this._calendarPortal.detach();
312312
}
313+
314+
const completeClose = () => {
315+
this._opened = false;
316+
this.closedStream.emit();
317+
this._focusedElementBeforeOpen = null;
318+
};
319+
313320
if (this._focusedElementBeforeOpen &&
314321
typeof this._focusedElementBeforeOpen.focus === 'function') {
315-
322+
// Because IE moves focus asynchronously, we can't count on it being restored before we've
323+
// marked the datepicker as closed. If the event fires out of sequence and the element that
324+
// we're refocusing opens the datepicker on focus, the user could be stuck with not being
325+
// able to close the calendar at all. We work around it by making the logic, that marks
326+
// the datepicker as closed, async as well.
316327
this._focusedElementBeforeOpen.focus();
317-
this._focusedElementBeforeOpen = null;
328+
setTimeout(completeClose);
329+
} else {
330+
completeClose();
318331
}
319-
320-
this._opened = false;
321-
this.closedStream.emit();
322332
}
323333

324334
/** Open the calendar as a dialog. */

0 commit comments

Comments
 (0)