66 * found in the LICENSE file at https://angular.dev/license
77 */
88
9- import { AnimationEvent } from '@angular/animations' ;
109import { _IdGenerator , CdkTrapFocus } from '@angular/cdk/a11y' ;
1110import { Directionality } from '@angular/cdk/bidi' ;
1211import { coerceStringArray } from '@angular/cdk/coercion' ;
@@ -34,6 +33,7 @@ import {DOCUMENT} from '@angular/common';
3433import {
3534 afterNextRender ,
3635 AfterViewInit ,
36+ ANIMATION_MODULE_TYPE ,
3737 booleanAttribute ,
3838 ChangeDetectionStrategy ,
3939 ChangeDetectorRef ,
@@ -46,10 +46,11 @@ import {
4646 InjectionToken ,
4747 Injector ,
4848 Input ,
49+ NgZone ,
4950 OnChanges ,
5051 OnDestroy ,
51- OnInit ,
5252 Output ,
53+ Renderer2 ,
5354 SimpleChanges ,
5455 ViewChild ,
5556 ViewContainerRef ,
@@ -70,7 +71,6 @@ import {
7071 ExtractDateTypeFromSelection ,
7172 MatDateSelectionModel ,
7273} from './date-selection-model' ;
73- import { matDatepickerAnimations } from './datepicker-animations' ;
7474import { createMissingDateImplError } from './datepicker-errors' ;
7575import { DateFilterFn } from './datepicker-input-base' ;
7676import { MatDatepickerIntl } from './datepicker-intl' ;
@@ -120,31 +120,34 @@ export const MAT_DATEPICKER_SCROLL_STRATEGY_FACTORY_PROVIDER = {
120120 host : {
121121 'class' : 'mat-datepicker-content' ,
122122 '[class]' : 'color ? "mat-" + color : ""' ,
123- '[@transformPanel]' : '_animationState' ,
124- '(@transformPanel.start)' : '_handleAnimationEvent($event)' ,
125- '(@transformPanel.done)' : '_handleAnimationEvent($event)' ,
126123 '[class.mat-datepicker-content-touch]' : 'datepicker.touchUi' ,
124+ '[class.mat-datepicker-content-animations-enabled]' : '!_animationsDisabled' ,
127125 } ,
128- animations : [ matDatepickerAnimations . transformPanel , matDatepickerAnimations . fadeInCalendar ] ,
129126 exportAs : 'matDatepickerContent' ,
130127 encapsulation : ViewEncapsulation . None ,
131128 changeDetection : ChangeDetectionStrategy . OnPush ,
132129 imports : [ CdkTrapFocus , MatCalendar , CdkPortalOutlet , MatButton ] ,
133130} )
134131export class MatDatepickerContent < S , D = ExtractDateTypeFromSelection < S > >
135- implements OnInit , AfterViewInit , OnDestroy
132+ implements AfterViewInit , OnDestroy
136133{
137- protected _elementRef = inject ( ElementRef ) ;
134+ protected _elementRef = inject < ElementRef < HTMLElement > > ( ElementRef ) ;
135+ protected _animationsDisabled =
136+ inject ( ANIMATION_MODULE_TYPE , { optional : true } ) === 'NoopAnimations' ;
138137 private _changeDetectorRef = inject ( ChangeDetectorRef ) ;
139138 private _globalModel = inject < MatDateSelectionModel < S , D > > ( MatDateSelectionModel ) ;
140139 private _dateAdapter = inject < DateAdapter < D > > ( DateAdapter ) ! ;
140+ private _ngZone = inject ( NgZone ) ;
141141 private _rangeSelectionStrategy = inject < MatDateRangeSelectionStrategy < D > > (
142142 MAT_DATE_RANGE_SELECTION_STRATEGY ,
143143 { optional : true } ,
144144 ) ;
145145
146- private _subscriptions = new Subscription ( ) ;
146+ private _stateChanges : Subscription | undefined ;
147147 private _model : MatDateSelectionModel < S , D > ;
148+ private _eventCleanups : ( ( ) => void ) [ ] | undefined ;
149+ private _animationFallback : ReturnType < typeof setTimeout > | undefined ;
150+
148151 /** Reference to the internal calendar component. */
149152 @ViewChild ( MatCalendar ) _calendar : MatCalendar < D > ;
150153
@@ -175,9 +178,6 @@ export class MatDatepickerContent<S, D = ExtractDateTypeFromSelection<S>>
175178 /** Whether the datepicker is above or below the input. */
176179 _isAbove : boolean ;
177180
178- /** Current state of the animation. */
179- _animationState : 'enter-dropdown' | 'enter-dialog' | 'void' ;
180-
181181 /** Emits when an animation has finished. */
182182 readonly _animationDone = new Subject < void > ( ) ;
183183
@@ -200,26 +200,31 @@ export class MatDatepickerContent<S, D = ExtractDateTypeFromSelection<S>>
200200
201201 constructor ( ) {
202202 inject ( _CdkPrivateStyleLoader ) . load ( _VisuallyHiddenLoader ) ;
203- const intl = inject ( MatDatepickerIntl ) ;
203+ this . _closeButtonText = inject ( MatDatepickerIntl ) . closeCalendarLabel ;
204204
205- this . _closeButtonText = intl . closeCalendarLabel ;
206- }
205+ if ( ! this . _animationsDisabled ) {
206+ const element = this . _elementRef . nativeElement ;
207+ const renderer = inject ( Renderer2 ) ;
207208
208- ngOnInit ( ) {
209- this . _animationState = this . datepicker . touchUi ? 'enter-dialog' : 'enter-dropdown' ;
209+ this . _eventCleanups = this . _ngZone . runOutsideAngular ( ( ) => [
210+ renderer . listen ( element , 'animationstart' , this . _handleAnimationEvent ) ,
211+ renderer . listen ( element , 'animationend' , this . _handleAnimationEvent ) ,
212+ renderer . listen ( element , 'animationcancel' , this . _handleAnimationEvent ) ,
213+ ] ) ;
214+ }
210215 }
211216
212217 ngAfterViewInit ( ) {
213- this . _subscriptions . add (
214- this . datepicker . stateChanges . subscribe ( ( ) => {
215- this . _changeDetectorRef . markForCheck ( ) ;
216- } ) ,
217- ) ;
218+ this . _stateChanges = this . datepicker . stateChanges . subscribe ( ( ) => {
219+ this . _changeDetectorRef . markForCheck ( ) ;
220+ } ) ;
218221 this . _calendar . focusActiveCell ( ) ;
219222 }
220223
221224 ngOnDestroy ( ) {
222- this . _subscriptions . unsubscribe ( ) ;
225+ clearTimeout ( this . _animationFallback ) ;
226+ this . _eventCleanups ?. forEach ( cleanup => cleanup ( ) ) ;
227+ this . _stateChanges ?. unsubscribe ( ) ;
223228 this . _animationDone . complete ( ) ;
224229 }
225230
@@ -258,17 +263,38 @@ export class MatDatepickerContent<S, D = ExtractDateTypeFromSelection<S>>
258263 }
259264
260265 _startExitAnimation ( ) {
261- this . _animationState = 'void' ;
262- this . _changeDetectorRef . markForCheck ( ) ;
266+ this . _elementRef . nativeElement . classList . add ( 'mat-datepicker-content-exit' ) ;
267+
268+ if ( this . _animationsDisabled ) {
269+ this . _animationDone . next ( ) ;
270+ } else {
271+ // Some internal apps disable animations in tests using `* {animation: none !important}`.
272+ // If that happens, the animation events won't fire and we'll never clean up the overlay.
273+ // Add a fallback that will fire if the animation doesn't run in a certain amount of time.
274+ clearTimeout ( this . _animationFallback ) ;
275+ this . _animationFallback = setTimeout ( ( ) => {
276+ if ( ! this . _isAnimating ) {
277+ this . _animationDone . next ( ) ;
278+ }
279+ } , 200 ) ;
280+ }
263281 }
264282
265- _handleAnimationEvent ( event : AnimationEvent ) {
266- this . _isAnimating = event . phaseName === 'start' ;
283+ private _handleAnimationEvent = ( event : AnimationEvent ) => {
284+ const element = this . _elementRef . nativeElement ;
285+
286+ if ( event . target !== element || ! event . animationName . startsWith ( '_mat-datepicker-content' ) ) {
287+ return ;
288+ }
289+
290+ clearTimeout ( this . _animationFallback ) ;
291+ this . _isAnimating = event . type === 'animationstart' ;
292+ element . classList . toggle ( 'mat-datepicker-content-animating' , this . _isAnimating ) ;
267293
268294 if ( ! this . _isAnimating ) {
269295 this . _animationDone . next ( ) ;
270296 }
271- }
297+ } ;
272298
273299 _getSelected ( ) {
274300 return this . _model . selection as unknown as D | DateRange < D > | null ;
@@ -672,7 +698,6 @@ export abstract class MatDatepickerBase<
672698
673699 if ( this . _componentRef ) {
674700 const { instance, location} = this . _componentRef ;
675- instance . _startExitAnimation ( ) ;
676701 instance . _animationDone . pipe ( take ( 1 ) ) . subscribe ( ( ) => {
677702 const activeElement = this . _document . activeElement ;
678703
@@ -690,6 +715,7 @@ export abstract class MatDatepickerBase<
690715 this . _focusedElementBeforeOpen = null ;
691716 this . _destroyOverlay ( ) ;
692717 } ) ;
718+ instance . _startExitAnimation ( ) ;
693719 }
694720
695721 if ( canRestoreFocus ) {
0 commit comments