Skip to content

Commit 2ee63ec

Browse files
crisbetojelbourn
authored andcommitted
fix(datepicker): wait for exit animation to finish before detaching content
This is something I ran into while working on aligning the datepicker with the most-recent Material design spec. Since #9639 we use a portal outlet to render the calendar header. The portal outlet directive will detach in `ngOnDestroy` and it won't wait for the parent animation to finish, which ends up shifting the entire calendar up while it's animating away. The only reason that this isn't visible at the moment is because the current animation isn't configured correctly, which causes it to go to `opacity: 0` immediately.
1 parent f1b65b6 commit 2ee63ec

File tree

2 files changed

+61
-5
lines changed

2 files changed

+61
-5
lines changed

src/lib/datepicker/datepicker.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,29 @@ describe('MatDatepicker', () => {
125125
testComponent.opened = false;
126126
fixture.detectChanges();
127127
flush();
128+
fixture.detectChanges();
129+
flush();
130+
fixture.detectChanges();
131+
132+
expect(document.querySelector('.mat-datepicker-content')).toBeNull();
133+
}));
134+
135+
it('should wait for the animation to finish before removing the content', fakeAsync(() => {
136+
testComponent.datepicker.open();
137+
fixture.detectChanges();
138+
flush();
139+
140+
expect(document.querySelector('.mat-datepicker-content')).not.toBeNull();
141+
142+
testComponent.datepicker.close();
143+
fixture.detectChanges();
144+
flush();
145+
146+
expect(document.querySelector('.mat-datepicker-content')).not.toBeNull();
147+
148+
fixture.detectChanges();
149+
flush();
150+
fixture.detectChanges();
128151

129152
expect(document.querySelector('.mat-datepicker-content')).toBeNull();
130153
}));
@@ -168,6 +191,9 @@ describe('MatDatepicker', () => {
168191
testComponent.datepicker.close();
169192
fixture.detectChanges();
170193
flush();
194+
fixture.detectChanges();
195+
flush();
196+
fixture.detectChanges();
171197

172198
expect(parseInt(getComputedStyle(popup).height as string)).toBe(0);
173199
}));
@@ -1024,9 +1050,13 @@ describe('MatDatepicker', () => {
10241050
testComponent.datepicker.close();
10251051
fixture.detectChanges();
10261052
flush();
1053+
fixture.detectChanges();
1054+
flush();
1055+
fixture.detectChanges();
10271056

10281057
testComponent.formField.color = 'warn';
10291058
testComponent.datepicker.open();
1059+
fixture.detectChanges();
10301060

10311061
contentEl = document.querySelector('.mat-datepicker-content')!;
10321062
fixture.detectChanges();

src/lib/datepicker/datepicker.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {take, filter} from 'rxjs/operators';
2222
import {
2323
AfterViewInit,
2424
ChangeDetectionStrategy,
25+
ChangeDetectorRef,
2526
Component,
2627
ComponentRef,
2728
ElementRef,
@@ -30,15 +31,16 @@ import {
3031
InjectionToken,
3132
Input,
3233
NgZone,
34+
OnDestroy,
3335
Optional,
3436
Output,
3537
ViewChild,
3638
ViewContainerRef,
3739
ViewEncapsulation,
38-
OnDestroy,
3940
} from '@angular/core';
4041
import {CanColor, DateAdapter, mixinColor, ThemePalette} from '@angular/material/core';
4142
import {MatDialog, MatDialogRef} from '@angular/material/dialog';
43+
import {AnimationEvent} from '@angular/animations';
4244
import {merge, Subject, Subscription} from 'rxjs';
4345
import {createMissingDateImplError} from './datepicker-errors';
4446
import {MatDatepickerInput} from './datepicker-input';
@@ -85,7 +87,8 @@ export const _MatDatepickerContentMixinBase = mixinColor(MatDatepickerContentBas
8587
styleUrls: ['datepicker-content.css'],
8688
host: {
8789
'class': 'mat-datepicker-content',
88-
'[@transformPanel]': '"enter"',
90+
'[@transformPanel]': '_animationState',
91+
'(@transformPanel.done)': '_animationDone.next($event)',
8992
'[class.mat-datepicker-content-touch]': 'datepicker.touchUi',
9093
},
9194
animations: [
@@ -98,7 +101,7 @@ export const _MatDatepickerContentMixinBase = mixinColor(MatDatepickerContentBas
98101
inputs: ['color'],
99102
})
100103
export class MatDatepickerContent<D> extends _MatDatepickerContentMixinBase
101-
implements AfterViewInit, CanColor {
104+
implements AfterViewInit, OnDestroy, CanColor {
102105

103106
/** Reference to the internal calendar component. */
104107
@ViewChild(MatCalendar) _calendar: MatCalendar<D>;
@@ -109,13 +112,30 @@ export class MatDatepickerContent<D> extends _MatDatepickerContentMixinBase
109112
/** Whether the datepicker is above or below the input. */
110113
_isAbove: boolean;
111114

112-
constructor(elementRef: ElementRef) {
115+
/** State of the datepicker's animation. */
116+
_animationState: 'enter' | 'void' = 'enter';
117+
118+
/** Emits whenever an animation on the datepicker completes. */
119+
_animationDone = new Subject<AnimationEvent>();
120+
121+
constructor(elementRef: ElementRef, private _changeDetectorRef: ChangeDetectorRef) {
113122
super(elementRef);
114123
}
115124

125+
/** Starts the datepicker's exiting animation. */
126+
_startExitAnimation() {
127+
this._animationState = 'void';
128+
this._changeDetectorRef.markForCheck();
129+
return this._animationDone;
130+
}
131+
116132
ngAfterViewInit() {
117133
this._calendar.focusActiveCell();
118134
}
135+
136+
ngOnDestroy() {
137+
this._animationDone.complete();
138+
}
119139
}
120140

121141

@@ -344,7 +364,13 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
344364
return;
345365
}
346366
if (this._popupRef && this._popupRef.hasAttached()) {
347-
this._popupRef.detach();
367+
const popupInstance = this._popupComponentRef!.instance;
368+
369+
// We have to wait for the exit animation to finish before detaching the content, because
370+
// we're using a portal outlet to render out the calendar header, which will detach
371+
// immediately in `ngOnDestroy` without waiting for the animation, because the animation
372+
// is on a parent component, which will cause the calendar to jump up.
373+
popupInstance._startExitAnimation().pipe(take(1)).subscribe(() => this._popupRef.detach());
348374
}
349375
if (this._dialogRef) {
350376
this._dialogRef.close();

0 commit comments

Comments
 (0)