From 5eb3b3a33b7841c8f2005b3a700841dec28e0cde Mon Sep 17 00:00:00 2001 From: Juliano Date: Thu, 1 Feb 2018 00:09:26 -0200 Subject: [PATCH 1/6] add max/min filter to multi year and year views --- src/lib/datepicker/calendar.html | 6 ++++++ src/lib/datepicker/multi-year-view.ts | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/lib/datepicker/calendar.html b/src/lib/datepicker/calendar.html index 5fab7ec8735e..adf9895daab8 100644 --- a/src/lib/datepicker/calendar.html +++ b/src/lib/datepicker/calendar.html @@ -48,10 +48,16 @@ *ngSwitchCase="'multi-year'" [activeDate]="_activeDate" [selected]="selected" +<<<<<<< HEAD [dateFilter]="dateFilter" [maxDate]="maxDate" [minDate]="minDate" (yearSelected)="_yearSelectedInMultiYearView($event)" +======= + [dateFilter]="_dateFilterForViews" + [maxDate]="maxDate" + [minDate]="minDate" +>>>>>>> add max/min filter to multi year and year views (selectedChange)="_goToDateInView($event, 'year')"> diff --git a/src/lib/datepicker/multi-year-view.ts b/src/lib/datepicker/multi-year-view.ts index 7935aa481156..b6f7f7b65400 100644 --- a/src/lib/datepicker/multi-year-view.ts +++ b/src/lib/datepicker/multi-year-view.ts @@ -161,6 +161,20 @@ export class MatMultiYearView implements AfterContentInit { return true; } + // disable if the year is greater than maxDate + if (this.maxDate) { + if (year > this._dateAdapter.getYear(this.maxDate)) { + return false; + } + } + + // disable if the year is lower than maxDate + if (this.minDate) { + if (year < this._dateAdapter.getYear(this.minDate)) { + return false; + } + } + const firstOfYear = this._dateAdapter.createDate(year, 0, 1); // If any date in the year is enabled count the year as enabled. From 9bcaeaab6d1652526d1b5c76b269b9317bb3269b Mon Sep 17 00:00:00 2001 From: Juliano Date: Thu, 1 Feb 2018 15:22:05 -0200 Subject: [PATCH 2/6] address @mmalerba's comments --- src/lib/datepicker/calendar.html | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/lib/datepicker/calendar.html b/src/lib/datepicker/calendar.html index adf9895daab8..5fab7ec8735e 100644 --- a/src/lib/datepicker/calendar.html +++ b/src/lib/datepicker/calendar.html @@ -48,16 +48,10 @@ *ngSwitchCase="'multi-year'" [activeDate]="_activeDate" [selected]="selected" -<<<<<<< HEAD [dateFilter]="dateFilter" [maxDate]="maxDate" [minDate]="minDate" (yearSelected)="_yearSelectedInMultiYearView($event)" -======= - [dateFilter]="_dateFilterForViews" - [maxDate]="maxDate" - [minDate]="minDate" ->>>>>>> add max/min filter to multi year and year views (selectedChange)="_goToDateInView($event, 'year')"> From 4bed1b1952a5f9217545ba6be0bd24e156f8cf16 Mon Sep 17 00:00:00 2001 From: Juliano Date: Fri, 2 Feb 2018 01:57:06 -0200 Subject: [PATCH 3/6] fix and tighten up logic --- src/lib/datepicker/multi-year-view.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/lib/datepicker/multi-year-view.ts b/src/lib/datepicker/multi-year-view.ts index b6f7f7b65400..7935aa481156 100644 --- a/src/lib/datepicker/multi-year-view.ts +++ b/src/lib/datepicker/multi-year-view.ts @@ -161,20 +161,6 @@ export class MatMultiYearView implements AfterContentInit { return true; } - // disable if the year is greater than maxDate - if (this.maxDate) { - if (year > this._dateAdapter.getYear(this.maxDate)) { - return false; - } - } - - // disable if the year is lower than maxDate - if (this.minDate) { - if (year < this._dateAdapter.getYear(this.minDate)) { - return false; - } - } - const firstOfYear = this._dateAdapter.createDate(year, 0, 1); // If any date in the year is enabled count the year as enabled. From 4572dd254421ab3eefdc1e674efe8433e9a6ce48 Mon Sep 17 00:00:00 2001 From: Juliano Date: Sat, 3 Feb 2018 09:32:01 -0200 Subject: [PATCH 4/6] Move datepicker handlers to view components --- src/lib/datepicker/calendar.html | 5 +- src/lib/datepicker/calendar.spec.ts | 462 ++------------------- src/lib/datepicker/calendar.ts | 201 +-------- src/lib/datepicker/datepicker.spec.ts | 7 +- src/lib/datepicker/datepicker.ts | 3 - src/lib/datepicker/month-view.html | 3 +- src/lib/datepicker/month-view.spec.ts | 194 ++++++++- src/lib/datepicker/month-view.ts | 115 ++++- src/lib/datepicker/multi-year-view.html | 3 +- src/lib/datepicker/multi-year-view.spec.ts | 154 ++++++- src/lib/datepicker/multi-year-view.ts | 95 ++++- src/lib/datepicker/year-view.html | 3 +- src/lib/datepicker/year-view.spec.ts | 201 ++++++++- src/lib/datepicker/year-view.ts | 97 ++++- 14 files changed, 861 insertions(+), 682 deletions(-) diff --git a/src/lib/datepicker/calendar.html b/src/lib/datepicker/calendar.html index 5fab7ec8735e..1245f7a2f263 100644 --- a/src/lib/datepicker/calendar.html +++ b/src/lib/datepicker/calendar.html @@ -20,11 +20,10 @@ -
+
{ 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; } } diff --git a/src/lib/datepicker/calendar.ts b/src/lib/datepicker/calendar.ts index 644496adc52f..c7533062fc90 100644 --- a/src/lib/datepicker/calendar.ts +++ b/src/lib/datepicker/calendar.ts @@ -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 implements AfterContentInit, OnDestroy, OnChanges { }[this._currentView]; } - constructor(private _elementRef: ElementRef, - private _intl: MatDatepickerIntl, - private _ngZone: NgZone, + constructor(private _intl: MatDatepickerIntl, @Optional() private _dateAdapter: DateAdapter, @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 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 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 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 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 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'; - } } diff --git a/src/lib/datepicker/datepicker.spec.ts b/src/lib/datepicker/datepicker.spec.ts index 701160870064..4130799ee871 100644 --- a/src/lib/datepicker/datepicker.spec.ts +++ b/src/lib/datepicker/datepicker.spec.ts @@ -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', () => { diff --git a/src/lib/datepicker/datepicker.ts b/src/lib/datepicker/datepicker.ts index 7eb04517d190..1b93287af682 100644 --- a/src/lib/datepicker/datepicker.ts +++ b/src/lib/datepicker/datepicker.ts @@ -21,7 +21,6 @@ import {ComponentPortal} from '@angular/cdk/portal'; import {take} from 'rxjs/operators/take'; import {filter} from 'rxjs/operators/filter'; import { - AfterContentInit, ChangeDetectionStrategy, Component, ComponentRef, @@ -33,7 +32,6 @@ import { OnDestroy, Optional, Output, - ViewChild, ViewContainerRef, ViewEncapsulation, ElementRef, @@ -44,7 +42,6 @@ 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'; diff --git a/src/lib/datepicker/month-view.html b/src/lib/datepicker/month-view.html index 3551bdef1e27..0785a27aa713 100644 --- a/src/lib/datepicker/month-view.html +++ b/src/lib/datepicker/month-view.html @@ -10,6 +10,7 @@ [selectedValue]="_selectedDate" [labelMinRequiredCells]="3" [activeCell]="_dateAdapter.getDate(activeDate) - 1" - (selectedValueChange)="_dateSelected($event)"> + (selectedValueChange)="_dateSelected($event)" + (keydown)="_handleCalendarBodyKeydown($event)"> diff --git a/src/lib/datepicker/month-view.spec.ts b/src/lib/datepicker/month-view.spec.ts index b54cb400d5a5..3a4a27450d4c 100644 --- a/src/lib/datepicker/month-view.spec.ts +++ b/src/lib/datepicker/month-view.spec.ts @@ -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: ``, + template: ``, }) class StandardMonthView { date = new Date(2017, JAN, 5); diff --git a/src/lib/datepicker/month-view.ts b/src/lib/datepicker/month-view.ts index 933608feef60..6f38b15efc18 100644 --- a/src/lib/datepicker/month-view.ts +++ b/src/lib/datepicker/month-view.ts @@ -6,21 +6,36 @@ * 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, Optional, Output, ViewEncapsulation, - ChangeDetectorRef, } from '@angular/core'; import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core'; +import {Directionality} from '@angular/cdk/bidi'; import {MatCalendarCell} from './calendar-body'; import {createMissingDateImplError} from './datepicker-errors'; +import {take} from 'rxjs/operators/take'; const DAYS_PER_WEEK = 7; @@ -37,7 +52,7 @@ const DAYS_PER_WEEK = 7; exportAs: 'matMonthView', encapsulation: ViewEncapsulation.None, preserveWhitespaces: false, - changeDetection: ChangeDetectionStrategy.OnPush, + changeDetection: ChangeDetectionStrategy.OnPush }) export class MatMonthView implements AfterContentInit { /** @@ -46,12 +61,16 @@ export class MatMonthView implements AfterContentInit { @Input() get activeDate(): D { return this._activeDate; } set activeDate(value: D) { - let oldActiveDate = this._activeDate; - this._activeDate = - this._getValidDateOrNull(this._dateAdapter.deserialize(value)) || this._dateAdapter.today(); + 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(); } + if (this._dateAdapter.compareDate(oldActiveDate, this._activeDate)) { + this.activeDateChange.emit(this._activeDate); + } } private _activeDate: D; @@ -89,6 +108,9 @@ export class MatMonthView implements AfterContentInit { /** Emits when any date is selected. */ @Output() readonly _userSelection: EventEmitter = new EventEmitter(); + /** Emits when any date is activated. */ + @Output() readonly activeDateChange: EventEmitter = new EventEmitter(); + /** The label for this month (e.g. "January 2017"). */ _monthLabel: string; @@ -110,9 +132,12 @@ export class MatMonthView implements AfterContentInit { /** The names of the weekdays. */ _weekdays: {long: string, narrow: string}[]; - constructor(@Optional() public _dateAdapter: DateAdapter, + constructor(private _changeDetectorRef: ChangeDetectorRef, + private _elementRef: ElementRef, + private _ngZone: NgZone, @Optional() @Inject(MAT_DATE_FORMATS) private _dateFormats: MatDateFormats, - private _changeDetectorRef: ChangeDetectorRef) { + @Optional() public _dateAdapter: DateAdapter, + @Optional() private _dir?: Directionality) { if (!this._dateAdapter) { throw createMissingDateImplError('DateAdapter'); } @@ -135,6 +160,7 @@ export class MatMonthView implements AfterContentInit { ngAfterContentInit() { this._init(); + this._focusActiveCell(); } /** Handles when a new date is selected. */ @@ -147,9 +173,79 @@ export class MatMonthView implements AfterContentInit { this.selectedChange.emit(selectedDate); } + this._userSelected(); + } + + _userSelected(): void { this._userSelection.emit(); } + /** 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(); + }); + }); + } + + /** 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 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._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(); + } + /** Initializes this month view. */ _init() { this._selectedDate = this._getDateInCurrentMonth(this.selected); @@ -218,4 +314,9 @@ export class MatMonthView 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'; + } } diff --git a/src/lib/datepicker/multi-year-view.html b/src/lib/datepicker/multi-year-view.html index 2962a4693107..ab28ea291b33 100644 --- a/src/lib/datepicker/multi-year-view.html +++ b/src/lib/datepicker/multi-year-view.html @@ -10,6 +10,7 @@ [numCols]="4" [cellAspectRatio]="4 / 7" [activeCell]="_getActiveCell()" - (selectedValueChange)="_yearSelected($event)"> + (selectedValueChange)="_yearSelected($event)" + (keydown)="_handleCalendarBodyKeydown($event)"> diff --git a/src/lib/datepicker/multi-year-view.spec.ts b/src/lib/datepicker/multi-year-view.spec.ts index 325b680bcf36..a7d04f9244e3 100644 --- a/src/lib/datepicker/multi-year-view.spec.ts +++ b/src/lib/datepicker/multi-year-view.spec.ts @@ -1,13 +1,26 @@ +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} from '@angular/core/testing'; +import {ComponentFixture, TestBed, async} from '@angular/core/testing'; +import {Direction, Directionality} from '@angular/cdk/bidi'; import {JAN, MatNativeDateModule} from '@angular/material/core'; -import {By} from '@angular/platform-browser'; 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; @@ -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; + @ViewChild(MatMultiYearView) multiYearView: MatMultiYearView; } @Component({ diff --git a/src/lib/datepicker/multi-year-view.ts b/src/lib/datepicker/multi-year-view.ts index 7935aa481156..81de9ee83ee3 100644 --- a/src/lib/datepicker/multi-year-view.ts +++ b/src/lib/datepicker/multi-year-view.ts @@ -6,21 +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, + ElementRef, EventEmitter, Input, + NgZone, Optional, Output, ViewEncapsulation } from '@angular/core'; import {DateAdapter} from '@angular/material/core'; +import {Directionality} from '@angular/cdk/bidi'; import {MatCalendarCell} from './calendar-body'; import {createMissingDateImplError} from './datepicker-errors'; - +import {take} from 'rxjs/operators/take'; export const yearsPerPage = 24; @@ -38,7 +52,7 @@ export const yearsPerRow = 4; exportAs: 'matMultiYearView', encapsulation: ViewEncapsulation.None, preserveWhitespaces: false, - changeDetection: ChangeDetectionStrategy.OnPush, + changeDetection: ChangeDetectionStrategy.OnPush }) export class MatMultiYearView implements AfterContentInit { /** The date to display in this multi-year view (everything other than the year is ignored). */ @@ -46,8 +60,9 @@ export class MatMultiYearView 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(); @@ -98,8 +113,11 @@ export class MatMultiYearView implements AfterContentInit { /** The year of the selected date. Null if the selected date is null. */ _selectedYear: number | null; - constructor(@Optional() public _dateAdapter: DateAdapter, - private _changeDetectorRef: ChangeDetectorRef) { + constructor(private _changeDetectorRef: ChangeDetectorRef, + private _elementRef: ElementRef, + private _ngZone: NgZone, + @Optional() public _dateAdapter: DateAdapter, + @Optional() private _dir?: Directionality) { if (!this._dateAdapter) { throw createMissingDateImplError('DateAdapter'); } @@ -109,6 +127,7 @@ export class MatMultiYearView implements AfterContentInit { ngAfterContentInit() { this._init(); + this._focusActiveCell(); } /** Initializes this multi-year view. */ @@ -137,6 +156,67 @@ export class MatMultiYearView implements AfterContentInit { Math.min(this._dateAdapter.getDate(this.activeDate), daysInMonth))); } + /** 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(); + }); + }); + } + + /** 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; } @@ -181,4 +261,9 @@ export class MatMultiYearView 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'; + } } diff --git a/src/lib/datepicker/year-view.html b/src/lib/datepicker/year-view.html index ba92dc8f8550..5d5b074e7c15 100644 --- a/src/lib/datepicker/year-view.html +++ b/src/lib/datepicker/year-view.html @@ -12,6 +12,7 @@ [numCols]="4" [cellAspectRatio]="4 / 7" [activeCell]="_dateAdapter.getMonth(activeDate)" - (selectedValueChange)="_monthSelected($event)"> + (selectedValueChange)="_monthSelected($event)" + (keydown)="_handleCalendarBodyKeydown($event)"> diff --git a/src/lib/datepicker/year-view.spec.ts b/src/lib/datepicker/year-view.spec.ts index b91914718ca4..e46bfbc8d96a 100644 --- a/src/lib/datepicker/year-view.spec.ts +++ b/src/lib/datepicker/year-view.spec.ts @@ -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', () => { diff --git a/src/lib/datepicker/year-view.ts b/src/lib/datepicker/year-view.ts index cfa082275d48..05dd6a78d077 100644 --- a/src/lib/datepicker/year-view.ts +++ b/src/lib/datepicker/year-view.ts @@ -6,22 +6,36 @@ * 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, Optional, Output, ViewEncapsulation, - ChangeDetectorRef, } from '@angular/core'; import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core'; +import {Directionality} from '@angular/cdk/bidi'; import {MatCalendarCell} from './calendar-body'; import {createMissingDateImplError} from './datepicker-errors'; - +import {take} from 'rxjs/operators/take'; /** * An internal component used to display a single year in the datepicker. @@ -34,7 +48,7 @@ import {createMissingDateImplError} from './datepicker-errors'; exportAs: 'matYearView', encapsulation: ViewEncapsulation.None, preserveWhitespaces: false, - changeDetection: ChangeDetectionStrategy.OnPush, + changeDetection: ChangeDetectionStrategy.OnPush }) export class MatYearView implements AfterContentInit { /** The date to display in this year view (everything other than the year is ignored). */ @@ -42,9 +56,10 @@ export class MatYearView 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(); } } @@ -99,9 +114,12 @@ export class MatYearView implements AfterContentInit { */ _selectedMonth: number | null; - constructor(@Optional() public _dateAdapter: DateAdapter, + constructor(private _changeDetectorRef: ChangeDetectorRef, + private _elementRef: ElementRef, + private _ngZone: NgZone, @Optional() @Inject(MAT_DATE_FORMATS) private _dateFormats: MatDateFormats, - private _changeDetectorRef: ChangeDetectorRef) { + @Optional() public _dateAdapter: DateAdapter, + @Optional() private _dir?: Directionality) { if (!this._dateAdapter) { throw createMissingDateImplError('DateAdapter'); } @@ -114,6 +132,7 @@ export class MatYearView implements AfterContentInit { ngAfterContentInit() { this._init(); + this._focusActiveCell(); } /** Handles when a new month is selected. */ @@ -130,6 +149,65 @@ export class MatYearView implements AfterContentInit { Math.min(this._dateAdapter.getDate(this.activeDate), daysInMonth))); } + /** 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(); + }); + }); + } + + /** 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); @@ -226,4 +304,9 @@ export class MatYearView 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'; + } } From 7f1bbd2fa14a6602e4a7f95d88789cbcbc027351 Mon Sep 17 00:00:00 2001 From: Juliano Date: Thu, 15 Feb 2018 18:22:57 -0200 Subject: [PATCH 5/6] move set focus function to MatCalendarBody --- src/lib/datepicker/calendar-body.ts | 19 +++++++++++++-- src/lib/datepicker/month-view.ts | 33 ++++++++++----------------- src/lib/datepicker/multi-year-view.ts | 27 +++++++++------------- src/lib/datepicker/year-view.ts | 25 ++++++++------------ 4 files changed, 50 insertions(+), 54 deletions(-) diff --git a/src/lib/datepicker/calendar-body.ts b/src/lib/datepicker/calendar-body.ts index 37aa2f238be0..92c4ef8f218a 100644 --- a/src/lib/datepicker/calendar-body.ts +++ b/src/lib/datepicker/calendar-body.ts @@ -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,10 @@ export class MatCalendarBody { /** Emits when a new value is selected. */ @Output() readonly selectedValueChange: EventEmitter = new EventEmitter(); + constructor(private _elementRef: ElementRef, + private _ngZone: NgZone) { + } + _cellClicked(cell: MatCalendarCell): void { if (!this.allowDisabledSelection && !cell.enabled) { return; @@ -104,4 +110,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(); + }); + }); + } } diff --git a/src/lib/datepicker/month-view.ts b/src/lib/datepicker/month-view.ts index 6f38b15efc18..2843f1fe665d 100644 --- a/src/lib/datepicker/month-view.ts +++ b/src/lib/datepicker/month-view.ts @@ -22,20 +22,18 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, - ElementRef, EventEmitter, Inject, Input, - NgZone, Optional, Output, ViewEncapsulation, + ViewChild, } from '@angular/core'; import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core'; import {Directionality} from '@angular/cdk/bidi'; -import {MatCalendarCell} from './calendar-body'; +import {MatCalendarBody, MatCalendarCell} from './calendar-body'; import {createMissingDateImplError} from './datepicker-errors'; -import {take} from 'rxjs/operators/take'; const DAYS_PER_WEEK = 7; @@ -63,7 +61,7 @@ export class MatMonthView implements AfterContentInit { set activeDate(value: D) { const oldActiveDate = this._activeDate; const validDate = - this._getValidDateOrNull(this._dateAdapter.deserialize(value)) || this._dateAdapter.today(); + 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(); @@ -111,6 +109,9 @@ export class MatMonthView implements AfterContentInit { /** Emits when any date is activated. */ @Output() readonly activeDateChange: EventEmitter = new EventEmitter(); + /** The body of calendar table */ + @ViewChild(MatCalendarBody) _matCalendarBody; + /** The label for this month (e.g. "January 2017"). */ _monthLabel: string; @@ -133,8 +134,6 @@ export class MatMonthView implements AfterContentInit { _weekdays: {long: string, narrow: string}[]; constructor(private _changeDetectorRef: ChangeDetectorRef, - private _elementRef: ElementRef, - private _ngZone: NgZone, @Optional() @Inject(MAT_DATE_FORMATS) private _dateFormats: MatDateFormats, @Optional() public _dateAdapter: DateAdapter, @Optional() private _dir?: Directionality) { @@ -173,22 +172,9 @@ export class MatMonthView implements AfterContentInit { this.selectedChange.emit(selectedDate); } - this._userSelected(); - } - - _userSelected(): void { this._userSelection.emit(); } - /** 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(); - }); - }); - } - /** 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 @@ -231,7 +217,7 @@ export class MatMonthView implements AfterContentInit { case ENTER: if (!this.dateFilter || this.dateFilter(this._activeDate)) { this._dateSelected(this._dateAdapter.getDate(this._activeDate)); - this._userSelected(); + this._userSelection.emit(); // Prevent unexpected default actions such as form submission. event.preventDefault(); } @@ -264,6 +250,11 @@ export class MatMonthView 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); diff --git a/src/lib/datepicker/multi-year-view.ts b/src/lib/datepicker/multi-year-view.ts index 81de9ee83ee3..39cc9dcce5c5 100644 --- a/src/lib/datepicker/multi-year-view.ts +++ b/src/lib/datepicker/multi-year-view.ts @@ -22,19 +22,17 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, - ElementRef, EventEmitter, Input, - NgZone, Optional, Output, - ViewEncapsulation + ViewChild, + ViewEncapsulation, } from '@angular/core'; import {DateAdapter} from '@angular/material/core'; import {Directionality} from '@angular/cdk/bidi'; -import {MatCalendarCell} from './calendar-body'; +import {MatCalendarBody, MatCalendarCell} from './calendar-body'; import {createMissingDateImplError} from './datepicker-errors'; -import {take} from 'rxjs/operators/take'; export const yearsPerPage = 24; @@ -104,6 +102,9 @@ export class MatMultiYearView implements AfterContentInit { /** Emits the selected year. This doesn't imply a change on the selected date */ @Output() readonly yearSelected: EventEmitter = new EventEmitter(); + /** The body of calendar table */ + @ViewChild(MatCalendarBody) _matCalendarBody; + /** Grid of calendar cells representing the currently displayed years. */ _years: MatCalendarCell[][]; @@ -114,8 +115,6 @@ export class MatMultiYearView implements AfterContentInit { _selectedYear: number | null; constructor(private _changeDetectorRef: ChangeDetectorRef, - private _elementRef: ElementRef, - private _ngZone: NgZone, @Optional() public _dateAdapter: DateAdapter, @Optional() private _dir?: Directionality) { if (!this._dateAdapter) { @@ -156,15 +155,6 @@ export class MatMultiYearView implements AfterContentInit { Math.min(this._dateAdapter.getDate(this.activeDate), daysInMonth))); } - /** 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(); - }); - }); - } - /** 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 @@ -221,6 +211,11 @@ export class MatMultiYearView implements AfterContentInit { 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)); diff --git a/src/lib/datepicker/year-view.ts b/src/lib/datepicker/year-view.ts index 05dd6a78d077..242bf0f2cdc0 100644 --- a/src/lib/datepicker/year-view.ts +++ b/src/lib/datepicker/year-view.ts @@ -22,20 +22,18 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, - ElementRef, EventEmitter, Inject, Input, - NgZone, Optional, Output, + ViewChild, ViewEncapsulation, } from '@angular/core'; import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core'; import {Directionality} from '@angular/cdk/bidi'; -import {MatCalendarCell} from './calendar-body'; +import {MatCalendarBody, MatCalendarCell} from './calendar-body'; import {createMissingDateImplError} from './datepicker-errors'; -import {take} from 'rxjs/operators/take'; /** * An internal component used to display a single year in the datepicker. @@ -99,6 +97,9 @@ export class MatYearView implements AfterContentInit { /** Emits the selected month. This doesn't imply a change on the selected date */ @Output() readonly monthSelected: EventEmitter = new EventEmitter(); + /** The body of calendar table */ + @ViewChild(MatCalendarBody) _matCalendarBody; + /** Grid of calendar cells representing the months of the year. */ _months: MatCalendarCell[][]; @@ -115,8 +116,6 @@ export class MatYearView implements AfterContentInit { _selectedMonth: number | null; constructor(private _changeDetectorRef: ChangeDetectorRef, - private _elementRef: ElementRef, - private _ngZone: NgZone, @Optional() @Inject(MAT_DATE_FORMATS) private _dateFormats: MatDateFormats, @Optional() public _dateAdapter: DateAdapter, @Optional() private _dir?: Directionality) { @@ -149,15 +148,6 @@ export class MatYearView implements AfterContentInit { Math.min(this._dateAdapter.getDate(this.activeDate), daysInMonth))); } - /** 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(); - }); - }); - } - /** 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 @@ -221,6 +211,11 @@ export class MatYearView 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. From 8bdf2859db5159a00733e3cb9e3527d452be14cb Mon Sep 17 00:00:00 2001 From: Juliano Date: Fri, 16 Feb 2018 08:49:53 -0200 Subject: [PATCH 6/6] rebasing and commnets... --- src/lib/datepicker/calendar-body.ts | 4 +--- src/lib/datepicker/datepicker.ts | 34 +++++++++++++++++++-------- src/lib/datepicker/month-view.ts | 9 ++++--- src/lib/datepicker/multi-year-view.ts | 2 +- 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/lib/datepicker/calendar-body.ts b/src/lib/datepicker/calendar-body.ts index 92c4ef8f218a..7ed2e5e2e1e9 100644 --- a/src/lib/datepicker/calendar-body.ts +++ b/src/lib/datepicker/calendar-body.ts @@ -83,9 +83,7 @@ export class MatCalendarBody { /** Emits when a new value is selected. */ @Output() readonly selectedValueChange: EventEmitter = new EventEmitter(); - constructor(private _elementRef: ElementRef, - private _ngZone: NgZone) { - } + constructor(private _elementRef: ElementRef, private _ngZone: NgZone) { } _cellClicked(cell: MatCalendarCell): void { if (!this.allowDisabledSelection && !cell.enabled) { diff --git a/src/lib/datepicker/datepicker.ts b/src/lib/datepicker/datepicker.ts index 1b93287af682..6106e5261158 100644 --- a/src/lib/datepicker/datepicker.ts +++ b/src/lib/datepicker/datepicker.ts @@ -21,9 +21,11 @@ import {ComponentPortal} from '@angular/cdk/portal'; import {take} from 'rxjs/operators/take'; import {filter} from 'rxjs/operators/filter'; import { + AfterContentInit, ChangeDetectionStrategy, Component, ComponentRef, + ElementRef, EventEmitter, Inject, InjectionToken, @@ -32,11 +34,11 @@ import { OnDestroy, Optional, Output, + 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'; @@ -44,7 +46,7 @@ import {Subscription} from 'rxjs/Subscription'; import {merge} from 'rxjs/observable/merge'; 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. */ @@ -70,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); @@ -102,12 +104,21 @@ export class MatDatepickerContent extends _MatDatepickerContentMixinBase @ViewChild(MatCalendar) _calendar: MatCalendar; - 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(); + }); + }); } } @@ -367,20 +378,23 @@ export class MatDatepicker implements OnDestroy, CanColor { /** Open the calendar as a dialog. */ private _openAsDialog(): void { - this._dialogRef = this._dialog.open(MatDatepickerContent, { + this._dialogRef = this._dialog.open>(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, + this._viewContainerRef); } if (!this._popupRef) { diff --git a/src/lib/datepicker/month-view.ts b/src/lib/datepicker/month-view.ts index 2843f1fe665d..dcfd05c2ebfd 100644 --- a/src/lib/datepicker/month-view.ts +++ b/src/lib/datepicker/month-view.ts @@ -66,9 +66,6 @@ export class MatMonthView implements AfterContentInit { if (!this._hasSameMonthAndYear(oldActiveDate, this._activeDate)) { this._init(); } - if (this._dateAdapter.compareDate(oldActiveDate, this._activeDate)) { - this.activeDateChange.emit(this._activeDate); - } } private _activeDate: D; @@ -181,6 +178,8 @@ export class MatMonthView implements AfterContentInit { // 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: @@ -227,6 +226,10 @@ export class MatMonthView implements AfterContentInit { 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(); diff --git a/src/lib/datepicker/multi-year-view.ts b/src/lib/datepicker/multi-year-view.ts index 39cc9dcce5c5..fef053e26451 100644 --- a/src/lib/datepicker/multi-year-view.ts +++ b/src/lib/datepicker/multi-year-view.ts @@ -96,7 +96,7 @@ export class MatMultiYearView 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 = new EventEmitter(); /** Emits the selected year. This doesn't imply a change on the selected date */