feat(datepicker): Add Custom Header to DatePicker (#9639)

* feature (add custom header to mat-calendar)

TODO: the custom header should be passed from datepicker

* refactor (define custom header component)

* refactor (remove useless text)

* refactor (pass custom header component type as input)

* refactor (add header as component portal)

* refactor (custom header as component portal)

- TODO: solve problem with directive cdkPortalOutlet in calendar.html

* refactor (switch for custom header component)

* refactor (define entry components)

* fix (correct entry point)

* refactor (custom header component): add test function to custom header component

* refactor (some code formatting)

* refactor (set indentation to two spaces)

* refactor (formatting)

* refactor (custom header for mat-calendar): renaming and code improvements

* refactor (calendarHeaderComponent) rename input of MatDatepicker

* refactor (calendarHeaderComponent): make a demo for custom header

* refactor (calendar header): make calendar header portal variable public

* refactor (calendar header): formatting

* tests (MatCalendar): fix tests

* tests (calendar): fix import issue

* tests (MatCalendar): remove unused import

* tests (MatCalendar): import MatDatepickerModule without redeclaration

* tests (MatCalendar): fix imports

* refactor (MatCalendar): default header comes with empty template

* refactor (MatCalendar): remove useless newline

* feature (MatCalendarHeader): move header in own template

* refactor (MatCalendar): some code reformatting

* fix (MatCalendar): add missin change detection for MatCalendarHeader

* refactor (MatCalendar): fix indentation

* fix (MatCalendar): clean stream up after component destruction

* fix (MatCalendar): fix change detection for MatCalendarHeader

* refactor (MatCalendarHeader): add dateadapter to constructor

* refactor (MatCalendarHeader): move methods to calendar header (ongoing)

* refactor (MatCalendarHeader): move label getter methods to MatCalendarHeader

* refactor (MatCalendarHeader): move MatCalendarHeader component in a separate file

* tests (MatCalendarHeader): move header related tests from MatCalendar (ongoing)

* refactor (MatCalendar): linting

* refactor (MatCalendarHeader): move MatCalendarHeader component back to calendar.ts (to avoid circular deps)

* refactor (MatCalendarHeader): move header related methods

* feature (MatCalendarHeader): add sample custom header controls to demo page

* refactor (MatCalendarHeader): fix some styling issues

* refactor (MatCalendarHeader): fix indentation

* refactor (MatCalendarHeader): make calendar private for custom header

* refactor (MatCalendarHeader): use type parameter for header (generics)

* refactor (MatCalendarHeader): make template file for custom header on demo page

* refactor (MatCalendarHeader): separate section own demo page

* refactor (MatCalendarHeader): style custom header

* refactor (MatCalendarHeader): fix styling issue

* refactor (package-lock.json): revert changes to version on master branch

* refactor (package-lock.json): revert to commit 6f63fd283e

* fix (MatCalendarHeader): correct forward reference to MatCalendar

* Add `stateChanges` to `MatCalendar`

* fix nits
This commit is contained in:
Tobias Schweizer
2018-03-22 17:59:29 +01:00
committed by mmalerba
parent f7b5d34449
commit 4e1bb265e3
14 changed files with 464 additions and 263 deletions
+1 -1
View File
@@ -8136,7 +8136,7 @@
"github": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/github/-/github-12.1.0.tgz",
"integrity": "sha512-HhWjhd/OATC4Hjj7xfGjGRtwWzo/fzTc55EkvsRatI9G6Vp47mVcdBIt1lQ56A9Qit/yVQRX1+M9jbWlcJvgug==",
"integrity": "sha1-8qLcvUQReBVZQiV0kaS8CL9mHdc=",
"dev": true,
"requires": {
"dotenv": "4.0.0",
@@ -0,0 +1,7 @@
<div class="custom-header">
<button mat-icon-button (click)="previousClicked('year')">&lt;&lt;</button>
<button mat-icon-button (click)="previousClicked('month')">&lt;</button>
<span class="custom-header-label">{{periodLabel}}</span>
<button mat-icon-button (click)="nextClicked('month')">&gt;</button>
<button mat-icon-button (click)="nextClicked('year')">&gt;&gt;</button>
</div>
@@ -0,0 +1,10 @@
.custom-header {
padding: 1em 1.5em;
display: flex;
align-items: center;
}
.custom-header-label {
flex: 1;
text-align: center;
}
@@ -144,3 +144,14 @@
[startView]="yearView ? 'year' : 'month'"></mat-datepicker>
</mat-form-field>
</p>
<h2>Datepicker with custom header</h2>
<p>
<mat-form-field>
<mat-label>Custom calendar header</mat-label>
<input matInput [matDatepicker]="customCalendarHeaderPicker"
[disabled]="inputDisabled">
<mat-datepicker-toggle matSuffix [for]="customCalendarHeaderPicker"></mat-datepicker-toggle>
<mat-datepicker #customCalendarHeaderPicker [touchUi]="touch" [disabled]="datepickerDisabled" [calendarHeaderComponent]="customHeader"></mat-datepicker>
</mat-form-field>
</p>
+37 -2
View File
@@ -6,12 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ChangeDetectionStrategy, Component} from '@angular/core';
import {ChangeDetectionStrategy, Component, Host} from '@angular/core';
import {FormControl} from '@angular/forms';
import {MatDatepickerInputEvent} from '@angular/material/datepicker';
import {DateAdapter} from '@angular/material/core';
import {MatCalendar} from '@angular/material';
import {ThemePalette} from '@angular/material/core';
@Component({
moduleId: module.id,
selector: 'datepicker-demo',
@@ -40,4 +41,38 @@ export class DatepickerDemo {
onDateInput = (e: MatDatepickerInputEvent<Date>) => this.lastDateInput = e.value;
onDateChange = (e: MatDatepickerInputEvent<Date>) => this.lastDateChange = e.value;
// pass custom header component type as input
customHeader = CustomHeader;
}
// Custom header component for datepicker
@Component({
moduleId: module.id,
selector: 'custom-header',
templateUrl: 'custom-header.html',
styleUrls: ['custom-header.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomHeader<D> {
constructor(@Host() private _calendar: MatCalendar<D>,
private _dateAdapter: DateAdapter<D>) {}
get periodLabel() {
const year = this._dateAdapter.getYearName(this._calendar.activeDate);
const month = (this._dateAdapter.getMonth(this._calendar.activeDate) + 1);
return `${month}/${year}`;
}
previousClicked(mode: 'month' | 'year') {
this._calendar.activeDate = mode == 'month' ?
this._dateAdapter.addCalendarMonths(this._calendar.activeDate, -1) :
this._dateAdapter.addCalendarYears(this._calendar.activeDate, -1);
}
nextClicked(mode: 'month' | 'year') {
this._calendar.activeDate = mode == 'month' ?
this._dateAdapter.addCalendarMonths(this._calendar.activeDate, 1) :
this._dateAdapter.addCalendarYears(this._calendar.activeDate, 1);
}
}
+3 -1
View File
@@ -19,7 +19,7 @@ import {ButtonDemo} from '../button/button-demo';
import {CardDemo} from '../card/card-demo';
import {CheckboxDemo, MatCheckboxDemoNestedChecklist} from '../checkbox/checkbox-demo';
import {ChipsDemo} from '../chips/chips-demo';
import {DatepickerDemo} from '../datepicker/datepicker-demo';
import {CustomHeader, DatepickerDemo} from '../datepicker/datepicker-demo';
import {DemoMaterialModule} from '../demo-material-module';
import {ContentElementDialog, DialogDemo, IFrameDialog, JazzDialog} from '../dialog/dialog-demo';
import {DrawerDemo} from '../drawer/drawer-demo';
@@ -88,6 +88,7 @@ import {ConnectedOverlayDemo, DemoOverlay} from '../connected-overlay/connected-
ChipsDemo,
ContentElementDialog,
DatepickerDemo,
CustomHeader,
DemoApp,
DialogDemo,
DrawerDemo,
@@ -148,6 +149,7 @@ import {ConnectedOverlayDemo, DemoOverlay} from '../connected-overlay/connected-
ScienceJoke,
SpagettiPanel,
ExampleBottomSheet,
CustomHeader,
DemoOverlay,
],
})
+21
View File
@@ -0,0 +1,21 @@
<div class="mat-calendar-header">
<div class="mat-calendar-controls">
<button mat-button class="mat-calendar-period-button"
(click)="currentPeriodClicked()" [attr.aria-label]="periodButtonLabel">
{{periodButtonText}}
<div class="mat-calendar-arrow" [class.mat-calendar-invert]="calendar.currentView != 'month'"></div>
</button>
<div class="mat-calendar-spacer"></div>
<button mat-icon-button class="mat-calendar-previous-button"
[disabled]="!previousEnabled()" (click)="previousClicked()"
[attr.aria-label]="prevButtonLabel">
</button>
<button mat-icon-button class="mat-calendar-next-button"
[disabled]="!nextEnabled()" (click)="nextClicked()"
[attr.aria-label]="nextButtonLabel">
</button>
</div>
</div>
+170
View File
@@ -0,0 +1,170 @@
import {Direction, Directionality} from '@angular/cdk/bidi';
import {MatDatepickerModule} from './datepicker-module';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {MatDatepickerIntl} from './datepicker-intl';
import {DEC, FEB, JAN, MatNativeDateModule} from '@angular/material/core';
import {Component} from '@angular/core';
import {MatCalendar} from './calendar';
import {By} from '@angular/platform-browser';
import {yearsPerPage} from './multi-year-view';
describe('MatCalendarHeader', () => {
let dir: { value: Direction };
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MatNativeDateModule,
MatDatepickerModule,
],
declarations: [
// Test components.
StandardCalendar,
],
providers: [
MatDatepickerIntl,
{provide: Directionality, useFactory: () => dir = {value: 'ltr'}}
],
});
TestBed.compileComponents();
}));
describe('standard calendar', () => {
let fixture: ComponentFixture<StandardCalendar>;
let testComponent: StandardCalendar;
let calendarElement: HTMLElement;
let periodButton: HTMLElement;
let prevButton: HTMLElement;
let nextButton: HTMLElement;
let calendarInstance: MatCalendar<Date>;
beforeEach(() => {
fixture = TestBed.createComponent(StandardCalendar);
fixture.detectChanges();
let calendarDebugElement = fixture.debugElement.query(By.directive(MatCalendar));
calendarElement = calendarDebugElement.nativeElement;
periodButton = calendarElement.querySelector('.mat-calendar-period-button') as HTMLElement;
prevButton = calendarElement.querySelector('.mat-calendar-previous-button') as HTMLElement;
nextButton = calendarElement.querySelector('.mat-calendar-next-button') as HTMLElement;
calendarInstance = calendarDebugElement.componentInstance;
testComponent = fixture.componentInstance;
});
it('should be in month view with specified month active', () => {
expect(calendarInstance.currentView).toBe('month');
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
});
it('should toggle view when period clicked', () => {
expect(calendarInstance.currentView).toBe('month');
periodButton.click();
fixture.detectChanges();
expect(calendarInstance.currentView).toBe('multi-year');
periodButton.click();
fixture.detectChanges();
expect(calendarInstance.currentView).toBe('month');
});
it('should go to next and previous month', () => {
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
nextButton.click();
fixture.detectChanges();
expect(calendarInstance.activeDate).toEqual(new Date(2017, FEB, 28));
prevButton.click();
fixture.detectChanges();
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 28));
});
it('should go to previous and next year', () => {
periodButton.click();
fixture.detectChanges();
expect(calendarInstance.currentView).toBe('multi-year');
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
(calendarElement.querySelector('.mat-calendar-body-active') as HTMLElement).click();
fixture.detectChanges();
expect(calendarInstance.currentView).toBe('year');
nextButton.click();
fixture.detectChanges();
expect(calendarInstance.activeDate).toEqual(new Date(2018, JAN, 31));
prevButton.click();
fixture.detectChanges();
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
});
it('should go to previous and next multi-year range', () => {
periodButton.click();
fixture.detectChanges();
expect(calendarInstance.currentView).toBe('multi-year');
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
nextButton.click();
fixture.detectChanges();
expect(calendarInstance.activeDate).toEqual(new Date(2017 + yearsPerPage, JAN, 31));
prevButton.click();
fixture.detectChanges();
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
});
it('should go back to month view after selecting year and month', () => {
periodButton.click();
fixture.detectChanges();
expect(calendarInstance.currentView).toBe('multi-year');
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
let yearCells = calendarElement.querySelectorAll('.mat-calendar-body-cell');
(yearCells[0] as HTMLElement).click();
fixture.detectChanges();
expect(calendarInstance.currentView).toBe('year');
expect(calendarInstance.activeDate).toEqual(new Date(2016, JAN, 31));
let monthCells = calendarElement.querySelectorAll('.mat-calendar-body-cell');
(monthCells[monthCells.length - 1] as HTMLElement).click();
fixture.detectChanges();
expect(calendarInstance.currentView).toBe('month');
expect(calendarInstance.activeDate).toEqual(new Date(2016, DEC, 31));
expect(testComponent.selected).toBeFalsy('no date should be selected yet');
});
});
});
@Component({
template: `
<mat-calendar
[startAt]="startDate"
[(selected)]="selected"
(yearSelected)="selectedYear=$event"
(monthSelected)="selectedMonth=$event">
</mat-calendar>`
})
class StandardCalendar {
selected: Date;
selectedYear: Date;
selectedMonth: Date;
startDate = new Date(2017, JAN, 31);
}
+5 -24
View File
@@ -1,29 +1,10 @@
<div class="mat-calendar-header">
<div class="mat-calendar-controls">
<button mat-button class="mat-calendar-period-button"
(click)="_currentPeriodClicked()" [attr.aria-label]="_periodButtonLabel">
{{_periodButtonText}}
<div class="mat-calendar-arrow" [class.mat-calendar-invert]="_currentView != 'month'"></div>
</button>
<div class="mat-calendar-spacer"></div>
<ng-template [cdkPortalOutlet]="_calendarHeaderPortal"></ng-template>
<button mat-icon-button class="mat-calendar-previous-button"
[disabled]="!_previousEnabled()" (click)="_previousClicked()"
[attr.aria-label]="_prevButtonLabel">
</button>
<button mat-icon-button class="mat-calendar-next-button"
[disabled]="!_nextEnabled()" (click)="_nextClicked()"
[attr.aria-label]="_nextButtonLabel">
</button>
</div>
</div>
<div class="mat-calendar-content" [ngSwitch]="_currentView" cdkMonitorSubtreeFocus tabindex="-1">
<div class="mat-calendar-content" [ngSwitch]="currentView" cdkMonitorSubtreeFocus tabindex="-1">
<mat-month-view
*ngSwitchCase="'month'"
[(activeDate)]="_activeDate"
[(activeDate)]="activeDate"
[selected]="selected"
[dateFilter]="dateFilter"
[maxDate]="maxDate"
@@ -34,7 +15,7 @@
<mat-year-view
*ngSwitchCase="'year'"
[activeDate]="_activeDate"
[activeDate]="activeDate"
[selected]="selected"
[dateFilter]="dateFilter"
[maxDate]="maxDate"
@@ -45,7 +26,7 @@
<mat-multi-year-view
*ngSwitchCase="'multi-year'"
[activeDate]="_activeDate"
[activeDate]="activeDate"
[selected]="selected"
[dateFilter]="dateFilter"
[maxDate]="maxDate"
+31 -138
View File
@@ -8,14 +8,9 @@ import {ComponentFixture, TestBed, async, inject} from '@angular/core/testing';
import {DEC, FEB, JAN, MatNativeDateModule, NOV} from '@angular/material/core';
import {By} from '@angular/platform-browser';
import {Direction, Directionality} from '@angular/cdk/bidi';
import {MatButtonModule} from '../button/index';
import {MatCalendar} from './calendar';
import {MatCalendarBody} from './calendar-body';
import {MatDatepickerIntl} from './datepicker-intl';
import {MatMonthView} from './month-view';
import {MatMultiYearView, yearsPerPage} from './multi-year-view';
import {MatYearView} from './year-view';
import {MatDatepickerModule} from './datepicker-module';
describe('MatCalendar', () => {
let dir: {value: Direction};
@@ -23,16 +18,10 @@ describe('MatCalendar', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MatButtonModule,
MatNativeDateModule,
MatDatepickerModule,
],
declarations: [
MatCalendar,
MatCalendarBody,
MatMonthView,
MatYearView,
MatMultiYearView,
// Test components.
StandardCalendar,
CalendarWithMinMax,
@@ -52,8 +41,6 @@ describe('MatCalendar', () => {
let testComponent: StandardCalendar;
let calendarElement: HTMLElement;
let periodButton: HTMLElement;
let prevButton: HTMLElement;
let nextButton: HTMLElement;
let calendarInstance: MatCalendar<Date>;
beforeEach(() => {
@@ -63,108 +50,14 @@ describe('MatCalendar', () => {
let calendarDebugElement = fixture.debugElement.query(By.directive(MatCalendar));
calendarElement = calendarDebugElement.nativeElement;
periodButton = calendarElement.querySelector('.mat-calendar-period-button') as HTMLElement;
prevButton = calendarElement.querySelector('.mat-calendar-previous-button') as HTMLElement;
nextButton = calendarElement.querySelector('.mat-calendar-next-button') as HTMLElement;
calendarInstance = calendarDebugElement.componentInstance;
testComponent = fixture.componentInstance;
});
it('should be in month view with specified month active', () => {
expect(calendarInstance._currentView).toBe('month');
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
});
it('should toggle view when period clicked', () => {
expect(calendarInstance._currentView).toBe('month');
periodButton.click();
fixture.detectChanges();
expect(calendarInstance._currentView).toBe('multi-year');
periodButton.click();
fixture.detectChanges();
expect(calendarInstance._currentView).toBe('month');
});
it('should go to next and previous month', () => {
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
nextButton.click();
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 28));
prevButton.click();
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 28));
});
it('should go to previous and next year', () => {
periodButton.click();
fixture.detectChanges();
expect(calendarInstance._currentView).toBe('multi-year');
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
(calendarElement.querySelector('.mat-calendar-body-active') as HTMLElement).click();
fixture.detectChanges();
expect(calendarInstance._currentView).toBe('year');
nextButton.click();
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2018, JAN, 31));
prevButton.click();
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
});
it('should go to previous and next multi-year range', () => {
periodButton.click();
fixture.detectChanges();
expect(calendarInstance._currentView).toBe('multi-year');
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
nextButton.click();
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017 + yearsPerPage, JAN, 31));
prevButton.click();
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
});
it('should go back to month view after selecting year and month', () => {
periodButton.click();
fixture.detectChanges();
expect(calendarInstance._currentView).toBe('multi-year');
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
let yearCells = calendarElement.querySelectorAll('.mat-calendar-body-cell');
(yearCells[0] as HTMLElement).click();
fixture.detectChanges();
expect(calendarInstance._currentView).toBe('year');
expect(calendarInstance._activeDate).toEqual(new Date(2016, JAN, 31));
let monthCells = calendarElement.querySelectorAll('.mat-calendar-body-cell');
(monthCells[monthCells.length - 1] as HTMLElement).click();
fixture.detectChanges();
expect(calendarInstance._currentView).toBe('month');
expect(calendarInstance._activeDate).toEqual(new Date(2016, DEC, 31));
expect(testComponent.selected).toBeFalsy('no date should be selected yet');
expect(calendarInstance.currentView).toBe('month');
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
});
it('should select date in month view', () => {
@@ -172,7 +65,7 @@ describe('MatCalendar', () => {
(monthCells[monthCells.length - 1] as HTMLElement).click();
fixture.detectChanges();
expect(calendarInstance._currentView).toBe('month');
expect(calendarInstance.currentView).toBe('month');
expect(testComponent.selected).toEqual(new Date(2017, JAN, 31));
});
@@ -180,14 +73,14 @@ describe('MatCalendar', () => {
periodButton.click();
fixture.detectChanges();
expect(calendarInstance._currentView).toBe('multi-year');
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
expect(calendarInstance.currentView).toBe('multi-year');
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
(calendarElement.querySelector('.mat-calendar-body-active') as HTMLElement).click();
fixture.detectChanges();
expect(calendarInstance._currentView).toBe('year');
expect(calendarInstance.currentView).toBe('year');
(calendarElement.querySelector('.mat-calendar-body-active') as HTMLElement).click();
@@ -199,8 +92,8 @@ describe('MatCalendar', () => {
periodButton.click();
fixture.detectChanges();
expect(calendarInstance._currentView).toBe('multi-year');
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
expect(calendarInstance.currentView).toBe('multi-year');
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
(calendarElement.querySelector('.mat-calendar-body-active') as HTMLElement).click();
@@ -237,7 +130,7 @@ describe('MatCalendar', () => {
});
it('should initially set start date active', () => {
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
});
it('should make the calendar body focusable', () => {
@@ -249,12 +142,12 @@ describe('MatCalendar', () => {
dispatchMouseEvent(periodButton, 'click');
fixture.detectChanges();
expect(calendarInstance._currentView).toBe('multi-year');
expect(calendarInstance.currentView).toBe('multi-year');
(calendarBodyEl.querySelector('.mat-calendar-body-active') as HTMLElement).click();
fixture.detectChanges();
expect(calendarInstance._currentView).toBe('year');
expect(calendarInstance.currentView).toBe('year');
});
it('should return to month view on enter', () => {
@@ -266,8 +159,8 @@ describe('MatCalendar', () => {
dispatchKeyboardEvent(tableBodyEl, 'keydown', ENTER);
fixture.detectChanges();
expect(calendarInstance._currentView).toBe('month');
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 28));
expect(calendarInstance.currentView).toBe('month');
expect(calendarInstance.activeDate).toEqual(new Date(2017, FEB, 28));
expect(testComponent.selected).toBeUndefined();
});
});
@@ -277,7 +170,7 @@ describe('MatCalendar', () => {
dispatchMouseEvent(periodButton, 'click');
fixture.detectChanges();
expect(calendarInstance._currentView).toBe('multi-year');
expect(calendarInstance.currentView).toBe('multi-year');
});
it('should go to year view on enter', () => {
@@ -289,8 +182,8 @@ describe('MatCalendar', () => {
dispatchKeyboardEvent(tableBodyEl, 'keydown', ENTER);
fixture.detectChanges();
expect(calendarInstance._currentView).toBe('year');
expect(calendarInstance._activeDate).toEqual(new Date(2018, JAN, 31));
expect(calendarInstance.currentView).toBe('year');
expect(calendarInstance.activeDate).toEqual(new Date(2018, JAN, 31));
expect(testComponent.selected).toBeUndefined();
});
});
@@ -319,14 +212,14 @@ describe('MatCalendar', () => {
testComponent.startAt = new Date(2000, JAN, 1);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2016, JAN, 1));
expect(calendarInstance.activeDate).toEqual(new Date(2016, JAN, 1));
});
it('should clamp startAt value above max date', () => {
testComponent.startAt = new Date(2020, JAN, 1);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2018, JAN, 1));
expect(calendarInstance.activeDate).toEqual(new Date(2018, JAN, 1));
});
it('should not go back past min date', () => {
@@ -337,18 +230,18 @@ describe('MatCalendar', () => {
calendarElement.querySelector('.mat-calendar-previous-button') as HTMLButtonElement;
expect(prevButton.disabled).toBe(false, 'previous button should not be disabled');
expect(calendarInstance._activeDate).toEqual(new Date(2016, FEB, 1));
expect(calendarInstance.activeDate).toEqual(new Date(2016, FEB, 1));
prevButton.click();
fixture.detectChanges();
expect(prevButton.disabled).toBe(true, 'previous button should be disabled');
expect(calendarInstance._activeDate).toEqual(new Date(2016, JAN, 1));
expect(calendarInstance.activeDate).toEqual(new Date(2016, JAN, 1));
prevButton.click();
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2016, JAN, 1));
expect(calendarInstance.activeDate).toEqual(new Date(2016, JAN, 1));
});
it('should not go forward past max date', () => {
@@ -359,18 +252,18 @@ describe('MatCalendar', () => {
calendarElement.querySelector('.mat-calendar-next-button') as HTMLButtonElement;
expect(nextButton.disabled).toBe(false, 'next button should not be disabled');
expect(calendarInstance._activeDate).toEqual(new Date(2017, DEC, 1));
expect(calendarInstance.activeDate).toEqual(new Date(2017, DEC, 1));
nextButton.click();
fixture.detectChanges();
expect(nextButton.disabled).toBe(true, 'next button should be disabled');
expect(calendarInstance._activeDate).toEqual(new Date(2018, JAN, 1));
expect(calendarInstance.activeDate).toEqual(new Date(2018, JAN, 1));
nextButton.click();
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2018, JAN, 1));
expect(calendarInstance.activeDate).toEqual(new Date(2018, JAN, 1));
});
it('should re-render the month view when the minDate changes', () => {
@@ -501,8 +394,8 @@ describe('MatCalendar', () => {
});
it('should not allow selection of disabled date in month view', () => {
expect(calendarInstance._currentView).toBe('month');
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 1));
expect(calendarInstance.currentView).toBe('month');
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 1));
dispatchKeyboardEvent(tableBodyEl, 'keydown', ENTER);
fixture.detectChanges();
@@ -519,16 +412,16 @@ describe('MatCalendar', () => {
(calendarElement.querySelector('.mat-calendar-body-active') as HTMLElement).click();
fixture.detectChanges();
calendarInstance._activeDate = new Date(2017, NOV, 1);
calendarInstance.activeDate = new Date(2017, NOV, 1);
fixture.detectChanges();
expect(calendarInstance._currentView).toBe('year');
expect(calendarInstance.currentView).toBe('year');
tableBodyEl = calendarElement.querySelector('.mat-calendar-body') as HTMLElement;
dispatchKeyboardEvent(tableBodyEl, 'keydown', ENTER);
fixture.detectChanges();
expect(calendarInstance._currentView).toBe('month');
expect(calendarInstance.currentView).toBe('month');
expect(testComponent.selected).toBeUndefined();
});
});
+157 -95
View File
@@ -6,12 +6,15 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ComponentPortal, ComponentType, Portal} from '@angular/cdk/portal';
import {
AfterContentInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
forwardRef,
Host,
Inject,
Input,
OnChanges,
@@ -23,6 +26,9 @@ import {
ViewEncapsulation,
} from '@angular/core';
import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core';
import {Observable} from 'rxjs/Observable';
import {takeUntil} from 'rxjs/operators/takeUntil';
import {Subject} from 'rxjs/Subject';
import {Subscription} from 'rxjs/Subscription';
import {createMissingDateImplError} from './datepicker-errors';
import {MatDatepickerIntl} from './datepicker-intl';
@@ -30,6 +36,127 @@ import {MatMonthView} from './month-view';
import {MatMultiYearView, yearsPerPage} from './multi-year-view';
import {MatYearView} from './year-view';
/** Default header for MatCalendar */
@Component({
moduleId: module.id,
selector: 'mat-calendar-header',
templateUrl: 'calendar-header.html',
encapsulation: ViewEncapsulation.None,
preserveWhitespaces: false,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MatCalendarHeader<D> implements OnDestroy {
/** Subject that emits when the component has been destroyed. */
private _destroyed = new Subject<void>();
constructor(private _intl: MatDatepickerIntl,
@Host() @Inject(forwardRef(() => MatCalendar)) public calendar: MatCalendar<D>,
@Optional() private _dateAdapter: DateAdapter<D>,
@Optional() @Inject(MAT_DATE_FORMATS) private _dateFormats: MatDateFormats,
changeDetectorRef: ChangeDetectorRef) {
this.calendar.stateChanges.pipe(takeUntil(this._destroyed))
.subscribe(() => changeDetectorRef.markForCheck());
}
/** The label for the current calendar view. */
get periodButtonText(): string {
if (this.calendar.currentView == 'month') {
return this._dateAdapter
.format(this.calendar.activeDate, this._dateFormats.display.monthYearLabel)
.toLocaleUpperCase();
}
if (this.calendar.currentView == 'year') {
return this._dateAdapter.getYearName(this.calendar.activeDate);
}
const activeYear = this._dateAdapter.getYear(this.calendar.activeDate);
const firstYearInView = this._dateAdapter.getYearName(
this._dateAdapter.createDate(activeYear - activeYear % 24, 0, 1));
const lastYearInView = this._dateAdapter.getYearName(
this._dateAdapter.createDate(activeYear + yearsPerPage - 1 - activeYear % 24, 0, 1));
return `${firstYearInView} \u2013 ${lastYearInView}`;
}
get periodButtonLabel(): string {
return this.calendar.currentView == 'month' ?
this._intl.switchToMultiYearViewLabel : this._intl.switchToMonthViewLabel;
}
/** The label for the the previous button. */
get prevButtonLabel(): string {
return {
'month': this._intl.prevMonthLabel,
'year': this._intl.prevYearLabel,
'multi-year': this._intl.prevMultiYearLabel
}[this.calendar.currentView];
}
/** The label for the the next button. */
get nextButtonLabel(): string {
return {
'month': this._intl.nextMonthLabel,
'year': this._intl.nextYearLabel,
'multi-year': this._intl.nextMultiYearLabel
}[this.calendar.currentView];
}
/** Handles user clicks on the period label. */
currentPeriodClicked(): void {
this.calendar.currentView = this.calendar.currentView == 'month' ? 'multi-year' : 'month';
}
/** Handles user clicks on the previous button. */
previousClicked(): void {
this.calendar.activeDate = this.calendar.currentView == 'month' ?
this._dateAdapter.addCalendarMonths(this.calendar.activeDate, -1) :
this._dateAdapter.addCalendarYears(
this.calendar.activeDate, this.calendar.currentView == 'year' ? -1 : -yearsPerPage
);
}
/** Handles user clicks on the next button. */
nextClicked(): void {
this.calendar.activeDate = this.calendar.currentView == 'month' ?
this._dateAdapter.addCalendarMonths(this.calendar.activeDate, 1) :
this._dateAdapter.addCalendarYears(
this.calendar.activeDate,
this.calendar.currentView == 'year' ? 1 : yearsPerPage
);
}
/** Whether the previous period button is enabled. */
previousEnabled(): boolean {
if (!this.calendar.minDate) {
return true;
}
return !this.calendar.minDate ||
!this._isSameView(this.calendar.activeDate, this.calendar.minDate);
}
/** Whether the next period button is enabled. */
nextEnabled(): boolean {
return !this.calendar.maxDate ||
!this._isSameView(this.calendar.activeDate, this.calendar.maxDate);
}
/** Whether the two dates represent the same view in the current view mode (month or year). */
private _isSameView(date1: D, date2: D): boolean {
if (this.calendar.currentView == 'month') {
return this._dateAdapter.getYear(date1) == this._dateAdapter.getYear(date2) &&
this._dateAdapter.getMonth(date1) == this._dateAdapter.getMonth(date2);
}
if (this.calendar.currentView == 'year') {
return this._dateAdapter.getYear(date1) == this._dateAdapter.getYear(date2);
}
// Otherwise we are in 'multi-year' view.
return Math.floor(this._dateAdapter.getYear(date1) / yearsPerPage) ==
Math.floor(this._dateAdapter.getYear(date2) / yearsPerPage);
}
ngOnDestroy() {
this._destroyed.next();
this._destroyed.complete();
}
}
/**
* A calendar that is used as part of the datepicker.
@@ -48,6 +175,12 @@ import {MatYearView} from './year-view';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
/** An input indicating the type of the header component, if set. */
@Input() headerComponent: ComponentType<any>;
/** A portal containing the header component type for this calendar. */
_calendarHeaderPortal: Portal<any>;
private _intlChanges: Subscription;
/** A date representing the period (month or year) to start the calendar in. */
@@ -119,56 +252,26 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
* The current active date. This determines which time period is shown and which date is
* highlighted when using keyboard navigation.
*/
get _activeDate(): D { return this._clampedActiveDate; }
set _activeDate(value: D) {
get activeDate(): D { return this._clampedActiveDate; }
set activeDate(value: D) {
this._clampedActiveDate = this._dateAdapter.clampDate(value, this.minDate, this.maxDate);
this._stateChanges.next();
}
private _clampedActiveDate: D;
/** Whether the calendar is in month view. */
_currentView: 'month' | 'year' | 'multi-year';
currentView: 'month' | 'year' | 'multi-year';
/** The label for the current calendar view. */
get _periodButtonText(): string {
if (this._currentView == 'month') {
return this._dateAdapter.format(this._activeDate, this._dateFormats.display.monthYearLabel)
.toLocaleUpperCase();
}
if (this._currentView == 'year') {
return this._dateAdapter.getYearName(this._activeDate);
}
const activeYear = this._dateAdapter.getYear(this._activeDate);
const firstYearInView = this._dateAdapter.getYearName(
this._dateAdapter.createDate(activeYear - activeYear % 24, 0, 1));
const lastYearInView = this._dateAdapter.getYearName(
this._dateAdapter.createDate(activeYear + yearsPerPage - 1 - activeYear % 24, 0, 1));
return `${firstYearInView} \u2013 ${lastYearInView}`;
/**
* An observable that emits whenever there is a state change that the header may need to respond
* to.
*/
get stateChanges(): Observable<void> {
return this._stateChanges.asObservable();
}
private _stateChanges = new Subject<void>();
get _periodButtonLabel(): string {
return this._currentView == 'month' ?
this._intl.switchToMultiYearViewLabel : this._intl.switchToMonthViewLabel;
}
/** The label for the the previous button. */
get _prevButtonLabel(): string {
return {
'month': this._intl.prevMonthLabel,
'year': this._intl.prevYearLabel,
'multi-year': this._intl.prevMultiYearLabel
}[this._currentView];
}
/** The label for the the next button. */
get _nextButtonLabel(): string {
return {
'month': this._intl.nextMonthLabel,
'year': this._intl.nextYearLabel,
'multi-year': this._intl.nextMultiYearLabel
}[this._currentView];
}
constructor(private _intl: MatDatepickerIntl,
constructor(_intl: MatDatepickerIntl,
@Optional() private _dateAdapter: DateAdapter<D>,
@Optional() @Inject(MAT_DATE_FORMATS) private _dateFormats: MatDateFormats,
changeDetectorRef: ChangeDetectorRef) {
@@ -181,12 +284,17 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
throw createMissingDateImplError('MAT_DATE_FORMATS');
}
this._intlChanges = _intl.changes.subscribe(() => changeDetectorRef.markForCheck());
this._intlChanges = _intl.changes.subscribe(() => {
changeDetectorRef.markForCheck();
this._stateChanges.next();
});
}
ngAfterContentInit() {
this._activeDate = this.startAt || this._dateAdapter.today();
this._currentView = this.startView;
this._calendarHeaderPortal = new ComponentPortal(this.headerComponent || MatCalendarHeader);
this.activeDate = this.startAt || this._dateAdapter.today();
this.currentView = this.startView;
}
ngOnDestroy() {
@@ -203,6 +311,8 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
view._init();
}
}
this._stateChanges.next();
}
/** Handles date selection in the month view. */
@@ -228,56 +338,8 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
/** Handles year/month selection in the multi-year/year views. */
_goToDateInView(date: D, view: 'month' | 'year' | 'multi-year'): void {
this._activeDate = date;
this._currentView = view;
}
/** Handles user clicks on the period label. */
_currentPeriodClicked(): void {
this._currentView = this._currentView == 'month' ? 'multi-year' : 'month';
}
/** Handles user clicks on the previous button. */
_previousClicked(): void {
this._activeDate = this._currentView == 'month' ?
this._dateAdapter.addCalendarMonths(this._activeDate, -1) :
this._dateAdapter.addCalendarYears(
this._activeDate, this._currentView == 'year' ? -1 : -yearsPerPage);
}
/** Handles user clicks on the next button. */
_nextClicked(): void {
this._activeDate = this._currentView == 'month' ?
this._dateAdapter.addCalendarMonths(this._activeDate, 1) :
this._dateAdapter.addCalendarYears(
this._activeDate, this._currentView == 'year' ? 1 : yearsPerPage);
}
/** Whether the previous period button is enabled. */
_previousEnabled(): boolean {
if (!this.minDate) {
return true;
}
return !this.minDate || !this._isSameView(this._activeDate, this.minDate);
}
/** Whether the next period button is enabled. */
_nextEnabled(): boolean {
return !this.maxDate || !this._isSameView(this._activeDate, this.maxDate);
}
/** Whether the two dates represent the same view in the current view mode (month or year). */
private _isSameView(date1: D, date2: D): boolean {
if (this._currentView == 'month') {
return this._dateAdapter.getYear(date1) == this._dateAdapter.getYear(date2) &&
this._dateAdapter.getMonth(date1) == this._dateAdapter.getMonth(date2);
}
if (this._currentView == 'year') {
return this._dateAdapter.getYear(date1) == this._dateAdapter.getYear(date2);
}
// Otherwise we are in 'multi-year' view.
return Math.floor(this._dateAdapter.getYear(date1) / yearsPerPage) ==
Math.floor(this._dateAdapter.getYear(date2) / yearsPerPage);
this.activeDate = date;
this.currentView = view;
}
/**
@@ -6,6 +6,7 @@
[minDate]="datepicker._minDate"
[maxDate]="datepicker._maxDate"
[dateFilter]="datepicker._dateFilter"
[headerComponent]="datepicker.calendarHeaderComponent"
[selected]="datepicker._selected"
[@fadeInCalendar]="'enter'"
(selectedChange)="datepicker._select($event)"
+6 -1
View File
@@ -13,6 +13,7 @@ import {NgModule} from '@angular/core';
import {MatButtonModule} from '@angular/material/button';
import {MatDialogModule} from '@angular/material/dialog';
import {MatCalendar} from './calendar';
import {MatCalendarHeader} from './calendar';
import {MatCalendarBody} from './calendar-body';
import {MatDatepicker, MatDatepickerContent} from './datepicker';
import {MatDatepickerInput} from './datepicker-input';
@@ -21,15 +22,17 @@ import {MatDatepickerToggle, MatDatepickerToggleIcon} from './datepicker-toggle'
import {MatMonthView} from './month-view';
import {MatMultiYearView} from './multi-year-view';
import {MatYearView} from './year-view';
import {PortalModule} from '@angular/cdk/portal';
@NgModule({
imports: [
A11yModule,
CommonModule,
MatButtonModule,
MatDialogModule,
OverlayModule,
A11yModule,
PortalModule,
],
exports: [
MatCalendar,
@@ -54,12 +57,14 @@ import {MatYearView} from './year-view';
MatMonthView,
MatYearView,
MatMultiYearView,
MatCalendarHeader
],
providers: [
MatDatepickerIntl,
],
entryComponents: [
MatDatepickerContent,
MatCalendarHeader,
]
})
export class MatDatepickerModule {}
+4 -1
View File
@@ -17,7 +17,7 @@ import {
PositionStrategy,
ScrollStrategy,
} from '@angular/cdk/overlay';
import {ComponentPortal} from '@angular/cdk/portal';
import {ComponentPortal, ComponentType} from '@angular/cdk/portal';
import {DOCUMENT} from '@angular/common';
import {
AfterContentInit,
@@ -176,6 +176,9 @@ export class MatDatepickerContent<D> extends _MatDatepickerContentMixinBase
encapsulation: ViewEncapsulation.None,
})
export class MatDatepicker<D> implements OnDestroy, CanColor {
/** An input indicating the type of the custom header component for the calendar, if set. */
@Input() calendarHeaderComponent: ComponentType<any>;
/** The date to open the calendar to initially. */
@Input()
get startAt(): D | null {