From 6b3e8ec29cdfe68643efc73c8620c33b629b1940 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Wed, 15 Mar 2017 12:08:28 -0700 Subject: [PATCH 01/11] add active cell support to calendar-table, month-view, and year-view --- src/lib/datepicker/calendar-table.html | 7 ++--- src/lib/datepicker/calendar-table.spec.ts | 7 +++++ src/lib/datepicker/calendar-table.ts | 3 +++ src/lib/datepicker/calendar.html | 4 +-- src/lib/datepicker/month-view.html | 1 + src/lib/datepicker/month-view.spec.ts | 12 ++++++--- src/lib/datepicker/month-view.ts | 32 ++++++++++++++--------- src/lib/datepicker/year-view.html | 1 + src/lib/datepicker/year-view.spec.ts | 9 ++++++- src/lib/datepicker/year-view.ts | 21 ++++++++------- 10 files changed, 67 insertions(+), 30 deletions(-) diff --git a/src/lib/datepicker/calendar-table.html b/src/lib/datepicker/calendar-table.html index 37b29b265d79..52f71aa1c811 100644 --- a/src/lib/datepicker/calendar-table.html +++ b/src/lib/datepicker/calendar-table.html @@ -5,15 +5,16 @@ - - + {{_firstRowOffset >= labelMinRequiredCells ? label : ''}} -
{ expect(todayElement.classList) .toContain('mat-calendar-table-selected', 'today should be selected'); }); + + it('should mark active date', () => { + let cellEls = calendarTableNativeElement.querySelectorAll('.mat-calendar-table-cell'); + expect((cellEls[10] as HTMLElement).innerText.trim()).toBe('11'); + expect(cellEls[10].classList).toContain('mat-calendar-table-active'); + }); }); describe('calendar table with disabled cells', () => { @@ -129,6 +135,7 @@ describe('MdCalendarTable', () => { [selectedValue]="selectedValue" [labelMinRequiredCells]="labelMinRequiredCells" [numCols]="numCols" + [activeCell]="10" (selectedValueChange)="onSelect($event)"> `, }) diff --git a/src/lib/datepicker/calendar-table.ts b/src/lib/datepicker/calendar-table.ts index aabe9c76fe70..e2abd9304078 100644 --- a/src/lib/datepicker/calendar-table.ts +++ b/src/lib/datepicker/calendar-table.ts @@ -51,6 +51,9 @@ export class MdCalendarTable { /** Whether to allow selection of disabled cells. */ @Input() allowDisabledSelection = false; + /** The cell number of the active cell in the table. */ + @Input() activeCell = 0; + /** Emits when a new value is selected. */ @Output() selectedValueChange = new EventEmitter(); diff --git a/src/lib/datepicker/calendar.html b/src/lib/datepicker/calendar.html index e570acda369f..670104d46125 100644 --- a/src/lib/datepicker/calendar.html +++ b/src/lib/datepicker/calendar.html @@ -32,7 +32,7 @@
@@ -40,7 +40,7 @@ diff --git a/src/lib/datepicker/month-view.html b/src/lib/datepicker/month-view.html index e0e0559cfc9c..af76b0a61a4e 100644 --- a/src/lib/datepicker/month-view.html +++ b/src/lib/datepicker/month-view.html @@ -3,5 +3,6 @@ [todayValue]="_todayDate" [selectedValue]="_selectedDate" [labelMinRequiredCells]="3" + [activeCell]="activeDate.date - 1" (selectedValueChange)="_dateSelected($event)"> diff --git a/src/lib/datepicker/month-view.spec.ts b/src/lib/datepicker/month-view.spec.ts index 5a3fee271e61..d7fdefc1e985 100644 --- a/src/lib/datepicker/month-view.spec.ts +++ b/src/lib/datepicker/month-view.spec.ts @@ -71,6 +71,12 @@ describe('MdMonthView', () => { let selectedEl = monthViewNativeElement.querySelector('.mat-calendar-table-selected'); expect(selectedEl.innerHTML.trim()).toBe('31'); }); + + it('should mark active date', () => { + let cellEls = monthViewNativeElement.querySelectorAll('.mat-calendar-table-cell'); + expect((cellEls[4] as HTMLElement).innerText.trim()).toBe('5'); + expect(cellEls[4].classList).toContain('mat-calendar-table-active'); + }); }); describe('month view with date filter', () => { @@ -87,7 +93,7 @@ describe('MdMonthView', () => { testComponent = fixture.componentInstance; }); - it('should disabled filtered dates', () => { + it('should disable filtered dates', () => { let cells = monthViewNativeElement.querySelectorAll('.mat-calendar-table-cell'); expect(cells[0].classList).toContain('mat-calendar-table-disabled'); expect(cells[1].classList).not.toContain('mat-calendar-table-disabled'); @@ -97,7 +103,7 @@ describe('MdMonthView', () => { @Component({ - template: ``, + template: ``, }) class StandardMonthView { date = new SimpleDate(2017, 0, 5); @@ -106,7 +112,7 @@ class StandardMonthView { @Component({ - template: `` + template: `` }) class MonthViewWithDateFilter { dateFilter(date: SimpleDate) { diff --git a/src/lib/datepicker/month-view.ts b/src/lib/datepicker/month-view.ts index ced1014d9e00..411405f9ef96 100644 --- a/src/lib/datepicker/month-view.ts +++ b/src/lib/datepicker/month-view.ts @@ -31,12 +31,15 @@ export class MdMonthView implements AfterContentInit { * The date to display in this month view (everything other than the month and year is ignored). */ @Input() - get date() { return this._date; } - set date(value) { - this._date = this._locale.parseDate(value) || SimpleDate.today(); - this._init(); + get activeDate() { return this._activeDate; } + set activeDate(value) { + let oldActiveDate = this._activeDate; + this._activeDate = this._locale.parseDate(value) || SimpleDate.today(); + if (!this._hasSameMonthAndYear(oldActiveDate, this._activeDate)) { + this._init(); + } } - private _date = SimpleDate.today(); + private _activeDate = SimpleDate.today(); /** The currently selected date. */ @Input() @@ -82,16 +85,16 @@ export class MdMonthView implements AfterContentInit { if (this._selectedDate == date) { return; } - this.selectedChange.emit(new SimpleDate(this.date.year, this.date.month, date)); + this.selectedChange.emit(new SimpleDate(this.activeDate.year, this.activeDate.month, date)); } /** Initializes this month view. */ private _init() { this._selectedDate = this._getDateInCurrentMonth(this.selected); this._todayDate = this._getDateInCurrentMonth(SimpleDate.today()); - this._monthLabel = this._locale.shortMonths[this.date.month].toLocaleUpperCase(); + this._monthLabel = this._locale.shortMonths[this.activeDate.month].toLocaleUpperCase(); - let firstOfMonth = new SimpleDate(this.date.year, this.date.month, 1); + let firstOfMonth = new SimpleDate(this.activeDate.year, this.activeDate.month, 1); this._firstWeekOffset = (DAYS_PER_WEEK + firstOfMonth.day - this._locale.firstDayOfWeek) % DAYS_PER_WEEK; @@ -100,7 +103,7 @@ export class MdMonthView implements AfterContentInit { /** Creates MdCalendarCells for the dates in this month. */ private _createWeekCells() { - let daysInMonth = new SimpleDate(this.date.year, this.date.month + 1, 0).date; + let daysInMonth = new SimpleDate(this.activeDate.year, this.activeDate.month + 1, 0).date; this._weeks = [[]]; for (let i = 0, cell = this._firstWeekOffset; i < daysInMonth; i++, cell++) { if (cell == DAYS_PER_WEEK) { @@ -108,7 +111,7 @@ export class MdMonthView implements AfterContentInit { cell = 0; } let enabled = !this.dateFilter || - this.dateFilter(new SimpleDate(this.date.year, this.date.month, i + 1)); + this.dateFilter(new SimpleDate(this.activeDate.year, this.activeDate.month, i + 1)); this._weeks[this._weeks.length - 1] .push(new MdCalendarCell(i + 1, this._locale.dates[i + 1], enabled)); } @@ -118,7 +121,12 @@ export class MdMonthView implements AfterContentInit { * Gets the date in this month that the given Date falls on. * Returns null if the given Date is in another month. */ - private _getDateInCurrentMonth(date: SimpleDate) { - return date && date.month == this.date.month && date.year == this.date.year ? date.date : null; + private _getDateInCurrentMonth(date: SimpleDate): number { + return this._hasSameMonthAndYear(date, this.activeDate) ? date.date : null; + } + + /** Checks whether the 2 dates are non-null and fall within the same month of the same year. */ + private _hasSameMonthAndYear(d1: SimpleDate, d2: SimpleDate): boolean { + return !!(d1 && d2 && d1.month == d2.month && d1.year == d2.year); } } diff --git a/src/lib/datepicker/year-view.html b/src/lib/datepicker/year-view.html index 2e70db4cf0a3..3ec753e219cc 100644 --- a/src/lib/datepicker/year-view.html +++ b/src/lib/datepicker/year-view.html @@ -4,5 +4,6 @@ [todayValue]="_todayMonth" [selectedValue]="_selectedMonth" [labelMinRequiredCells]="2" + [activeCell]="activeDate.month" (selectedValueChange)="_monthSelected($event)"> diff --git a/src/lib/datepicker/year-view.spec.ts b/src/lib/datepicker/year-view.spec.ts index a5cb6118df21..66674cba4c25 100644 --- a/src/lib/datepicker/year-view.spec.ts +++ b/src/lib/datepicker/year-view.spec.ts @@ -71,6 +71,12 @@ describe('MdYearView', () => { let selectedEl = yearViewNativeElement.querySelector('.mat-calendar-table-selected'); expect(selectedEl.innerHTML.trim()).toBe('DEC'); }); + + it('should mark active date', () => { + let cellEls = yearViewNativeElement.querySelectorAll('.mat-calendar-table-cell'); + expect((cellEls[0] as HTMLElement).innerText.trim()).toBe('JAN'); + expect(cellEls[0].classList).toContain('mat-calendar-table-active'); + }); }); describe('year view with date filter', () => { @@ -97,7 +103,8 @@ describe('MdYearView', () => { @Component({ - template: ``, + template: ` + `, }) class StandardYearView { date = new SimpleDate(2017, 0, 5); diff --git a/src/lib/datepicker/year-view.ts b/src/lib/datepicker/year-view.ts index 40f5d8f1bbd2..dae8219ba0d5 100644 --- a/src/lib/datepicker/year-view.ts +++ b/src/lib/datepicker/year-view.ts @@ -26,12 +26,15 @@ import {SimpleDate} from '../core/datetime/simple-date'; export class MdYearView implements AfterContentInit { /** The date to display in this year view (everything other than the year is ignored). */ @Input() - get date() { return this._date; } - set date(value) { - this._date = this._locale.parseDate(value) || SimpleDate.today(); - this._init(); + get activeDate() { return this._activeDate; } + set activeDate(value) { + let oldActiveDate = this._activeDate; + this._activeDate = this._locale.parseDate(value) || SimpleDate.today(); + if (oldActiveDate.year != this._activeDate.year) { + this._init(); + } } - private _date = SimpleDate.today(); + private _activeDate = SimpleDate.today(); /** The currently selected date. */ @Input() @@ -71,14 +74,14 @@ export class MdYearView implements AfterContentInit { /** Handles when a new month is selected. */ _monthSelected(month: number) { - this.selectedChange.emit(new SimpleDate(this.date.year, month, 1)); + this.selectedChange.emit(new SimpleDate(this.activeDate.year, month, 1)); } /** Initializes this month view. */ private _init() { this._selectedMonth = this._getMonthInCurrentYear(this.selected); this._todayMonth = this._getMonthInCurrentYear(SimpleDate.today()); - this._yearLabel = this._locale.getCalendarYearHeaderLabel(this._date); + this._yearLabel = this._locale.getCalendarYearHeaderLabel(this.activeDate); // First row of months only contains 5 elements so we can fit the year label on the same row. this._months = [[0, 1, 2, 3, 4], [5, 6, 7, 8, 9, 10, 11]].map(row => row.map( @@ -90,7 +93,7 @@ export class MdYearView implements AfterContentInit { * Returns null if the given Date is in another year. */ private _getMonthInCurrentYear(date: SimpleDate) { - return date && date.year == this.date.year ? date.month : null; + return date && date.year == this.activeDate.year ? date.month : null; } /** Creates an MdCalendarCell for the given month. */ @@ -106,7 +109,7 @@ export class MdYearView implements AfterContentInit { } // If any date in the month is enabled count the month as enabled. - for (let date = new SimpleDate(this.date.year, month, 1); date.month === month; + for (let date = new SimpleDate(this.activeDate.year, month, 1); date.month === month; date = date.add({days: 1})) { if (this.dateFilter(date)) { return true; From ffda7d35cb17535a8ce5ab16c126189e2c18887a Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Wed, 15 Mar 2017 15:58:08 -0700 Subject: [PATCH 02/11] stop normalizing the active date in calendar since we now care about the exact active date --- src/lib/datepicker/calendar-table.html | 2 +- src/lib/datepicker/calendar-table.spec.ts | 1 - src/lib/datepicker/calendar-table.ts | 15 ++++++++-- src/lib/datepicker/calendar.html | 4 +-- src/lib/datepicker/calendar.spec.ts | 36 +++++++++++------------ src/lib/datepicker/calendar.ts | 36 +++++++++++------------ 6 files changed, 52 insertions(+), 42 deletions(-) diff --git a/src/lib/datepicker/calendar-table.html b/src/lib/datepicker/calendar-table.html index 52f71aa1c811..af00052cfc34 100644 --- a/src/lib/datepicker/calendar-table.html +++ b/src/lib/datepicker/calendar-table.html @@ -14,7 +14,7 @@
{ }); it('should mark active date', () => { - let cellEls = calendarTableNativeElement.querySelectorAll('.mat-calendar-table-cell'); expect((cellEls[10] as HTMLElement).innerText.trim()).toBe('11'); expect(cellEls[10].classList).toContain('mat-calendar-table-active'); }); diff --git a/src/lib/datepicker/calendar-table.ts b/src/lib/datepicker/calendar-table.ts index e2abd9304078..a9195f71cd8d 100644 --- a/src/lib/datepicker/calendar-table.ts +++ b/src/lib/datepicker/calendar-table.ts @@ -57,7 +57,7 @@ export class MdCalendarTable { /** Emits when a new value is selected. */ @Output() selectedValueChange = new EventEmitter(); - _cellClicked(cell: MdCalendarCell) { + _cellClicked(cell: MdCalendarCell): void { if (!this.allowDisabledSelection && !cell.enabled) { return; } @@ -65,8 +65,19 @@ export class MdCalendarTable { } /** The number of blank cells to put at the beginning for the first row. */ - get _firstRowOffset() { + get _firstRowOffset(): number { return this.rows && this.rows.length && this.rows[0].length ? this.numCols - this.rows[0].length : 0; } + + _isActiveCell(rowIndex: number, colIndex: number): boolean { + let cellNumber = rowIndex * this.numCols + colIndex; + + // Account for the fact that the first row may not have as many cells. + if (rowIndex) { + cellNumber -= this._firstRowOffset; + } + + return cellNumber == this.activeCell; + } } diff --git a/src/lib/datepicker/calendar.html b/src/lib/datepicker/calendar.html index 670104d46125..17bc8589fadd 100644 --- a/src/lib/datepicker/calendar.html +++ b/src/lib/datepicker/calendar.html @@ -32,7 +32,7 @@
@@ -40,7 +40,7 @@ diff --git a/src/lib/datepicker/calendar.spec.ts b/src/lib/datepicker/calendar.spec.ts index 7c40c71ce0a7..dc0956dd36a9 100644 --- a/src/lib/datepicker/calendar.spec.ts +++ b/src/lib/datepicker/calendar.spec.ts @@ -54,9 +54,9 @@ describe('MdCalendar', () => { testComponent = fixture.componentInstance; }); - it('should be in month view with specified month visible', () => { + it('should be in month view with specified month active', () => { expect(calendarInstance._monthView).toBe(true, 'should be in month view'); - expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 0, 1)); + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 31)); }); it('should toggle view when period clicked', () => { @@ -74,17 +74,17 @@ describe('MdCalendar', () => { }); it('should go to next and previous month', () => { - expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 0, 1)); + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 31)); nextButton.click(); fixture.detectChanges(); - expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 1, 1)); + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 1)); prevButton.click(); fixture.detectChanges(); - expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 0, 1)); + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 1)); }); it('should go to previous and next year', () => { @@ -92,17 +92,17 @@ describe('MdCalendar', () => { fixture.detectChanges(); expect(calendarInstance._monthView).toBe(false, 'should be in year view'); - expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 0, 1)); + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 31)); nextButton.click(); fixture.detectChanges(); - expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2018, 0, 1)); + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2018, 0, 1)); prevButton.click(); fixture.detectChanges(); - expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 0, 1)); + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 1)); }); it('should go back to month view after selecting month in year view', () => { @@ -110,14 +110,14 @@ describe('MdCalendar', () => { fixture.detectChanges(); expect(calendarInstance._monthView).toBe(false, 'should be in year view'); - expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 0, 1)); + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 31)); let monthCells = calendarElement.querySelectorAll('.mat-calendar-table-cell'); (monthCells[monthCells.length - 1] as HTMLElement).click(); fixture.detectChanges(); expect(calendarInstance._monthView).toBe(true, 'should be in month view'); - expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 11, 1)); + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 11, 1)); expect(testComponent.selected).toBeFalsy('no date should be selected yet'); }); @@ -154,14 +154,14 @@ describe('MdCalendar', () => { testComponent.startAt = new SimpleDate(2000, 0, 1); fixture.detectChanges(); - expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2016, 0, 1)); + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 0, 1)); }); it('should clamp startAt value above max date', () => { testComponent.startAt = new SimpleDate(2020, 0, 1); fixture.detectChanges(); - expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2018, 0, 1)); + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2018, 0, 1)); }); it('should not go back past min date', () => { @@ -169,18 +169,18 @@ describe('MdCalendar', () => { fixture.detectChanges(); expect(prevButton.classList).not.toContain('mat-calendar-disabled'); - expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2016, 1, 1)); + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 1, 1)); prevButton.click(); fixture.detectChanges(); expect(prevButton.classList).toContain('mat-calendar-disabled'); - expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2016, 0, 1)); + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 0, 1)); prevButton.click(); fixture.detectChanges(); - expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2016, 0, 1)); + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 0, 1)); }); it('should not go forward past max date', () => { @@ -188,18 +188,18 @@ describe('MdCalendar', () => { fixture.detectChanges(); expect(nextButton.classList).not.toContain('mat-calendar-disabled'); - expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 11, 1)); + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 11, 1)); nextButton.click(); fixture.detectChanges(); expect(nextButton.classList).toContain('mat-calendar-disabled'); - expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2018, 0, 1)); + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2018, 0, 1)); nextButton.click(); fixture.detectChanges(); - expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2018, 0, 1)); + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2018, 0, 1)); }); }); diff --git a/src/lib/datepicker/calendar.ts b/src/lib/datepicker/calendar.ts index ed2d587a84a2..04170efbe34d 100644 --- a/src/lib/datepicker/calendar.ts +++ b/src/lib/datepicker/calendar.ts @@ -67,16 +67,14 @@ export class MdCalendar implements AfterContentInit { } /** - * A date representing the current period shown in the calendar. The current period is always - * normalized to the 1st of a month, this prevents date overflow issues (e.g. adding a month to - * January 31st and overflowing into March). + * The current active date. This determines which time period is shown and which date is + * highlighted when using keyboard navigation. */ - get _currentPeriod() { return this._normalizedCurrentPeriod; } - set _currentPeriod(value: SimpleDate) { - const clampedValue = value.clamp(this.minDate, this.maxDate); - this._normalizedCurrentPeriod = new SimpleDate(clampedValue.year, clampedValue.month, 1); + get _activeDate() { return this._clampedActiveDate; } + set _activeDate(value: SimpleDate) { + this._clampedActiveDate = value.clamp(this.minDate, this.maxDate); } - private _normalizedCurrentPeriod: SimpleDate; + private _clampedActiveDate: SimpleDate; /** Whether the calendar is in month view. */ _monthView: boolean; @@ -87,8 +85,8 @@ export class MdCalendar implements AfterContentInit { /** The label for the current calendar view. */ get _label(): string { return this._monthView ? - this._locale.getCalendarMonthHeaderLabel(this._currentPeriod).toLocaleUpperCase() : - this._locale.getCalendarYearHeaderLabel(this._currentPeriod); + this._locale.getCalendarMonthHeaderLabel(this._activeDate).toLocaleUpperCase() : + this._locale.getCalendarYearHeaderLabel(this._activeDate); } constructor(private _locale: CalendarLocale) { @@ -97,7 +95,7 @@ export class MdCalendar implements AfterContentInit { } ngAfterContentInit() { - this._currentPeriod = this.startAt || SimpleDate.today(); + this._activeDate = this.startAt || SimpleDate.today(); this._monthView = this.startView != 'year'; } @@ -110,7 +108,7 @@ export class MdCalendar implements AfterContentInit { /** Handles month selection in the year view. */ _monthSelected(month: SimpleDate) { - this._currentPeriod = month; + this._activeDate = month; this._monthView = true; } @@ -121,14 +119,16 @@ export class MdCalendar implements AfterContentInit { /** Handles user clicks on the previous button. */ _previousClicked() { - let amount = this._monthView ? {months: -1} : {years: -1}; - this._currentPeriod = this._currentPeriod.add(amount); + return this._activeDate = this._monthView ? + new SimpleDate(this._activeDate.year, this._activeDate.month - 1, 1) : + new SimpleDate(this._activeDate.year - 1, 0, 1); } /** Handles user clicks on the next button. */ _nextClicked() { - let amount = this._monthView ? {months: 1} : {years: 1}; - this._currentPeriod = this._currentPeriod.add(amount); + return this._activeDate = this._monthView ? + new SimpleDate(this._activeDate.year, this._activeDate.month + 1, 1) : + new SimpleDate(this._activeDate.year + 1, 0, 1); } /** Whether the previous period button is enabled. */ @@ -136,12 +136,12 @@ export class MdCalendar implements AfterContentInit { if (!this.minDate) { return true; } - return !this.minDate || !this._isSameView(this._currentPeriod, this.minDate); + return !this.minDate || !this._isSameView(this._activeDate, this.minDate); } /** Whether the next period button is enabled. */ _nextEnabled() { - return !this.maxDate || !this._isSameView(this._currentPeriod, this.maxDate); + return !this.maxDate || !this._isSameView(this._activeDate, this.maxDate); } /** Whether the two dates represent the same view in the current view mode (month or year). */ From 2b93c674d5b0173d1386a889c905afae23b41588 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Thu, 16 Mar 2017 08:56:09 -0700 Subject: [PATCH 03/11] add some key handlers --- src/lib/datepicker/calendar.html | 2 +- src/lib/datepicker/calendar.ts | 53 ++++++++++++++++++++++++-------- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/src/lib/datepicker/calendar.html b/src/lib/datepicker/calendar.html index 17bc8589fadd..016ab4deff41 100644 --- a/src/lib/datepicker/calendar.html +++ b/src/lib/datepicker/calendar.html @@ -29,7 +29,7 @@
-
+
Date: Thu, 16 Mar 2017 15:09:16 -0700 Subject: [PATCH 04/11] add keyboard support, break some tests --- src/lib/datepicker/calendar-table.scss | 4 + src/lib/datepicker/calendar.ts | 128 ++++++++++++++++++++++--- src/lib/datepicker/year-view.ts | 2 +- 3 files changed, 120 insertions(+), 14 deletions(-) diff --git a/src/lib/datepicker/calendar-table.scss b/src/lib/datepicker/calendar-table.scss index c96189cd84ac..ceeb77366d6a 100644 --- a/src/lib/datepicker/calendar-table.scss +++ b/src/lib/datepicker/calendar-table.scss @@ -40,6 +40,10 @@ $mat-calendar-table-cell-content-size: 100% - $mat-calendar-table-cell-content-m text-align: center; } +.mat-calendar-table-active { + box-shadow: 0 0 0 1px red; // for testing only +} + .mat-calendar-table-cell-content { position: absolute; top: $mat-calendar-table-cell-content-margin; diff --git a/src/lib/datepicker/calendar.ts b/src/lib/datepicker/calendar.ts index 1c6548cadaa1..cfc73c7b6a8f 100644 --- a/src/lib/datepicker/calendar.ts +++ b/src/lib/datepicker/calendar.ts @@ -9,7 +9,16 @@ import { } from '@angular/core'; import {SimpleDate} from '../core/datetime/simple-date'; import {CalendarLocale} from '../core/datetime/calendar-locale'; -import {DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW, UP_ARROW} from '../core/keyboard/keycodes'; +import { + DOWN_ARROW, + END, ENTER, + HOME, + LEFT_ARROW, + PAGE_DOWN, + PAGE_UP, + RIGHT_ARROW, + UP_ARROW +} from '../core/keyboard/keycodes'; /** @@ -123,15 +132,14 @@ export class MdCalendar implements AfterContentInit { /** Handles user clicks on the previous button. */ _previousClicked(): void { this._activeDate = this._monthView ? - new SimpleDate(this._activeDate.year, this._activeDate.month - 1, 1) : - new SimpleDate(this._activeDate.year - 1, 0, 1); + this._addCalendarMonths(this._activeDate, -1) : + this._addCalendarYears(this._activeDate, -1); } /** Handles user clicks on the next button. */ _nextClicked(): void { this._activeDate = this._monthView ? - new SimpleDate(this._activeDate.year, this._activeDate.month + 1, 1) : - new SimpleDate(this._activeDate.year + 1, 0, 1); + this._addCalendarMonths(this._activeDate, 1) : this._addCalendarYears(this._activeDate, 1); } /** Whether the previous period button is enabled. */ @@ -154,24 +162,63 @@ export class MdCalendar implements AfterContentInit { date1.year == date2.year; } + /** Handles keydown events on the calendar body. */ _handleCalendarBodyKeydown(event: KeyboardEvent): void { - let normalizedActiveDate = this._monthView ? this._activeDate : - new SimpleDate(this._activeDate.year, this._activeDate.month, 1); - let unit = this._monthView ? 'days' : 'months'; - switch (event.keyCode) { case LEFT_ARROW: - this._activeDate = normalizedActiveDate.add({[unit]: -1}); + this._activeDate = this._monthView ? + this._addCalendarDays(this._activeDate, -1) : + this._addCalendarMonths(this._activeDate, -1); break; case RIGHT_ARROW: - this._activeDate = normalizedActiveDate.add({[unit]: 1}); + this._activeDate = this._monthView ? + this._addCalendarDays(this._activeDate, 1) : + this._addCalendarMonths(this._activeDate, 1); break; case UP_ARROW: - this._activeDate = normalizedActiveDate.add({[unit]: -7}); + this._activeDate = this._monthView ? + this._addCalendarDays(this._activeDate, -7) : + this._prevMonthInSameCol(this._activeDate); break; case DOWN_ARROW: - this._activeDate = normalizedActiveDate.add({[unit]: 7}); + this._activeDate = this._monthView ? + this._addCalendarDays(this._activeDate, 7) : + this._nextMonthInSameCol(this._activeDate); + break; + case HOME: + this._activeDate = this._monthView ? + new SimpleDate(this._activeDate.year, this._activeDate.month, 1) : + this._addCalendarMonths(this._activeDate, -this._activeDate.month); + break; + case END: + this._activeDate = this._monthView ? + new SimpleDate(this._activeDate.year, this._activeDate.month + 1, 0) : + this._addCalendarMonths(this._activeDate, 11 - this._activeDate.month); break; + case PAGE_UP: + if (event.altKey) { + this._activeDate = this._addCalendarYears(this._activeDate, this._monthView ? -1 : -10); + } else { + this._activeDate = this._monthView ? + this._addCalendarMonths(this._activeDate, -1) : + this._addCalendarYears(this._activeDate, -1); + } + break; + case PAGE_DOWN: + if (event.altKey) { + this._activeDate = this._addCalendarYears(this._activeDate, this._monthView ? 1 : 10); + } else { + this._activeDate = this._monthView ? + this._addCalendarMonths(this._activeDate, 1) : + this._addCalendarYears(this._activeDate, 1); + } + break; + case ENTER: + if (this._monthView) { + this._dateSelected(this._activeDate); + } else { + this._monthSelected(this._activeDate); + } default: // Don't prevent default on keys that we don't explicitly handle. return; @@ -179,4 +226,59 @@ export class MdCalendar implements AfterContentInit { event.preventDefault(); } + + /** Adds the given number of days to the date. */ + private _addCalendarDays(date: SimpleDate, days: number): SimpleDate { + return date.add({'days': days}); + } + + /** + * Adds the given number of months to the date. Months are counted as if flipping pages on a + * calendar and then finding the closest date in the new month. For example when adding 1 month to + * Jan 31 2017 the resulting date will be Feb 28 2017. + */ + private _addCalendarMonths(date: SimpleDate, months: number): SimpleDate { + let newDate = date.add({'months': months}); + + // It's possible to wind up in the wrong month if the original month has more days than the new + // month. In this case we want to go to the last day of the desired month. + // Note: the additional + 12 % 12 ensures we end up with a positive number, since JS % doesn't + // guarantee this. + if (newDate.month != ((date.month + months) % 12 + 12) % 12) { + newDate = new SimpleDate(newDate.year, newDate.month, 0); + } + + return newDate; + } + + /** + * Adds the given number of months to the date. Months are counted as if flipping 12 pages for + * each year on a calendar and then finding the closest date in the new month. For example when + * adding 1 year to Feb 29 2016 the resulting date will be Feb 28 2017. + */ + private _addCalendarYears(date: SimpleDate, years: number): SimpleDate { + return this._addCalendarMonths(date, years * 12); + } + + /** + * Determine the date for the month that comes before the given month in the same column in the + * calendar table. + */ + private _prevMonthInSameCol(date: SimpleDate) { + // Determine how many months to jump forward given that there are 2 empty slots at the beginning + // of each year. + let increment = date.month <= 4 ? -5 : (date.month >= 7 ? -7 : -12); + return this._addCalendarMonths(date, increment); + } + + /** + * Determine the date for the month that comes after the given month in the same column in the + * calendar table. + */ + private _nextMonthInSameCol(date: SimpleDate): SimpleDate { + // Determine how many months to jump forward given that there are 2 empty slots at the beginning + // of each year. + let increment = date.month <= 4 ? 7 : (date.month >= 7 ? 5 : 12); + return this._addCalendarMonths(date, increment); + } } diff --git a/src/lib/datepicker/year-view.ts b/src/lib/datepicker/year-view.ts index dae8219ba0d5..39ce2a1600ec 100644 --- a/src/lib/datepicker/year-view.ts +++ b/src/lib/datepicker/year-view.ts @@ -74,7 +74,7 @@ export class MdYearView implements AfterContentInit { /** Handles when a new month is selected. */ _monthSelected(month: number) { - this.selectedChange.emit(new SimpleDate(this.activeDate.year, month, 1)); + this.selectedChange.emit(new SimpleDate(this.activeDate.year, month, this._activeDate.date)); } /** Initializes this month view. */ From 6a59fc02f43d383eb9a5a0c1cee02720ade4b455 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Thu, 16 Mar 2017 17:28:53 -0700 Subject: [PATCH 05/11] add tests --- src/lib/datepicker/calendar.spec.ts | 328 +++++++++++++++++++++++++++- 1 file changed, 322 insertions(+), 6 deletions(-) diff --git a/src/lib/datepicker/calendar.spec.ts b/src/lib/datepicker/calendar.spec.ts index dc0956dd36a9..bbc4bd8caf67 100644 --- a/src/lib/datepicker/calendar.spec.ts +++ b/src/lib/datepicker/calendar.spec.ts @@ -7,6 +7,21 @@ import {MdMonthView} from './month-view'; import {MdYearView} from './year-view'; import {MdCalendarTable} from './calendar-table'; import {DatetimeModule} from '../core/datetime/index'; +import { + dispatchFakeEvent, dispatchKeyboardEvent, + dispatchMouseEvent +} from '../core/testing/dispatch-events'; +import { + DOWN_ARROW, + END, + ENTER, + HOME, + LEFT_ARROW, + PAGE_DOWN, + PAGE_UP, + RIGHT_ARROW, + UP_ARROW +} from '../core/keyboard/keycodes'; describe('MdCalendar', () => { @@ -79,12 +94,12 @@ describe('MdCalendar', () => { nextButton.click(); fixture.detectChanges(); - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 1)); + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 28)); prevButton.click(); fixture.detectChanges(); - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 1)); + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 28)); }); it('should go to previous and next year', () => { @@ -97,12 +112,12 @@ describe('MdCalendar', () => { nextButton.click(); fixture.detectChanges(); - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2018, 0, 1)); + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2018, 0, 31)); prevButton.click(); fixture.detectChanges(); - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 1)); + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 31)); }); it('should go back to month view after selecting month in year view', () => { @@ -117,7 +132,7 @@ describe('MdCalendar', () => { fixture.detectChanges(); expect(calendarInstance._monthView).toBe(true, 'should be in month view'); - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 11, 1)); + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 11, 31)); expect(testComponent.selected).toBeFalsy('no date should be selected yet'); }); @@ -232,6 +247,307 @@ describe('MdCalendar', () => { expect(testComponent.selected).toEqual(new SimpleDate(2017, 0, 2)); }); }); + + describe('a11y', () => { + let fixture: ComponentFixture; + let testComponent: StandardCalendar; + let calendarElement: HTMLElement; + let calendarInstance: MdCalendar; + + beforeEach(() => { + fixture = TestBed.createComponent(StandardCalendar); + fixture.detectChanges(); + + let calendarDebugElement = fixture.debugElement.query(By.directive(MdCalendar)); + calendarElement = calendarDebugElement.nativeElement; + calendarInstance = calendarDebugElement.componentInstance; + testComponent = fixture.componentInstance; + }); + + describe('calendar body', () => { + let calendarBodyEl: HTMLElement; + + beforeEach(() => { + calendarBodyEl = calendarElement.querySelector('.mat-calendar-body') as HTMLElement; + expect(calendarBodyEl).not.toBeNull(); + + dispatchFakeEvent(calendarBodyEl, 'focus'); + fixture.detectChanges(); + }); + + it('should initially set start date active', () => { + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 31)); + }); + + describe('month view', () => { + it('should decrement date on left arrow press', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 30)); + + calendarInstance._activeDate = new SimpleDate(2017, 0, 1); + fixture.detectChanges(); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 11, 31)); + }); + + it('should increment date on right arrow press', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 1)); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 2)); + }); + + it('should go up a row on up arrow press', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 24)); + + calendarInstance._activeDate = new SimpleDate(2017, 0, 7); + fixture.detectChanges(); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 11, 31)); + }); + + it('should go down a row on down arrow press', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 7)); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 14)); + }); + + it('should go to beginning of the month on home press', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 1)); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 1)); + }); + + it('should go to end of the month on end press', () => { + calendarInstance._activeDate = new SimpleDate(2017, 0, 10); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', END); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 31)); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', END); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 31)); + }); + + it('should go back one month on page up press', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 11, 31)); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 10, 30)); + }); + + it('should go forward one month on page down press', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 28)); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 2, 28)); + }); + + it('should select active date on enter', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW); + fixture.detectChanges(); + + expect(testComponent.selected).toBeNull(); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER); + fixture.detectChanges(); + + expect(testComponent.selected).toEqual(new SimpleDate(2017, 0, 30)); + }); + }); + + describe('year view', () => { + beforeEach(() => { + let periodButton = + calendarElement.querySelector('.mat-calendar-period-button') as HTMLElement; + dispatchMouseEvent(periodButton, 'click'); + fixture.detectChanges(); + + expect(calendarInstance._monthView).toBe(false); + }); + + it('should decrement month on left arrow press', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 11, 31)); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 10, 30)); + }); + + it('should increment month on right arrow press', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 28)); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 2, 28)); + }); + + it('should go up a row on up arrow press', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 7, 31)); + + calendarInstance._activeDate = new SimpleDate(2017, 6, 1); + fixture.detectChanges(); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 6, 1)); + + calendarInstance._activeDate = new SimpleDate(2017, 11, 10); + fixture.detectChanges(); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 4, 10)); + }); + + it('should go down a row on down arrow press', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 7, 31)); + + calendarInstance._activeDate = new SimpleDate(2017, 5, 1); + fixture.detectChanges(); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2018, 5, 1)); + + calendarInstance._activeDate = new SimpleDate(2017, 8, 30); + fixture.detectChanges(); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2018, 1, 28)); + }); + + it('should go to first month of the year on home press', () => { + calendarInstance._activeDate = new SimpleDate(2017, 8, 30); + fixture.detectChanges(); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 30)); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 30)); + }); + + it('should go to last month of the year on end press', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', END); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 11, 31)); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', END); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 11, 31)); + }); + + it('should go back one year on page up press', () => { + calendarInstance._activeDate = new SimpleDate(2016, 1, 29); + fixture.detectChanges(); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2015, 1, 28)); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2014, 1, 28)); + }); + + it('should go forward one year on page down press', () => { + calendarInstance._activeDate = new SimpleDate(2016, 1, 29); + fixture.detectChanges(); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 28)); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2018, 1, 28)); + }); + + it('should return to month view on enter', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW); + fixture.detectChanges(); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER); + fixture.detectChanges(); + + expect(calendarInstance._monthView).toBe(true); + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 28)); + expect(testComponent.selected).toBeNull(); + }); + }); + }); + }); }); @@ -239,7 +555,7 @@ describe('MdCalendar', () => { template: `` }) class StandardCalendar { - selected: SimpleDate; + selected: SimpleDate = null; } From eaa34f6311568bd03fbb66bc0a382f9416bbdf4f Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Thu, 16 Mar 2017 17:58:46 -0700 Subject: [PATCH 06/11] some finishing touches --- src/lib/datepicker/_datepicker-theme.scss | 3 +- src/lib/datepicker/calendar-table.scss | 4 --- src/lib/datepicker/calendar.html | 3 +- src/lib/datepicker/calendar.spec.ts | 44 +++++++++++++++++++++-- src/lib/datepicker/calendar.ts | 5 ++- src/lib/datepicker/index.ts | 2 ++ 6 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/lib/datepicker/_datepicker-theme.scss b/src/lib/datepicker/_datepicker-theme.scss index d3904a327186..bd3d1a56726e 100644 --- a/src/lib/datepicker/_datepicker-theme.scss +++ b/src/lib/datepicker/_datepicker-theme.scss @@ -45,7 +45,8 @@ } } - :not(.mat-calendar-table-disabled):hover { + :not(.mat-calendar-table-disabled):hover, + .cdk-keyboard-focused .mat-calendar-table-active { & > .mat-calendar-table-cell-content:not(.mat-calendar-table-selected) { background-color: mat-color($background, hover); } diff --git a/src/lib/datepicker/calendar-table.scss b/src/lib/datepicker/calendar-table.scss index ceeb77366d6a..c96189cd84ac 100644 --- a/src/lib/datepicker/calendar-table.scss +++ b/src/lib/datepicker/calendar-table.scss @@ -40,10 +40,6 @@ $mat-calendar-table-cell-content-size: 100% - $mat-calendar-table-cell-content-m text-align: center; } -.mat-calendar-table-active { - box-shadow: 0 0 0 1px red; // for testing only -} - .mat-calendar-table-cell-content { position: absolute; top: $mat-calendar-table-cell-content-margin; diff --git a/src/lib/datepicker/calendar.html b/src/lib/datepicker/calendar.html index 016ab4deff41..681132d1ea32 100644 --- a/src/lib/datepicker/calendar.html +++ b/src/lib/datepicker/calendar.html @@ -29,7 +29,8 @@
-
+
{ expect(testComponent.selected).toEqual(new SimpleDate(2017, 0, 2)); }); + + describe('a11y', () => { + let calendarBodyEl: HTMLElement; + + beforeEach(() => { + calendarBodyEl = calendarElement.querySelector('.mat-calendar-body') as HTMLElement; + expect(calendarBodyEl).not.toBeNull(); + + dispatchFakeEvent(calendarBodyEl, 'focus'); + fixture.detectChanges(); + }); + + it('should not allow selection of disabled date in month view', () => { + expect(calendarInstance._monthView).toBe(true); + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 1)); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER); + fixture.detectChanges(); + + expect(testComponent.selected).toBeNull(); + }); + + it('should allow entering month view at disabled month', () => { + let periodButton = + calendarElement.querySelector('.mat-calendar-period-button') as HTMLElement; + dispatchMouseEvent(periodButton, 'click'); + fixture.detectChanges(); + + calendarInstance._activeDate = new SimpleDate(2017, 10, 1); + fixture.detectChanges(); + + expect(calendarInstance._monthView).toBe(false); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER); + fixture.detectChanges(); + + expect(calendarInstance._monthView).toBe(true); + expect(testComponent.selected).toBeNull(); + }); + }); }); describe('a11y', () => { @@ -573,9 +613,9 @@ class CalendarWithMinMax { ` }) class CalendarWithDateFilter { - selected: SimpleDate; + selected: SimpleDate = null; dateFilter (date: SimpleDate) { - return date.date % 2 == 0; + return date.date % 2 == 0 && date.month != 10; } } diff --git a/src/lib/datepicker/calendar.ts b/src/lib/datepicker/calendar.ts index cfc73c7b6a8f..d2bac91d92a5 100644 --- a/src/lib/datepicker/calendar.ts +++ b/src/lib/datepicker/calendar.ts @@ -215,10 +215,13 @@ export class MdCalendar implements AfterContentInit { break; case ENTER: if (this._monthView) { - this._dateSelected(this._activeDate); + if (this._dateFilterForViews(this._activeDate)) { + this._dateSelected(this._activeDate); + } } else { this._monthSelected(this._activeDate); } + break; default: // Don't prevent default on keys that we don't explicitly handle. return; diff --git a/src/lib/datepicker/index.ts b/src/lib/datepicker/index.ts index f6afa6f473f2..7b924a7b5bdc 100644 --- a/src/lib/datepicker/index.ts +++ b/src/lib/datepicker/index.ts @@ -10,6 +10,7 @@ import {MdDatepickerInput} from './datepicker-input'; import {MdDialogModule} from '../dialog/index'; import {MdCalendar} from './calendar'; import {MdDatepickerToggle} from './datepicker-toggle'; +import {StyleModule} from '../core/style/index'; export * from './calendar'; @@ -26,6 +27,7 @@ export * from './year-view'; DatetimeModule, MdDialogModule, OverlayModule, + StyleModule, ], exports: [ MdDatepicker, From d684d6fb33c0fe454832fd7f0b51a91c070501d7 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Fri, 17 Mar 2017 13:40:20 -0700 Subject: [PATCH 07/11] fix tabindex --- src/lib/datepicker/calendar.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/datepicker/calendar.html b/src/lib/datepicker/calendar.html index 681132d1ea32..1c84c68480c7 100644 --- a/src/lib/datepicker/calendar.html +++ b/src/lib/datepicker/calendar.html @@ -29,7 +29,7 @@
-
Date: Fri, 17 Mar 2017 15:42:35 -0700 Subject: [PATCH 08/11] fix rxjs import --- src/lib/datepicker/datepicker-input.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/datepicker/datepicker-input.ts b/src/lib/datepicker/datepicker-input.ts index f24dddf04c3d..e4426b0567fd 100644 --- a/src/lib/datepicker/datepicker-input.ts +++ b/src/lib/datepicker/datepicker-input.ts @@ -12,7 +12,7 @@ import {MdDatepicker} from './datepicker'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; import {SimpleDate} from '../core/datetime/simple-date'; import {CalendarLocale} from '../core/datetime/calendar-locale'; -import {Subscription} from 'rxjs'; +import {Subscription} from 'rxjs/Subscription'; import {MdInputContainer} from '../input/input-container'; import {DOWN_ARROW} from '../core/keyboard/keycodes'; From 948335e945c4759f4fcdab2897bf714c83650e5a Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Thu, 23 Mar 2017 10:35:10 -0700 Subject: [PATCH 09/11] addressed some comments --- src/lib/datepicker/calendar.spec.ts | 585 ++++++++++++++-------------- src/lib/datepicker/calendar.ts | 11 +- 2 files changed, 291 insertions(+), 305 deletions(-) diff --git a/src/lib/datepicker/calendar.spec.ts b/src/lib/datepicker/calendar.spec.ts index 33145c159407..29f484bbdb9a 100644 --- a/src/lib/datepicker/calendar.spec.ts +++ b/src/lib/datepicker/calendar.spec.ts @@ -144,6 +144,290 @@ describe('MdCalendar', () => { expect(calendarInstance._monthView).toBe(true, 'should be in month view'); expect(testComponent.selected).toEqual(new SimpleDate(2017, 0, 31)); }); + + describe('a11y', () => { + describe('calendar body', () => { + let calendarBodyEl: HTMLElement; + + beforeEach(() => { + calendarBodyEl = calendarElement.querySelector('.mat-calendar-body') as HTMLElement; + expect(calendarBodyEl).not.toBeNull(); + + dispatchFakeEvent(calendarBodyEl, 'focus'); + fixture.detectChanges(); + }); + + it('should initially set start date active', () => { + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 31)); + }); + + describe('month view', () => { + it('should decrement date on left arrow press', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 30)); + + calendarInstance._activeDate = new SimpleDate(2017, 0, 1); + fixture.detectChanges(); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 11, 31)); + }); + + it('should increment date on right arrow press', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 1)); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 2)); + }); + + it('should go up a row on up arrow press', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 24)); + + calendarInstance._activeDate = new SimpleDate(2017, 0, 7); + fixture.detectChanges(); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 11, 31)); + }); + + it('should go down a row on down arrow press', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 7)); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 14)); + }); + + it('should go to beginning of the month on home press', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 1)); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 1)); + }); + + it('should go to end of the month on end press', () => { + calendarInstance._activeDate = new SimpleDate(2017, 0, 10); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', END); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 31)); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', END); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 31)); + }); + + it('should go back one month on page up press', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 11, 31)); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 10, 30)); + }); + + it('should go forward one month on page down press', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 28)); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 2, 28)); + }); + + it('should select active date on enter', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW); + fixture.detectChanges(); + + expect(testComponent.selected).toBeNull(); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER); + fixture.detectChanges(); + + expect(testComponent.selected).toEqual(new SimpleDate(2017, 0, 30)); + }); + }); + + describe('year view', () => { + beforeEach(() => { + dispatchMouseEvent(periodButton, 'click'); + fixture.detectChanges(); + + expect(calendarInstance._monthView).toBe(false); + }); + + it('should decrement month on left arrow press', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 11, 31)); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 10, 30)); + }); + + it('should increment month on right arrow press', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 28)); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 2, 28)); + }); + + it('should go up a row on up arrow press', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 7, 31)); + + calendarInstance._activeDate = new SimpleDate(2017, 6, 1); + fixture.detectChanges(); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 6, 1)); + + calendarInstance._activeDate = new SimpleDate(2017, 11, 10); + fixture.detectChanges(); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 4, 10)); + }); + + it('should go down a row on down arrow press', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 7, 31)); + + calendarInstance._activeDate = new SimpleDate(2017, 5, 1); + fixture.detectChanges(); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2018, 5, 1)); + + calendarInstance._activeDate = new SimpleDate(2017, 8, 30); + fixture.detectChanges(); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2018, 1, 28)); + }); + + it('should go to first month of the year on home press', () => { + calendarInstance._activeDate = new SimpleDate(2017, 8, 30); + fixture.detectChanges(); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 30)); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 30)); + }); + + it('should go to last month of the year on end press', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', END); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 11, 31)); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', END); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 11, 31)); + }); + + it('should go back one year on page up press', () => { + calendarInstance._activeDate = new SimpleDate(2016, 1, 29); + fixture.detectChanges(); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2015, 1, 28)); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2014, 1, 28)); + }); + + it('should go forward one year on page down press', () => { + calendarInstance._activeDate = new SimpleDate(2016, 1, 29); + fixture.detectChanges(); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 28)); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN); + fixture.detectChanges(); + + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2018, 1, 28)); + }); + + it('should return to month view on enter', () => { + dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW); + fixture.detectChanges(); + + dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER); + fixture.detectChanges(); + + expect(calendarInstance._monthView).toBe(true); + expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 28)); + expect(testComponent.selected).toBeNull(); + }); + }); + }); + }); }); describe('calendar with min and max date', () => { @@ -287,307 +571,6 @@ describe('MdCalendar', () => { }); }); }); - - describe('a11y', () => { - let fixture: ComponentFixture; - let testComponent: StandardCalendar; - let calendarElement: HTMLElement; - let calendarInstance: MdCalendar; - - beforeEach(() => { - fixture = TestBed.createComponent(StandardCalendar); - fixture.detectChanges(); - - let calendarDebugElement = fixture.debugElement.query(By.directive(MdCalendar)); - calendarElement = calendarDebugElement.nativeElement; - calendarInstance = calendarDebugElement.componentInstance; - testComponent = fixture.componentInstance; - }); - - describe('calendar body', () => { - let calendarBodyEl: HTMLElement; - - beforeEach(() => { - calendarBodyEl = calendarElement.querySelector('.mat-calendar-body') as HTMLElement; - expect(calendarBodyEl).not.toBeNull(); - - dispatchFakeEvent(calendarBodyEl, 'focus'); - fixture.detectChanges(); - }); - - it('should initially set start date active', () => { - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 31)); - }); - - describe('month view', () => { - it('should decrement date on left arrow press', () => { - dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 30)); - - calendarInstance._activeDate = new SimpleDate(2017, 0, 1); - fixture.detectChanges(); - - dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 11, 31)); - }); - - it('should increment date on right arrow press', () => { - dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 1)); - - dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 2)); - }); - - it('should go up a row on up arrow press', () => { - dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 24)); - - calendarInstance._activeDate = new SimpleDate(2017, 0, 7); - fixture.detectChanges(); - - dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 11, 31)); - }); - - it('should go down a row on down arrow press', () => { - dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 7)); - - dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 14)); - }); - - it('should go to beginning of the month on home press', () => { - dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 1)); - - dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 1)); - }); - - it('should go to end of the month on end press', () => { - calendarInstance._activeDate = new SimpleDate(2017, 0, 10); - - dispatchKeyboardEvent(calendarBodyEl, 'keydown', END); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 31)); - - dispatchKeyboardEvent(calendarBodyEl, 'keydown', END); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 31)); - }); - - it('should go back one month on page up press', () => { - dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 11, 31)); - - dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 10, 30)); - }); - - it('should go forward one month on page down press', () => { - dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 28)); - - dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 2, 28)); - }); - - it('should select active date on enter', () => { - dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW); - fixture.detectChanges(); - - expect(testComponent.selected).toBeNull(); - - dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER); - fixture.detectChanges(); - - expect(testComponent.selected).toEqual(new SimpleDate(2017, 0, 30)); - }); - }); - - describe('year view', () => { - beforeEach(() => { - let periodButton = - calendarElement.querySelector('.mat-calendar-period-button') as HTMLElement; - dispatchMouseEvent(periodButton, 'click'); - fixture.detectChanges(); - - expect(calendarInstance._monthView).toBe(false); - }); - - it('should decrement month on left arrow press', () => { - dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 11, 31)); - - dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 10, 30)); - }); - - it('should increment month on right arrow press', () => { - dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 28)); - - dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 2, 28)); - }); - - it('should go up a row on up arrow press', () => { - dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 7, 31)); - - calendarInstance._activeDate = new SimpleDate(2017, 6, 1); - fixture.detectChanges(); - - dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2016, 6, 1)); - - calendarInstance._activeDate = new SimpleDate(2017, 11, 10); - fixture.detectChanges(); - - dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 4, 10)); - }); - - it('should go down a row on down arrow press', () => { - dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 7, 31)); - - calendarInstance._activeDate = new SimpleDate(2017, 5, 1); - fixture.detectChanges(); - - dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2018, 5, 1)); - - calendarInstance._activeDate = new SimpleDate(2017, 8, 30); - fixture.detectChanges(); - - dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2018, 1, 28)); - }); - - it('should go to first month of the year on home press', () => { - calendarInstance._activeDate = new SimpleDate(2017, 8, 30); - fixture.detectChanges(); - - dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 30)); - - dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 0, 30)); - }); - - it('should go to last month of the year on end press', () => { - dispatchKeyboardEvent(calendarBodyEl, 'keydown', END); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 11, 31)); - - dispatchKeyboardEvent(calendarBodyEl, 'keydown', END); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 11, 31)); - }); - - it('should go back one year on page up press', () => { - calendarInstance._activeDate = new SimpleDate(2016, 1, 29); - fixture.detectChanges(); - - dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2015, 1, 28)); - - dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2014, 1, 28)); - }); - - it('should go forward one year on page down press', () => { - calendarInstance._activeDate = new SimpleDate(2016, 1, 29); - fixture.detectChanges(); - - dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 28)); - - dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN); - fixture.detectChanges(); - - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2018, 1, 28)); - }); - - it('should return to month view on enter', () => { - dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW); - fixture.detectChanges(); - - dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER); - fixture.detectChanges(); - - expect(calendarInstance._monthView).toBe(true); - expect(calendarInstance._activeDate).toEqual(new SimpleDate(2017, 1, 28)); - expect(testComponent.selected).toBeNull(); - }); - }); - }); - }); }); diff --git a/src/lib/datepicker/calendar.ts b/src/lib/datepicker/calendar.ts index d2bac91d92a5..723e89baa4ee 100644 --- a/src/lib/datepicker/calendar.ts +++ b/src/lib/datepicker/calendar.ts @@ -164,6 +164,9 @@ export class MdCalendar implements AfterContentInit { /** 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. switch (event.keyCode) { case LEFT_ARROW: this._activeDate = this._monthView ? @@ -232,16 +235,16 @@ export class MdCalendar implements AfterContentInit { /** Adds the given number of days to the date. */ private _addCalendarDays(date: SimpleDate, days: number): SimpleDate { - return date.add({'days': days}); + return date.add({days}); } /** * Adds the given number of months to the date. Months are counted as if flipping pages on a * calendar and then finding the closest date in the new month. For example when adding 1 month to - * Jan 31 2017 the resulting date will be Feb 28 2017. + * Jan 31, 2017 the resulting date will be Feb 28, 2017. */ private _addCalendarMonths(date: SimpleDate, months: number): SimpleDate { - let newDate = date.add({'months': months}); + let newDate = date.add({months}); // It's possible to wind up in the wrong month if the original month has more days than the new // month. In this case we want to go to the last day of the desired month. @@ -257,7 +260,7 @@ export class MdCalendar implements AfterContentInit { /** * Adds the given number of months to the date. Months are counted as if flipping 12 pages for * each year on a calendar and then finding the closest date in the new month. For example when - * adding 1 year to Feb 29 2016 the resulting date will be Feb 28 2017. + * adding 1 year to Feb 29, 2016 the resulting date will be Feb 28, 2017. */ private _addCalendarYears(date: SimpleDate, years: number): SimpleDate { return this._addCalendarMonths(date, years * 12); From c7f3cfcd860311d485335ec5ddf4acd450056486 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Thu, 23 Mar 2017 11:59:30 -0700 Subject: [PATCH 10/11] refactor handleKeydown --- src/lib/datepicker/calendar.ts | 100 ++++++++++++++++++++------------- 1 file changed, 62 insertions(+), 38 deletions(-) diff --git a/src/lib/datepicker/calendar.ts b/src/lib/datepicker/calendar.ts index 723e89baa4ee..ae3b22ba0c06 100644 --- a/src/lib/datepicker/calendar.ts +++ b/src/lib/datepicker/calendar.ts @@ -167,63 +167,87 @@ export class MdCalendar implements AfterContentInit { // 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._monthView) { + this._handleCalendarBodyKeydownInMonthView(event); + } else { + this._handleCalendarBodyKeydownInYearView(event); + } + } + + /** Handles keydown events on the calendar body when calendar is in month view. */ + private _handleCalendarBodyKeydownInMonthView(event: KeyboardEvent): void { switch (event.keyCode) { case LEFT_ARROW: - this._activeDate = this._monthView ? - this._addCalendarDays(this._activeDate, -1) : - this._addCalendarMonths(this._activeDate, -1); + this._activeDate = this._addCalendarDays(this._activeDate, -1); break; case RIGHT_ARROW: - this._activeDate = this._monthView ? - this._addCalendarDays(this._activeDate, 1) : - this._addCalendarMonths(this._activeDate, 1); + this._activeDate = this._addCalendarDays(this._activeDate, 1); break; case UP_ARROW: - this._activeDate = this._monthView ? - this._addCalendarDays(this._activeDate, -7) : - this._prevMonthInSameCol(this._activeDate); + this._activeDate = this._addCalendarDays(this._activeDate, -7); break; case DOWN_ARROW: - this._activeDate = this._monthView ? - this._addCalendarDays(this._activeDate, 7) : - this._nextMonthInSameCol(this._activeDate); + this._activeDate = this._addCalendarDays(this._activeDate, 7); break; case HOME: - this._activeDate = this._monthView ? - new SimpleDate(this._activeDate.year, this._activeDate.month, 1) : - this._addCalendarMonths(this._activeDate, -this._activeDate.month); + this._activeDate = new SimpleDate(this._activeDate.year, this._activeDate.month, 1); break; case END: - this._activeDate = this._monthView ? - new SimpleDate(this._activeDate.year, this._activeDate.month + 1, 0) : - this._addCalendarMonths(this._activeDate, 11 - this._activeDate.month); + this._activeDate = new SimpleDate(this._activeDate.year, this._activeDate.month + 1, 0); break; case PAGE_UP: - if (event.altKey) { - this._activeDate = this._addCalendarYears(this._activeDate, this._monthView ? -1 : -10); - } else { - this._activeDate = this._monthView ? - this._addCalendarMonths(this._activeDate, -1) : - this._addCalendarYears(this._activeDate, -1); - } + this._activeDate = event.altKey ? + this._addCalendarYears(this._activeDate, -1) : + this._addCalendarMonths(this._activeDate, -1); break; case PAGE_DOWN: - if (event.altKey) { - this._activeDate = this._addCalendarYears(this._activeDate, this._monthView ? 1 : 10); - } else { - this._activeDate = this._monthView ? - this._addCalendarMonths(this._activeDate, 1) : - this._addCalendarYears(this._activeDate, 1); - } + this._activeDate = event.altKey ? + this._addCalendarYears(this._activeDate, 1) : + this._addCalendarMonths(this._activeDate, 1); break; case ENTER: - if (this._monthView) { - if (this._dateFilterForViews(this._activeDate)) { - this._dateSelected(this._activeDate); - } - } else { - this._monthSelected(this._activeDate); + if (this._dateFilterForViews(this._activeDate)) { + this._dateSelected(this._activeDate); + break; } + return; + default: + // Don't prevent default on keys that we don't explicitly handle. + return; + } + + event.preventDefault(); + } + + /** Handles keydown events on the calendar body when calendar is in year view. */ + private _handleCalendarBodyKeydownInYearView(event: KeyboardEvent): void { + switch (event.keyCode) { + case LEFT_ARROW: + this._activeDate = this._addCalendarMonths(this._activeDate, -1); + break; + case RIGHT_ARROW: + this._activeDate = this._addCalendarMonths(this._activeDate, 1); + break; + case UP_ARROW: + this._activeDate = this._prevMonthInSameCol(this._activeDate); + break; + case DOWN_ARROW: + this._activeDate = this._nextMonthInSameCol(this._activeDate); + break; + case HOME: + this._activeDate = this._addCalendarMonths(this._activeDate, -this._activeDate.month); + break; + case END: + this._activeDate = this._addCalendarMonths(this._activeDate, 11 - this._activeDate.month); + break; + case PAGE_UP: + this._activeDate = this._addCalendarYears(this._activeDate, event.altKey ? -10 : -1); + break; + case PAGE_DOWN: + this._activeDate = this._addCalendarYears(this._activeDate, event.altKey ? 10 : 1); + break; + case ENTER: + this._monthSelected(this._activeDate); break; default: // Don't prevent default on keys that we don't explicitly handle. From 0ee27c3bd8028c79aaa380d26acb9ff2b06a3918 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Fri, 24 Mar 2017 15:56:19 -0700 Subject: [PATCH 11/11] fix commas --- src/lib/datepicker/calendar.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/datepicker/calendar.ts b/src/lib/datepicker/calendar.ts index ae3b22ba0c06..048d7c227c55 100644 --- a/src/lib/datepicker/calendar.ts +++ b/src/lib/datepicker/calendar.ts @@ -265,7 +265,7 @@ export class MdCalendar implements AfterContentInit { /** * Adds the given number of months to the date. Months are counted as if flipping pages on a * calendar and then finding the closest date in the new month. For example when adding 1 month to - * Jan 31, 2017 the resulting date will be Feb 28, 2017. + * Jan 31, 2017, the resulting date will be Feb 28, 2017. */ private _addCalendarMonths(date: SimpleDate, months: number): SimpleDate { let newDate = date.add({months}); @@ -284,7 +284,7 @@ export class MdCalendar implements AfterContentInit { /** * Adds the given number of months to the date. Months are counted as if flipping 12 pages for * each year on a calendar and then finding the closest date in the new month. For example when - * adding 1 year to Feb 29, 2016 the resulting date will be Feb 28, 2017. + * adding 1 year to Feb 29, 2016, the resulting date will be Feb 28, 2017. */ private _addCalendarYears(date: SimpleDate, years: number): SimpleDate { return this._addCalendarMonths(date, years * 12);