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).

Fixes angular#23483
  • Loading branch information
zarend committed Dec 21, 2021
1 parent a15b662 commit 10d5357
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 4 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 @@ -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">
Expand Down
16 changes: 15 additions & 1 deletion 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: 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;
Expand Down Expand Up @@ -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}));
}
}
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
23 changes: 22 additions & 1 deletion src/material/datepicker/month-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,28 @@ export class MatMonthView<D> implements AfterContentInit, OnChanges, OnDestroy {
this._changeDetectorRef.markForCheck();
}

_dateBecomesActive(event: MatCalendarUserEvent<number>) {
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
Expand Down Expand Up @@ -329,7 +351,6 @@ export class MatMonthView<D> implements AfterContentInit, OnChanges, OnDestroy {
this.activeDateChange.emit(this.activeDate);
}

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
16 changes: 15 additions & 1 deletion 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,7 +293,6 @@ export class MatMultiYearView<D> implements AfterContentInit, OnDestroy {
this.activeDateChange.emit(this.activeDate);
}

this._focusActiveCell();
// Prevent unexpected default actions such as form submission.
event.preventDefault();
}
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
20 changes: 19 additions & 1 deletion 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,7 +280,6 @@ export class MatYearView<D> implements AfterContentInit, OnDestroy {
this.activeDateChange.emit(this.activeDate);
}

this._focusActiveCell();
// Prevent unexpected default actions such as form submission.
event.preventDefault();
}
Expand Down

0 comments on commit 10d5357

Please sign in to comment.