Skip to content

Commit c3e267f

Browse files
crisbetoandrewseguin
authored andcommitted
feat(datepicker): add animation to calendar popup (#8542)
Adds an animation when opening and closing the datepicker's calendar.
1 parent 3a52624 commit c3e267f

File tree

4 files changed

+114
-5
lines changed

4 files changed

+114
-5
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,13 +28,18 @@ $mat-datepicker-touch-max-height: 788px;
2828
@include mat-elevation(8);
2929

3030
display: block;
31+
transform-origin: top center;
3132

3233
.mat-calendar {
3334
width: $mat-datepicker-non-touch-calendar-width;
3435
height: $mat-datepicker-non-touch-calendar-height;
3536
}
3637
}
3738

39+
.mat-datepicker-content-above {
40+
transform-origin: bottom center;
41+
}
42+
3843
.mat-datepicker-content-touch {
3944
@include mat-elevation(0);
4045

src/lib/datepicker/datepicker.spec.ts

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ describe('MatDatepicker', () => {
167167
expect(parseInt(getComputedStyle(popup).height as string)).toBe(0);
168168
});
169169

170-
it('should close the popup when pressing ESCAPE', () => {
170+
it('should close the popup when pressing ESCAPE', fakeAsync(() => {
171171
testComponent.datepicker.open();
172172
fixture.detectChanges();
173173

@@ -179,14 +179,15 @@ describe('MatDatepicker', () => {
179179

180180
dispatchEvent(content, keyboardEvent);
181181
fixture.detectChanges();
182+
flush();
182183

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

185186
expect(content).toBeFalsy('Expected datepicker to be closed.');
186187
expect(stopPropagationSpy).toHaveBeenCalled();
187188
expect(keyboardEvent.defaultPrevented)
188189
.toBe(true, 'Expected default ESCAPE action to be prevented.');
189-
});
190+
}));
190191

191192
it('close should close dialog', async(() => {
192193
testComponent.touch = true;
@@ -1236,6 +1237,54 @@ describe('MatDatepicker', () => {
12361237
});
12371238
}));
12381239
});
1240+
1241+
describe('popup animations', () => {
1242+
let fixture: ComponentFixture<StandardDatepicker>;
1243+
let testComponent: StandardDatepicker;
1244+
1245+
beforeEach(fakeAsync(() => {
1246+
TestBed.configureTestingModule({
1247+
imports: [MatDatepickerModule, MatNativeDateModule, NoopAnimationsModule],
1248+
declarations: [StandardDatepicker],
1249+
}).compileComponents();
1250+
1251+
fixture = TestBed.createComponent(StandardDatepicker);
1252+
fixture.detectChanges();
1253+
testComponent = fixture.componentInstance;
1254+
}));
1255+
1256+
it('should not set the `mat-datepicker-content-above` class when opening downwards',
1257+
fakeAsync(() => {
1258+
fixture.componentInstance.datepicker.open();
1259+
fixture.detectChanges();
1260+
flush();
1261+
fixture.detectChanges();
1262+
1263+
const content =
1264+
document.querySelector('.cdk-overlay-pane mat-datepicker-content')! as HTMLElement;
1265+
1266+
expect(content.classList).not.toContain('mat-datepicker-content-above');
1267+
}));
1268+
1269+
it('should set the `mat-datepicker-content-above` class when opening upwards', fakeAsync(() => {
1270+
const input = fixture.debugElement.nativeElement.querySelector('input');
1271+
1272+
// Push the input to the bottom of the page to force the calendar to open upwards
1273+
input.style.position = 'fixed';
1274+
input.style.bottom = '0';
1275+
1276+
fixture.componentInstance.datepicker.open();
1277+
fixture.detectChanges();
1278+
flush();
1279+
fixture.detectChanges();
1280+
1281+
const content =
1282+
document.querySelector('.cdk-overlay-pane mat-datepicker-content')! as HTMLElement;
1283+
1284+
expect(content.classList).toContain('mat-datepicker-content-above');
1285+
}));
1286+
1287+
});
12391288
});
12401289

12411290

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 {take} from 'rxjs/operators/take';
@@ -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.
@@ -214,7 +268,7 @@ export class MatDatepicker<D> implements OnDestroy {
214268
}
215269

216270
/** A reference to the overlay when the calendar is opened as a popup. */
217-
private _popupRef: OverlayRef;
271+
_popupRef: OverlayRef;
218272

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

0 commit comments

Comments
 (0)