refactor(datepicker): move keyboard events handlers into the views (#9775)

This commit is contained in:
Juliano
2018-02-17 23:09:59 -02:00
committed by Jeremy Elbourn
parent 949a69bcf7
commit 8e3ab8d40b
15 changed files with 887 additions and 697 deletions
+15 -2
View File
@@ -9,12 +9,14 @@
import {
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
Input,
Output,
ViewEncapsulation
ViewEncapsulation,
NgZone,
} from '@angular/core';
import {take} from 'rxjs/operators/take';
/**
* An internal class that represents the data corresponding to a single calendar cell.
@@ -81,6 +83,8 @@ export class MatCalendarBody {
/** Emits when a new value is selected. */
@Output() readonly selectedValueChange: EventEmitter<number> = new EventEmitter<number>();
constructor(private _elementRef: ElementRef, private _ngZone: NgZone) { }
_cellClicked(cell: MatCalendarCell): void {
if (!this.allowDisabledSelection && !cell.enabled) {
return;
@@ -104,4 +108,13 @@ export class MatCalendarBody {
return cellNumber == this.activeCell;
}
/** Focuses the active cell after the microtask queue is empty. */
_focusActiveCell() {
this._ngZone.runOutsideAngular(() => {
this._ngZone.onStable.asObservable().pipe(take(1)).subscribe(() => {
this._elementRef.nativeElement.querySelector('.mat-calendar-body-active').focus();
});
});
}
}
+2 -3
View File
@@ -20,11 +20,10 @@
</div>
</div>
<div class="mat-calendar-content" (keydown)="_handleCalendarBodyKeydown($event)"
[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"
+25 -437
View File
@@ -1,31 +1,11 @@
import {
DOWN_ARROW,
END,
ENTER,
HOME,
LEFT_ARROW,
PAGE_DOWN,
PAGE_UP,
RIGHT_ARROW,
UP_ARROW,
} from '@angular/cdk/keycodes';
import {dispatchFakeEvent, dispatchKeyboardEvent, dispatchMouseEvent} from '@angular/cdk/testing';
import {Component} from '@angular/core';
import {async, ComponentFixture, inject, TestBed} from '@angular/core/testing';
import {
AUG,
DEC,
FEB,
JAN,
JUL,
JUN,
MAR,
MatNativeDateModule,
MAY,
NOV,
OCT,
SEP,
} from '@angular/material/core';
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';
@@ -33,7 +13,7 @@ import {MatCalendar} from './calendar';
import {MatCalendarBody} from './calendar-body';
import {MatDatepickerIntl} from './datepicker-intl';
import {MatMonthView} from './month-view';
import {MatMultiYearView, yearsPerPage, yearsPerRow} from './multi-year-view';
import {MatMultiYearView, yearsPerPage} from './multi-year-view';
import {MatYearView} from './year-view';
@@ -240,7 +220,9 @@ describe('MatCalendar', () => {
fixture.detectChanges();
expect(button.getAttribute('aria-label')).toBe('Go to multi-year view?');
}));
})
);
describe('a11y', () => {
describe('calendar body', () => {
@@ -262,155 +244,6 @@ describe('MatCalendar', () => {
expect(calendarBodyEl.getAttribute('tabindex')).toBe('-1');
});
describe('month view', () => {
it('should decrement date on left arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 30));
calendarInstance._activeDate = new Date(2017, JAN, 1);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2016, DEC, 31));
});
it('should increment date on left arrow press in rtl', () => {
dir.value = 'rtl';
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 1));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 2));
});
it('should increment date on right arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 1));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 2));
});
it('should decrement date on right arrow press in rtl', () => {
dir.value = 'rtl';
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 30));
calendarInstance._activeDate = new Date(2017, JAN, 1);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2016, DEC, 31));
});
it('should go up a row on up arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 24));
calendarInstance._activeDate = new Date(2017, JAN, 7);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2016, DEC, 31));
});
it('should go down a row on down arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 7));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 14));
});
it('should go to beginning of the month on home press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 1));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 1));
});
it('should go to end of the month on end press', () => {
calendarInstance._activeDate = new Date(2017, JAN, 10);
dispatchKeyboardEvent(calendarBodyEl, 'keydown', END);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', END);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
});
it('should go back one month on page up press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2016, DEC, 31));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2016, NOV, 30));
});
it('should go forward one month on page down press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 28));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, MAR, 28));
});
it('should select active date on enter', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(testComponent.selected).toBeUndefined();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER);
fixture.detectChanges();
expect(testComponent.selected).toEqual(new Date(2017, JAN, 30));
});
});
describe('year view', () => {
beforeEach(() => {
dispatchMouseEvent(periodButton, 'click');
@@ -424,166 +257,13 @@ describe('MatCalendar', () => {
expect(calendarInstance._currentView).toBe('year');
});
it('should decrement month on left arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2016, DEC, 31));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2016, NOV, 30));
});
it('should increment month on left arrow press in rtl', () => {
dir.value = 'rtl';
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 28));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, MAR, 28));
});
it('should increment month on right arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 28));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, MAR, 28));
});
it('should decrement month on right arrow press in rtl', () => {
dir.value = 'rtl';
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2016, DEC, 31));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2016, NOV, 30));
});
it('should go up a row on up arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2016, SEP, 30));
calendarInstance._activeDate = new Date(2017, JUL, 1);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, MAR, 1));
calendarInstance._activeDate = new Date(2017, DEC, 10);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, AUG, 10));
});
it('should go down a row on down arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, MAY, 31));
calendarInstance._activeDate = new Date(2017, JUN, 1);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, OCT, 1));
calendarInstance._activeDate = new Date(2017, SEP, 30);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2018, JAN, 30));
});
it('should go to first month of the year on home press', () => {
calendarInstance._activeDate = new Date(2017, SEP, 30);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 30));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 30));
});
it('should go to last month of the year on end press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', END);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, DEC, 31));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', END);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, DEC, 31));
});
it('should go back one year on page up press', () => {
calendarInstance._activeDate = new Date(2016, FEB, 29);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2015, FEB, 28));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2014, FEB, 28));
});
it('should go forward one year on page down press', () => {
calendarInstance._activeDate = new Date(2016, FEB, 29);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 28));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2018, FEB, 28));
});
it('should return to month view on enter', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
const tableBodyEl = calendarBodyEl.querySelector('.mat-calendar-body') as HTMLElement;
dispatchKeyboardEvent(tableBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER);
dispatchKeyboardEvent(tableBodyEl, 'keydown', ENTER);
fixture.detectChanges();
expect(calendarInstance._currentView).toBe('month');
@@ -600,109 +280,13 @@ describe('MatCalendar', () => {
expect(calendarInstance._currentView).toBe('multi-year');
});
it('should decrement year on left arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2016, JAN, 31));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2015, JAN, 31));
});
it('should increment year on right arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2018, JAN, 31));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2019, JAN, 31));
});
it('should go up a row on up arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017 - yearsPerRow, JAN, 31));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017 - yearsPerRow * 2, JAN, 31));
});
it('should go down a row on down arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017 + yearsPerRow, JAN, 31));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017 + yearsPerRow * 2, JAN, 31));
});
it('should go to first year in current range on home press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2016, JAN, 31));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2016, JAN, 31));
});
it('should go to last year in current range on end press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', END);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2039, JAN, 31));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', END);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2039, JAN, 31));
});
it('should go to same index in previous year range page up press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017 - yearsPerPage, JAN, 31));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP);
fixture.detectChanges();
expect(calendarInstance._activeDate)
.toEqual(new Date(2017 - yearsPerPage * 2, JAN, 31));
});
it('should go to same index in next year range on page down press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017 + yearsPerPage, JAN, 31));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN);
fixture.detectChanges();
expect(calendarInstance._activeDate)
.toEqual(new Date(2017 + yearsPerPage * 2, JAN, 31));
});
it('should go to year view on enter', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
const tableBodyEl = calendarBodyEl.querySelector('.mat-calendar-body') as HTMLElement;
dispatchKeyboardEvent(tableBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER);
dispatchKeyboardEvent(tableBodyEl, 'keydown', ENTER);
fixture.detectChanges();
expect(calendarInstance._currentView).toBe('year');
@@ -710,8 +294,10 @@ describe('MatCalendar', () => {
expect(testComponent.selected).toBeUndefined();
});
});
});
});
});
describe('calendar with min and max date', () => {
@@ -904,13 +490,13 @@ describe('MatCalendar', () => {
});
describe('a11y', () => {
let calendarBodyEl: HTMLElement;
let tableBodyEl: HTMLElement;
beforeEach(() => {
calendarBodyEl = calendarElement.querySelector('.mat-calendar-content') as HTMLElement;
expect(calendarBodyEl).not.toBeNull();
tableBodyEl = calendarElement.querySelector('.mat-calendar-body') as HTMLElement;
expect(tableBodyEl).not.toBeNull();
dispatchFakeEvent(calendarBodyEl, 'focus');
dispatchFakeEvent(tableBodyEl, 'focus');
fixture.detectChanges();
});
@@ -918,7 +504,7 @@ describe('MatCalendar', () => {
expect(calendarInstance._currentView).toBe('month');
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 1));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER);
dispatchKeyboardEvent(tableBodyEl, 'keydown', ENTER);
fixture.detectChanges();
expect(testComponent.selected).toBeUndefined();
@@ -938,13 +524,15 @@ describe('MatCalendar', () => {
expect(calendarInstance._currentView).toBe('year');
dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER);
tableBodyEl = calendarElement.querySelector('.mat-calendar-body') as HTMLElement;
dispatchKeyboardEvent(tableBodyEl, 'keydown', ENTER);
fixture.detectChanges();
expect(calendarInstance._currentView).toBe('month');
expect(testComponent.selected).toBeUndefined();
});
});
});
});
@@ -989,6 +577,6 @@ class CalendarWithDateFilter {
startDate = new Date(2017, JAN, 1);
dateFilter (date: Date) {
return date.getDate() % 2 == 0 && date.getMonth() != NOV;
return !(date.getDate() % 2) && date.getMonth() !== NOV;
}
}
+4 -197
View File
@@ -6,27 +6,14 @@
* found in the LICENSE file at https://angular.io/license
*/
import {
DOWN_ARROW,
END,
ENTER,
HOME,
LEFT_ARROW,
PAGE_DOWN,
PAGE_UP,
RIGHT_ARROW,
UP_ARROW,
} from '@angular/cdk/keycodes';
import {
AfterContentInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
Inject,
Input,
NgZone,
OnChanges,
OnDestroy,
Optional,
@@ -36,14 +23,12 @@ import {
ViewEncapsulation,
} from '@angular/core';
import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core';
import {take} from 'rxjs/operators/take';
import {Subscription} from 'rxjs/Subscription';
import {createMissingDateImplError} from './datepicker-errors';
import {MatDatepickerIntl} from './datepicker-intl';
import {MatMonthView} from './month-view';
import {MatMultiYearView, yearsPerPage, yearsPerRow} from './multi-year-view';
import {MatMultiYearView, yearsPerPage} from './multi-year-view';
import {MatYearView} from './year-view';
import {Directionality} from '@angular/cdk/bidi';
/**
@@ -184,13 +169,10 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
}[this._currentView];
}
constructor(private _elementRef: ElementRef,
private _intl: MatDatepickerIntl,
private _ngZone: NgZone,
constructor(private _intl: MatDatepickerIntl,
@Optional() private _dateAdapter: DateAdapter<D>,
@Optional() @Inject(MAT_DATE_FORMATS) private _dateFormats: MatDateFormats,
changeDetectorRef: ChangeDetectorRef,
@Optional() private _dir?: Directionality) {
changeDetectorRef: ChangeDetectorRef) {
if (!this._dateAdapter) {
throw createMissingDateImplError('DateAdapter');
@@ -205,7 +187,6 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
ngAfterContentInit() {
this._activeDate = this.startAt || this._dateAdapter.today();
this._focusActiveCell();
this._currentView = this.startView;
}
@@ -246,7 +227,7 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
this._userSelection.emit();
}
/** Handles month selection in the multi-year view. */
/** 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;
@@ -286,29 +267,6 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
return !this.maxDate || !this._isSameView(this._activeDate, this.maxDate);
}
/** Handles keydown events on the calendar body. */
_handleCalendarBodyKeydown(event: KeyboardEvent): void {
// TODO(mmalerba): We currently allow keyboard navigation to disabled dates, but just prevent
// disabled ones from being selected. This may not be ideal, we should look into whether
// navigation should skip over disabled dates, and if so, how to implement that efficiently.
if (this._currentView == 'month') {
this._handleCalendarBodyKeydownInMonthView(event);
} else if (this._currentView == 'year') {
this._handleCalendarBodyKeydownInYearView(event);
} else {
this._handleCalendarBodyKeydownInMultiYearView(event);
}
}
/** Focuses the active cell after the microtask queue is empty. */
_focusActiveCell() {
this._ngZone.runOutsideAngular(() => {
this._ngZone.onStable.asObservable().pipe(take(1)).subscribe(() => {
this._elementRef.nativeElement.querySelector('.mat-calendar-body-active').focus();
});
});
}
/** 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') {
@@ -323,152 +281,6 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
Math.floor(this._dateAdapter.getYear(date2) / yearsPerPage);
}
/** Handles keydown events on the calendar body when calendar is in month view. */
private _handleCalendarBodyKeydownInMonthView(event: KeyboardEvent): void {
const isRtl = this._isRtl();
switch (event.keyCode) {
case LEFT_ARROW:
this._activeDate = this._dateAdapter.addCalendarDays(this._activeDate, isRtl ? 1 : -1);
break;
case RIGHT_ARROW:
this._activeDate = this._dateAdapter.addCalendarDays(this._activeDate, isRtl ? -1 : 1);
break;
case UP_ARROW:
this._activeDate = this._dateAdapter.addCalendarDays(this._activeDate, -7);
break;
case DOWN_ARROW:
this._activeDate = this._dateAdapter.addCalendarDays(this._activeDate, 7);
break;
case HOME:
this._activeDate = this._dateAdapter.addCalendarDays(this._activeDate,
1 - this._dateAdapter.getDate(this._activeDate));
break;
case END:
this._activeDate = this._dateAdapter.addCalendarDays(this._activeDate,
(this._dateAdapter.getNumDaysInMonth(this._activeDate) -
this._dateAdapter.getDate(this._activeDate)));
break;
case PAGE_UP:
this._activeDate = event.altKey ?
this._dateAdapter.addCalendarYears(this._activeDate, -1) :
this._dateAdapter.addCalendarMonths(this._activeDate, -1);
break;
case PAGE_DOWN:
this._activeDate = event.altKey ?
this._dateAdapter.addCalendarYears(this._activeDate, 1) :
this._dateAdapter.addCalendarMonths(this._activeDate, 1);
break;
case ENTER:
if (!this.dateFilter || this.dateFilter(this._activeDate)) {
this._dateSelected(this._activeDate);
this._userSelected();
// Prevent unexpected default actions such as form submission.
event.preventDefault();
}
return;
default:
// Don't prevent default or focus active cell on keys that we don't explicitly handle.
return;
}
this._focusActiveCell();
// Prevent unexpected default actions such as form submission.
event.preventDefault();
}
/** Handles keydown events on the calendar body when calendar is in year view. */
private _handleCalendarBodyKeydownInYearView(event: KeyboardEvent): void {
const isRtl = this._isRtl();
switch (event.keyCode) {
case LEFT_ARROW:
this._activeDate = this._dateAdapter.addCalendarMonths(this._activeDate, isRtl ? 1 : -1);
break;
case RIGHT_ARROW:
this._activeDate = this._dateAdapter.addCalendarMonths(this._activeDate, isRtl ? -1 : 1);
break;
case UP_ARROW:
this._activeDate = this._dateAdapter.addCalendarMonths(this._activeDate, -4);
break;
case DOWN_ARROW:
this._activeDate = this._dateAdapter.addCalendarMonths(this._activeDate, 4);
break;
case HOME:
this._activeDate = this._dateAdapter.addCalendarMonths(this._activeDate,
-this._dateAdapter.getMonth(this._activeDate));
break;
case END:
this._activeDate = this._dateAdapter.addCalendarMonths(this._activeDate,
11 - this._dateAdapter.getMonth(this._activeDate));
break;
case PAGE_UP:
this._activeDate =
this._dateAdapter.addCalendarYears(this._activeDate, event.altKey ? -10 : -1);
break;
case PAGE_DOWN:
this._activeDate =
this._dateAdapter.addCalendarYears(this._activeDate, event.altKey ? 10 : 1);
break;
case ENTER:
this._goToDateInView(this._activeDate, 'month');
break;
default:
// Don't prevent default or focus active cell on keys that we don't explicitly handle.
return;
}
this._focusActiveCell();
// Prevent unexpected default actions such as form submission.
event.preventDefault();
}
/** Handles keydown events on the calendar body when calendar is in multi-year view. */
private _handleCalendarBodyKeydownInMultiYearView(event: KeyboardEvent): void {
switch (event.keyCode) {
case LEFT_ARROW:
this._activeDate = this._dateAdapter.addCalendarYears(this._activeDate, -1);
break;
case RIGHT_ARROW:
this._activeDate = this._dateAdapter.addCalendarYears(this._activeDate, 1);
break;
case UP_ARROW:
this._activeDate = this._dateAdapter.addCalendarYears(this._activeDate, -yearsPerRow);
break;
case DOWN_ARROW:
this._activeDate = this._dateAdapter.addCalendarYears(this._activeDate, yearsPerRow);
break;
case HOME:
this._activeDate = this._dateAdapter.addCalendarYears(this._activeDate,
-this._dateAdapter.getYear(this._activeDate) % yearsPerPage);
break;
case END:
this._activeDate = this._dateAdapter.addCalendarYears(this._activeDate,
yearsPerPage - this._dateAdapter.getYear(this._activeDate) % yearsPerPage - 1);
break;
case PAGE_UP:
this._activeDate =
this._dateAdapter.addCalendarYears(
this._activeDate, event.altKey ? -yearsPerPage * 10 : -yearsPerPage);
break;
case PAGE_DOWN:
this._activeDate =
this._dateAdapter.addCalendarYears(
this._activeDate, event.altKey ? yearsPerPage * 10 : yearsPerPage);
break;
case ENTER:
this._goToDateInView(this._activeDate, 'year');
break;
default:
// Don't prevent default or focus active cell on keys that we don't explicitly handle.
return;
}
this._focusActiveCell();
// Prevent unexpected default actions such as form submission.
event.preventDefault();
}
/**
* @param obj The object to check.
* @returns The given object if it is both a date instance and valid, otherwise null.
@@ -476,9 +288,4 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
private _getValidDateOrNull(obj: any): D | null {
return (this._dateAdapter.isDateInstance(obj) && this._dateAdapter.isValid(obj)) ? obj : null;
}
/** Determines whether the user has the RTL layout direction. */
private _isRtl() {
return this._dir && this._dir.value === 'rtl';
}
}
+4 -3
View File
@@ -234,7 +234,7 @@ describe('MatDatepicker', () => {
expect(document.querySelector('mat-dialog-container')).not.toBeNull();
expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 1));
let calendarBodyEl = document.querySelector('.mat-calendar-content') as HTMLElement;
let calendarBodyEl = document.querySelector('.mat-calendar-body') as HTMLElement;
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
@@ -279,7 +279,7 @@ describe('MatDatepicker', () => {
testComponent.datepicker.open();
fixture.detectChanges();
let calendarBodyEl = document.querySelector('.mat-calendar-content') as HTMLElement;
let calendarBodyEl = document.querySelector('.mat-calendar-body') as HTMLElement;
expect(calendarBodyEl).not.toBeNull();
expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 1));
@@ -402,7 +402,8 @@ describe('MatDatepicker', () => {
fixture.detectChanges();
expect(testComponent.datepicker.opened).toBe(false);
})));
}))
);
});
describe('datepicker with too many inputs', () => {
+22 -11
View File
@@ -25,6 +25,7 @@ import {
ChangeDetectionStrategy,
Component,
ComponentRef,
ElementRef,
EventEmitter,
Inject,
InjectionToken,
@@ -36,18 +37,16 @@ import {
ViewChild,
ViewContainerRef,
ViewEncapsulation,
ElementRef,
} from '@angular/core';
import {DateAdapter} from '@angular/material/core';
import {CanColor, DateAdapter, mixinColor, ThemePalette} from '@angular/material/core';
import {MatDialog, MatDialogRef} from '@angular/material/dialog';
import {DOCUMENT} from '@angular/common';
import {Subject} from 'rxjs/Subject';
import {Subscription} from 'rxjs/Subscription';
import {merge} from 'rxjs/observable/merge';
import {MatCalendar} from './calendar';
import {createMissingDateImplError} from './datepicker-errors';
import {MatDatepickerInput} from './datepicker-input';
import {CanColor, mixinColor, ThemePalette} from '@angular/material/core';
import {MatCalendar} from './calendar';
/** Used to generate a unique ID for each datepicker instance. */
@@ -73,7 +72,7 @@ export const MAT_DATEPICKER_SCROLL_STRATEGY_PROVIDER = {
// Boilerplate for applying mixins to MatDatepickerContent.
/** @docs-private */
export class MatDatepickerContentBase {
constructor(public _elementRef: ElementRef) {}
constructor(public _elementRef: ElementRef) { }
}
export const _MatDatepickerContentMixinBase = mixinColor(MatDatepickerContentBase);
@@ -105,12 +104,21 @@ export class MatDatepickerContent<D> extends _MatDatepickerContentMixinBase
@ViewChild(MatCalendar) _calendar: MatCalendar<D>;
constructor(elementRef: ElementRef) {
constructor(elementRef: ElementRef, private _ngZone: NgZone) {
super(elementRef);
}
ngAfterContentInit() {
this._calendar._focusActiveCell();
this._focusActiveCell();
}
/** Focuses the active cell after the microtask queue is empty. */
private _focusActiveCell() {
this._ngZone.runOutsideAngular(() => {
this._ngZone.onStable.asObservable().pipe(take(1)).subscribe(() => {
this._elementRef.nativeElement.querySelector('.mat-calendar-body-active').focus();
});
});
}
}
@@ -370,20 +378,23 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
/** Open the calendar as a dialog. */
private _openAsDialog(): void {
this._dialogRef = this._dialog.open(MatDatepickerContent, {
this._dialogRef = this._dialog.open<MatDatepickerContent<D>>(MatDatepickerContent, {
direction: this._dir ? this._dir.value : 'ltr',
viewContainerRef: this._viewContainerRef,
panelClass: 'mat-datepicker-dialog',
});
this._dialogRef.afterClosed().subscribe(() => this.close());
this._dialogRef.componentInstance.datepicker = this;
if (this._dialogRef) {
this._dialogRef.afterClosed().subscribe(() => this.close());
this._dialogRef.componentInstance.datepicker = this;
}
this._setColor();
}
/** Open the calendar as a popup. */
private _openAsPopup(): void {
if (!this._calendarPortal) {
this._calendarPortal = new ComponentPortal(MatDatepickerContent, this._viewContainerRef);
this._calendarPortal = new ComponentPortal<MatDatepickerContent<D>>(MatDatepickerContent,
this._viewContainerRef);
}
if (!this._popupRef) {
+2 -1
View File
@@ -10,6 +10,7 @@
[selectedValue]="_selectedDate"
[labelMinRequiredCells]="3"
[activeCell]="_dateAdapter.getDate(activeDate) - 1"
(selectedValueChange)="_dateSelected($event)">
(selectedValueChange)="_dateSelected($event)"
(keydown)="_handleCalendarBodyKeydown($event)">
</tbody>
</table>
+188 -6
View File
@@ -1,12 +1,27 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {Component} from '@angular/core';
import {
DOWN_ARROW,
END,
ENTER,
HOME,
LEFT_ARROW,
PAGE_DOWN,
PAGE_UP,
RIGHT_ARROW,
UP_ARROW,
} from '@angular/cdk/keycodes';
import {By} from '@angular/platform-browser';
import {MatMonthView} from './month-view';
import {Component} from '@angular/core';
import {Direction, Directionality} from '@angular/cdk/bidi';
import {JAN, MAR, NOV, FEB} from '@angular/material/core';
import {MatCalendarBody} from './calendar-body';
import {MatNativeDateModule} from '@angular/material/core';
import {JAN, MAR} from '@angular/material/core';
import {MatMonthView} from './month-view';
import {MatNativeDateModule, DEC} from '@angular/material/core';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {dispatchKeyboardEvent, dispatchFakeEvent} from '@angular/cdk/testing';
describe('MatMonthView', () => {
let dir: {value: Direction};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
@@ -20,6 +35,9 @@ describe('MatMonthView', () => {
StandardMonthView,
MonthViewWithDateFilter,
],
providers: [
{provide: Directionality, useFactory: () => dir = {value: 'ltr'}}
]
});
TestBed.compileComponents();
@@ -76,6 +94,170 @@ describe('MatMonthView', () => {
expect((cellEls[4] as HTMLElement).innerText.trim()).toBe('5');
expect(cellEls[4].classList).toContain('mat-calendar-body-active');
});
describe('a11y', () => {
describe('calendar body', () => {
let calendarBodyEl: HTMLElement;
let calendarInstance: StandardMonthView;
beforeEach(() => {
calendarInstance = fixture.componentInstance;
calendarBodyEl =
fixture.debugElement.nativeElement.querySelector('.mat-calendar-body') as HTMLElement;
expect(calendarBodyEl).not.toBeNull();
dir.value = 'ltr';
fixture.componentInstance.date = new Date(2017, JAN, 5);
dispatchFakeEvent(calendarBodyEl, 'focus');
fixture.detectChanges();
});
it('should decrement date on left arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(calendarInstance.date).toEqual(new Date(2017, JAN, 4));
calendarInstance.date = new Date(2017, JAN, 1);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(calendarInstance.date).toEqual(new Date(2016, DEC, 31));
});
it('should increment date on left arrow press in rtl', () => {
dir.value = 'rtl';
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(calendarInstance.date).toEqual(new Date(2017, JAN, 6));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(calendarInstance.date).toEqual(new Date(2017, JAN, 7));
});
it('should increment date on right arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(calendarInstance.date).toEqual(new Date(2017, JAN, 6));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(calendarInstance.date).toEqual(new Date(2017, JAN, 7));
});
it('should decrement date on right arrow press in rtl', () => {
dir.value = 'rtl';
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(calendarInstance.date).toEqual(new Date(2017, JAN, 4));
calendarInstance.date = new Date(2017, JAN, 1);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(calendarInstance.date).toEqual(new Date(2016, DEC, 31));
});
it('should go up a row on up arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW);
fixture.detectChanges();
expect(calendarInstance.date).toEqual(new Date(2016, DEC, 29));
calendarInstance.date = new Date(2017, JAN, 7);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW);
fixture.detectChanges();
expect(calendarInstance.date).toEqual(new Date(2016, DEC, 31));
});
it('should go down a row on down arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW);
fixture.detectChanges();
expect(calendarInstance.date).toEqual(new Date(2017, JAN, 12));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW);
fixture.detectChanges();
expect(calendarInstance.date).toEqual(new Date(2017, JAN, 19));
});
it('should go to beginning of the month on home press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME);
fixture.detectChanges();
expect(calendarInstance.date).toEqual(new Date(2017, JAN, 1));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME);
fixture.detectChanges();
expect(calendarInstance.date).toEqual(new Date(2017, JAN, 1));
});
it('should go to end of the month on end press', () => {
calendarInstance.date = new Date(2017, JAN, 10);
dispatchKeyboardEvent(calendarBodyEl, 'keydown', END);
fixture.detectChanges();
expect(calendarInstance.date).toEqual(new Date(2017, JAN, 31));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', END);
fixture.detectChanges();
expect(calendarInstance.date).toEqual(new Date(2017, JAN, 31));
});
it('should go back one month on page up press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP);
fixture.detectChanges();
expect(calendarInstance.date).toEqual(new Date(2016, DEC, 5));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP);
fixture.detectChanges();
expect(calendarInstance.date).toEqual(new Date(2016, NOV, 5));
});
it('should go forward one month on page down press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN);
fixture.detectChanges();
expect(calendarInstance.date).toEqual(new Date(2017, FEB, 5));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN);
fixture.detectChanges();
expect(calendarInstance.date).toEqual(new Date(2017, MAR, 5));
});
it('should select active date on enter', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(testComponent.selected).toEqual(new Date(2017, JAN, 10));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER);
fixture.detectChanges();
expect(testComponent.selected).toEqual(new Date(2017, JAN, 4));
});
});
});
});
describe('month view with date filter', () => {
@@ -102,7 +284,7 @@ describe('MatMonthView', () => {
@Component({
template: `<mat-month-view [activeDate]="date" [(selected)]="selected"></mat-month-view>`,
template: `<mat-month-view [(activeDate)]="date" [(selected)]="selected"></mat-month-view>`,
})
class StandardMonthView {
date = new Date(2017, JAN, 5);
+102 -7
View File
@@ -6,9 +6,21 @@
* found in the LICENSE file at https://angular.io/license
*/
import {
DOWN_ARROW,
END,
ENTER,
HOME,
LEFT_ARROW,
PAGE_DOWN,
PAGE_UP,
RIGHT_ARROW,
UP_ARROW,
} from '@angular/cdk/keycodes';
import {
AfterContentInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
Inject,
@@ -16,10 +28,11 @@ import {
Optional,
Output,
ViewEncapsulation,
ChangeDetectorRef,
ViewChild,
} from '@angular/core';
import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core';
import {MatCalendarCell} from './calendar-body';
import {Directionality} from '@angular/cdk/bidi';
import {MatCalendarBody, MatCalendarCell} from './calendar-body';
import {createMissingDateImplError} from './datepicker-errors';
@@ -37,7 +50,7 @@ const DAYS_PER_WEEK = 7;
exportAs: 'matMonthView',
encapsulation: ViewEncapsulation.None,
preserveWhitespaces: false,
changeDetection: ChangeDetectionStrategy.OnPush,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MatMonthView<D> implements AfterContentInit {
/**
@@ -46,9 +59,10 @@ export class MatMonthView<D> implements AfterContentInit {
@Input()
get activeDate(): D { return this._activeDate; }
set activeDate(value: D) {
let oldActiveDate = this._activeDate;
this._activeDate =
const oldActiveDate = this._activeDate;
const validDate =
this._getValidDateOrNull(this._dateAdapter.deserialize(value)) || this._dateAdapter.today();
this._activeDate = this._dateAdapter.clampDate(validDate, this.minDate, this.maxDate);
if (!this._hasSameMonthAndYear(oldActiveDate, this._activeDate)) {
this._init();
}
@@ -89,6 +103,12 @@ export class MatMonthView<D> implements AfterContentInit {
/** Emits when any date is selected. */
@Output() readonly _userSelection: EventEmitter<void> = new EventEmitter<void>();
/** Emits when any date is activated. */
@Output() readonly activeDateChange: EventEmitter<D> = new EventEmitter<D>();
/** The body of calendar table */
@ViewChild(MatCalendarBody) _matCalendarBody;
/** The label for this month (e.g. "January 2017"). */
_monthLabel: string;
@@ -110,9 +130,10 @@ export class MatMonthView<D> implements AfterContentInit {
/** The names of the weekdays. */
_weekdays: {long: string, narrow: string}[];
constructor(@Optional() public _dateAdapter: DateAdapter<D>,
constructor(private _changeDetectorRef: ChangeDetectorRef,
@Optional() @Inject(MAT_DATE_FORMATS) private _dateFormats: MatDateFormats,
private _changeDetectorRef: ChangeDetectorRef) {
@Optional() public _dateAdapter: DateAdapter<D>,
@Optional() private _dir?: Directionality) {
if (!this._dateAdapter) {
throw createMissingDateImplError('DateAdapter');
}
@@ -135,6 +156,7 @@ export class MatMonthView<D> implements AfterContentInit {
ngAfterContentInit() {
this._init();
this._focusActiveCell();
}
/** Handles when a new date is selected. */
@@ -150,6 +172,69 @@ export class MatMonthView<D> implements AfterContentInit {
this._userSelection.emit();
}
/** Handles keydown events on the calendar body when calendar is in month view. */
_handleCalendarBodyKeydown(event: KeyboardEvent): void {
// TODO(mmalerba): We currently allow keyboard navigation to disabled dates, but just prevent
// disabled ones from being selected. This may not be ideal, we should look into whether
// navigation should skip over disabled dates, and if so, how to implement that efficiently.
const oldActiveDate = this._activeDate;
const isRtl = this._isRtl();
switch (event.keyCode) {
case LEFT_ARROW:
this.activeDate = this._dateAdapter.addCalendarDays(this._activeDate, isRtl ? 1 : -1);
break;
case RIGHT_ARROW:
this.activeDate = this._dateAdapter.addCalendarDays(this._activeDate, isRtl ? -1 : 1);
break;
case UP_ARROW:
this.activeDate = this._dateAdapter.addCalendarDays(this._activeDate, -7);
break;
case DOWN_ARROW:
this.activeDate = this._dateAdapter.addCalendarDays(this._activeDate, 7);
break;
case HOME:
this.activeDate = this._dateAdapter.addCalendarDays(this._activeDate,
1 - this._dateAdapter.getDate(this._activeDate));
break;
case END:
this.activeDate = this._dateAdapter.addCalendarDays(this._activeDate,
(this._dateAdapter.getNumDaysInMonth(this._activeDate) -
this._dateAdapter.getDate(this._activeDate)));
break;
case PAGE_UP:
this.activeDate = event.altKey ?
this._dateAdapter.addCalendarYears(this._activeDate, -1) :
this._dateAdapter.addCalendarMonths(this._activeDate, -1);
break;
case PAGE_DOWN:
this.activeDate = event.altKey ?
this._dateAdapter.addCalendarYears(this._activeDate, 1) :
this._dateAdapter.addCalendarMonths(this._activeDate, 1);
break;
case ENTER:
if (!this.dateFilter || this.dateFilter(this._activeDate)) {
this._dateSelected(this._dateAdapter.getDate(this._activeDate));
this._userSelection.emit();
// Prevent unexpected default actions such as form submission.
event.preventDefault();
}
return;
default:
// Don't prevent default or focus active cell on keys that we don't explicitly handle.
return;
}
if (this._dateAdapter.compareDate(oldActiveDate, this.activeDate)) {
this.activeDateChange.emit(this.activeDate);
}
this._focusActiveCell();
// Prevent unexpected default actions such as form submission.
event.preventDefault();
}
/** Initializes this month view. */
_init() {
this._selectedDate = this._getDateInCurrentMonth(this.selected);
@@ -168,6 +253,11 @@ export class MatMonthView<D> implements AfterContentInit {
this._changeDetectorRef.markForCheck();
}
/** Focuses the active cell after the microtask queue is empty. */
private _focusActiveCell() {
this._matCalendarBody._focusActiveCell();
}
/** Creates MatCalendarCells for the dates in this month. */
private _createWeekCells() {
const daysInMonth = this._dateAdapter.getNumDaysInMonth(this.activeDate);
@@ -218,4 +308,9 @@ export class MatMonthView<D> implements AfterContentInit {
private _getValidDateOrNull(obj: any): D | null {
return (this._dateAdapter.isDateInstance(obj) && this._dateAdapter.isValid(obj)) ? obj : null;
}
/** Determines whether the user has the RTL layout direction. */
private _isRtl() {
return this._dir && this._dir.value === 'rtl';
}
}
+2 -1
View File
@@ -10,6 +10,7 @@
[numCols]="4"
[cellAspectRatio]="4 / 7"
[activeCell]="_getActiveCell()"
(selectedValueChange)="_yearSelected($event)">
(selectedValueChange)="_yearSelected($event)"
(keydown)="_handleCalendarBodyKeydown($event)">
</tbody>
</table>
+148 -8
View File
@@ -1,13 +1,26 @@
import {Component, ViewChild} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {JAN, MatNativeDateModule} from '@angular/material/core';
import {
DOWN_ARROW,
END,
HOME,
LEFT_ARROW,
PAGE_DOWN,
PAGE_UP,
RIGHT_ARROW,
UP_ARROW,
} from '@angular/cdk/keycodes';
import {By} from '@angular/platform-browser';
import {Component, ViewChild} from '@angular/core';
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
import {Direction, Directionality} from '@angular/cdk/bidi';
import {JAN, MatNativeDateModule} from '@angular/material/core';
import {MatCalendarBody} from './calendar-body';
import {MatMultiYearView, yearsPerPage} from './multi-year-view';
import {MatYearView} from './year-view';
import {MatMultiYearView, yearsPerPage, yearsPerRow} from './multi-year-view';
import {dispatchKeyboardEvent, dispatchFakeEvent} from '@angular/cdk/testing';
describe('MatMultiYearView', () => {
beforeEach(() => {
let dir: {value: Direction};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MatNativeDateModule,
@@ -20,10 +33,13 @@ describe('MatMultiYearView', () => {
StandardMultiYearView,
MultiYearViewWithDateFilter,
],
providers: [
{provide: Directionality, useFactory: () => dir = {value: 'ltr'}}
]
});
TestBed.compileComponents();
});
}));
describe('standard multi-year view', () => {
let fixture: ComponentFixture<StandardMultiYearView>;
@@ -81,6 +97,130 @@ describe('MatMultiYearView', () => {
expect((cellEls[1] as HTMLElement).innerText.trim()).toBe('2017');
expect(cellEls[1].classList).toContain('mat-calendar-body-active');
});
describe('a11y', () => {
describe('calendar body', () => {
let calendarBodyEl: HTMLElement;
let calendarInstance: StandardMultiYearView;
beforeEach(() => {
calendarInstance = fixture.componentInstance;
calendarBodyEl =
fixture.debugElement.nativeElement.querySelector('.mat-calendar-body') as HTMLElement;
expect(calendarBodyEl).not.toBeNull();
dir.value = 'ltr';
fixture.componentInstance.date = new Date(2017, JAN, 1);
dispatchFakeEvent(calendarBodyEl, 'focus');
fixture.detectChanges();
});
it('should decrement year on left arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(calendarInstance.multiYearView.activeDate).toEqual(new Date(2016, JAN, 1));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(calendarInstance.multiYearView.activeDate).toEqual(new Date(2015, JAN, 1));
});
it('should increment year on right arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(calendarInstance.multiYearView.activeDate).toEqual(new Date(2018, JAN, 1));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(calendarInstance.multiYearView.activeDate).toEqual(new Date(2019, JAN, 1));
});
it('should go up a row on up arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW);
fixture.detectChanges();
expect(calendarInstance.multiYearView.activeDate)
.toEqual(new Date(2017 - yearsPerRow, JAN, 1));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW);
fixture.detectChanges();
expect(calendarInstance.multiYearView.activeDate)
.toEqual(new Date(2017 - yearsPerRow * 2, JAN, 1));
});
it('should go down a row on down arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW);
fixture.detectChanges();
expect(calendarInstance.multiYearView.activeDate)
.toEqual(new Date(2017 + yearsPerRow, JAN, 1));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW);
fixture.detectChanges();
expect(calendarInstance.multiYearView.activeDate)
.toEqual(new Date(2017 + yearsPerRow * 2, JAN, 1));
});
it('should go to first year in current range on home press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME);
fixture.detectChanges();
expect(calendarInstance.multiYearView.activeDate).toEqual(new Date(2016, JAN, 1));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME);
fixture.detectChanges();
expect(calendarInstance.multiYearView.activeDate).toEqual(new Date(2016, JAN, 1));
});
it('should go to last year in current range on end press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', END);
fixture.detectChanges();
expect(calendarInstance.multiYearView.activeDate).toEqual(new Date(2039, JAN, 1));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', END);
fixture.detectChanges();
expect(calendarInstance.multiYearView.activeDate).toEqual(new Date(2039, JAN, 1));
});
it('should go to same index in previous year range page up press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP);
fixture.detectChanges();
expect(calendarInstance.multiYearView.activeDate)
.toEqual(new Date(2017 - yearsPerPage, JAN, 1));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP);
fixture.detectChanges();
expect(calendarInstance.multiYearView.activeDate)
.toEqual(new Date(2017 - yearsPerPage * 2, JAN, 1));
});
it('should go to same index in next year range on page down press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN);
fixture.detectChanges();
expect(calendarInstance.multiYearView.activeDate)
.toEqual(new Date(2017 + yearsPerPage, JAN, 1));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN);
fixture.detectChanges();
expect(calendarInstance.multiYearView.activeDate)
.toEqual(new Date(2017 + yearsPerPage * 2, JAN, 1));
});
});
});
});
describe('multi year view with date filter', () => {
@@ -116,7 +256,7 @@ class StandardMultiYearView {
selected = new Date(2020, JAN, 1);
selectedYear: Date;
@ViewChild(MatYearView) yearView: MatYearView<Date>;
@ViewChild(MatMultiYearView) multiYearView: MatMultiYearView<Date>;
}
@Component({
+88 -8
View File
@@ -6,6 +6,17 @@
* found in the LICENSE file at https://angular.io/license
*/
import {
DOWN_ARROW,
END,
ENTER,
HOME,
LEFT_ARROW,
PAGE_DOWN,
PAGE_UP,
RIGHT_ARROW,
UP_ARROW,
} from '@angular/cdk/keycodes';
import {
AfterContentInit,
ChangeDetectionStrategy,
@@ -15,13 +26,14 @@ import {
Input,
Optional,
Output,
ViewEncapsulation
ViewChild,
ViewEncapsulation,
} from '@angular/core';
import {DateAdapter} from '@angular/material/core';
import {MatCalendarCell} from './calendar-body';
import {Directionality} from '@angular/cdk/bidi';
import {MatCalendarBody, MatCalendarCell} from './calendar-body';
import {createMissingDateImplError} from './datepicker-errors';
export const yearsPerPage = 24;
export const yearsPerRow = 4;
@@ -38,7 +50,7 @@ export const yearsPerRow = 4;
exportAs: 'matMultiYearView',
encapsulation: ViewEncapsulation.None,
preserveWhitespaces: false,
changeDetection: ChangeDetectionStrategy.OnPush,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MatMultiYearView<D> implements AfterContentInit {
/** The date to display in this multi-year view (everything other than the year is ignored). */
@@ -46,8 +58,9 @@ export class MatMultiYearView<D> implements AfterContentInit {
get activeDate(): D { return this._activeDate; }
set activeDate(value: D) {
let oldActiveDate = this._activeDate;
this._activeDate =
const validDate =
this._getValidDateOrNull(this._dateAdapter.deserialize(value)) || this._dateAdapter.today();
this._activeDate = this._dateAdapter.clampDate(validDate, this.minDate, this.maxDate);
if (Math.floor(this._dateAdapter.getYear(oldActiveDate) / yearsPerPage) !=
Math.floor(this._dateAdapter.getYear(this._activeDate) / yearsPerPage)) {
this._init();
@@ -83,12 +96,15 @@ export class MatMultiYearView<D> implements AfterContentInit {
/** A function used to filter which dates are selectable. */
@Input() dateFilter: (date: D) => boolean;
/** Emits when a new month is selected. */
/** Emits when a new year is selected. */
@Output() readonly selectedChange: EventEmitter<D> = new EventEmitter<D>();
/** Emits the selected year. This doesn't imply a change on the selected date */
@Output() readonly yearSelected: EventEmitter<D> = new EventEmitter<D>();
/** The body of calendar table */
@ViewChild(MatCalendarBody) _matCalendarBody;
/** Grid of calendar cells representing the currently displayed years. */
_years: MatCalendarCell[][];
@@ -98,8 +114,9 @@ export class MatMultiYearView<D> implements AfterContentInit {
/** The year of the selected date. Null if the selected date is null. */
_selectedYear: number | null;
constructor(@Optional() public _dateAdapter: DateAdapter<D>,
private _changeDetectorRef: ChangeDetectorRef) {
constructor(private _changeDetectorRef: ChangeDetectorRef,
@Optional() public _dateAdapter: DateAdapter<D>,
@Optional() private _dir?: Directionality) {
if (!this._dateAdapter) {
throw createMissingDateImplError('DateAdapter');
}
@@ -109,6 +126,7 @@ export class MatMultiYearView<D> implements AfterContentInit {
ngAfterContentInit() {
this._init();
this._focusActiveCell();
}
/** Initializes this multi-year view. */
@@ -137,10 +155,67 @@ export class MatMultiYearView<D> implements AfterContentInit {
Math.min(this._dateAdapter.getDate(this.activeDate), daysInMonth)));
}
/** Handles keydown events on the calendar body when calendar is in multi-year view. */
_handleCalendarBodyKeydown(event: KeyboardEvent): void {
// TODO(mmalerba): We currently allow keyboard navigation to disabled dates, but just prevent
// disabled ones from being selected. This may not be ideal, we should look into whether
// navigation should skip over disabled dates, and if so, how to implement that efficiently.
const isRtl = this._isRtl();
switch (event.keyCode) {
case LEFT_ARROW:
this.activeDate = this._dateAdapter.addCalendarYears(this._activeDate, isRtl ? 1 : -1);
break;
case RIGHT_ARROW:
this.activeDate = this._dateAdapter.addCalendarYears(this._activeDate, isRtl ? -1 : 1);
break;
case UP_ARROW:
this.activeDate = this._dateAdapter.addCalendarYears(this._activeDate, -yearsPerRow);
break;
case DOWN_ARROW:
this.activeDate = this._dateAdapter.addCalendarYears(this._activeDate, yearsPerRow);
break;
case HOME:
this.activeDate = this._dateAdapter.addCalendarYears(this._activeDate,
-this._dateAdapter.getYear(this._activeDate) % yearsPerPage);
break;
case END:
this.activeDate = this._dateAdapter.addCalendarYears(this._activeDate,
yearsPerPage - this._dateAdapter.getYear(this._activeDate) % yearsPerPage - 1);
break;
case PAGE_UP:
this.activeDate =
this._dateAdapter.addCalendarYears(
this._activeDate, event.altKey ? -yearsPerPage * 10 : -yearsPerPage);
break;
case PAGE_DOWN:
this.activeDate =
this._dateAdapter.addCalendarYears(
this._activeDate, event.altKey ? yearsPerPage * 10 : yearsPerPage);
break;
case ENTER:
this._yearSelected(this._dateAdapter.getYear(this._activeDate));
break;
default:
// Don't prevent default or focus active cell on keys that we don't explicitly handle.
return;
}
this._focusActiveCell();
// Prevent unexpected default actions such as form submission.
event.preventDefault();
}
_getActiveCell(): number {
return this._dateAdapter.getYear(this.activeDate) % yearsPerPage;
}
/** Focuses the active cell after the microtask queue is empty. */
private _focusActiveCell() {
this._matCalendarBody._focusActiveCell();
}
/** Creates an MatCalendarCell for the given year. */
private _createCellForYear(year: number) {
let yearName = this._dateAdapter.getYearName(this._dateAdapter.createDate(year, 0, 1));
@@ -181,4 +256,9 @@ export class MatMultiYearView<D> implements AfterContentInit {
private _getValidDateOrNull(obj: any): D | null {
return (this._dateAdapter.isDateInstance(obj) && this._dateAdapter.isValid(obj)) ? obj : null;
}
/** Determines whether the user has the RTL layout direction. */
private _isRtl() {
return this._dir && this._dir.value === 'rtl';
}
}
+2 -1
View File
@@ -12,6 +12,7 @@
[numCols]="4"
[cellAspectRatio]="4 / 7"
[activeCell]="_dateAdapter.getMonth(activeDate)"
(selectedValueChange)="_monthSelected($event)">
(selectedValueChange)="_monthSelected($event)"
(keydown)="_handleCalendarBodyKeydown($event)">
</tbody>
</table>
+197 -4
View File
@@ -1,12 +1,26 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {Component, ViewChild} from '@angular/core';
import {
DOWN_ARROW,
END,
HOME,
LEFT_ARROW,
PAGE_DOWN,
PAGE_UP,
RIGHT_ARROW,
UP_ARROW,
} from '@angular/cdk/keycodes';
import {By} from '@angular/platform-browser';
import {MatYearView} from './year-view';
import {Component, ViewChild} from '@angular/core';
import {Direction, Directionality} from '@angular/cdk/bidi';
import {FEB, JAN, JUL, JUN, MAR, DEC, NOV, AUG, MAY, OCT, SEP} from '@angular/material/core';
import {MatCalendarBody} from './calendar-body';
import {MatNativeDateModule} from '@angular/material/core';
import {FEB, JAN, JUL, JUN, MAR} from '@angular/material/core';
import {MatYearView} from './year-view';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {dispatchKeyboardEvent, dispatchFakeEvent} from '@angular/cdk/testing';
describe('MatYearView', () => {
let dir: {value: Direction};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
@@ -20,6 +34,9 @@ describe('MatYearView', () => {
StandardYearView,
YearViewWithDateFilter,
],
providers: [
{provide: Directionality, useFactory: () => dir = {value: 'ltr'}}
]
});
TestBed.compileComponents();
@@ -96,6 +113,182 @@ describe('MatYearView', () => {
expect(testComponent.selected).toEqual(new Date(2017, JUN, 30));
});
describe('a11y', () => {
describe('calendar body', () => {
let calendarBodyEl: HTMLElement;
let calendarInstance: StandardYearView;
beforeEach(() => {
calendarInstance = fixture.componentInstance;
calendarBodyEl =
fixture.debugElement.nativeElement.querySelector('.mat-calendar-body') as HTMLElement;
expect(calendarBodyEl).not.toBeNull();
dir.value = 'ltr';
fixture.componentInstance.date = new Date(2017, JAN, 5);
dispatchFakeEvent(calendarBodyEl, 'focus');
fixture.detectChanges();
});
it('should decrement month on left arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(calendarInstance.yearView.activeDate).toEqual(new Date(2016, DEC, 5));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(calendarInstance.yearView.activeDate).toEqual(new Date(2016, NOV, 5));
});
it('should increment month on left arrow press in rtl', () => {
dir.value = 'rtl';
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(calendarInstance.yearView.activeDate).toEqual(new Date(2017, FEB, 5));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(calendarInstance.yearView.activeDate).toEqual(new Date(2017, MAR, 5));
});
it('should increment month on right arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(calendarInstance.yearView.activeDate).toEqual(new Date(2017, FEB, 5));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(calendarInstance.yearView.activeDate).toEqual(new Date(2017, MAR, 5));
});
it('should decrement month on right arrow press in rtl', () => {
dir.value = 'rtl';
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(calendarInstance.yearView.activeDate).toEqual(new Date(2016, DEC, 5));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(calendarInstance.yearView.activeDate).toEqual(new Date(2016, NOV, 5));
});
it('should go up a row on up arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW);
fixture.detectChanges();
expect(calendarInstance.yearView.activeDate).toEqual(new Date(2016, SEP, 5));
calendarInstance.yearView.activeDate = new Date(2017, JUL, 1);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW);
fixture.detectChanges();
expect(calendarInstance.yearView.activeDate).toEqual(new Date(2017, MAR, 1));
calendarInstance.yearView.activeDate = new Date(2017, DEC, 10);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW);
fixture.detectChanges();
expect(calendarInstance.yearView.activeDate).toEqual(new Date(2017, AUG, 10));
});
it('should go down a row on down arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW);
fixture.detectChanges();
expect(calendarInstance.yearView.activeDate).toEqual(new Date(2017, MAY, 5));
calendarInstance.yearView.activeDate = new Date(2017, JUN, 1);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW);
fixture.detectChanges();
expect(calendarInstance.yearView.activeDate).toEqual(new Date(2017, OCT, 1));
calendarInstance.yearView.activeDate = new Date(2017, SEP, 30);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW);
fixture.detectChanges();
expect(calendarInstance.yearView.activeDate).toEqual(new Date(2018, JAN, 30));
});
it('should go to first month of the year on home press', () => {
calendarInstance.yearView.activeDate = new Date(2017, SEP, 30);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME);
fixture.detectChanges();
expect(calendarInstance.yearView.activeDate).toEqual(new Date(2017, JAN, 30));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME);
fixture.detectChanges();
expect(calendarInstance.yearView.activeDate).toEqual(new Date(2017, JAN, 30));
});
it('should go to last month of the year on end press', () => {
calendarInstance.yearView.activeDate = new Date(2017, OCT, 31);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', END);
fixture.detectChanges();
expect(calendarInstance.yearView.activeDate).toEqual(new Date(2017, DEC, 31));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', END);
fixture.detectChanges();
expect(calendarInstance.yearView.activeDate).toEqual(new Date(2017, DEC, 31));
});
it('should go back one year on page up press', () => {
calendarInstance.yearView.activeDate = new Date(2016, FEB, 29);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP);
fixture.detectChanges();
expect(calendarInstance.yearView.activeDate).toEqual(new Date(2015, FEB, 28));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP);
fixture.detectChanges();
expect(calendarInstance.yearView.activeDate).toEqual(new Date(2014, FEB, 28));
});
it('should go forward one year on page down press', () => {
calendarInstance.yearView.activeDate = new Date(2016, FEB, 29);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN);
fixture.detectChanges();
expect(calendarInstance.yearView.activeDate).toEqual(new Date(2017, FEB, 28));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN);
fixture.detectChanges();
expect(calendarInstance.yearView.activeDate).toEqual(new Date(2018, FEB, 28));
});
});
});
});
describe('year view with date filter', () => {
+86 -8
View File
@@ -6,23 +6,35 @@
* found in the LICENSE file at https://angular.io/license
*/
import {
DOWN_ARROW,
END,
ENTER,
HOME,
LEFT_ARROW,
PAGE_DOWN,
PAGE_UP,
RIGHT_ARROW,
UP_ARROW,
} from '@angular/cdk/keycodes';
import {
AfterContentInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
Inject,
Input,
Optional,
Output,
ViewChild,
ViewEncapsulation,
ChangeDetectorRef,
} from '@angular/core';
import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core';
import {MatCalendarCell} from './calendar-body';
import {Directionality} from '@angular/cdk/bidi';
import {MatCalendarBody, MatCalendarCell} from './calendar-body';
import {createMissingDateImplError} from './datepicker-errors';
/**
* An internal component used to display a single year in the datepicker.
* @docs-private
@@ -34,7 +46,7 @@ import {createMissingDateImplError} from './datepicker-errors';
exportAs: 'matYearView',
encapsulation: ViewEncapsulation.None,
preserveWhitespaces: false,
changeDetection: ChangeDetectionStrategy.OnPush,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MatYearView<D> implements AfterContentInit {
/** The date to display in this year view (everything other than the year is ignored). */
@@ -42,9 +54,10 @@ export class MatYearView<D> implements AfterContentInit {
get activeDate(): D { return this._activeDate; }
set activeDate(value: D) {
let oldActiveDate = this._activeDate;
this._activeDate =
const validDate =
this._getValidDateOrNull(this._dateAdapter.deserialize(value)) || this._dateAdapter.today();
if (this._dateAdapter.getYear(oldActiveDate) != this._dateAdapter.getYear(this._activeDate)) {
this._activeDate = this._dateAdapter.clampDate(validDate, this.minDate, this.maxDate);
if (this._dateAdapter.getYear(oldActiveDate) !== this._dateAdapter.getYear(this._activeDate)) {
this._init();
}
}
@@ -84,6 +97,9 @@ export class MatYearView<D> implements AfterContentInit {
/** Emits the selected month. This doesn't imply a change on the selected date */
@Output() readonly monthSelected: EventEmitter<D> = new EventEmitter<D>();
/** The body of calendar table */
@ViewChild(MatCalendarBody) _matCalendarBody;
/** Grid of calendar cells representing the months of the year. */
_months: MatCalendarCell[][];
@@ -99,9 +115,10 @@ export class MatYearView<D> implements AfterContentInit {
*/
_selectedMonth: number | null;
constructor(@Optional() public _dateAdapter: DateAdapter<D>,
constructor(private _changeDetectorRef: ChangeDetectorRef,
@Optional() @Inject(MAT_DATE_FORMATS) private _dateFormats: MatDateFormats,
private _changeDetectorRef: ChangeDetectorRef) {
@Optional() public _dateAdapter: DateAdapter<D>,
@Optional() private _dir?: Directionality) {
if (!this._dateAdapter) {
throw createMissingDateImplError('DateAdapter');
}
@@ -114,6 +131,7 @@ export class MatYearView<D> implements AfterContentInit {
ngAfterContentInit() {
this._init();
this._focusActiveCell();
}
/** Handles when a new month is selected. */
@@ -130,6 +148,56 @@ export class MatYearView<D> implements AfterContentInit {
Math.min(this._dateAdapter.getDate(this.activeDate), daysInMonth)));
}
/** Handles keydown events on the calendar body when calendar is in year view. */
_handleCalendarBodyKeydown(event: KeyboardEvent): void {
// TODO(mmalerba): We currently allow keyboard navigation to disabled dates, but just prevent
// disabled ones from being selected. This may not be ideal, we should look into whether
// navigation should skip over disabled dates, and if so, how to implement that efficiently.
const isRtl = this._isRtl();
switch (event.keyCode) {
case LEFT_ARROW:
this.activeDate = this._dateAdapter.addCalendarMonths(this._activeDate, isRtl ? 1 : -1);
break;
case RIGHT_ARROW:
this.activeDate = this._dateAdapter.addCalendarMonths(this._activeDate, isRtl ? -1 : 1);
break;
case UP_ARROW:
this.activeDate = this._dateAdapter.addCalendarMonths(this._activeDate, -4);
break;
case DOWN_ARROW:
this.activeDate = this._dateAdapter.addCalendarMonths(this._activeDate, 4);
break;
case HOME:
this.activeDate = this._dateAdapter.addCalendarMonths(this._activeDate,
-this._dateAdapter.getMonth(this._activeDate));
break;
case END:
this.activeDate = this._dateAdapter.addCalendarMonths(this._activeDate,
11 - this._dateAdapter.getMonth(this._activeDate));
break;
case PAGE_UP:
this.activeDate =
this._dateAdapter.addCalendarYears(this._activeDate, event.altKey ? -10 : -1);
break;
case PAGE_DOWN:
this.activeDate =
this._dateAdapter.addCalendarYears(this._activeDate, event.altKey ? 10 : 1);
break;
case ENTER:
this._monthSelected(this._dateAdapter.getMonth(this._activeDate));
break;
default:
// Don't prevent default or focus active cell on keys that we don't explicitly handle.
return;
}
this._focusActiveCell();
// Prevent unexpected default actions such as form submission.
event.preventDefault();
}
/** Initializes this year view. */
_init() {
this._selectedMonth = this._getMonthInCurrentYear(this.selected);
@@ -143,6 +211,11 @@ export class MatYearView<D> implements AfterContentInit {
this._changeDetectorRef.markForCheck();
}
/** Focuses the active cell after the microtask queue is empty. */
private _focusActiveCell() {
this._matCalendarBody._focusActiveCell();
}
/**
* Gets the month in this year that the given Date falls on.
* Returns null if the given Date is in another year.
@@ -226,4 +299,9 @@ export class MatYearView<D> implements AfterContentInit {
private _getValidDateOrNull(obj: any): D | null {
return (this._dateAdapter.isDateInstance(obj) && this._dateAdapter.isValid(obj)) ? obj : null;
}
/** Determines whether the user has the RTL layout direction. */
private _isRtl() {
return this._dir && this._dir.value === 'rtl';
}
}