diff --git a/src/material/datepicker/datepicker-base.ts b/src/material/datepicker/datepicker-base.ts index ed28441ef6b7..d6c6376bee4c 100644 --- a/src/material/datepicker/datepicker-base.ts +++ b/src/material/datepicker/datepicker-base.ts @@ -51,6 +51,7 @@ import { inject, } from '@angular/core'; import {CanColor, DateAdapter, mixinColor, ThemePalette} from '@angular/material/core'; +import {AnimationEvent} from '@angular/animations'; import {merge, Subject, Observable, Subscription} from 'rxjs'; import {filter, take} from 'rxjs/operators'; import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform'; @@ -119,7 +120,8 @@ const _MatDatepickerContentBase = mixinColor( host: { 'class': 'mat-datepicker-content', '[@transformPanel]': '_animationState', - '(@transformPanel.done)': '_animationDone.next()', + '(@transformPanel.start)': '_handleAnimationEvent($event)', + '(@transformPanel.done)': '_handleAnimationEvent($event)', '[class.mat-datepicker-content-touch]': 'datepicker.touchUi', }, animations: [matDatepickerAnimations.transformPanel, matDatepickerAnimations.fadeInCalendar], @@ -161,6 +163,9 @@ export class MatDatepickerContent> /** Emits when an animation has finished. */ readonly _animationDone = new Subject(); + /** Whether there is an in-progress animation. */ + _isAnimating = false; + /** Text for the close button. */ _closeButtonText: string; @@ -240,6 +245,14 @@ export class MatDatepickerContent> this._changeDetectorRef.markForCheck(); } + _handleAnimationEvent(event: AnimationEvent) { + this._isAnimating = event.phaseName === 'start'; + + if (!this._isAnimating) { + this._animationDone.next(); + } + } + _getSelected() { return this._model.selection as unknown as D | DateRange | null; } @@ -596,7 +609,9 @@ export abstract class MatDatepickerBase< /** Open the calendar. */ open(): void { - if (this._opened || this.disabled) { + // Skip reopening if there's an in-progress animation to avoid overlapping + // sequences which can cause "changed after checked" errors. See #25837. + if (this._opened || this.disabled || this._componentRef?.instance._isAnimating) { return; } @@ -612,7 +627,9 @@ export abstract class MatDatepickerBase< /** Close the calendar. */ close(): void { - if (!this._opened) { + // Skip reopening if there's an in-progress animation to avoid overlapping + // sequences which can cause "changed after checked" errors. See #25837. + if (!this._opened || this._componentRef?.instance._isAnimating) { return; } diff --git a/tools/public_api_guard/material/datepicker.md b/tools/public_api_guard/material/datepicker.md index 992d012f98c4..a761685505fb 100644 --- a/tools/public_api_guard/material/datepicker.md +++ b/tools/public_api_guard/material/datepicker.md @@ -9,6 +9,7 @@ import { AbstractControl } from '@angular/forms'; import { AfterContentInit } from '@angular/core'; import { AfterViewChecked } from '@angular/core'; import { AfterViewInit } from '@angular/core'; +import { AnimationEvent as AnimationEvent_2 } from '@angular/animations'; import { AnimationTriggerMetadata } from '@angular/animations'; import { BooleanInput } from '@angular/cdk/coercion'; import { CanColor } from '@angular/material/core'; @@ -382,8 +383,11 @@ export class MatDatepickerContent> extend // (undocumented) _getSelected(): D | DateRange | null; // (undocumented) + _handleAnimationEvent(event: AnimationEvent_2): void; + // (undocumented) _handleUserSelection(event: MatCalendarUserEvent): void; _isAbove: boolean; + _isAnimating: boolean; // (undocumented) ngAfterViewInit(): void; // (undocumented)