Skip to content

Commit bb41ac8

Browse files
committed
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). Addresses #23483
1 parent 74099a0 commit bb41ac8

File tree

9 files changed

+78
-1
lines changed

9 files changed

+78
-1
lines changed

src/material/datepicker/calendar-body.html

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
[attr.aria-selected]="_isSelected(item.compareValue)"
5252
[attr.aria-current]="todayValue === item.compareValue ? 'date' : null"
5353
(click)="_cellClicked(item, $event)"
54+
(focus)="_cellFocused(item, $event)"
5455
[style.width]="_cellWidth"
5556
[style.paddingTop]="_cellPadding"
5657
[style.paddingBottom]="_cellPadding">

src/material/datepicker/calendar-body.ts

+10
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ export class MatCalendarBody implements OnChanges, OnDestroy {
122122
/** Emits when a new value is selected. */
123123
@Output() readonly selectedValueChange = new EventEmitter<MatCalendarUserEvent<number>>();
124124

125+
/** Emits when a new date becomes active. */
126+
@Output() readonly activeValueChange = new EventEmitter<MatCalendarUserEvent<number>>();
127+
125128
/** Emits when the preview has changed as a result of a user action. */
126129
@Output() readonly previewChange = new EventEmitter<
127130
MatCalendarUserEvent<MatCalendarCell | null>
@@ -153,6 +156,13 @@ export class MatCalendarBody implements OnChanges, OnDestroy {
153156
}
154157
}
155158

159+
/** Called when a cell is focused. */
160+
_cellFocused(cell: MatCalendarCell, event: FocusEvent): void {
161+
if (cell.enabled) {
162+
this.activeValueChange.emit({value: cell.value, event});
163+
}
164+
}
165+
156166
/** Returns whether a cell should be marked as selected. */
157167
_isSelected(value: number) {
158168
return this.startValue === value || this.endValue === value;

src/material/datepicker/month-view.html

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
[labelMinRequiredCells]="3"
2525
[activeCell]="_dateAdapter.getDate(activeDate) - 1"
2626
(selectedValueChange)="_dateSelected($event)"
27+
(activeValueChange)="_dateBecomesActive($event)"
2728
(previewChange)="_previewChanged($event)"
2829
(keyup)="_handleCalendarBodyKeyup($event)"
2930
(keydown)="_handleCalendarBodyKeydown($event)">

src/material/datepicker/month-view.ts

+16
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,17 @@ export class MatMonthView<D> implements AfterContentInit, OnChanges, OnDestroy {
252252
this._changeDetectorRef.markForCheck();
253253
}
254254

255+
_dateBecomesActive(event: MatCalendarUserEvent<number>) {
256+
const date = event.value;
257+
const activeYear = this._dateAdapter.getYear(this.activeDate);
258+
const activeMonth = this._dateAdapter.getMonth(this.activeDate);
259+
const activeDate = this._dateAdapter.createDate(activeYear, activeMonth, date);
260+
261+
if (!this._dateAdapter.sameDate(activeDate, this._activeDate)) {
262+
this.activeDateChange.emit(activeDate);
263+
}
264+
}
265+
255266
/** Handles keydown events on the calendar body when calendar is in month view. */
256267
_handleCalendarBodyKeydown(event: KeyboardEvent): void {
257268
// TODO(mmalerba): We currently allow keyboard navigation to disabled dates, but just prevent
@@ -329,6 +340,11 @@ export class MatMonthView<D> implements AfterContentInit, OnChanges, OnDestroy {
329340
this.activeDateChange.emit(this.activeDate);
330341
}
331342

343+
if (!event.isTrusted) {
344+
// 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.
345+
this._changeDetectorRef.detectChanges();
346+
}
347+
332348
this._focusActiveCell();
333349
// Prevent unexpected default actions such as form submission.
334350
event.preventDefault();

src/material/datepicker/multi-year-view.html

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
[cellAspectRatio]="4 / 7"
1212
[activeCell]="_getActiveCell()"
1313
(selectedValueChange)="_yearSelected($event)"
14+
(activeValueChange)="_yearBecomesActive($event)"
1415
(keyup)="_handleCalendarBodyKeyup($event)"
1516
(keydown)="_handleCalendarBodyKeydown($event)">
1617
</tbody>

src/material/datepicker/multi-year-view.ts

+18
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,21 @@ export class MatMultiYearView<D> implements AfterContentInit, OnDestroy {
218218
);
219219
}
220220

221+
_yearBecomesActive(event: MatCalendarUserEvent<number>) {
222+
const year = event.value;
223+
let month = this._dateAdapter.getMonth(this.activeDate);
224+
let daysInMonth = this._dateAdapter.getNumDaysInMonth(
225+
this._dateAdapter.createDate(year, month, 1),
226+
);
227+
this.activeDateChange.emit(
228+
this._dateAdapter.createDate(
229+
year,
230+
month,
231+
Math.min(this._dateAdapter.getDate(this.activeDate), daysInMonth),
232+
),
233+
);
234+
}
235+
221236
/** Handles keydown events on the calendar body when calendar is in multi-year view. */
222237
_handleCalendarBodyKeydown(event: KeyboardEvent): void {
223238
const oldActiveDate = this._activeDate;
@@ -278,6 +293,9 @@ export class MatMultiYearView<D> implements AfterContentInit, OnDestroy {
278293
this.activeDateChange.emit(this.activeDate);
279294
}
280295

296+
// Ensure the calendar body has the correct active cell.
297+
this._changeDetectorRef.detectChanges();
298+
281299
this._focusActiveCell();
282300
// Prevent unexpected default actions such as form submission.
283301
event.preventDefault();

src/material/datepicker/year-view.html

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
[cellAspectRatio]="4 / 7"
1414
[activeCell]="_dateAdapter.getMonth(activeDate)"
1515
(selectedValueChange)="_monthSelected($event)"
16+
(activeValueChange)="_monthBecomesActive($event)"
1617
(keyup)="_handleCalendarBodyKeyup($event)"
1718
(keydown)="_handleCalendarBodyKeydown($event)">
1819
</tbody>

src/material/datepicker/year-view.ts

+22
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,25 @@ export class MatYearView<D> implements AfterContentInit, OnDestroy {
198198
);
199199
}
200200

201+
/** Handles when a new month becomes active. */
202+
_monthBecomesActive(event: MatCalendarUserEvent<number>) {
203+
const month = event.value;
204+
const normalizedDate = this._dateAdapter.createDate(
205+
this._dateAdapter.getYear(this.activeDate),
206+
month,
207+
1,
208+
);
209+
210+
const daysInMonth = this._dateAdapter.getNumDaysInMonth(normalizedDate);
211+
212+
this.activeDateChange.emit(
213+
this._dateAdapter.createDate(
214+
this._dateAdapter.getYear(this.activeDate),
215+
month,
216+
Math.min(this._dateAdapter.getDate(this.activeDate), daysInMonth),
217+
),
218+
);
219+
}
201220
/** Handles keydown events on the calendar body when calendar is in year view. */
202221
_handleCalendarBodyKeydown(event: KeyboardEvent): void {
203222
// TODO(mmalerba): We currently allow keyboard navigation to disabled dates, but just prevent
@@ -261,6 +280,9 @@ export class MatYearView<D> implements AfterContentInit, OnDestroy {
261280
this.activeDateChange.emit(this.activeDate);
262281
}
263282

283+
// Ensure the calendar body has the correct active cell.
284+
this._changeDetectorRef.detectChanges();
285+
264286
this._focusActiveCell();
265287
// Prevent unexpected default actions such as form submission.
266288
event.preventDefault();

tools/public_api_guard/material/datepicker.md

+8-1
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,10 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
200200
export class MatCalendarBody implements OnChanges, OnDestroy {
201201
constructor(_elementRef: ElementRef<HTMLElement>, _ngZone: NgZone);
202202
activeCell: number;
203+
readonly activeValueChange: EventEmitter<MatCalendarUserEvent<number>>;
203204
cellAspectRatio: number;
204205
_cellClicked(cell: MatCalendarCell, event: MouseEvent): void;
206+
_cellFocused(cell: MatCalendarCell, event: FocusEvent): void;
205207
_cellPadding: string;
206208
_cellWidth: string;
207209
comparisonEnd: number | null;
@@ -239,7 +241,7 @@ export class MatCalendarBody implements OnChanges, OnDestroy {
239241
startValue: number;
240242
todayValue: number;
241243
// (undocumented)
242-
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>;
244+
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>;
243245
// (undocumented)
244246
static ɵfac: i0.ɵɵFactoryDeclaration<MatCalendarBody, never>;
245247
}
@@ -759,6 +761,8 @@ export class MatMonthView<D> implements AfterContentInit, OnChanges, OnDestroy {
759761
comparisonStart: D | null;
760762
// (undocumented)
761763
_dateAdapter: DateAdapter<D>;
764+
// (undocumented)
765+
_dateBecomesActive(event: MatCalendarUserEvent<number>): void;
762766
dateClass: MatCalendarCellClassFunction<D>;
763767
dateFilter: (date: D) => boolean;
764768
_dateSelected(event: MatCalendarUserEvent<number>): void;
@@ -831,6 +835,8 @@ export class MatMultiYearView<D> implements AfterContentInit, OnDestroy {
831835
readonly selectedChange: EventEmitter<D>;
832836
_selectedYear: number | null;
833837
_todayYear: number;
838+
// (undocumented)
839+
_yearBecomesActive(event: MatCalendarUserEvent<number>): void;
834840
_years: MatCalendarCell[][];
835841
readonly yearSelected: EventEmitter<D>;
836842
_yearSelected(event: MatCalendarUserEvent<number>): void;
@@ -907,6 +913,7 @@ export class MatYearView<D> implements AfterContentInit, OnDestroy {
907913
set maxDate(value: D | null);
908914
get minDate(): D | null;
909915
set minDate(value: D | null);
916+
_monthBecomesActive(event: MatCalendarUserEvent<number>): void;
910917
_months: MatCalendarCell[][];
911918
readonly monthSelected: EventEmitter<D>;
912919
_monthSelected(event: MatCalendarUserEvent<number>): void;

0 commit comments

Comments
 (0)