From 84c8010b6b04516f7428f137c288c1bb13511a34 Mon Sep 17 00:00:00 2001 From: Zach Arend Date: Thu, 16 Dec 2021 21:17:23 +0000 Subject: [PATCH 1/4] build: add .vimrc to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index ce0991dfb3e0..2a71b14567d2 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,8 @@ node_modules /.vs *.swo *.swp +.vimrc +.nvimrc # misc .DS_Store From a15b662d740e6dcf38c8399133a3e67578304132 Mon Sep 17 00:00:00 2001 From: Zach Arend Date: Thu, 16 Dec 2021 19:19:02 +0000 Subject: [PATCH 2/4] fix(material/datepicker): change calendar cells to buttons Makes changes to the DOM structure of calendar cells for better screen reader experience. Previously, the DOM structure looksed like this: ``` > ``` Using the `gridcell` role allows screenreaders to use table specific navigation and some screenreaders would announce that the cells are interactible because of the presence of `aria-selected`. However, some screenreaders did not announce the cells as interactable and treated it the same as a cell in a static table (e.g. VoiceOver announces element type incorrectly angular#23476). This changes the DOM structure to nest buttons inside of a gridcell to make it more explicit that the table cells can be interacted with and are not static content. The gridcell role is still present, so table navigation will continue to work, but the interaction is done with buttons nested inside the `td` elements. The `td` element is only for adding information to the a11y tree and not used for visual purposes. Updated DOM structure: ```
>
``` Fixes angular#23476 --- src/material/datepicker/calendar-body.html | 86 +++++++++++++--------- src/material/datepicker/calendar-body.scss | 5 ++ src/material/datepicker/calendar-body.ts | 4 +- 3 files changed, 59 insertions(+), 36 deletions(-) diff --git a/src/material/datepicker/calendar-body.html b/src/material/datepicker/calendar-body.html index ffdfef27c1cd..0a83c6ce8a85 100644 --- a/src/material/datepicker/calendar-body.html +++ b/src/material/datepicker/calendar-body.html @@ -26,40 +26,58 @@ [style.paddingBottom]="_cellPadding"> {{_firstRowOffset >= labelMinRequiredCells ? label : ''}} - -
- {{item.displayValue}} + + +
+
+ {{item.displayValue}} +
+ +
diff --git a/src/material/datepicker/calendar-body.scss b/src/material/datepicker/calendar-body.scss index c2d2f78df048..d77f2fc4377c 100644 --- a/src/material/datepicker/calendar-body.scss +++ b/src/material/datepicker/calendar-body.scss @@ -31,7 +31,12 @@ $calendar-range-end-body-cell-size: padding-right: $calendar-body-label-side-padding; } +.mat-calendar-body-cell-wrapper { + display: contents; +} + .mat-calendar-body-cell { + display: table-cell; position: relative; height: 0; line-height: 0; diff --git a/src/material/datepicker/calendar-body.ts b/src/material/datepicker/calendar-body.ts index 895db4b41472..84440fa4b51e 100644 --- a/src/material/datepicker/calendar-body.ts +++ b/src/material/datepicker/calendar-body.ts @@ -337,7 +337,7 @@ export class MatCalendarBody implements OnChanges, OnDestroy { // Only reset the preview end value when leaving cells. This looks better, because // we have a gap between the cells and the rows and we don't want to remove the // range just for it to show up again when the user moves a few pixels to the side. - if (event.target && isTableCell(event.target as HTMLElement)) { + if (event.target && ((event.target as HTMLElement).parentElement) && isTableCell((event.target as HTMLElement).parentElement as HTMLElement)) { this._ngZone.run(() => this.previewChange.emit({value: null, event})); } } @@ -350,7 +350,7 @@ export class MatCalendarBody implements OnChanges, OnDestroy { if (isTableCell(element)) { cell = element; } else if (isTableCell(element.parentNode!)) { - cell = element.parentNode as HTMLElement; + cell = element; } if (cell) { From 10d5357c3f569d0bd49307c5704a7e9eab1b31d7 Mon Sep 17 00:00:00 2001 From: Zach Arend Date: Wed, 15 Dec 2021 18:36:47 +0000 Subject: [PATCH 3/4] fix(material/datepicker): update active date on focus When a a date cell on the calendar recieves focus, set the active date to that cell. This ensures that the active date matches the date with browser focus. Previously, we set the active date on keydown and click, but that was problematic for screenreaders. That's because many screenreaders trigger a focus event instead of a keydown event when using screenreader specific navigation (VoiceOver, Chromevox, NVDA). Fixes #23483 --- src/material/datepicker/calendar-body.html | 1 + src/material/datepicker/calendar-body.ts | 16 +++++++++++++- src/material/datepicker/month-view.html | 1 + src/material/datepicker/month-view.ts | 23 +++++++++++++++++++- src/material/datepicker/multi-year-view.html | 1 + src/material/datepicker/multi-year-view.ts | 16 +++++++++++++- src/material/datepicker/year-view.html | 1 + src/material/datepicker/year-view.ts | 20 ++++++++++++++++- 8 files changed, 75 insertions(+), 4 deletions(-) diff --git a/src/material/datepicker/calendar-body.html b/src/material/datepicker/calendar-body.html index 0a83c6ce8a85..d0c1f7870611 100644 --- a/src/material/datepicker/calendar-body.html +++ b/src/material/datepicker/calendar-body.html @@ -68,6 +68,7 @@ [attr.aria-selected]="_isSelected(item.compareValue)" [attr.aria-current]="todayValue === item.compareValue ? 'date' : null" (click)="_cellClicked(item, $event)" + (focus)="_cellFocused(item, $event)" [style.width]="_cellWidth" [style.paddingTop]="_cellPadding" [style.paddingBottom]="_cellPadding"> diff --git a/src/material/datepicker/calendar-body.ts b/src/material/datepicker/calendar-body.ts index 84440fa4b51e..ecb25003ee8f 100644 --- a/src/material/datepicker/calendar-body.ts +++ b/src/material/datepicker/calendar-body.ts @@ -122,6 +122,9 @@ export class MatCalendarBody implements OnChanges, OnDestroy { /** Emits when a new value is selected. */ @Output() readonly selectedValueChange = new EventEmitter>(); + /** Emits when a new date becomes active. */ + @Output() readonly activeValueChange = new EventEmitter>(); + /** Emits when the preview has changed as a result of a user action. */ @Output() readonly previewChange = new EventEmitter< MatCalendarUserEvent @@ -153,6 +156,13 @@ export class MatCalendarBody implements OnChanges, OnDestroy { } } + /** Called when a cell is focused. */ + _cellFocused(cell: MatCalendarCell, event: any): void { + if (cell.enabled) { + this.activeValueChange.emit({value: cell.value, event}); + } + } + /** Returns whether a cell should be marked as selected. */ _isSelected(value: number) { return this.startValue === value || this.endValue === value; @@ -337,7 +347,11 @@ export class MatCalendarBody implements OnChanges, OnDestroy { // Only reset the preview end value when leaving cells. This looks better, because // we have a gap between the cells and the rows and we don't want to remove the // range just for it to show up again when the user moves a few pixels to the side. - if (event.target && ((event.target as HTMLElement).parentElement) && isTableCell((event.target as HTMLElement).parentElement as HTMLElement)) { + if ( + event.target && + (event.target as HTMLElement).parentElement && + isTableCell((event.target as HTMLElement).parentElement as HTMLElement) + ) { this._ngZone.run(() => this.previewChange.emit({value: null, event})); } } diff --git a/src/material/datepicker/month-view.html b/src/material/datepicker/month-view.html index 35deaf2339c2..5b6e09c104cc 100644 --- a/src/material/datepicker/month-view.html +++ b/src/material/datepicker/month-view.html @@ -24,6 +24,7 @@ [labelMinRequiredCells]="3" [activeCell]="_dateAdapter.getDate(activeDate) - 1" (selectedValueChange)="_dateSelected($event)" + (activeValueChange)="_dateBecomesActive($event)" (previewChange)="_previewChanged($event)" (keyup)="_handleCalendarBodyKeyup($event)" (keydown)="_handleCalendarBodyKeydown($event)"> diff --git a/src/material/datepicker/month-view.ts b/src/material/datepicker/month-view.ts index a46abfb4dfbd..94eb81dd1a3c 100644 --- a/src/material/datepicker/month-view.ts +++ b/src/material/datepicker/month-view.ts @@ -252,6 +252,28 @@ export class MatMonthView implements AfterContentInit, OnChanges, OnDestroy { this._changeDetectorRef.markForCheck(); } + _dateBecomesActive(event: MatCalendarUserEvent) { + const date = event.value; + const selectedYear = this._dateAdapter.getYear(this.activeDate); + const selectedMonth = this._dateAdapter.getMonth(this.activeDate); + const activeDate = this._dateAdapter.createDate(selectedYear, selectedMonth, date); + let rangeStartDate: number | null; + let rangeEndDate: number | null; + + if (this._activeDate instanceof DateRange) { + rangeStartDate = this._getDateInCurrentMonth(this._activeDate.start); + rangeEndDate = this._getDateInCurrentMonth(this._activeDate.end); + } else { + rangeStartDate = rangeEndDate = this._getDateInCurrentMonth(this._activeDate); + } + + if (rangeStartDate !== date || rangeEndDate !== date) { + this.activeDateChange.emit(activeDate); + } + + this._changeDetectorRef.markForCheck(); + } + /** 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 @@ -329,7 +351,6 @@ export class MatMonthView implements AfterContentInit, OnChanges, OnDestroy { this.activeDateChange.emit(this.activeDate); } - this._focusActiveCell(); // Prevent unexpected default actions such as form submission. event.preventDefault(); } diff --git a/src/material/datepicker/multi-year-view.html b/src/material/datepicker/multi-year-view.html index ee12a9e67d29..da2de1cf26e4 100644 --- a/src/material/datepicker/multi-year-view.html +++ b/src/material/datepicker/multi-year-view.html @@ -11,6 +11,7 @@ [cellAspectRatio]="4 / 7" [activeCell]="_getActiveCell()" (selectedValueChange)="_yearSelected($event)" + (activeValueChange)="_yearBecomesActive($event)" (keyup)="_handleCalendarBodyKeyup($event)" (keydown)="_handleCalendarBodyKeydown($event)"> diff --git a/src/material/datepicker/multi-year-view.ts b/src/material/datepicker/multi-year-view.ts index da7206057bdc..c11e58e08f54 100644 --- a/src/material/datepicker/multi-year-view.ts +++ b/src/material/datepicker/multi-year-view.ts @@ -218,6 +218,21 @@ export class MatMultiYearView implements AfterContentInit, OnDestroy { ); } + _yearBecomesActive(event: MatCalendarUserEvent) { + const year = event.value; + let month = this._dateAdapter.getMonth(this.activeDate); + let daysInMonth = this._dateAdapter.getNumDaysInMonth( + this._dateAdapter.createDate(year, month, 1), + ); + this.activeDateChange.emit( + this._dateAdapter.createDate( + year, + month, + Math.min(this._dateAdapter.getDate(this.activeDate), daysInMonth), + ), + ); + } + /** Handles keydown events on the calendar body when calendar is in multi-year view. */ _handleCalendarBodyKeydown(event: KeyboardEvent): void { const oldActiveDate = this._activeDate; @@ -278,7 +293,6 @@ export class MatMultiYearView implements AfterContentInit, OnDestroy { this.activeDateChange.emit(this.activeDate); } - this._focusActiveCell(); // Prevent unexpected default actions such as form submission. event.preventDefault(); } diff --git a/src/material/datepicker/year-view.html b/src/material/datepicker/year-view.html index dae81c5e2a27..c54b0c71269d 100644 --- a/src/material/datepicker/year-view.html +++ b/src/material/datepicker/year-view.html @@ -13,6 +13,7 @@ [cellAspectRatio]="4 / 7" [activeCell]="_dateAdapter.getMonth(activeDate)" (selectedValueChange)="_monthSelected($event)" + (activeValueChange)="_monthBecomesActive($event)" (keyup)="_handleCalendarBodyKeyup($event)" (keydown)="_handleCalendarBodyKeydown($event)"> diff --git a/src/material/datepicker/year-view.ts b/src/material/datepicker/year-view.ts index 2e121f75cc15..c7931e1acac0 100644 --- a/src/material/datepicker/year-view.ts +++ b/src/material/datepicker/year-view.ts @@ -198,6 +198,25 @@ export class MatYearView implements AfterContentInit, OnDestroy { ); } + /** Handles when a new month becomes active. */ + _monthBecomesActive(event: MatCalendarUserEvent) { + const month = event.value; + const normalizedDate = this._dateAdapter.createDate( + this._dateAdapter.getYear(this.activeDate), + month, + 1, + ); + + const daysInMonth = this._dateAdapter.getNumDaysInMonth(normalizedDate); + + this.activeDateChange.emit( + this._dateAdapter.createDate( + this._dateAdapter.getYear(this.activeDate), + month, + Math.min(this._dateAdapter.getDate(this.activeDate), daysInMonth), + ), + ); + } /** Handles keydown events on the calendar body when calendar is in year view. */ _handleCalendarBodyKeydown(event: KeyboardEvent): void { // TODO(mmalerba): We currently allow keyboard navigation to disabled dates, but just prevent @@ -261,7 +280,6 @@ export class MatYearView implements AfterContentInit, OnDestroy { this.activeDateChange.emit(this.activeDate); } - this._focusActiveCell(); // Prevent unexpected default actions such as form submission. event.preventDefault(); } From ef844a624ddf0d6f7dcf4a96f3f584ed25dfe17b Mon Sep 17 00:00:00 2001 From: Zach Arend Date: Tue, 21 Dec 2021 23:44:08 +0000 Subject: [PATCH 4/4] debugging test failure, save work --- src/material/datepicker/month-view.html | 2 +- src/material/datepicker/month-view.spec.ts | 14 ++++++++++++-- src/material/datepicker/month-view.ts | 7 ++++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/material/datepicker/month-view.html b/src/material/datepicker/month-view.html index 5b6e09c104cc..e3c5440d9ff3 100644 --- a/src/material/datepicker/month-view.html +++ b/src/material/datepicker/month-view.html @@ -24,7 +24,7 @@ [labelMinRequiredCells]="3" [activeCell]="_dateAdapter.getDate(activeDate) - 1" (selectedValueChange)="_dateSelected($event)" - (activeValueChange)="_dateBecomesActive($event)" + (activeValueChange)="_dayBecomesActive($event)" (previewChange)="_previewChanged($event)" (keyup)="_handleCalendarBodyKeyup($event)" (keydown)="_handleCalendarBodyKeydown($event)"> diff --git a/src/material/datepicker/month-view.spec.ts b/src/material/datepicker/month-view.spec.ts index 7a4909f8efbb..a769a80c67ec 100644 --- a/src/material/datepicker/month-view.spec.ts +++ b/src/material/datepicker/month-view.spec.ts @@ -149,13 +149,14 @@ describe('MatMonthView', () => { fixture.detectChanges(); }); - it('should decrement date on left arrow press', () => { + fit('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(); + expect(calendarInstance.date).toEqual(new Date(2017, JAN, 1)); dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW); fixture.detectChanges(); @@ -653,7 +654,16 @@ describe('MatMonthView', () => { (_userSelection)="userSelectionSpy($event)">`, }) class StandardMonthView { - date = new Date(2017, JAN, 5); + get date(): Date { + console.log('test code. `get date`', this._date); + return this._date; + } + set date(date: Date) { + console.log('test code. `set date`', date); + this._date = date; + } + _date = new Date(2017, JAN, 5); + selected: Date | DateRange = new Date(2017, JAN, 10); selectedChangeSpy = jasmine.createSpy('selectedChange'); userSelectionSpy = jasmine.createSpy('userSelection'); diff --git a/src/material/datepicker/month-view.ts b/src/material/datepicker/month-view.ts index 94eb81dd1a3c..bece84601f4f 100644 --- a/src/material/datepicker/month-view.ts +++ b/src/material/datepicker/month-view.ts @@ -252,7 +252,7 @@ export class MatMonthView implements AfterContentInit, OnChanges, OnDestroy { this._changeDetectorRef.markForCheck(); } - _dateBecomesActive(event: MatCalendarUserEvent) { + _dayBecomesActive(event: MatCalendarUserEvent) { const date = event.value; const selectedYear = this._dateAdapter.getYear(this.activeDate); const selectedMonth = this._dateAdapter.getMonth(this.activeDate); @@ -267,6 +267,8 @@ export class MatMonthView implements AfterContentInit, OnChanges, OnDestroy { rangeStartDate = rangeEndDate = this._getDateInCurrentMonth(this._activeDate); } + this.activeDate = activeDate; + if (rangeStartDate !== date || rangeEndDate !== date) { this.activeDateChange.emit(activeDate); } @@ -348,9 +350,12 @@ export class MatMonthView implements AfterContentInit, OnChanges, OnDestroy { } if (this._dateAdapter.compareDate(oldActiveDate, this.activeDate)) { + console.log("firing activeDateChange", this.activeDate); this.activeDateChange.emit(this.activeDate); } + this._focusActiveCell(); + // Prevent unexpected default actions such as form submission. event.preventDefault(); }