Skip to content

Commit 83c7e2c

Browse files
committed
feat(datepicker): add animation to calendar popup
Adds an animation when opening and closing the datepicker's calendar.
1 parent 0f954a0 commit 83c7e2c

File tree

4 files changed

+115
-6
lines changed

4 files changed

+115
-6
lines changed

src/lib/datepicker/datepicker-content.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
[maxDate]="datepicker._maxDate"
88
[dateFilter]="datepicker._dateFilter"
99
[selected]="datepicker._selected"
10+
[@fadeInCalendar]="'enter'"
1011
(selectedChange)="datepicker._select($event)"
1112
(_userSelection)="datepicker.close()">
1213
</mat-calendar>

src/lib/datepicker/datepicker-content.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ $mat-datepicker-touch-max-height: 788px;
2828
@include mat-elevation(8);
2929

3030
display: block;
31+
transform-origin: top center;
32+
}
33+
34+
.mat-datepicker-content-above {
35+
transform-origin: bottom center;
3136
}
3237

3338
.mat-calendar {

src/lib/datepicker/datepicker.spec.ts

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
dispatchMouseEvent,
88
} from '@angular/cdk/testing';
99
import {Component, ViewChild} from '@angular/core';
10-
import {async, ComponentFixture, inject, TestBed} from '@angular/core/testing';
10+
import {async, ComponentFixture, inject, TestBed, fakeAsync, flush} from '@angular/core/testing';
1111
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
1212
import {
1313
DEC,
@@ -156,7 +156,7 @@ describe('MatDatepicker', () => {
156156
});
157157
});
158158

159-
it('should close the popup when pressing ESCAPE', () => {
159+
it('should close the popup when pressing ESCAPE', fakeAsync(() => {
160160
testComponent.datepicker.open();
161161
fixture.detectChanges();
162162

@@ -168,14 +168,15 @@ describe('MatDatepicker', () => {
168168

169169
dispatchEvent(content, keyboardEvent);
170170
fixture.detectChanges();
171+
flush();
171172

172173
content = document.querySelector('.cdk-overlay-pane mat-datepicker-content')!;
173174

174175
expect(content).toBeFalsy('Expected datepicker to be closed.');
175176
expect(stopPropagationSpy).toHaveBeenCalled();
176177
expect(keyboardEvent.defaultPrevented)
177178
.toBe(true, 'Expected default ESCAPE action to be prevented.');
178-
});
179+
}));
179180

180181
it('close should close dialog', () => {
181182
testComponent.touch = true;
@@ -1090,6 +1091,54 @@ describe('MatDatepicker', () => {
10901091
});
10911092
});
10921093
});
1094+
1095+
describe('popup animations', () => {
1096+
let fixture: ComponentFixture<StandardDatepicker>;
1097+
let testComponent: StandardDatepicker;
1098+
1099+
beforeEach(fakeAsync(() => {
1100+
TestBed.configureTestingModule({
1101+
imports: [MatDatepickerModule, MatNativeDateModule, NoopAnimationsModule],
1102+
declarations: [StandardDatepicker],
1103+
}).compileComponents();
1104+
1105+
fixture = TestBed.createComponent(StandardDatepicker);
1106+
fixture.detectChanges();
1107+
testComponent = fixture.componentInstance;
1108+
}));
1109+
1110+
it('should not set the `mat-datepicker-content-above` class when opening downwards',
1111+
fakeAsync(() => {
1112+
fixture.componentInstance.datepicker.open();
1113+
fixture.detectChanges();
1114+
flush();
1115+
fixture.detectChanges();
1116+
1117+
const content =
1118+
document.querySelector('.cdk-overlay-pane mat-datepicker-content')! as HTMLElement;
1119+
1120+
expect(content.classList).not.toContain('mat-datepicker-content-above');
1121+
}));
1122+
1123+
it('should set the `mat-datepicker-content-above` class when opening upwards', fakeAsync(() => {
1124+
const input = fixture.debugElement.nativeElement.querySelector('input');
1125+
1126+
// Push the input to the bottom of the page to force the calendar to open upwards
1127+
input.style.position = 'fixed';
1128+
input.style.bottom = '0';
1129+
1130+
fixture.componentInstance.datepicker.open();
1131+
fixture.detectChanges();
1132+
flush();
1133+
fixture.detectChanges();
1134+
1135+
const content =
1136+
document.querySelector('.cdk-overlay-pane mat-datepicker-content')! as HTMLElement;
1137+
1138+
expect(content.classList).toContain('mat-datepicker-content-above');
1139+
}));
1140+
1141+
});
10931142
});
10941143

10951144

src/lib/datepicker/datepicker.ts

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
PositionStrategy,
1717
RepositionScrollStrategy,
1818
ScrollStrategy,
19+
ConnectedPositionStrategy,
1920
} from '@angular/cdk/overlay';
2021
import {ComponentPortal} from '@angular/cdk/portal';
2122
import {first} from 'rxjs/operators/first';
@@ -35,6 +36,8 @@ import {
3536
ViewChild,
3637
ViewContainerRef,
3738
ViewEncapsulation,
39+
ChangeDetectorRef,
40+
OnInit,
3841
} from '@angular/core';
3942
import {DateAdapter} from '@angular/material/core';
4043
import {MatDialog, MatDialogRef} from '@angular/material/dialog';
@@ -44,6 +47,7 @@ import {Subscription} from 'rxjs/Subscription';
4447
import {MatCalendar} from './calendar';
4548
import {createMissingDateImplError} from './datepicker-errors';
4649
import {MatDatepickerInput} from './datepicker-input';
50+
import{trigger, state, style, animate, transition} from '@angular/animations';
4751

4852

4953
/** Used to generate a unique ID for each datepicker instance. */
@@ -81,23 +85,73 @@ export const MAT_DATEPICKER_SCROLL_STRATEGY_PROVIDER = {
8185
styleUrls: ['datepicker-content.css'],
8286
host: {
8387
'class': 'mat-datepicker-content',
88+
'[@tranformPanel]': '"enter"',
8489
'[class.mat-datepicker-content-touch]': 'datepicker.touchUi',
90+
'[class.mat-datepicker-content-above]': '_isAbove',
8591
'(keydown)': '_handleKeydown($event)',
8692
},
93+
animations: [
94+
trigger('tranformPanel', [
95+
state('void', style({opacity: 0, transform: 'scale(1, 0)'})),
96+
state('enter', style({opacity: 1, transform: 'scale(1, 1)'})),
97+
transition('void => enter', animate('400ms cubic-bezier(0.25, 0.8, 0.25, 1)')),
98+
transition('* => void', animate('100ms linear', style({opacity: 0})))
99+
]),
100+
trigger('fadeInCalendar', [
101+
state('void', style({opacity: 0})),
102+
state('enter', style({opacity: 1})),
103+
transition('void => *', animate('400ms 100ms cubic-bezier(0.55, 0, 0.55, 0.2)'))
104+
])
105+
],
87106
exportAs: 'matDatepickerContent',
88107
encapsulation: ViewEncapsulation.None,
89108
preserveWhitespaces: false,
90109
changeDetection: ChangeDetectionStrategy.OnPush,
91110
})
92-
export class MatDatepickerContent<D> implements AfterContentInit {
93-
datepicker: MatDatepicker<D>;
111+
export class MatDatepickerContent<D> implements AfterContentInit, OnInit, OnDestroy {
112+
/** Subscription to changes in the overlay's position. */
113+
private _positionChange: Subscription|null;
94114

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

118+
/** Reference to the datepicker that created the overlay. */
119+
datepicker: MatDatepicker<D>;
120+
121+
/** Whether the datepicker is above or below the input. */
122+
_isAbove: boolean;
123+
124+
constructor(private _changeDetectorRef: ChangeDetectorRef) {}
125+
126+
ngOnInit() {
127+
if (!this.datepicker._popupRef || this._positionChange) {
128+
return;
129+
}
130+
131+
const positionStrategy =
132+
this.datepicker._popupRef.getConfig().positionStrategy! as ConnectedPositionStrategy;
133+
134+
this._positionChange = positionStrategy.onPositionChange.subscribe(change => {
135+
const isAbove = change.connectionPair.overlayY === 'bottom';
136+
137+
if (isAbove !== this._isAbove) {
138+
this._isAbove = isAbove;
139+
this._changeDetectorRef.markForCheck();
140+
}
141+
});
142+
}
143+
97144
ngAfterContentInit() {
98145
this._calendar._focusActiveCell();
99146
}
100147

148+
ngOnDestroy() {
149+
if (this._positionChange) {
150+
this._positionChange.unsubscribe();
151+
this._positionChange = null;
152+
}
153+
}
154+
101155
/**
102156
* Handles keydown event on datepicker content.
103157
* @param event The event.
@@ -211,7 +265,7 @@ export class MatDatepicker<D> implements OnDestroy {
211265
}
212266

213267
/** A reference to the overlay when the calendar is opened as a popup. */
214-
private _popupRef: OverlayRef;
268+
_popupRef: OverlayRef;
215269

216270
/** A reference to the dialog when the calendar is opened as a dialog. */
217271
private _dialogRef: MatDialogRef<any> | null;

0 commit comments

Comments
 (0)