Skip to content

Commit

Permalink
fix(datepicker): inconsistent focus restoration timing in touchUi mode (
Browse files Browse the repository at this point in the history
#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.
  • Loading branch information
crisbeto authored and jelbourn committed Apr 23, 2020
1 parent 1daada5 commit d199500
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 1 deletion.
39 changes: 39 additions & 0 deletions src/material/datepicker/datepicker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1015,6 +1015,45 @@ describe('MatDatepicker', () => {
expect(document.activeElement).toBe(toggle, 'Expected focus to be restored to toggle.');
});

it('should not override focus if it was moved inside the closed event in touchUI mode',
fakeAsync(() => {
const focusTarget = document.createElement('button');
const datepicker = fixture.componentInstance.datepicker;
const subscription = datepicker.closedStream.subscribe(() => focusTarget.focus());
const input = fixture.nativeElement.querySelector('input');

focusTarget.setAttribute('tabindex', '0');
document.body.appendChild(focusTarget);

// Important: we're testing the touchUI behavior on particular.
fixture.componentInstance.touchUI = true;
fixture.detectChanges();

// Focus the input before opening so that the datepicker restores focus to it on close.
input.focus();

expect(document.activeElement).toBe(input, 'Expected input to be focused on init.');

datepicker.open();
fixture.detectChanges();
tick(500);
fixture.detectChanges();

expect(document.activeElement)
.not.toBe(input, 'Expected input not to be focused while dialog is open.');

datepicker.close();
fixture.detectChanges();
tick(500);
fixture.detectChanges();

expect(document.activeElement)
.toBe(focusTarget, 'Expected alternate focus target to be focused after closing.');

focusTarget.parentNode!.removeChild(focusTarget);
subscription.unsubscribe();
}));

it('should re-render when the i18n labels change',
inject([MatDatepickerIntl], (intl: MatDatepickerIntl) => {
const toggle = fixture.debugElement.query(By.css('button'))!.nativeElement;
Expand Down
9 changes: 8 additions & 1 deletion src/material/datepicker/datepicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,14 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
maxHeight: '',
position: {},
autoFocus: true,
restoreFocus: true

// `MatDialog` has focus restoration built in, however we want to disable it since the
// datepicker also has focus restoration for dropdown mode. We want to do this, in order
// to ensure that the timing is consistent between dropdown and dialog modes since `MatDialog`
// restores focus when the animation is finished, but the datepicker does it immediately.
// Furthermore, this avoids any conflicts where the datepicker consumer might move focus
// inside the `closed` event which is dispatched immediately.
restoreFocus: false
});

this._dialogRef.afterClosed().subscribe(() => this.close());
Expand Down

0 comments on commit d199500

Please sign in to comment.