diff --git a/package-lock.json b/package-lock.json
index b6149e63f9c4..a7bf587e3031 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/src/demo-app/datepicker/custom-header.html b/src/demo-app/datepicker/custom-header.html
new file mode 100644
index 000000000000..2c3f30415c0a
--- /dev/null
+++ b/src/demo-app/datepicker/custom-header.html
@@ -0,0 +1,7 @@
+
diff --git a/src/demo-app/datepicker/custom-header.scss b/src/demo-app/datepicker/custom-header.scss
new file mode 100644
index 000000000000..2d928abd5ed8
--- /dev/null
+++ b/src/demo-app/datepicker/custom-header.scss
@@ -0,0 +1,10 @@
+.custom-header {
+ padding: 1em 1.5em;
+ display: flex;
+ align-items: center;
+}
+
+.custom-header-label {
+ flex: 1;
+ text-align: center;
+}
diff --git a/src/demo-app/datepicker/datepicker-demo.html b/src/demo-app/datepicker/datepicker-demo.html
index e5771e8fa609..03af10a442b3 100644
--- a/src/demo-app/datepicker/datepicker-demo.html
+++ b/src/demo-app/datepicker/datepicker-demo.html
@@ -144,3 +144,14 @@ Datepicker with value property binding
[startView]="yearView ? 'year' : 'month'">
+
+Datepicker with custom header
+
+
+ Custom calendar header
+
+
+
+
+
diff --git a/src/demo-app/datepicker/datepicker-demo.ts b/src/demo-app/datepicker/datepicker-demo.ts
index 61d6a5146883..f01da8398345 100644
--- a/src/demo-app/datepicker/datepicker-demo.ts
+++ b/src/demo-app/datepicker/datepicker-demo.ts
@@ -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) => this.lastDateInput = e.value;
onDateChange = (e: MatDatepickerInputEvent) => 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 {
+ constructor(@Host() private _calendar: MatCalendar,
+ private _dateAdapter: DateAdapter) {}
+
+ 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);
+ }
}
diff --git a/src/demo-app/demo-app/demo-module.ts b/src/demo-app/demo-app/demo-module.ts
index 9d116439a02d..919b4e1e2629 100644
--- a/src/demo-app/demo-app/demo-module.ts
+++ b/src/demo-app/demo-app/demo-module.ts
@@ -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,
],
})
diff --git a/src/lib/datepicker/calendar-header.html b/src/lib/datepicker/calendar-header.html
new file mode 100644
index 000000000000..c11663b6227e
--- /dev/null
+++ b/src/lib/datepicker/calendar-header.html
@@ -0,0 +1,21 @@
+
diff --git a/src/lib/datepicker/calendar-header.spec.ts b/src/lib/datepicker/calendar-header.spec.ts
new file mode 100644
index 000000000000..a143b10f5868
--- /dev/null
+++ b/src/lib/datepicker/calendar-header.spec.ts
@@ -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;
+ let testComponent: StandardCalendar;
+ let calendarElement: HTMLElement;
+ let periodButton: HTMLElement;
+ let prevButton: HTMLElement;
+ let nextButton: HTMLElement;
+ let calendarInstance: MatCalendar;
+
+ 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: `
+
+ `
+})
+class StandardCalendar {
+ selected: Date;
+ selectedYear: Date;
+ selectedMonth: Date;
+ startDate = new Date(2017, JAN, 31);
+}
diff --git a/src/lib/datepicker/calendar.html b/src/lib/datepicker/calendar.html
index 1245f7a2f263..284affeb69f9 100644
--- a/src/lib/datepicker/calendar.html
+++ b/src/lib/datepicker/calendar.html
@@ -1,29 +1,10 @@
-
-
-
+
{
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;
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();
});
});
diff --git a/src/lib/datepicker/calendar.ts b/src/lib/datepicker/calendar.ts
index 1d25bd2b86bf..966718f193eb 100644
--- a/src/lib/datepicker/calendar.ts
+++ b/src/lib/datepicker/calendar.ts
@@ -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 implements OnDestroy {
+ /** Subject that emits when the component has been destroyed. */
+ private _destroyed = new Subject();
+
+ constructor(private _intl: MatDatepickerIntl,
+ @Host() @Inject(forwardRef(() => MatCalendar)) public calendar: MatCalendar,
+ @Optional() private _dateAdapter: DateAdapter,
+ @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 implements AfterContentInit, OnDestroy, OnChanges {
+ /** An input indicating the type of the header component, if set. */
+ @Input() headerComponent: ComponentType;
+
+ /** A portal containing the header component type for this calendar. */
+ _calendarHeaderPortal: Portal;
+
private _intlChanges: Subscription;
/** A date representing the period (month or year) to start the calendar in. */
@@ -119,56 +252,26 @@ export class MatCalendar 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';
-
- /** 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}`;
- }
+ currentView: 'month' | 'year' | 'multi-year';
- 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];
+ /**
+ * An observable that emits whenever there is a state change that the header may need to respond
+ * to.
+ */
+ get stateChanges(): Observable {
+ return this._stateChanges.asObservable();
}
+ private _stateChanges = new Subject();
- constructor(private _intl: MatDatepickerIntl,
+ constructor(_intl: MatDatepickerIntl,
@Optional() private _dateAdapter: DateAdapter,
@Optional() @Inject(MAT_DATE_FORMATS) private _dateFormats: MatDateFormats,
changeDetectorRef: ChangeDetectorRef) {
@@ -181,12 +284,17 @@ export class MatCalendar 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 implements AfterContentInit, OnDestroy, OnChanges {
view._init();
}
}
+
+ this._stateChanges.next();
}
/** Handles date selection in the month view. */
@@ -228,56 +338,8 @@ export class MatCalendar 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;
}
/**
diff --git a/src/lib/datepicker/datepicker-content.html b/src/lib/datepicker/datepicker-content.html
index 3ade43b79336..ea90dbc71087 100644
--- a/src/lib/datepicker/datepicker-content.html
+++ b/src/lib/datepicker/datepicker-content.html
@@ -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)"
diff --git a/src/lib/datepicker/datepicker-module.ts b/src/lib/datepicker/datepicker-module.ts
index 2e9460f77301..4efd6d18a4c5 100644
--- a/src/lib/datepicker/datepicker-module.ts
+++ b/src/lib/datepicker/datepicker-module.ts
@@ -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 {}
diff --git a/src/lib/datepicker/datepicker.ts b/src/lib/datepicker/datepicker.ts
index 0a5707b7bb7a..036bb17701d2 100644
--- a/src/lib/datepicker/datepicker.ts
+++ b/src/lib/datepicker/datepicker.ts
@@ -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 extends _MatDatepickerContentMixinBase
encapsulation: ViewEncapsulation.None,
})
export class MatDatepicker implements OnDestroy, CanColor {
+ /** An input indicating the type of the custom header component for the calendar, if set. */
+ @Input() calendarHeaderComponent: ComponentType;
+
/** The date to open the calendar to initially. */
@Input()
get startAt(): D | null {