Skip to content

fix(datepicker): wait for exit animation to finish before detaching content #12440

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

Closed
Closed
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
34 changes: 32 additions & 2 deletions src/material/datepicker/datepicker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,29 @@ describe('MatDatepicker', () => {
testComponent.opened = false;
fixture.detectChanges();
flush();
fixture.detectChanges();
flush();
fixture.detectChanges();

expect(document.querySelector('.mat-datepicker-content')).toBeNull();
}));

it('should wait for the animation to finish before removing the content', fakeAsync(() => {
testComponent.datepicker.open();
fixture.detectChanges();
flush();

expect(document.querySelector('.mat-datepicker-content')).not.toBeNull();

testComponent.datepicker.close();
fixture.detectChanges();
flush();

expect(document.querySelector('.mat-datepicker-content')).not.toBeNull();

fixture.detectChanges();
flush();
fixture.detectChanges();

expect(document.querySelector('.mat-datepicker-content')).toBeNull();
}));
Expand Down Expand Up @@ -178,13 +201,16 @@ describe('MatDatepicker', () => {

const popup = document.querySelector('.cdk-overlay-pane')!;
expect(popup).not.toBeNull();
expect(parseInt(getComputedStyle(popup).height as string)).not.toBe(0);
expect(parseInt(getComputedStyle(popup).height || '0')).not.toBe(0);

testComponent.datepicker.close();
fixture.detectChanges();
flush();
fixture.detectChanges();
flush();
fixture.detectChanges();

expect(parseInt(getComputedStyle(popup).height as string)).toBe(0);
expect(parseInt(getComputedStyle(popup).height || '0')).toBeFalsy();
}));

it('should close the popup when pressing ESCAPE', fakeAsync(() => {
Expand Down Expand Up @@ -1092,9 +1118,13 @@ describe('MatDatepicker', () => {
testComponent.datepicker.close();
fixture.detectChanges();
flush();
fixture.detectChanges();
flush();
fixture.detectChanges();

testComponent.formField.color = 'warn';
testComponent.datepicker.open();
fixture.detectChanges();

contentEl = document.querySelector('.mat-datepicker-content')!;
fixture.detectChanges();
Expand Down
34 changes: 30 additions & 4 deletions src/material/datepicker/datepicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {DOCUMENT} from '@angular/common';
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ComponentRef,
ElementRef,
Expand All @@ -44,6 +45,7 @@ import {
ThemePalette,
} from '@angular/material/core';
import {MatDialog, MatDialogRef} from '@angular/material/dialog';
import {AnimationEvent} from '@angular/animations';
import {merge, Subject, Subscription} from 'rxjs';
import {filter, take} from 'rxjs/operators';
import {MatCalendar} from './calendar';
Expand Down Expand Up @@ -93,7 +95,8 @@ const _MatDatepickerContentMixinBase: CanColorCtor & typeof MatDatepickerContent
styleUrls: ['datepicker-content.css'],
host: {
'class': 'mat-datepicker-content',
'[@transformPanel]': '"enter"',
'[@transformPanel]': '_animationState',
'(@transformPanel.done)': '_animationDone.next($event)',
'[class.mat-datepicker-content-touch]': 'datepicker.touchUi',
},
animations: [
Expand All @@ -106,7 +109,7 @@ const _MatDatepickerContentMixinBase: CanColorCtor & typeof MatDatepickerContent
inputs: ['color'],
})
export class MatDatepickerContent<D> extends _MatDatepickerContentMixinBase
implements AfterViewInit, CanColor {
implements AfterViewInit, OnDestroy, CanColor {

/** Reference to the internal calendar component. */
@ViewChild(MatCalendar, {static: false}) _calendar: MatCalendar<D>;
Expand All @@ -117,13 +120,30 @@ export class MatDatepickerContent<D> extends _MatDatepickerContentMixinBase
/** Whether the datepicker is above or below the input. */
_isAbove: boolean;

constructor(elementRef: ElementRef) {
/** State of the datepicker's animation. */
_animationState: 'enter' | 'void' = 'enter';

/** Emits whenever an animation on the datepicker completes. */
_animationDone = new Subject<AnimationEvent>();

constructor(elementRef: ElementRef, private _changeDetectorRef: ChangeDetectorRef) {
super(elementRef);
}

/** Starts the datepicker's exiting animation. */
_startExitAnimation() {
this._animationState = 'void';
this._changeDetectorRef.markForCheck();
return this._animationDone;
}

ngAfterViewInit() {
this._calendar.focusActiveCell();
}

ngOnDestroy() {
this._animationDone.complete();
}
}


Expand Down Expand Up @@ -359,7 +379,13 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
return;
}
if (this._popupRef && this._popupRef.hasAttached()) {
this._popupRef.detach();
const popupInstance = this._popupComponentRef!.instance;

// We have to wait for the exit animation to finish before detaching the content, because
// we're using a portal outlet to render out the calendar header, which will detach
// immediately in `ngOnDestroy` without waiting for the animation, because the animation
// is on a parent component, which will cause the calendar to jump up.
popupInstance._startExitAnimation().pipe(take(1)).subscribe(() => this._popupRef.detach());
}
if (this._dialogRef) {
this._dialogRef.close();
Expand Down
6 changes: 5 additions & 1 deletion tools/public_api_guard/material/datepicker.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,15 @@ export declare const matDatepickerAnimations: {
};

export declare class MatDatepickerContent<D> extends _MatDatepickerContentMixinBase implements AfterViewInit, CanColor {
_animationDone: Subject<AnimationEvent>;
_animationState: 'enter' | 'void';
_calendar: MatCalendar<D>;
_isAbove: boolean;
datepicker: MatDatepicker<D>;
constructor(elementRef: ElementRef);
constructor(elementRef: ElementRef, _changeDetectorRef: ChangeDetectorRef);
_startExitAnimation(): Subject<AnimationEvent>;
ngAfterViewInit(): void;
ngOnDestroy(): void;
}

export declare class MatDatepickerInput<D> implements ControlValueAccessor, OnDestroy, Validator {
Expand Down