feat(datepicker): add animation to calendar popup (#8999)

Adds an animation when opening and closing the datepicker's calendar.

BREAKING CHANGES:
* `MatDatePicker` now requires an animations module to be loaded
This commit is contained in:
Kristiyan Kostadinov
2018-03-09 10:26:07 -05:00
committed by Jeremy Elbourn
parent c224300dd2
commit c42549ee29
6 changed files with 165 additions and 47 deletions
@@ -0,0 +1,36 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {
animate,
state,
style,
transition,
trigger,
AnimationTriggerMetadata,
} from '@angular/animations';
/** Animations used by the Material datepicker. */
export const matDatepickerAnimations: {
readonly transformPanel: AnimationTriggerMetadata;
readonly fadeInCalendar: AnimationTriggerMetadata;
} = {
/** Transforms the height of the datepicker's calendar. */
transformPanel: trigger('transformPanel', [
state('void', style({opacity: 0, transform: 'scale(1, 0)'})),
state('enter', style({opacity: 1, transform: 'scale(1, 1)'})),
transition('void => enter', animate('400ms cubic-bezier(0.25, 0.8, 0.25, 1)')),
transition('* => void', animate('100ms linear', style({opacity: 0})))
]),
/** Fades in the content of the calendar. */
fadeInCalendar: trigger('fadeInCalendar', [
state('void', style({opacity: 0})),
state('enter', style({opacity: 1})),
transition('void => *', animate('400ms 100ms cubic-bezier(0.55, 0, 0.55, 0.2)'))
])
};
@@ -7,6 +7,7 @@
[maxDate]="datepicker._maxDate"
[dateFilter]="datepicker._dateFilter"
[selected]="datepicker._selected"
[@fadeInCalendar]="'enter'"
(selectedChange)="datepicker._select($event)"
(yearSelected)="datepicker._selectYear($event)"
(monthSelected)="datepicker._selectMonth($event)"
@@ -29,6 +29,7 @@ $mat-datepicker-touch-max-height: 788px;
display: block;
border-radius: 2px;
transform-origin: top center;
.mat-calendar {
width: $mat-datepicker-non-touch-calendar-width;
@@ -36,6 +37,10 @@ $mat-datepicker-touch-max-height: 788px;
}
}
.mat-datepicker-content-above {
transform-origin: bottom center;
}
.mat-datepicker-content-touch {
@include mat-elevation(0);
+69 -41
View File
@@ -98,40 +98,19 @@ describe('MatDatepicker', () => {
.not.toBeNull();
});
it('should pass the datepicker theme color to the overlay', fakeAsync(() => {
testComponent.datepicker.color = 'primary';
testComponent.datepicker.open();
fixture.detectChanges();
let contentEl = document.querySelector('.mat-datepicker-content')!;
expect(contentEl.classList).toContain('mat-primary');
testComponent.datepicker.close();
fixture.detectChanges();
flush();
testComponent.datepicker.color = 'warn';
testComponent.datepicker.open();
contentEl = document.querySelector('.mat-datepicker-content')!;
fixture.detectChanges();
expect(contentEl.classList).toContain('mat-warn');
expect(contentEl.classList).not.toContain('mat-primary');
}));
it('should open datepicker if opened input is set to true', () => {
it('should open datepicker if opened input is set to true', fakeAsync(() => {
testComponent.opened = true;
fixture.detectChanges();
flush();
expect(document.querySelector('.mat-datepicker-content')).not.toBeNull();
testComponent.opened = false;
fixture.detectChanges();
flush();
expect(document.querySelector('.mat-datepicker-content')).toBeNull();
});
}));
it('open in disabled mode should not open the calendar', () => {
testComponent.disabled = true;
@@ -165,7 +144,7 @@ describe('MatDatepicker', () => {
fixture.detectChanges();
flush();
let popup = document.querySelector('.cdk-overlay-pane')!;
const popup = document.querySelector('.cdk-overlay-pane')!;
expect(popup).not.toBeNull();
expect(parseInt(getComputedStyle(popup).height as string)).not.toBe(0);
@@ -211,6 +190,7 @@ describe('MatDatepicker', () => {
testComponent.datepicker.open();
fixture.detectChanges();
flush();
expect(document.querySelector('mat-dialog-container')).not.toBeNull();
expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 1));
@@ -224,28 +204,30 @@ describe('MatDatepicker', () => {
expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 2));
}));
it('setting selected via enter press should update input and close calendar', () => {
testComponent.touch = true;
fixture.detectChanges();
it('setting selected via enter press should update input and close calendar',
fakeAsync(() => {
testComponent.touch = true;
fixture.detectChanges();
testComponent.datepicker.open();
fixture.detectChanges();
testComponent.datepicker.open();
fixture.detectChanges();
flush();
expect(document.querySelector('mat-dialog-container')).not.toBeNull();
expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 1));
expect(document.querySelector('mat-dialog-container')).not.toBeNull();
expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 1));
let calendarBodyEl = document.querySelector('.mat-calendar-body') as HTMLElement;
let calendarBodyEl = document.querySelector('.mat-calendar-body') as HTMLElement;
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
flush();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER);
fixture.detectChanges();
flush();
fixture.whenStable().then(() => {
expect(document.querySelector('mat-dialog-container')).toBeNull();
expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 2));
});
});
}));
it('clicking the currently selected date should close the calendar ' +
'without firing selectedChanged', fakeAsync(() => {
@@ -1342,6 +1324,52 @@ describe('MatDatepicker', () => {
expect(testComponent.datepickerInput.value).toBe(selected);
}));
});
describe('popup animations', () => {
let fixture: ComponentFixture<StandardDatepicker>;
beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
imports: [MatDatepickerModule, MatNativeDateModule, NoopAnimationsModule],
declarations: [StandardDatepicker],
}).compileComponents();
fixture = TestBed.createComponent(StandardDatepicker);
fixture.detectChanges();
}));
it('should not set the `mat-datepicker-content-above` class when opening downwards',
fakeAsync(() => {
fixture.componentInstance.datepicker.open();
fixture.detectChanges();
flush();
fixture.detectChanges();
const content =
document.querySelector('.cdk-overlay-pane mat-datepicker-content')! as HTMLElement;
expect(content.classList).not.toContain('mat-datepicker-content-above');
}));
it('should set the `mat-datepicker-content-above` class when opening upwards', fakeAsync(() => {
const input = fixture.debugElement.nativeElement.querySelector('input');
// Push the input to the bottom of the page to force the calendar to open upwards
input.style.position = 'fixed';
input.style.bottom = '0';
fixture.componentInstance.datepicker.open();
fixture.detectChanges();
flush();
fixture.detectChanges();
const content =
document.querySelector('.cdk-overlay-pane mat-datepicker-content')! as HTMLElement;
expect(content.classList).toContain('mat-datepicker-content-above');
}));
});
});
+53 -5
View File
@@ -16,6 +16,7 @@ import {
PositionStrategy,
RepositionScrollStrategy,
ScrollStrategy,
ConnectedPositionStrategy,
} from '@angular/cdk/overlay';
import {ComponentPortal} from '@angular/cdk/portal';
import {take} from 'rxjs/operators/take';
@@ -37,6 +38,8 @@ import {
ViewChild,
ViewContainerRef,
ViewEncapsulation,
ChangeDetectorRef,
OnInit,
} from '@angular/core';
import {CanColor, DateAdapter, mixinColor, ThemePalette} from '@angular/material/core';
import {MatDialog, MatDialogRef} from '@angular/material/dialog';
@@ -47,7 +50,7 @@ import {merge} from 'rxjs/observable/merge';
import {createMissingDateImplError} from './datepicker-errors';
import {MatDatepickerInput} from './datepicker-input';
import {MatCalendar} from './calendar';
import {matDatepickerAnimations} from './datepicker-animations';
/** Used to generate a unique ID for each datepicker instance. */
let datepickerUid = 0;
@@ -90,23 +93,61 @@ export const _MatDatepickerContentMixinBase = mixinColor(MatDatepickerContentBas
styleUrls: ['datepicker-content.css'],
host: {
'class': 'mat-datepicker-content',
'[@transformPanel]': '"enter"',
'[class.mat-datepicker-content-touch]': 'datepicker.touchUi',
'[class.mat-datepicker-content-above]': '_isAbove',
},
animations: [
matDatepickerAnimations.transformPanel,
matDatepickerAnimations.fadeInCalendar,
],
exportAs: 'matDatepickerContent',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
inputs: ['color'],
})
export class MatDatepickerContent<D> extends _MatDatepickerContentMixinBase
implements AfterContentInit, CanColor {
datepicker: MatDatepicker<D>;
implements AfterContentInit, CanColor, OnInit, OnDestroy {
/** Subscription to changes in the overlay's position. */
private _positionChange: Subscription|null;
/** Reference to the internal calendar component. */
@ViewChild(MatCalendar) _calendar: MatCalendar<D>;
constructor(elementRef: ElementRef, private _ngZone: NgZone) {
/** Reference to the datepicker that created the overlay. */
datepicker: MatDatepicker<D>;
/** Whether the datepicker is above or below the input. */
_isAbove: boolean;
constructor(
elementRef: ElementRef,
private _changeDetectorRef: ChangeDetectorRef,
private _ngZone: NgZone) {
super(elementRef);
}
ngOnInit() {
if (!this.datepicker._popupRef || this._positionChange) {
return;
}
const positionStrategy =
this.datepicker._popupRef.getConfig().positionStrategy! as ConnectedPositionStrategy;
this._positionChange = positionStrategy.onPositionChange.subscribe(change => {
const isAbove = change.connectionPair.overlayY === 'bottom';
if (isAbove !== this._isAbove) {
this._ngZone.run(() => {
this._isAbove = isAbove;
this._changeDetectorRef.markForCheck();
});
}
});
}
ngAfterContentInit() {
this._focusActiveCell();
}
@@ -119,6 +160,13 @@ export class MatDatepickerContent<D> extends _MatDatepickerContentMixinBase
});
});
}
ngOnDestroy() {
if (this._positionChange) {
this._positionChange.unsubscribe();
this._positionChange = null;
}
}
}
@@ -246,7 +294,7 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
}
/** A reference to the overlay when the calendar is opened as a popup. */
private _popupRef: OverlayRef;
_popupRef: OverlayRef;
/** A reference to the dialog when the calendar is opened as a dialog. */
private _dialogRef: MatDialogRef<MatDatepickerContent<D>> | null;
+1 -1
View File
@@ -10,9 +10,9 @@ export * from './datepicker-module';
export * from './calendar';
export * from './calendar-body';
export * from './datepicker';
export * from './datepicker-animations';
export * from './datepicker-input';
export * from './datepicker-intl';
export * from './datepicker-toggle';
export * from './month-view';
export * from './year-view';