Skip to content

fix(material/datepicker): switch away from animations module #30360

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/material/datepicker/datepicker-animations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
/**
* Animations used by the Material datepicker.
* @docs-private
* @deprecated No longer used, will be removed.
* @breaking-change 21.0.0
*/
export const matDatepickerAnimations: {
readonly transformPanel: AnimationTriggerMetadata;
Expand Down
86 changes: 56 additions & 30 deletions src/material/datepicker/datepicker-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.dev/license
*/

import {AnimationEvent} from '@angular/animations';
import {_IdGenerator, CdkTrapFocus} from '@angular/cdk/a11y';
import {Directionality} from '@angular/cdk/bidi';
import {coerceStringArray} from '@angular/cdk/coercion';
Expand Down Expand Up @@ -34,6 +33,7 @@ import {DOCUMENT} from '@angular/common';
import {
afterNextRender,
AfterViewInit,
ANIMATION_MODULE_TYPE,
booleanAttribute,
ChangeDetectionStrategy,
ChangeDetectorRef,
Expand All @@ -46,10 +46,11 @@ import {
InjectionToken,
Injector,
Input,
NgZone,
OnChanges,
OnDestroy,
OnInit,
Output,
Renderer2,
SimpleChanges,
ViewChild,
ViewContainerRef,
Expand All @@ -70,7 +71,6 @@ import {
ExtractDateTypeFromSelection,
MatDateSelectionModel,
} from './date-selection-model';
import {matDatepickerAnimations} from './datepicker-animations';
import {createMissingDateImplError} from './datepicker-errors';
import {DateFilterFn} from './datepicker-input-base';
import {MatDatepickerIntl} from './datepicker-intl';
Expand Down Expand Up @@ -120,31 +120,34 @@ export const MAT_DATEPICKER_SCROLL_STRATEGY_FACTORY_PROVIDER = {
host: {
'class': 'mat-datepicker-content',
'[class]': 'color ? "mat-" + color : ""',
'[@transformPanel]': '_animationState',
'(@transformPanel.start)': '_handleAnimationEvent($event)',
'(@transformPanel.done)': '_handleAnimationEvent($event)',
'[class.mat-datepicker-content-touch]': 'datepicker.touchUi',
'[class.mat-datepicker-content-animations-enabled]': '!_animationsDisabled',
},
animations: [matDatepickerAnimations.transformPanel, matDatepickerAnimations.fadeInCalendar],
exportAs: 'matDatepickerContent',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CdkTrapFocus, MatCalendar, CdkPortalOutlet, MatButton],
})
export class MatDatepickerContent<S, D = ExtractDateTypeFromSelection<S>>
implements OnInit, AfterViewInit, OnDestroy
implements AfterViewInit, OnDestroy
{
protected _elementRef = inject(ElementRef);
protected _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
protected _animationsDisabled =
inject(ANIMATION_MODULE_TYPE, {optional: true}) === 'NoopAnimations';
private _changeDetectorRef = inject(ChangeDetectorRef);
private _globalModel = inject<MatDateSelectionModel<S, D>>(MatDateSelectionModel);
private _dateAdapter = inject<DateAdapter<D>>(DateAdapter)!;
private _ngZone = inject(NgZone);
private _rangeSelectionStrategy = inject<MatDateRangeSelectionStrategy<D>>(
MAT_DATE_RANGE_SELECTION_STRATEGY,
{optional: true},
);

private _subscriptions = new Subscription();
private _stateChanges: Subscription | undefined;
private _model: MatDateSelectionModel<S, D>;
private _eventCleanups: (() => void)[] | undefined;
private _animationFallback: ReturnType<typeof setTimeout> | undefined;

/** Reference to the internal calendar component. */
@ViewChild(MatCalendar) _calendar: MatCalendar<D>;

Expand Down Expand Up @@ -175,9 +178,6 @@ export class MatDatepickerContent<S, D = ExtractDateTypeFromSelection<S>>
/** Whether the datepicker is above or below the input. */
_isAbove: boolean;

/** Current state of the animation. */
_animationState: 'enter-dropdown' | 'enter-dialog' | 'void';

/** Emits when an animation has finished. */
readonly _animationDone = new Subject<void>();

Expand All @@ -200,26 +200,31 @@ export class MatDatepickerContent<S, D = ExtractDateTypeFromSelection<S>>

constructor() {
inject(_CdkPrivateStyleLoader).load(_VisuallyHiddenLoader);
const intl = inject(MatDatepickerIntl);
this._closeButtonText = inject(MatDatepickerIntl).closeCalendarLabel;

this._closeButtonText = intl.closeCalendarLabel;
}
if (!this._animationsDisabled) {
const element = this._elementRef.nativeElement;
const renderer = inject(Renderer2);

ngOnInit() {
this._animationState = this.datepicker.touchUi ? 'enter-dialog' : 'enter-dropdown';
this._eventCleanups = this._ngZone.runOutsideAngular(() => [
renderer.listen(element, 'animationstart', this._handleAnimationEvent),
renderer.listen(element, 'animationend', this._handleAnimationEvent),
renderer.listen(element, 'animationcancel', this._handleAnimationEvent),
]);
}
}

ngAfterViewInit() {
this._subscriptions.add(
this.datepicker.stateChanges.subscribe(() => {
this._changeDetectorRef.markForCheck();
}),
);
this._stateChanges = this.datepicker.stateChanges.subscribe(() => {
this._changeDetectorRef.markForCheck();
});
this._calendar.focusActiveCell();
}

ngOnDestroy() {
this._subscriptions.unsubscribe();
clearTimeout(this._animationFallback);
this._eventCleanups?.forEach(cleanup => cleanup());
this._stateChanges?.unsubscribe();
this._animationDone.complete();
}

Expand Down Expand Up @@ -258,17 +263,38 @@ export class MatDatepickerContent<S, D = ExtractDateTypeFromSelection<S>>
}

_startExitAnimation() {
this._animationState = 'void';
this._changeDetectorRef.markForCheck();
this._elementRef.nativeElement.classList.add('mat-datepicker-content-exit');

if (this._animationsDisabled) {
this._animationDone.next();
} else {
// Some internal apps disable animations in tests using `* {animation: none !important}`.
// If that happens, the animation events won't fire and we'll never clean up the overlay.
// Add a fallback that will fire if the animation doesn't run in a certain amount of time.
clearTimeout(this._animationFallback);
this._animationFallback = setTimeout(() => {
if (!this._isAnimating) {
this._animationDone.next();
}
}, 200);
}
}

_handleAnimationEvent(event: AnimationEvent) {
this._isAnimating = event.phaseName === 'start';
private _handleAnimationEvent = (event: AnimationEvent) => {
const element = this._elementRef.nativeElement;

if (event.target !== element || !event.animationName.startsWith('_mat-datepicker-content')) {
return;
}

clearTimeout(this._animationFallback);
this._isAnimating = event.type === 'animationstart';
element.classList.toggle('mat-datepicker-content-animating', this._isAnimating);

if (!this._isAnimating) {
this._animationDone.next();
}
}
};

_getSelected() {
return this._model.selection as unknown as D | DateRange<D> | null;
Expand Down Expand Up @@ -672,7 +698,6 @@ export abstract class MatDatepickerBase<

if (this._componentRef) {
const {instance, location} = this._componentRef;
instance._startExitAnimation();
instance._animationDone.pipe(take(1)).subscribe(() => {
const activeElement = this._document.activeElement;

Expand All @@ -690,6 +715,7 @@ export abstract class MatDatepickerBase<
this._focusedElementBeforeOpen = null;
this._destroyOverlay();
});
instance._startExitAnimation();
}

if (canRestoreFocus) {
Expand Down
1 change: 0 additions & 1 deletion src/material/datepicker/datepicker-content.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
[dateClass]="datepicker.dateClass"
[comparisonStart]="comparisonStart"
[comparisonEnd]="comparisonEnd"
[@fadeInCalendar]="'enter'"
[startDateAccessibleName]="startDateAccessibleName"
[endDateAccessibleName]="endDateAccessibleName"
(yearSelected)="datepicker._selectYear($event)"
Expand Down
47 changes: 46 additions & 1 deletion src/material/datepicker/datepicker-content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,39 @@ $touch-min-height: 312px;
$touch-max-width: 750px;
$touch-max-height: 788px;

@keyframes _mat-datepicker-content-dropdown-enter {
from {
opacity: 0;
transform: scaleY(0.8);
}

to {
opacity: 1;
transform: none;
}
}

@keyframes _mat-datepicker-content-dialog-enter {
from {
opacity: 0;
transform: scale(0.8);
}

to {
opacity: 1;
transform: none;
}
}

@keyframes _mat-datepicker-content-exit {
from {
opacity: 1;
}

to {
opacity: 0;
}
}

.mat-datepicker-content {
display: block;
Expand All @@ -37,6 +70,10 @@ $touch-max-height: 788px;
@include token-utils.create-token-slot(border-radius, calendar-container-shape);
}

&.mat-datepicker-content-animations-enabled {
animation: _mat-datepicker-content-dropdown-enter 120ms cubic-bezier(0, 0, 0.2, 1);
}

.mat-calendar {
width: $non-touch-calendar-width;
height: $non-touch-calendar-height;
Expand All @@ -59,7 +96,7 @@ $touch-max-height: 788px;

// Hide the button while the overlay is animating, because it's rendered
// outside of it and it seems to cause scrollbars in some cases (see #21493).
.ng-animating & {
.mat-datepicker-content-animating & {
display: none;
}
}
Expand Down Expand Up @@ -89,6 +126,10 @@ $touch-max-height: 788px;
// Prevents the content from jumping around on Windows while the animation is running.
overflow: visible;

&.mat-datepicker-content-animations-enabled {
animation: _mat-datepicker-content-dialog-enter 150ms cubic-bezier(0, 0, 0.2, 1);
}

.mat-datepicker-content-container {
min-height: $touch-min-height;
max-height: $touch-max-height;
Expand All @@ -102,6 +143,10 @@ $touch-max-height: 788px;
}
}

.mat-datepicker-content-exit.mat-datepicker-content-animations-enabled {
animation: _mat-datepicker-content-exit 100ms linear;
}

@media all and (orientation: landscape) {
.mat-datepicker-content-touch .mat-datepicker-content-container {
width: $touch-landscape-width;
Expand Down
5 changes: 0 additions & 5 deletions src/material/datepicker/datepicker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,25 +481,20 @@ describe('MatDatepicker', () => {
for (let i = 0; i < 3; i++) {
testComponent.datepicker.open();
fixture.detectChanges();
tick();

testComponent.datepicker.close();
fixture.detectChanges();
tick();
}

testComponent.datepicker.open();
fixture.detectChanges();
tick();
flush();

const spy = jasmine.createSpy('close event spy');
const subscription = testComponent.datepicker.closedStream.subscribe(spy);
const backdrop = document.querySelector('.cdk-overlay-backdrop')! as HTMLElement;

backdrop.click();
fixture.detectChanges();
flush();

expect(spy).toHaveBeenCalledTimes(1);
expect(testComponent.datepicker.opened).toBe(false);
Expand Down
14 changes: 5 additions & 9 deletions tools/public_api_guard/material/datepicker.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ 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 { ChangeDetectorRef } from '@angular/core';
import { ComponentType } from '@angular/cdk/portal';
Expand Down Expand Up @@ -338,7 +337,7 @@ export class MatDatepickerActions implements AfterViewInit, OnDestroy {
static ɵfac: i0.ɵɵFactoryDeclaration<MatDatepickerActions, never>;
}

// @public
// @public @deprecated
export const matDatepickerAnimations: {
readonly transformPanel: AnimationTriggerMetadata;
readonly fadeInCalendar: AnimationTriggerMetadata;
Expand Down Expand Up @@ -367,11 +366,12 @@ export class MatDatepickerCancel {
}

// @public
export class MatDatepickerContent<S, D = ExtractDateTypeFromSelection<S>> implements OnInit, AfterViewInit, OnDestroy {
export class MatDatepickerContent<S, D = ExtractDateTypeFromSelection<S>> implements AfterViewInit, OnDestroy {
constructor(...args: unknown[]);
_actionsPortal: TemplatePortal | null;
readonly _animationDone: Subject<void>;
_animationState: 'enter-dropdown' | 'enter-dialog' | 'void';
// (undocumented)
protected _animationsDisabled: boolean;
_applyPendingSelection(): void;
_assignActions(portal: TemplatePortal<any> | null, forceRerender: boolean): void;
_calendar: MatCalendar<D>;
Expand All @@ -383,13 +383,11 @@ export class MatDatepickerContent<S, D = ExtractDateTypeFromSelection<S>> implem
datepicker: MatDatepickerBase<any, S, D>;
_dialogLabelId: string | null;
// (undocumented)
protected _elementRef: ElementRef<any>;
protected _elementRef: ElementRef<HTMLElement>;
endDateAccessibleName: string | null;
// (undocumented)
_getSelected(): D | DateRange<D> | null;
// (undocumented)
_handleAnimationEvent(event: AnimationEvent_2): void;
// (undocumented)
_handleUserDragDrop(event: MatCalendarUserEvent<DateRange<D>>): void;
// (undocumented)
_handleUserSelection(event: MatCalendarUserEvent<D | null>): void;
Expand All @@ -399,8 +397,6 @@ export class MatDatepickerContent<S, D = ExtractDateTypeFromSelection<S>> implem
ngAfterViewInit(): void;
// (undocumented)
ngOnDestroy(): void;
// (undocumented)
ngOnInit(): void;
startDateAccessibleName: string | null;
// (undocumented)
_startExitAnimation(): void;
Expand Down
Loading