Skip to content

Commit b3e35f3

Browse files
committed
fix(material/datepicker): restore focus after closing animation (#25567)
Currently the datepicker restores focus when the closing sequence starts, but in some cases that can cause focus to be lost. When the calendar view changes, we delay moving focus for a while in order to work around a VoiceOver issue. This means that if a closing sequence is kicked off while there's an in-progress view change, focus may end up being restored, then moved back into the calendar which then gets destroyed. This can be seen in the "Datepicker emulating a Year and month picker" example. These changes resolve the issue by moving the focus restoration all the way to the end of the closing sequence. Fixes #25564. (cherry picked from commit 814ba1b)
1 parent 6275f9d commit b3e35f3

File tree

1 file changed

+30
-12
lines changed

1 file changed

+30
-12
lines changed

src/material/datepicker/datepicker-base.ts

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
OnChanges,
4949
SimpleChanges,
5050
OnInit,
51+
inject,
5152
} from '@angular/core';
5253
import {CanColor, DateAdapter, mixinColor, ThemePalette} from '@angular/material/core';
5354
import {merge, Subject, Observable, Subscription} from 'rxjs';
@@ -68,6 +69,7 @@ import {
6869
MatDateRangeSelectionStrategy,
6970
} from './date-range-selection-strategy';
7071
import {MatDatepickerIntl} from './datepicker-intl';
72+
import {DOCUMENT} from '@angular/common';
7173

7274
/** Used to generate a unique ID for each datepicker instance. */
7375
let datepickerUid = 0;
@@ -314,6 +316,7 @@ export abstract class MatDatepickerBase<
314316
{
315317
private _scrollStrategy: () => ScrollStrategy;
316318
private _inputStateChanges = Subscription.EMPTY;
319+
private _document = inject(DOCUMENT);
317320

318321
/** An input indicating the type of the custom header component for the calendar, if set. */
319322
@Input() calendarHeaderComponent: ComponentType<any>;
@@ -607,33 +610,48 @@ export abstract class MatDatepickerBase<
607610
return;
608611
}
609612

610-
if (this._componentRef) {
611-
const instance = this._componentRef.instance;
612-
instance._startExitAnimation();
613-
instance._animationDone.pipe(take(1)).subscribe(() => this._destroyOverlay());
614-
}
613+
const canRestoreFocus =
614+
this._restoreFocus &&
615+
this._focusedElementBeforeOpen &&
616+
typeof this._focusedElementBeforeOpen.focus === 'function';
615617

616618
const completeClose = () => {
617619
// The `_opened` could've been reset already if
618620
// we got two events in quick succession.
619621
if (this._opened) {
620622
this._opened = false;
621623
this.closedStream.emit();
622-
this._focusedElementBeforeOpen = null;
623624
}
624625
};
625626

626-
if (
627-
this._restoreFocus &&
628-
this._focusedElementBeforeOpen &&
629-
typeof this._focusedElementBeforeOpen.focus === 'function'
630-
) {
627+
if (this._componentRef) {
628+
const {instance, location} = this._componentRef;
629+
instance._startExitAnimation();
630+
instance._animationDone.pipe(take(1)).subscribe(() => {
631+
const activeElement = this._document.activeElement;
632+
633+
// Since we restore focus after the exit animation, we have to check that
634+
// the user didn't move focus themselves inside the `close` handler.
635+
if (
636+
canRestoreFocus &&
637+
(!activeElement ||
638+
activeElement === this._document.activeElement ||
639+
location.nativeElement.contains(activeElement))
640+
) {
641+
this._focusedElementBeforeOpen!.focus();
642+
}
643+
644+
this._focusedElementBeforeOpen = null;
645+
this._destroyOverlay();
646+
});
647+
}
648+
649+
if (canRestoreFocus) {
631650
// Because IE moves focus asynchronously, we can't count on it being restored before we've
632651
// marked the datepicker as closed. If the event fires out of sequence and the element that
633652
// we're refocusing opens the datepicker on focus, the user could be stuck with not being
634653
// able to close the calendar at all. We work around it by making the logic, that marks
635654
// the datepicker as closed, async as well.
636-
this._focusedElementBeforeOpen.focus();
637655
setTimeout(completeClose);
638656
} else {
639657
completeClose();

0 commit comments

Comments
 (0)