Skip to content

Commit d5fc98d

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 176213d commit d5fc98d

File tree

5 files changed

+63
-0
lines changed

5 files changed

+63
-0
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: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ import {
1919
SimpleChanges,
2020
OnDestroy,
2121
AfterViewChecked,
22+
inject,
23+
InjectFlags,
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};
@@ -80,6 +83,8 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked {
8083
*/
8184
private _focusActiveCellAfterViewChecked = false;
8285

86+
private _intl = inject(MatDatepickerIntl, InjectFlags.Optional);
87+
8388
/** The label for the table. (e.g. "Jan 2017"). */
8489
@Input() label: string;
8590

@@ -174,6 +179,28 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked {
174179
}
175180
}
176181

182+
/**
183+
* Provides an aria-description for the cell to communicate if it is the
184+
* start or end of the selected date range.
185+
*/
186+
_getAriaDescription(value: number): string | null {
187+
if (!this.isRange) {
188+
return null;
189+
}
190+
if (!this._intl) {
191+
return null;
192+
}
193+
194+
if (this.startValue === value && this.endValue === value) {
195+
return this._intl.startAndEndDateLabel;
196+
} else if (this.startValue === value) {
197+
return this._intl.startDateLabel;
198+
} else if (this.endValue === value) {
199+
return this._intl.endDateLabel;
200+
}
201+
return null;
202+
}
203+
177204
/** Returns whether a cell should be marked as selected. */
178205
_isSelected(value: number) {
179206
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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked {
213213
endValue: number;
214214
_firstRowOffset: number;
215215
_focusActiveCell(movePreview?: boolean): void;
216+
_getAriaDescription(value: number): string | null;
216217
_isActiveCell(rowIndex: number, colIndex: number): boolean;
217218
_isComparisonBridgeEnd(value: number, rowIndex: number, colIndex: number): boolean;
218219
_isComparisonBridgeStart(value: number, rowIndex: number, colIndex: number): boolean;
@@ -535,6 +536,7 @@ export class MatDatepickerIntl {
535536
prevMonthLabel: string;
536537
prevMultiYearLabel: string;
537538
prevYearLabel: string;
539+
startAndEndDateLabel: string;
538540
startDateLabel: string;
539541
switchToMonthViewLabel: string;
540542
switchToMultiYearViewLabel: string;

0 commit comments

Comments
 (0)