Skip to content

Commit bed96ed

Browse files
committed
fix(material/datepicker): VoiceOver reading out cell content twice
Adds a workaround to address the issue where VoiceOver reads out the content of the cells twice.
1 parent 5fcc634 commit bed96ed

File tree

11 files changed

+117
-91
lines changed

11 files changed

+117
-91
lines changed

src/material/datepicker/calendar-body.html

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
[class.mat-calendar-body-preview-start]="_isPreviewStart(item.compareValue)"
4747
[class.mat-calendar-body-preview-end]="_isPreviewEnd(item.compareValue)"
4848
[class.mat-calendar-body-in-preview]="_isInPreview(item.compareValue)"
49-
[attr.aria-label]="item.ariaLabel"
5049
[attr.aria-disabled]="!item.enabled || null"
5150
[attr.aria-selected]="_isSelected(item.compareValue)"
5251
[attr.aria-current]="todayValue === item.compareValue ? 'date' : null"
@@ -58,7 +57,18 @@
5857
[class.mat-calendar-body-selected]="_isSelected(item.compareValue)"
5958
[class.mat-calendar-body-comparison-identical]="_isComparisonIdentical(item.compareValue)"
6059
[class.mat-calendar-body-today]="todayValue === item.compareValue">
61-
{{item.displayValue}}
60+
<!--
61+
Usually we'd want to set the label as an `aria-label` attribute, but VoiceOver where it
62+
will read out both the `aria-label` and the cell content which is repetitive. We work
63+
around the issue by rendering it as a hidden element and hiding the visual label with
64+
`aria-hidden`. An alternative approach is to keep the `aria-label` and only set
65+
`aria-hidden` on the visual content, but that causes VoiceOver to read out the
66+
word "blank" after each cell.
67+
-->
68+
<span class="cdk-visually-hidden">{{item.ariaLabel}}</span>
69+
<span
70+
class="mat-calendar-body-cell-visual-label"
71+
aria-hidden="true">{{item.displayValue}}</span>
6272
</div>
6373
<div class="mat-calendar-body-cell-preview" aria-hidden="true"></div>
6474
</td>

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

Lines changed: 20 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -47,49 +47,17 @@ describe('MatCalendarBody', () => {
4747
});
4848

4949
it('highlights today', () => {
50-
const todayCells = calendarBodyNativeElement.querySelectorAll('.mat-calendar-body-today')!;
51-
expect(todayCells.length).toBe(1);
52-
53-
const todayCell = todayCells[0];
54-
55-
expect(todayCell).not.toBeNull();
56-
expect(todayCell.textContent!.trim()).toBe('3');
57-
});
58-
59-
it('sets aria-current="date" on today', () => {
60-
const todayCells = calendarBodyNativeElement.querySelectorAll(
61-
'[aria-current="date"] .mat-calendar-body-today',
62-
)!;
63-
expect(todayCells.length).toBe(1);
64-
65-
const todayCell = todayCells[0];
66-
67-
expect(todayCell).not.toBeNull();
68-
expect(todayCell.textContent!.trim()).toBe('3');
69-
});
70-
71-
it('does not highlight today if today is not within the scope', () => {
72-
testComponent.todayValue = 100000;
73-
fixture.detectChanges();
74-
7550
const todayCell = calendarBodyNativeElement.querySelector('.mat-calendar-body-today')!;
76-
expect(todayCell).toBeNull();
77-
});
78-
79-
it('does not set aria-current="date" on any cell if today is not ' + 'the scope', () => {
80-
testComponent.todayValue = 100000;
81-
fixture.detectChanges();
82-
83-
const todayCell = calendarBodyNativeElement.querySelector(
84-
'[aria-current="date"] .mat-calendar-body-today',
85-
)!;
86-
expect(todayCell).toBeNull();
51+
const todayContent = todayCell.querySelector('.mat-calendar-body-cell-visual-label')!;
52+
expect(todayCell).not.toBeNull();
53+
expect(todayContent.textContent!.trim()).toBe('3');
8754
});
8855

8956
it('highlights selected', () => {
9057
const selectedCell = calendarBodyNativeElement.querySelector('.mat-calendar-body-selected')!;
58+
const selectedContent = selectedCell.querySelector('.mat-calendar-body-cell-visual-label')!;
9159
expect(selectedCell).not.toBeNull();
92-
expect(selectedCell.innerHTML.trim()).toBe('4');
60+
expect(selectedContent.textContent!.trim()).toBe('4');
9361
});
9462

9563
it('should set aria-selected correctly', () => {
@@ -132,15 +100,24 @@ describe('MatCalendarBody', () => {
132100
});
133101

134102
it('should mark active date', () => {
135-
expect((cellEls[10] as HTMLElement).innerText.trim()).toBe('11');
136-
expect(cellEls[10].classList).toContain('mat-calendar-body-active');
103+
const cell = cellEls[10] as HTMLElement;
104+
const content = cell.querySelector('.mat-calendar-body-cell-visual-label') as HTMLElement;
105+
106+
expect(content.innerText.trim()).toBe('11');
107+
expect(cell.classList).toContain('mat-calendar-body-active');
137108
});
138109

139110
it('should set a class on even dates', () => {
140-
expect((cellEls[0] as HTMLElement).innerText.trim()).toBe('1');
141-
expect((cellEls[1] as HTMLElement).innerText.trim()).toBe('2');
142-
expect(cellEls[0].classList).not.toContain('even');
143-
expect(cellEls[1].classList).toContain('even');
111+
const labelClass = '.mat-calendar-body-cell-visual-label';
112+
const firstCell = cellEls[0] as HTMLElement;
113+
const secondCell = cellEls[1] as HTMLElement;
114+
const firstContent = firstCell.querySelector(labelClass) as HTMLElement;
115+
const secondContent = secondCell.querySelector(labelClass) as HTMLElement;
116+
117+
expect(firstContent.innerText.trim()).toBe('1');
118+
expect(secondContent.innerText.trim()).toBe('2');
119+
expect(firstCell.classList).not.toContain('even');
120+
expect(secondCell.classList).toContain('even');
144121
});
145122

146123
it('should have a focus indicator', () => {

src/material/datepicker/calendar.spec.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,17 +67,21 @@ describe('MatCalendar', () => {
6767
calendarInstance.updateTodaysDate();
6868
fixture.detectChanges();
6969

70-
let todayCell = calendarElement.querySelector('.mat-calendar-body-today')!;
71-
expect(todayCell).not.toBeNull();
72-
expect(todayCell.innerHTML.trim()).toBe('1');
70+
let todayContent = calendarElement.querySelector(
71+
'.mat-calendar-body-today .mat-calendar-body-cell-visual-label',
72+
)!;
73+
expect(todayContent).not.toBeNull();
74+
expect(todayContent.innerHTML.trim()).toBe('1');
7375

7476
fakeToday = new Date(2018, 0, 10);
7577
calendarInstance.updateTodaysDate();
7678
fixture.detectChanges();
7779

78-
todayCell = calendarElement.querySelector('.mat-calendar-body-today')!;
79-
expect(todayCell).not.toBeNull();
80-
expect(todayCell.innerHTML.trim()).toBe('10');
80+
todayContent = calendarElement.querySelector(
81+
'.mat-calendar-body-today .mat-calendar-body-cell-visual-label',
82+
)!;
83+
expect(todayContent).not.toBeNull();
84+
expect(todayContent.innerHTML.trim()).toBe('10');
8185
}));
8286

8387
it('should be in month view with specified month active', () => {

src/material/datepicker/date-range-input.spec.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -536,7 +536,10 @@ describe('MatDateRangeInput', () => {
536536
'.mat-calendar-body-range-end',
537537
].join(','),
538538
),
539-
).map(cell => cell.textContent!.trim());
539+
).map(cell => {
540+
const content = cell.querySelector('.mat-calendar-body-cell-visual-label') as HTMLElement;
541+
return content.textContent!.trim();
542+
});
540543

541544
expect(rangeTexts).toEqual(['2', '3', '4', '5']);
542545
}));
@@ -569,7 +572,10 @@ describe('MatDateRangeInput', () => {
569572
'.mat-calendar-body-comparison-end',
570573
].join(','),
571574
),
572-
).map(cell => cell.textContent!.trim());
575+
).map(cell => {
576+
const content = cell.querySelector('.mat-calendar-body-cell-visual-label') as HTMLElement;
577+
return content.textContent!.trim();
578+
});
573579

574580
expect(rangeTexts).toEqual(['2', '3', '4', '5']);
575581
}));

src/material/datepicker/datepicker.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -681,7 +681,7 @@ describe('MatDatepicker', () => {
681681
testComponent.datepicker.open();
682682
fixture.detectChanges();
683683

684-
const firstCalendarCell = document.querySelector('.mat-calendar-body-cell')!;
684+
const firstCalendarCell = document.querySelector('.mat-calendar-body-cell-visual-label')!;
685685

686686
// When the calendar is in year view, the first cell should be for a month rather than
687687
// for a date.
@@ -732,7 +732,7 @@ describe('MatDatepicker', () => {
732732
testComponent.datepicker.open();
733733
fixture.detectChanges();
734734

735-
const firstCalendarCell = document.querySelector('.mat-calendar-body-cell')!;
735+
const firstCalendarCell = document.querySelector('.mat-calendar-body-cell-visual-label')!;
736736

737737
// When the calendar is in year view, the first cell should be for a month rather than
738738
// for a date.

src/material/datepicker/month-view.spec.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,10 @@ describe('MatMonthView', () => {
8484
});
8585

8686
it('shows selected date if in same month', () => {
87-
let selectedEl = monthViewNativeElement.querySelector('.mat-calendar-body-selected')!;
88-
expect(selectedEl.innerHTML.trim()).toBe('10');
87+
const selectedContent = monthViewNativeElement.querySelector(
88+
'.mat-calendar-body-selected .mat-calendar-body-cell-visual-label',
89+
)!;
90+
expect(selectedContent.textContent!.trim()).toBe('10');
8991
});
9092

9193
it('does not show selected date if in different month', () => {
@@ -97,18 +99,23 @@ describe('MatMonthView', () => {
9799
});
98100

99101
it('fires selected change event on cell clicked', () => {
100-
let cellEls = monthViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
102+
const cellEls = monthViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
101103
(cellEls[cellEls.length - 1] as HTMLElement).click();
102104
fixture.detectChanges();
103105

104-
let selectedEl = monthViewNativeElement.querySelector('.mat-calendar-body-selected')!;
105-
expect(selectedEl.innerHTML.trim()).toBe('31');
106+
const selectedContent = monthViewNativeElement.querySelector(
107+
'.mat-calendar-body-selected .mat-calendar-body-cell-visual-label',
108+
)!;
109+
expect(selectedContent.textContent!.trim()).toBe('31');
106110
});
107111

108112
it('should mark active date', () => {
109-
let cellEls = monthViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
110-
expect((cellEls[4] as HTMLElement).innerText.trim()).toBe('5');
111-
expect(cellEls[4].classList).toContain('mat-calendar-body-active');
113+
const cellEls = monthViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
114+
const cell = cellEls[4] as HTMLElement;
115+
const content = cell.querySelector('.mat-calendar-body-cell-visual-label') as HTMLElement;
116+
117+
expect(content.innerText.trim()).toBe('5');
118+
expect(cell.classList).toContain('mat-calendar-body-active');
112119
});
113120

114121
describe('a11y', () => {

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

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,10 @@ describe('MatMultiYearView', () => {
6262
});
6363

6464
it('shows selected year if in same range', () => {
65-
let selectedEl = multiYearViewNativeElement.querySelector('.mat-calendar-body-selected')!;
66-
expect(selectedEl.innerHTML.trim()).toBe('2020');
65+
const selectedContent = multiYearViewNativeElement.querySelector(
66+
'.mat-calendar-body-selected .mat-calendar-body-cell-visual-label',
67+
)!;
68+
expect(selectedContent.innerHTML.trim()).toBe('2020');
6769
});
6870

6971
it('does not show selected year if in different range', () => {
@@ -79,8 +81,10 @@ describe('MatMultiYearView', () => {
7981
(cellEls[cellEls.length - 1] as HTMLElement).click();
8082
fixture.detectChanges();
8183

82-
let selectedEl = multiYearViewNativeElement.querySelector('.mat-calendar-body-selected')!;
83-
expect(selectedEl.innerHTML.trim()).toBe('2039');
84+
const selectedContent = multiYearViewNativeElement.querySelector(
85+
'.mat-calendar-body-selected .mat-calendar-body-cell-visual-label',
86+
)!;
87+
expect(selectedContent.textContent!.trim()).toBe('2039');
8488
});
8589

8690
it('should emit the selected year on cell clicked', () => {
@@ -94,9 +98,12 @@ describe('MatMultiYearView', () => {
9498
});
9599

96100
it('should mark active date', () => {
97-
let cellEls = multiYearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
98-
expect((cellEls[1] as HTMLElement).innerText.trim()).toBe('2017');
99-
expect(cellEls[1].classList).toContain('mat-calendar-body-active');
101+
const cellEls = multiYearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
102+
const cell = cellEls[1] as HTMLElement;
103+
const content = cell.querySelector('.mat-calendar-body-cell-visual-label') as HTMLElement;
104+
105+
expect(content.innerText.trim()).toBe('2017');
106+
expect(cell.classList).toContain('mat-calendar-body-active');
100107
});
101108

102109
describe('a11y', () => {
@@ -278,7 +285,12 @@ describe('MatMultiYearView', () => {
278285
fixture.detectChanges();
279286

280287
const cells = multiYearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
281-
expect((cells[0] as HTMLElement).innerText.trim()).toBe('2014');
288+
const firstCell = cells[0] as HTMLElement;
289+
const content = firstCell.querySelector(
290+
'.mat-calendar-body-cell-visual-label',
291+
) as HTMLElement;
292+
293+
expect(content.innerText.trim()).toBe('2014');
282294
});
283295
});
284296

@@ -301,7 +313,9 @@ describe('MatMultiYearView', () => {
301313
fixture.detectChanges();
302314

303315
const cells = multiYearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
304-
expect((cells[cells.length - 1] as HTMLElement).innerText.trim()).toBe('2020');
316+
const lastCell = cells[cells.length - 1] as HTMLElement;
317+
const content = lastCell.querySelector('.mat-calendar-body-cell-visual-label') as HTMLElement;
318+
expect(content.innerText.trim()).toBe('2020');
305319
});
306320
});
307321

@@ -324,7 +338,9 @@ describe('MatMultiYearView', () => {
324338
fixture.detectChanges();
325339

326340
const cells = multiYearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
327-
expect((cells[cells.length - 1] as HTMLElement).innerText.trim()).toBe('2020');
341+
const lastCell = cells[cells.length - 1] as HTMLElement;
342+
const content = lastCell.querySelector('.mat-calendar-body-cell-visual-label') as HTMLElement;
343+
expect(content.innerText.trim()).toBe('2020');
328344
});
329345

330346
it('should disable dates before minDate', () => {

src/material/datepicker/testing/calendar-cell-harness.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ export class MatCalendarCellHarness extends ComponentHarness {
1616
/** Reference to the inner content element inside the cell. */
1717
private _content = this.locatorFor('.mat-calendar-body-cell-content');
1818

19+
/** Inner element containing the visual label. */
20+
private _visualLabel = this.locatorFor('.mat-calendar-body-cell-visual-label');
21+
1922
/**
2023
* Gets a `HarnessPredicate` that can be used to search for a `MatCalendarCellHarness`
2124
* that meets certain criteria.
@@ -56,10 +59,14 @@ export class MatCalendarCellHarness extends ComponentHarness {
5659

5760
/** Gets the text of the calendar cell. */
5861
async getText(): Promise<string> {
59-
return (await this._content()).text();
62+
return (await this._visualLabel()).text();
6063
}
6164

62-
/** Gets the aria-label of the calendar cell. */
65+
/**
66+
* Gets the aria-label of the calendar cell.
67+
* @deprecated Calendar cells no longer have an `aria-label`. To be removed.
68+
* @breaking-change 14.0.0
69+
*/
6370
async getAriaLabel(): Promise<string> {
6471
// We're guaranteed for the `aria-label` to be defined
6572
// since this is a private element that we control.

src/material/datepicker/testing/calendar-harness-shared.spec.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -101,15 +101,6 @@ export function runCalendarHarnessTests(
101101
expect(await targetCell.isSelected()).toBe(true);
102102
});
103103

104-
it('should get the aria-label of a cell', async () => {
105-
const calendar = await loader.getHarness(calendarHarness.with({selector: '#single'}));
106-
const cells = await calendar.getCells();
107-
108-
expect(await cells[0].getAriaLabel()).toBe('August 1, 2020');
109-
expect(await cells[15].getAriaLabel()).toBe('August 16, 2020');
110-
expect(await cells[30].getAriaLabel()).toBe('August 31, 2020');
111-
});
112-
113104
it('should get the disabled state of a cell', async () => {
114105
fixture.componentInstance.minDate = new Date(
115106
calendarDate.getFullYear(),

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,10 @@ describe('MatYearView', () => {
6666
});
6767

6868
it('shows selected month if in same year', () => {
69-
let selectedEl = yearViewNativeElement.querySelector('.mat-calendar-body-selected')!;
70-
expect(selectedEl.innerHTML.trim()).toBe('MAR');
69+
const selectedContent = yearViewNativeElement.querySelector(
70+
'.mat-calendar-body-selected .mat-calendar-body-cell-visual-label',
71+
)!;
72+
expect(selectedContent.textContent!.trim()).toBe('MAR');
7173
});
7274

7375
it('does not show selected month if in different year', () => {
@@ -83,8 +85,10 @@ describe('MatYearView', () => {
8385
(cellEls[cellEls.length - 1] as HTMLElement).click();
8486
fixture.detectChanges();
8587

86-
let selectedEl = yearViewNativeElement.querySelector('.mat-calendar-body-selected')!;
87-
expect(selectedEl.innerHTML.trim()).toBe('DEC');
88+
const selectedContent = yearViewNativeElement.querySelector(
89+
'.mat-calendar-body-selected .mat-calendar-body-cell-visual-label',
90+
)!;
91+
expect(selectedContent.textContent!.trim()).toBe('DEC');
8892
});
8993

9094
it('should emit the selected month on cell clicked', () => {
@@ -98,9 +102,12 @@ describe('MatYearView', () => {
98102
});
99103

100104
it('should mark active date', () => {
101-
let cellEls = yearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
102-
expect((cellEls[0] as HTMLElement).innerText.trim()).toBe('JAN');
103-
expect(cellEls[0].classList).toContain('mat-calendar-body-active');
105+
const cellEls = yearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
106+
const cell = cellEls[0] as HTMLElement;
107+
const content = cell.querySelector('.mat-calendar-body-cell-visual-label') as HTMLElement;
108+
109+
expect(content.innerText.trim()).toBe('JAN');
110+
expect(cell.classList).toContain('mat-calendar-body-active');
104111
});
105112

106113
it('should allow selection of month with less days than current active date', () => {

tools/public_api_guard/material/datepicker-testing.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export interface DateRangeInputHarnessFilters extends BaseHarnessFilters {
5454
export class MatCalendarCellHarness extends ComponentHarness {
5555
blur(): Promise<void>;
5656
focus(): Promise<void>;
57+
// @deprecated
5758
getAriaLabel(): Promise<string>;
5859
getText(): Promise<string>;
5960
// (undocumented)

0 commit comments

Comments
 (0)