Skip to content

Commit 34a5997

Browse files
committed
fix(datepicker): inconsistent focus restoration timing in touchUi mode
`MatDatepicker` has focus restoration which happens (almost) synchronously, whereas `MatDialog` restores focus asynchronously when the animation is finished. This means that in `touchUi` mode focus ends up being restored both by the datepicker and the dialog within a 150ms window. The problem is that if the consumer of `MatDatepicker` decides to move focus inside the `closed` event, it'll be ovewritten by the dialog 150ms later. These changes disable the dialog's focus restoration because it's unnecessary. Fixes #17560.
1 parent 93dc69f commit 34a5997

File tree

2 files changed

+47
-0
lines changed

2 files changed

+47
-0
lines changed

src/material/datepicker/datepicker.spec.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -976,6 +976,45 @@ describe('MatDatepicker', () => {
976976
expect(document.activeElement).toBe(toggle, 'Expected focus to be restored to toggle.');
977977
});
978978

979+
it('should not override focus if it was moved inside the closed event in touchUI mode',
980+
fakeAsync(() => {
981+
const focusTarget = document.createElement('button');
982+
const datepicker = fixture.componentInstance.datepicker;
983+
const subscription = datepicker.closedStream.subscribe(() => focusTarget.focus());
984+
const input = fixture.nativeElement.querySelector('input');
985+
986+
focusTarget.setAttribute('tabindex', '0');
987+
document.body.appendChild(focusTarget);
988+
989+
// Important: we're testing the touchUI behavior on particular.
990+
fixture.componentInstance.touchUI = true;
991+
fixture.detectChanges();
992+
993+
// Focus the input before opening so that the datepicker restores focus to it on close.
994+
input.focus();
995+
996+
expect(document.activeElement).toBe(input, 'Expected input to be focused on init.');
997+
998+
datepicker.open();
999+
fixture.detectChanges();
1000+
tick(500);
1001+
fixture.detectChanges();
1002+
1003+
expect(document.activeElement)
1004+
.not.toBe(input, 'Expected input not to be focused while dialog is open.');
1005+
1006+
datepicker.close();
1007+
fixture.detectChanges();
1008+
tick(500);
1009+
fixture.detectChanges();
1010+
1011+
expect(document.activeElement)
1012+
.toBe(focusTarget, 'Expected alternate focus target to be focused after closing.');
1013+
1014+
focusTarget.parentNode!.removeChild(focusTarget);
1015+
subscription.unsubscribe();
1016+
}));
1017+
9791018
it('should re-render when the i18n labels change',
9801019
inject([MatDatepickerIntl], (intl: MatDatepickerIntl) => {
9811020
const toggle = fixture.debugElement.query(By.css('button'))!.nativeElement;

src/material/datepicker/datepicker.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,14 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
405405
direction: this._dir ? this._dir.value : 'ltr',
406406
viewContainerRef: this._viewContainerRef,
407407
panelClass: 'mat-datepicker-dialog',
408+
409+
// `MatDialog` has focus restoration built in, however we want to disable it since the
410+
// datepicker also has focus restoration for dropdown mode. We want to do this, in order
411+
// to ensure that the timing is consistent between dropdown and dialog modes since `MatDialog`
412+
// restores focus when the animation is finished, but the datepicker does it immediately.
413+
// Furthermore, this avoids any conflicts where the datepicker consumer might move focus
414+
// inside the `closed` event which is dispatched immediately.
415+
restoreFocus: false
408416
});
409417

410418
this._dialogRef.afterClosed().subscribe(() => this.close());

0 commit comments

Comments
 (0)