Skip to content

Commit abd2c0d

Browse files
committed
fix(material/datepicker): add aria descriptions to calendar for start/end dates
Add aria descriptions to calendar cells that are the first or last date of a date range. Describe first date of a date range as 'Start date', last date as 'End date'. Date ranges of exactly one day are described as 'Start and end date'. Fix accessibility issue where screen reader does not communicate if the selected cell is the start date or the end date (#23442). Fixes #23442
1 parent 799cf7c commit abd2c0d

File tree

5 files changed

+75
-3
lines changed

5 files changed

+75
-3
lines changed

src/material/datepicker/calendar-body.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
[class.mat-calendar-body-preview-end]="_isPreviewEnd(item.compareValue)"
6161
[class.mat-calendar-body-in-preview]="_isInPreview(item.compareValue)"
6262
[attr.aria-label]="item.ariaLabel"
63+
[attr.aria-description]="_getAriaDescription(item.compareValue)"
6364
[attr.aria-disabled]="!item.enabled || null"
6465
[attr.aria-pressed]="_isSelected(item.compareValue)"
6566
[attr.aria-current]="todayValue === item.compareValue ? 'date' : null"

src/material/datepicker/calendar-body.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,13 @@ describe('MatCalendarBody', () => {
142142
it('should have a focus indicator', () => {
143143
expect(cellEls.every(element => !!element.querySelector('.mat-focus-indicator'))).toBe(true);
144144
});
145+
146+
it('should not have `aria-description` on cells', () => {
147+
expect(
148+
calendarBodyNativeElement.querySelectorAll('.mat-calendar-body-cell[aria-description]')
149+
.length,
150+
).toBe(0);
151+
});
145152
});
146153

147154
describe('range calendar body', () => {
@@ -167,6 +174,29 @@ describe('MatCalendarBody', () => {
167174
cells = Array.from(fixture.nativeElement.querySelectorAll('.mat-calendar-body-cell'));
168175
});
169176

177+
it('should have aria-description on start and end days of a 3-day range', () => {
178+
testComponent.startValue = 2;
179+
testComponent.endValue = 4;
180+
fixture.detectChanges();
181+
182+
expect(cells[1].getAttribute('aria-description')).toBe('Start date');
183+
expect(cells[3].getAttribute('aria-description')).toBe('End date');
184+
expect(
185+
fixture.nativeElement.querySelectorAll('.mat-calendar-body-cell[aria-description]').length,
186+
).toBe(2);
187+
});
188+
189+
it('should have aria-description on a cell that is both the first and last day of a date range', () => {
190+
testComponent.startValue = 3;
191+
testComponent.endValue = 3;
192+
fixture.detectChanges();
193+
194+
expect(cells[2].getAttribute('aria-description')).toBe('Start and end date');
195+
expect(
196+
fixture.nativeElement.querySelectorAll('.mat-calendar-body-cell[aria-description]').length,
197+
).toBe(1);
198+
});
199+
170200
it('should render a range', () => {
171201
testComponent.startValue = 1;
172202
testComponent.endValue = 5;

src/material/datepicker/calendar-body.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
*/
88

99
import {
10+
Optional,
11+
SkipSelf,
1012
ChangeDetectionStrategy,
1113
Component,
1214
ElementRef,
@@ -21,6 +23,7 @@ import {
2123
AfterViewChecked,
2224
} from '@angular/core';
2325
import {take} from 'rxjs/operators';
26+
import {MatDatepickerIntl} from './datepicker-intl';
2427

2528
/** Extra CSS classes that can be associated with a calendar cell. */
2629
export type MatCalendarCellCssClasses = string | string[] | Set<string> | {[key: string]: any};
@@ -151,7 +154,17 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked {
151154
/** Width of an individual cell. */
152155
_cellWidth: string;
153156

154-
constructor(private _elementRef: ElementRef<HTMLElement>, private _ngZone: NgZone) {
157+
constructor(
158+
private _elementRef: ElementRef<HTMLElement>,
159+
private _ngZone: NgZone,
160+
/**
161+
* @deprecated `_intl` parameter to become required
162+
* @breaking-change 15.0.0
163+
*/
164+
@Optional()
165+
@SkipSelf()
166+
private _intl: MatDatepickerIntl | null,
167+
) {
155168
_ngZone.runOutsideAngular(() => {
156169
const element = _elementRef.nativeElement;
157170
element.addEventListener('mouseenter', this._enterHandler, true);
@@ -174,6 +187,28 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked {
174187
}
175188
}
176189

190+
/**
191+
* Provides an aria-description for the cell to communicate if it is the
192+
* start or end of the selected date range.
193+
*/
194+
_getAriaDescription(value: number): string | null {
195+
if (!this.isRange) {
196+
return null;
197+
}
198+
if (this._intl === null) {
199+
return null;
200+
}
201+
202+
if (this.startValue === value && this.endValue === value) {
203+
return this._intl.startAndEndDateLabel;
204+
} else if (this.startValue === value) {
205+
return this._intl.startDateLabel;
206+
} else if (this.endValue === value) {
207+
return this._intl.endDateLabel;
208+
}
209+
return null;
210+
}
211+
177212
/** Returns whether a cell should be marked as selected. */
178213
_isSelected(value: number) {
179214
return this.startValue === value || this.endValue === value;

src/material/datepicker/datepicker-intl.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ export class MatDatepickerIntl {
5757
/** A label for the last date of a range of dates (used by screen readers). */
5858
endDateLabel = 'End date';
5959

60+
/** A label for a date range that starts and ends on the same day (used by screen readers). */
61+
startAndEndDateLabel = 'Start and end date';
62+
6063
/** Formats a range of years (used for visuals). */
6164
formatYearRange(start: string, end: string): string {
6265
return `${start} \u2013 ${end}`;

tools/public_api_guard/material/datepicker.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,8 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
198198

199199
// @public
200200
export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked {
201-
constructor(_elementRef: ElementRef<HTMLElement>, _ngZone: NgZone);
201+
constructor(_elementRef: ElementRef<HTMLElement>, _ngZone: NgZone,
202+
_intl: MatDatepickerIntl | null);
202203
activeCell: number;
203204
// (undocumented)
204205
readonly activeDateChange: EventEmitter<MatCalendarUserEvent<number>>;
@@ -213,6 +214,7 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked {
213214
endValue: number;
214215
_firstRowOffset: number;
215216
_focusActiveCell(movePreview?: boolean): void;
217+
_getAriaDescription(value: number): string | null;
216218
_isActiveCell(rowIndex: number, colIndex: number): boolean;
217219
_isComparisonBridgeEnd(value: number, rowIndex: number, colIndex: number): boolean;
218220
_isComparisonBridgeStart(value: number, rowIndex: number, colIndex: number): boolean;
@@ -248,7 +250,7 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked {
248250
// (undocumented)
249251
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"; "activeDateChange": "activeDateChange"; }, never, never, false>;
250252
// (undocumented)
251-
static ɵfac: i0.ɵɵFactoryDeclaration<MatCalendarBody, never>;
253+
static ɵfac: i0.ɵɵFactoryDeclaration<MatCalendarBody, [null, null, { optional: true; skipSelf: true; }]>;
252254
}
253255

254256
// @public
@@ -535,6 +537,7 @@ export class MatDatepickerIntl {
535537
prevMonthLabel: string;
536538
prevMultiYearLabel: string;
537539
prevYearLabel: string;
540+
startAndEndDateLabel: string;
538541
startDateLabel: string;
539542
switchToMonthViewLabel: string;
540543
switchToMultiYearViewLabel: string;

0 commit comments

Comments
 (0)