Skip to content

Commit 814ba1b

Browse files
authored
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.
1 parent c294a3a commit 814ba1b

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;
@@ -320,6 +322,7 @@ export abstract class MatDatepickerBase<
320322
{
321323
private _scrollStrategy: () => ScrollStrategy;
322324
private _inputStateChanges = Subscription.EMPTY;
325+
private _document = inject(DOCUMENT);
323326

324327
/** An input indicating the type of the custom header component for the calendar, if set. */
325328
@Input() calendarHeaderComponent: ComponentType<any>;
@@ -613,33 +616,48 @@ export abstract class MatDatepickerBase<
613616
return;
614617
}
615618

616-
if (this._componentRef) {
617-
const instance = this._componentRef.instance;
618-
instance._startExitAnimation();
619-
instance._animationDone.pipe(take(1)).subscribe(() => this._destroyOverlay());
620-
}
619+
const canRestoreFocus =
620+
this._restoreFocus &&
621+
this._focusedElementBeforeOpen &&
622+
typeof this._focusedElementBeforeOpen.focus === 'function';
621623

622624
const completeClose = () => {
623625
// The `_opened` could've been reset already if
624626
// we got two events in quick succession.
625627
if (this._opened) {
626628
this._opened = false;
627629
this.closedStream.emit();
628-
this._focusedElementBeforeOpen = null;
629630
}
630631
};
631632

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

0 commit comments

Comments
 (0)