Skip to content

Commit 2179f5c

Browse files
committed
fix(material/datepicker): add aria description to calendar body cells tocommunicate start or end of date range
Add 'Start of date range' aria description to first cell in selected date range, and add 'End of date range' aria description to last cell in selected date range. Fix accessibility issue where screen reader does not communicate if the selected cell is the start date or the end date. Fixes #23442
1 parent 25dcb36 commit 2179f5c

File tree

4 files changed

+68
-1
lines changed

4 files changed

+68
-1
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: 33 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,32 @@ 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].hasAttribute('aria-description')).toBe(true);
183+
expect(cells[1].getAttribute('aria-description')).toBe('Start date');
184+
expect(cells[3].hasAttribute('aria-description')).toBe(true);
185+
expect(cells[3].getAttribute('aria-description')).toBe('End date');
186+
expect(
187+
fixture.nativeElement.querySelectorAll('.mat-calendar-body-cell[aria-description]').length,
188+
).toBe(2);
189+
});
190+
191+
it('should have aria-description on a cell that is both the first and last day of a date range', () => {
192+
testComponent.startValue = 3;
193+
testComponent.endValue = 3;
194+
fixture.detectChanges();
195+
196+
expect(cells[2].hasAttribute('aria-description')).toBe(true);
197+
expect(cells[2].getAttribute('aria-description')).toBe('Start and end date');
198+
expect(
199+
fixture.nativeElement.querySelectorAll('.mat-calendar-body-cell[aria-description]').length,
200+
).toBe(1);
201+
});
202+
170203
it('should render a range', () => {
171204
testComponent.startValue = 1;
172205
testComponent.endValue = 5;

src/material/datepicker/calendar-body.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
AfterViewChecked,
2222
} from '@angular/core';
2323
import {take} from 'rxjs/operators';
24+
import {MatDatepickerIntl} from './datepicker-intl';
2425

2526
/** Extra CSS classes that can be associated with a calendar cell. */
2627
export type MatCalendarCellCssClasses = string | string[] | Set<string> | {[key: string]: any};
@@ -151,7 +152,11 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked {
151152
/** Width of an individual cell. */
152153
_cellWidth: string;
153154

154-
constructor(private _elementRef: ElementRef<HTMLElement>, private _ngZone: NgZone) {
155+
constructor(
156+
private _elementRef: ElementRef<HTMLElement>,
157+
private _ngZone: NgZone,
158+
private _intl: MatDatepickerIntl,
159+
) {
155160
_ngZone.runOutsideAngular(() => {
156161
const element = _elementRef.nativeElement;
157162
element.addEventListener('mouseenter', this._enterHandler, true);
@@ -174,6 +179,25 @@ 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+
191+
if (this.startValue === value && this.endValue === value) {
192+
return this._intl.startAndEndDateLabel;
193+
} else if (this.startValue === value) {
194+
return this._intl.startDateLabel;
195+
} else if (this.endValue === value) {
196+
return this._intl.endDateLabel;
197+
}
198+
return null;
199+
}
200+
177201
/** Returns whether a cell should be marked as selected. */
178202
_isSelected(value: number) {
179203
return this.startValue === value || this.endValue === value;

src/material/datepicker/datepicker-intl.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ export class MatDatepickerIntl {
5151
/** A label for the 'switch to year view' button (used by screen readers). */
5252
switchToMultiYearViewLabel: string = 'Choose month and year';
5353

54+
/** A label for the first date of a range of dates. */
55+
startDateLabel: string = 'Start date';
56+
57+
/** A label for the last date of a range of dates. */
58+
endDateLabel: string = 'End date';
59+
60+
/** A label for a date range that starts and ends on the same day. */
61+
startAndEndDateLabel: string = 'Start and end date';
62+
5463
/** Formats a range of years. */
5564
formatYearRange(start: string, end: string): string {
5665
return `${start} \u2013 ${end}`;

0 commit comments

Comments
 (0)