Skip to content

Commit b99a7f1

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 3723191 commit b99a7f1

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
@@ -147,6 +147,29 @@ describe('MatDatepicker', () => {
147147
testComponent.opened = false;
148148
fixture.detectChanges();
149149
flush();
150+
fixture.detectChanges();
151+
flush();
152+
fixture.detectChanges();
153+
154+
expect(document.querySelector('.mat-datepicker-content')).toBeNull();
155+
}));
156+
157+
it('should wait for the animation to finish before removing the content', fakeAsync(() => {
158+
testComponent.datepicker.open();
159+
fixture.detectChanges();
160+
flush();
161+
162+
expect(document.querySelector('.mat-datepicker-content')).not.toBeNull();
163+
164+
testComponent.datepicker.close();
165+
fixture.detectChanges();
166+
flush();
167+
168+
expect(document.querySelector('.mat-datepicker-content')).not.toBeNull();
169+
170+
fixture.detectChanges();
171+
flush();
172+
fixture.detectChanges();
150173

151174
expect(document.querySelector('.mat-datepicker-content')).toBeNull();
152175
}));
@@ -190,6 +213,9 @@ describe('MatDatepicker', () => {
190213
testComponent.datepicker.close();
191214
fixture.detectChanges();
192215
flush();
216+
fixture.detectChanges();
217+
flush();
218+
fixture.detectChanges();
193219

194220
expect(parseInt(getComputedStyle(popup).height as string)).toBe(0);
195221
}));
@@ -1046,9 +1072,13 @@ describe('MatDatepicker', () => {
10461072
testComponent.datepicker.close();
10471073
fixture.detectChanges();
10481074
flush();
1075+
fixture.detectChanges();
1076+
flush();
1077+
fixture.detectChanges();
10491078

10501079
testComponent.formField.color = 'warn';
10511080
testComponent.datepicker.open();
1081+
fixture.detectChanges();
10521082

10531083
contentEl = document.querySelector('.mat-datepicker-content')!;
10541084
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)