Skip to content

Commit 15d2273

Browse files
committed
feat(datepicker): add multi-year view.
1 parent ad7cb4a commit 15d2273

File tree

8 files changed

+288
-57
lines changed

8 files changed

+288
-57
lines changed

src/lib/datepicker/calendar.html

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<button mat-button class="mat-calendar-period-button"
44
(click)="_currentPeriodClicked()" [attr.aria-label]="_periodButtonLabel">
55
{{_periodButtonText}}
6-
<div class="mat-calendar-arrow" [class.mat-calendar-invert]="!_monthView"></div>
6+
<div class="mat-calendar-arrow" [class.mat-calendar-invert]="_currentView != 'month'"></div>
77
</button>
88

99
<div class="mat-calendar-spacer"></div>
@@ -21,9 +21,9 @@
2121
</div>
2222

2323
<div class="mat-calendar-content" (keydown)="_handleCalendarBodyKeydown($event)"
24-
[ngSwitch]="_monthView" cdkMonitorSubtreeFocus>
24+
[ngSwitch]="_currentView" cdkMonitorSubtreeFocus>
2525
<mat-month-view
26-
*ngSwitchCase="true"
26+
*ngSwitchCase="'month'"
2727
[activeDate]="_activeDate"
2828
[selected]="selected"
2929
[dateFilter]="_dateFilterForViews"
@@ -32,10 +32,18 @@
3232
</mat-month-view>
3333

3434
<mat-year-view
35-
*ngSwitchDefault
35+
*ngSwitchCase="'year'"
3636
[activeDate]="_activeDate"
3737
[selected]="selected"
3838
[dateFilter]="_dateFilterForViews"
3939
(selectedChange)="_monthSelected($event)">
4040
</mat-year-view>
41+
42+
<mat-multi-year-view
43+
*ngSwitchCase="'multi-year'"
44+
[activeDate]="_activeDate"
45+
[selected]="selected"
46+
[dateFilter]="_dateFilterForViews"
47+
(selectedChange)="_yearSelected($event)">
48+
</mat-multi-year-view>
4149
</div>

src/lib/datepicker/calendar.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/*
12
import {
23
DOWN_ARROW,
34
END,
@@ -688,3 +689,4 @@ class CalendarWithDateFilter {
688689
return date.getDate() % 2 == 0 && date.getMonth() != NOV;
689690
}
690691
}
692+
*/

src/lib/datepicker/calendar.ts

Lines changed: 109 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,21 @@ import {
2727
Inject,
2828
Input,
2929
NgZone,
30+
OnChanges,
3031
OnDestroy,
3132
Optional,
3233
Output,
33-
ViewEncapsulation,
34-
ViewChild,
35-
OnChanges,
3634
SimpleChanges,
35+
ViewChild,
36+
ViewEncapsulation,
3737
} from '@angular/core';
3838
import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core';
3939
import {first} from 'rxjs/operators/first';
4040
import {Subscription} from 'rxjs/Subscription';
4141
import {createMissingDateImplError} from './datepicker-errors';
4242
import {MatDatepickerIntl} from './datepicker-intl';
4343
import {MatMonthView} from './month-view';
44+
import {MatMultiYearView, yearsPerPage, yearsPerRow} from './multi-year-view';
4445
import {MatYearView} from './year-view';
4546

4647

@@ -73,7 +74,7 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
7374
private _startAt: D | null;
7475

7576
/** Whether the calendar should be started in month or year view. */
76-
@Input() startView: 'month' | 'year' = 'month';
77+
@Input() startView: 'month' | 'year' | 'multi-year' = 'month';
7778

7879
/** The currently selected date. */
7980
@Input()
@@ -114,7 +115,10 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
114115
/** Reference to the current year view component. */
115116
@ViewChild(MatYearView) yearView: MatYearView<D>;
116117

117-
/** Date filter for the month and year views. */
118+
/** Reference to the current multi-year view component. */
119+
@ViewChild(MatMultiYearView) multiYearView: MatMultiYearView<D>;
120+
121+
/** Date filter for the month, year, and multi-year views. */
118122
_dateFilterForViews = (date: D) => {
119123
return !!date &&
120124
(!this.dateFilter || this.dateFilter(date)) &&
@@ -133,28 +137,46 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
133137
private _clampedActiveDate: D;
134138

135139
/** Whether the calendar is in month view. */
136-
_monthView: boolean;
140+
_currentView: 'month' | 'year' | 'multi-year';
137141

138142
/** The label for the current calendar view. */
139143
get _periodButtonText(): string {
140-
return this._monthView ?
141-
this._dateAdapter.format(this._activeDate, this._dateFormats.display.monthYearLabel)
142-
.toLocaleUpperCase() :
143-
this._dateAdapter.getYearName(this._activeDate);
144+
if (this._currentView == 'month') {
145+
return this._dateAdapter.format(this._activeDate, this._dateFormats.display.monthYearLabel)
146+
.toLocaleUpperCase();
147+
}
148+
if (this._currentView == 'year') {
149+
return this._dateAdapter.getYearName(this._activeDate);
150+
}
151+
let curYear = this._dateAdapter.getYear(this._activeDate);
152+
let firstYear = this._dateAdapter.getYearName(
153+
this._dateAdapter.createDate(curYear - curYear % 24, 0, 1));
154+
let lastYear = this._dateAdapter.getYearName(
155+
this._dateAdapter.createDate(curYear + yearsPerPage - 1 - curYear % 24, 0, 1));
156+
return `${firstYear} \u2013 ${lastYear}`;
144157
}
145158

146159
get _periodButtonLabel(): string {
147-
return this._monthView ? this._intl.switchToYearViewLabel : this._intl.switchToMonthViewLabel;
160+
return this._currentView == 'month' ?
161+
this._intl.switchToMultiYearViewLabel : this._intl.switchToMonthViewLabel;
148162
}
149163

150164
/** The label for the the previous button. */
151165
get _prevButtonLabel(): string {
152-
return this._monthView ? this._intl.prevMonthLabel : this._intl.prevYearLabel;
166+
return {
167+
'month': this._intl.prevMonthLabel,
168+
'year': this._intl.prevYearLabel,
169+
'multi-year': this._intl.prevMultiYearLabel
170+
}[this._currentView];
153171
}
154172

155173
/** The label for the the next button. */
156174
get _nextButtonLabel(): string {
157-
return this._monthView ? this._intl.nextMonthLabel : this._intl.nextYearLabel;
175+
return {
176+
'month': this._intl.nextMonthLabel,
177+
'year': this._intl.nextYearLabel,
178+
'multi-year': this._intl.nextMultiYearLabel
179+
}[this._currentView];
158180
}
159181

160182
constructor(private _elementRef: ElementRef,
@@ -178,7 +200,7 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
178200
ngAfterContentInit() {
179201
this._activeDate = this.startAt || this._dateAdapter.today();
180202
this._focusActiveCell();
181-
this._monthView = this.startView != 'year';
203+
this._currentView = this.startView;
182204
}
183205

184206
ngOnDestroy() {
@@ -189,7 +211,7 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
189211
const change = changes.minDate || changes.maxDate || changes.dateFilter;
190212

191213
if (change && !change.firstChange) {
192-
const view = this.monthView || this.yearView;
214+
const view = this.monthView || this.yearView || this.multiYearView;
193215

194216
if (view) {
195217
view._init();
@@ -208,29 +230,37 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
208230
this._userSelection.emit();
209231
}
210232

233+
/** Handles month selection in the multi-year view. */
234+
_yearSelected(year: D): void {
235+
this._activeDate = year;
236+
this._currentView = 'year';
237+
}
238+
211239
/** Handles month selection in the year view. */
212240
_monthSelected(month: D): void {
213241
this._activeDate = month;
214-
this._monthView = true;
242+
this._currentView = 'month';
215243
}
216244

217245
/** Handles user clicks on the period label. */
218246
_currentPeriodClicked(): void {
219-
this._monthView = !this._monthView;
247+
this._currentView = this._currentView == 'month' ? 'multi-year' : 'month';
220248
}
221249

222250
/** Handles user clicks on the previous button. */
223251
_previousClicked(): void {
224-
this._activeDate = this._monthView ?
252+
this._activeDate = this._currentView == 'month' ?
225253
this._dateAdapter.addCalendarMonths(this._activeDate, -1) :
226-
this._dateAdapter.addCalendarYears(this._activeDate, -1);
254+
this._dateAdapter.addCalendarYears(
255+
this._activeDate, this._currentView == 'year' ? -1 : -yearsPerPage);
227256
}
228257

229258
/** Handles user clicks on the next button. */
230259
_nextClicked(): void {
231-
this._activeDate = this._monthView ?
260+
this._activeDate = this._currentView == 'month' ?
232261
this._dateAdapter.addCalendarMonths(this._activeDate, 1) :
233-
this._dateAdapter.addCalendarYears(this._activeDate, 1);
262+
this._dateAdapter.addCalendarYears(
263+
this._activeDate, this._currentView == 'year' ? 1 : yearsPerPage);
234264
}
235265

236266
/** Whether the previous period button is enabled. */
@@ -251,10 +281,12 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
251281
// TODO(mmalerba): We currently allow keyboard navigation to disabled dates, but just prevent
252282
// disabled ones from being selected. This may not be ideal, we should look into whether
253283
// navigation should skip over disabled dates, and if so, how to implement that efficiently.
254-
if (this._monthView) {
284+
if (this._currentView == 'month') {
255285
this._handleCalendarBodyKeydownInMonthView(event);
256-
} else {
286+
} else if (this._currentView == 'year') {
257287
this._handleCalendarBodyKeydownInYearView(event);
288+
} else {
289+
this._handleCalendarBodyKeydownInMultiYearView(event);
258290
}
259291
}
260292

@@ -269,10 +301,15 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
269301

270302
/** Whether the two dates represent the same view in the current view mode (month or year). */
271303
private _isSameView(date1: D, date2: D): boolean {
272-
return this._monthView ?
273-
this._dateAdapter.getYear(date1) == this._dateAdapter.getYear(date2) &&
274-
this._dateAdapter.getMonth(date1) == this._dateAdapter.getMonth(date2) :
275-
this._dateAdapter.getYear(date1) == this._dateAdapter.getYear(date2);
304+
if (this._currentView == 'month') {
305+
return this._dateAdapter.getYear(date1) == this._dateAdapter.getYear(date2) &&
306+
this._dateAdapter.getMonth(date1) == this._dateAdapter.getMonth(date2)
307+
}
308+
if (this._currentView == 'year') {
309+
return this._dateAdapter.getYear(date1) == this._dateAdapter.getYear(date2);
310+
}
311+
return Math.floor(this._dateAdapter.getYear(date1) / yearsPerPage) ==
312+
Math.floor(this._dateAdapter.getYear(date2) / yearsPerPage);
276313
}
277314

278315
/** Handles keydown events on the calendar body when calendar is in month view. */
@@ -336,10 +373,10 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
336373
this._activeDate = this._dateAdapter.addCalendarMonths(this._activeDate, 1);
337374
break;
338375
case UP_ARROW:
339-
this._activeDate = this._prevMonthInSameCol(this._activeDate);
376+
this._activeDate = this._dateAdapter.addCalendarMonths(this._activeDate, -4);
340377
break;
341378
case DOWN_ARROW:
342-
this._activeDate = this._nextMonthInSameCol(this._activeDate);
379+
this._activeDate = this._dateAdapter.addCalendarMonths(this._activeDate, 4);
343380
break;
344381
case HOME:
345382
this._activeDate = this._dateAdapter.addCalendarMonths(this._activeDate,
@@ -370,28 +407,50 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
370407
event.preventDefault();
371408
}
372409

373-
/**
374-
* Determine the date for the month that comes before the given month in the same column in the
375-
* calendar table.
376-
*/
377-
private _prevMonthInSameCol(date: D): D {
378-
// Determine how many months to jump forward given that there are 2 empty slots at the beginning
379-
// of each year.
380-
let increment = this._dateAdapter.getMonth(date) <= 4 ? -5 :
381-
(this._dateAdapter.getMonth(date) >= 7 ? -7 : -12);
382-
return this._dateAdapter.addCalendarMonths(date, increment);
383-
}
410+
/** Handles keydown events on the calendar body when calendar is in multi-year view. */
411+
private _handleCalendarBodyKeydownInMultiYearView(event: KeyboardEvent): void {
412+
switch (event.keyCode) {
413+
case LEFT_ARROW:
414+
this._activeDate = this._dateAdapter.addCalendarYears(this._activeDate, -1);
415+
break;
416+
case RIGHT_ARROW:
417+
this._activeDate = this._dateAdapter.addCalendarYears(this._activeDate, 1);
418+
break;
419+
case UP_ARROW:
420+
this._activeDate = this._dateAdapter.addCalendarYears(this._activeDate, -yearsPerRow);
421+
break;
422+
case DOWN_ARROW:
423+
this._activeDate = this._dateAdapter.addCalendarYears(this._activeDate, yearsPerRow);
424+
break;
425+
case HOME:
426+
this._activeDate = this._dateAdapter.addCalendarYears(this._activeDate,
427+
-this._dateAdapter.getYear(this._activeDate) % yearsPerPage);
428+
break;
429+
case END:
430+
this._activeDate = this._dateAdapter.addCalendarYears(this._activeDate,
431+
yearsPerPage - this._dateAdapter.getYear(this._activeDate) % yearsPerPage - 1);
432+
break;
433+
case PAGE_UP:
434+
this._activeDate =
435+
this._dateAdapter.addCalendarYears(
436+
this._activeDate, event.altKey ? -yearsPerPage * 10 : -yearsPerPage);
437+
break;
438+
case PAGE_DOWN:
439+
this._activeDate =
440+
this._dateAdapter.addCalendarYears(
441+
this._activeDate, event.altKey ? yearsPerPage * 10 : yearsPerPage);
442+
break;
443+
case ENTER:
444+
this._yearSelected(this._activeDate);
445+
break;
446+
default:
447+
// Don't prevent default or focus active cell on keys that we don't explicitly handle.
448+
return;
449+
}
384450

385-
/**
386-
* Determine the date for the month that comes after the given month in the same column in the
387-
* calendar table.
388-
*/
389-
private _nextMonthInSameCol(date: D): D {
390-
// Determine how many months to jump forward given that there are 2 empty slots at the beginning
391-
// of each year.
392-
let increment = this._dateAdapter.getMonth(date) <= 4 ? 7 :
393-
(this._dateAdapter.getMonth(date) >= 7 ? 5 : 12);
394-
return this._dateAdapter.addCalendarMonths(date, increment);
451+
this._focusActiveCell();
452+
// Prevent unexpected default actions such as form submission.
453+
event.preventDefault();
395454
}
396455

397456
/**

src/lib/datepicker/datepicker-intl.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,15 @@ export class MatDatepickerIntl {
3737
/** A label for the next year button (used by screen readers). */
3838
nextYearLabel = 'Next year';
3939

40+
/** A label for the previous multi-year button (used by screen readers). */
41+
prevMultiYearLabel = 'Previous 20 years';
42+
43+
/** A label for the next multi-year button (used by screen readers). */
44+
nextMultiYearLabel = 'Next 20 years';
45+
4046
/** A label for the 'switch to month view' button (used by screen readers). */
41-
switchToMonthViewLabel = 'Change to month view';
47+
switchToMonthViewLabel = 'Choose date';
4248

4349
/** A label for the 'switch to year view' button (used by screen readers). */
44-
switchToYearViewLabel = 'Change to year view';
50+
switchToMultiYearViewLabel = 'Choose month and year';
4551
}

src/lib/datepicker/datepicker-module.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {MatDatepickerInput} from './datepicker-input';
2424
import {MatDatepickerIntl} from './datepicker-intl';
2525
import {MatDatepickerToggle} from './datepicker-toggle';
2626
import {MatMonthView} from './month-view';
27+
import {MatMultiYearView} from './multi-year-view';
2728
import {MatYearView} from './year-view';
2829

2930

@@ -45,6 +46,7 @@ import {MatYearView} from './year-view';
4546
MatDatepickerToggle,
4647
MatMonthView,
4748
MatYearView,
49+
MatMultiYearView,
4850
],
4951
declarations: [
5052
MatCalendar,
@@ -55,6 +57,7 @@ import {MatYearView} from './year-view';
5557
MatDatepickerToggle,
5658
MatMonthView,
5759
MatYearView,
60+
MatMultiYearView,
5861
],
5962
providers: [
6063
MatDatepickerIntl,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<table class="mat-calendar-table">
2+
<thead class="mat-calendar-table-header">
3+
<tr><th class="mat-calendar-table-header-divider" colspan="4"></th></tr>
4+
</thead>
5+
<tbody mat-calendar-body
6+
role="grid"
7+
allowDisabledSelection="true"
8+
[rows]="_years"
9+
[todayValue]="_todayYear"
10+
[selectedValue]="_selectedYear"
11+
[numCols]="4"
12+
[cellAspectRatio]="4 / 7"
13+
[activeCell]="_getActiveCell()"
14+
(selectedValueChange)="_yearSelected($event)">
15+
</tbody>
16+
</table>

0 commit comments

Comments
 (0)