Skip to content

Commit

Permalink
fix(material/datepicker): update active date on focus
Browse files Browse the repository at this point in the history
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).

Addresses angular#23483
  • Loading branch information
zarend committed Jan 14, 2022
1 parent 74099a0 commit 42e7b8e
Show file tree
Hide file tree
Showing 10 changed files with 79 additions and 2 deletions.
1 change: 1 addition & 0 deletions src/material/datepicker/calendar-body.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,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">
Expand Down
10 changes: 10 additions & 0 deletions src/material/datepicker/calendar-body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ export class MatCalendarBody implements OnChanges, OnDestroy {
/** Emits when a new value is selected. */
@Output() readonly selectedValueChange = new EventEmitter<MatCalendarUserEvent<number>>();

/** Emits when a new date becomes active. */
@Output() readonly activeValueChange = new EventEmitter<MatCalendarUserEvent<number>>();

/** Emits when the preview has changed as a result of a user action. */
@Output() readonly previewChange = new EventEmitter<
MatCalendarUserEvent<MatCalendarCell | null>
Expand Down Expand Up @@ -153,6 +156,13 @@ export class MatCalendarBody implements OnChanges, OnDestroy {
}
}

/** Called when a cell is focused. */
_cellFocused(cell: MatCalendarCell, event: FocusEvent): 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;
Expand Down
1 change: 1 addition & 0 deletions src/material/datepicker/month-view.html
Original file line number Diff line number Diff line change
Expand Up @@ -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)">
Expand Down
16 changes: 16 additions & 0 deletions src/material/datepicker/month-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,17 @@ export class MatMonthView<D> implements AfterContentInit, OnChanges, OnDestroy {
this._changeDetectorRef.markForCheck();
}

_dateBecomesActive(event: MatCalendarUserEvent<number>) {
const date = event.value;
const activeYear = this._dateAdapter.getYear(this.activeDate);
const activeMonth = this._dateAdapter.getMonth(this.activeDate);
const activeDate = this._dateAdapter.createDate(activeYear, activeMonth, date);

if (!this._dateAdapter.sameDate(activeDate, this._activeDate)) {
this.activeDateChange.emit(activeDate);
}
}

/** 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
Expand Down Expand Up @@ -329,6 +340,11 @@ export class MatMonthView<D> implements AfterContentInit, OnChanges, OnDestroy {
this.activeDateChange.emit(this.activeDate);
}

if (!event.isTrusted) {
// Manually triggered events in unit tests do not trigger change detection. Ensures that the calendar body focuses on the date that is assigned to `this.activeDate` in this method.
this._changeDetectorRef.detectChanges();
}

this._focusActiveCell();
// Prevent unexpected default actions such as form submission.
event.preventDefault();
Expand Down
1 change: 1 addition & 0 deletions src/material/datepicker/multi-year-view.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
[cellAspectRatio]="4 / 7"
[activeCell]="_getActiveCell()"
(selectedValueChange)="_yearSelected($event)"
(activeValueChange)="_yearBecomesActive($event)"
(keyup)="_handleCalendarBodyKeyup($event)"
(keydown)="_handleCalendarBodyKeydown($event)">
</tbody>
Expand Down
18 changes: 18 additions & 0 deletions src/material/datepicker/multi-year-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,21 @@ export class MatMultiYearView<D> implements AfterContentInit, OnDestroy {
);
}

_yearBecomesActive(event: MatCalendarUserEvent<number>) {
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;
Expand Down Expand Up @@ -278,6 +293,9 @@ export class MatMultiYearView<D> implements AfterContentInit, OnDestroy {
this.activeDateChange.emit(this.activeDate);
}

// Ensure the calendar body has the correct active cell.
this._changeDetectorRef.detectChanges();

this._focusActiveCell();
// Prevent unexpected default actions such as form submission.
event.preventDefault();
Expand Down
2 changes: 1 addition & 1 deletion src/material/datepicker/testing/calendar-cell-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export class MatCalendarCellHarness extends ComponentHarness {
/** Whether the cell is selected. */
async isSelected(): Promise<boolean> {
const host = await this.host();
return (await host.getAttribute('aria-selected')) === 'true';
return (await host.getAttribute('aria-pressed')) === 'true';
}

/** Whether the cell is disabled. */
Expand Down
1 change: 1 addition & 0 deletions src/material/datepicker/year-view.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
[cellAspectRatio]="4 / 7"
[activeCell]="_dateAdapter.getMonth(activeDate)"
(selectedValueChange)="_monthSelected($event)"
(activeValueChange)="_monthBecomesActive($event)"
(keyup)="_handleCalendarBodyKeyup($event)"
(keydown)="_handleCalendarBodyKeydown($event)">
</tbody>
Expand Down
22 changes: 22 additions & 0 deletions src/material/datepicker/year-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,25 @@ export class MatYearView<D> implements AfterContentInit, OnDestroy {
);
}

/** Handles when a new month becomes active. */
_monthBecomesActive(event: MatCalendarUserEvent<number>) {
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
Expand Down Expand Up @@ -261,6 +280,9 @@ export class MatYearView<D> implements AfterContentInit, OnDestroy {
this.activeDateChange.emit(this.activeDate);
}

// Ensure the calendar body has the correct active cell.
this._changeDetectorRef.detectChanges();

this._focusActiveCell();
// Prevent unexpected default actions such as form submission.
event.preventDefault();
Expand Down
9 changes: 8 additions & 1 deletion tools/public_api_guard/material/datepicker.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,10 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
export class MatCalendarBody implements OnChanges, OnDestroy {
constructor(_elementRef: ElementRef<HTMLElement>, _ngZone: NgZone);
activeCell: number;
readonly activeValueChange: EventEmitter<MatCalendarUserEvent<number>>;
cellAspectRatio: number;
_cellClicked(cell: MatCalendarCell, event: MouseEvent): void;
_cellFocused(cell: MatCalendarCell, event: FocusEvent): void;
_cellPadding: string;
_cellWidth: string;
comparisonEnd: number | null;
Expand Down Expand Up @@ -239,7 +241,7 @@ export class MatCalendarBody implements OnChanges, OnDestroy {
startValue: number;
todayValue: number;
// (undocumented)
static ɵcmp: i0.ɵɵComponentDeclaration<MatCalendarBody, "[mat-calendar-body]", ["matCalendarBody"], { "label": "label"; "rows": "rows"; "todayValue": "todayValue"; "startValue": "startValue"; "endValue": "endValue"; "labelMinRequiredCells": "labelMinRequiredCells"; "numCols": "numCols"; "activeCell": "activeCell"; "isRange": "isRange"; "cellAspectRatio": "cellAspectRatio"; "comparisonStart": "comparisonStart"; "comparisonEnd": "comparisonEnd"; "previewStart": "previewStart"; "previewEnd": "previewEnd"; }, { "selectedValueChange": "selectedValueChange"; "previewChange": "previewChange"; }, never, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MatCalendarBody, "[mat-calendar-body]", ["matCalendarBody"], { "label": "label"; "rows": "rows"; "todayValue": "todayValue"; "startValue": "startValue"; "endValue": "endValue"; "labelMinRequiredCells": "labelMinRequiredCells"; "numCols": "numCols"; "activeCell": "activeCell"; "isRange": "isRange"; "cellAspectRatio": "cellAspectRatio"; "comparisonStart": "comparisonStart"; "comparisonEnd": "comparisonEnd"; "previewStart": "previewStart"; "previewEnd": "previewEnd"; }, { "selectedValueChange": "selectedValueChange"; "activeValueChange": "activeValueChange"; "previewChange": "previewChange"; }, never, never>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<MatCalendarBody, never>;
}
Expand Down Expand Up @@ -759,6 +761,8 @@ export class MatMonthView<D> implements AfterContentInit, OnChanges, OnDestroy {
comparisonStart: D | null;
// (undocumented)
_dateAdapter: DateAdapter<D>;
// (undocumented)
_dateBecomesActive(event: MatCalendarUserEvent<number>): void;
dateClass: MatCalendarCellClassFunction<D>;
dateFilter: (date: D) => boolean;
_dateSelected(event: MatCalendarUserEvent<number>): void;
Expand Down Expand Up @@ -831,6 +835,8 @@ export class MatMultiYearView<D> implements AfterContentInit, OnDestroy {
readonly selectedChange: EventEmitter<D>;
_selectedYear: number | null;
_todayYear: number;
// (undocumented)
_yearBecomesActive(event: MatCalendarUserEvent<number>): void;
_years: MatCalendarCell[][];
readonly yearSelected: EventEmitter<D>;
_yearSelected(event: MatCalendarUserEvent<number>): void;
Expand Down Expand Up @@ -907,6 +913,7 @@ export class MatYearView<D> implements AfterContentInit, OnDestroy {
set maxDate(value: D | null);
get minDate(): D | null;
set minDate(value: D | null);
_monthBecomesActive(event: MatCalendarUserEvent<number>): void;
_months: MatCalendarCell[][];
readonly monthSelected: EventEmitter<D>;
_monthSelected(event: MatCalendarUserEvent<number>): void;
Expand Down

0 comments on commit 42e7b8e

Please sign in to comment.