Skip to content

Commit bb30a3a

Browse files
authored
fix(datepicker): inconsistent focus restoration timing in touchUi mode (#17732)
`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 7f7e0a1 commit bb30a3a

File tree

2 files changed

+47
-1
lines changed

2 files changed

+47
-1
lines changed

src/material/datepicker/datepicker.spec.ts

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

1018+
it('should not override focus if it was moved inside the closed event in touchUI mode',
1019+
fakeAsync(() => {
1020+
const focusTarget = document.createElement('button');
1021+
const datepicker = fixture.componentInstance.datepicker;
1022+
const subscription = datepicker.closedStream.subscribe(() => focusTarget.focus());
1023+
const input = fixture.nativeElement.querySelector('input');
1024+
1025+
focusTarget.setAttribute('tabindex', '0');
1026+
document.body.appendChild(focusTarget);
1027+
1028+
// Important: we're testing the touchUI behavior on particular.
1029+
fixture.componentInstance.touchUI = true;
1030+
fixture.detectChanges();
1031+
1032+
// Focus the input before opening so that the datepicker restores focus to it on close.
1033+
input.focus();
1034+
1035+
expect(document.activeElement).toBe(input, 'Expected input to be focused on init.');
1036+
1037+
datepicker.open();
1038+
fixture.detectChanges();
1039+
tick(500);
1040+
fixture.detectChanges();
1041+
1042+
expect(document.activeElement)
1043+
.not.toBe(input, 'Expected input not to be focused while dialog is open.');
1044+
1045+
datepicker.close();
1046+
fixture.detectChanges();
1047+
tick(500);
1048+
fixture.detectChanges();
1049+
1050+
expect(document.activeElement)
1051+
.toBe(focusTarget, 'Expected alternate focus target to be focused after closing.');
1052+
1053+
focusTarget.parentNode!.removeChild(focusTarget);
1054+
subscription.unsubscribe();
1055+
}));
1056+
10181057
it('should re-render when the i18n labels change',
10191058
inject([MatDatepickerIntl], (intl: MatDatepickerIntl) => {
10201059
const toggle = fixture.debugElement.query(By.css('button'))!.nativeElement;

src/material/datepicker/datepicker.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,14 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
437437
maxHeight: '',
438438
position: {},
439439
autoFocus: true,
440-
restoreFocus: true
440+
441+
// `MatDialog` has focus restoration built in, however we want to disable it since the
442+
// datepicker also has focus restoration for dropdown mode. We want to do this, in order
443+
// to ensure that the timing is consistent between dropdown and dialog modes since `MatDialog`
444+
// restores focus when the animation is finished, but the datepicker does it immediately.
445+
// Furthermore, this avoids any conflicts where the datepicker consumer might move focus
446+
// inside the `closed` event which is dispatched immediately.
447+
restoreFocus: false
441448
});
442449

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

0 commit comments

Comments
 (0)