mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
Jeremy Elbourn
parent
c224300dd2
commit
c42549ee29
@@ -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);
|
||||
|
||||
|
||||
@@ -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');
|
||||
}));
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user