Skip to content

Commit

Permalink
fix(material/datepicker): VoiceOver reading out cell content twice
Browse files Browse the repository at this point in the history
Adds a workaround to address the issue where VoiceOver reads out the content of the cells twice.
  • Loading branch information
crisbeto committed Sep 10, 2021
1 parent 01734b3 commit db631a9
Show file tree
Hide file tree
Showing 10 changed files with 112 additions and 62 deletions.
14 changes: 12 additions & 2 deletions src/material/datepicker/calendar-body.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
[class.mat-calendar-body-preview-start]="_isPreviewStart(item.compareValue)"
[class.mat-calendar-body-preview-end]="_isPreviewEnd(item.compareValue)"
[class.mat-calendar-body-in-preview]="_isInPreview(item.compareValue)"
[attr.aria-label]="item.ariaLabel"
[attr.aria-disabled]="!item.enabled || null"
[attr.aria-selected]="_isSelected(item.compareValue)"
(click)="_cellClicked(item, $event)"
Expand All @@ -57,7 +56,18 @@
[class.mat-calendar-body-selected]="_isSelected(item.compareValue)"
[class.mat-calendar-body-comparison-identical]="_isComparisonIdentical(item.compareValue)"
[class.mat-calendar-body-today]="todayValue === item.compareValue">
{{item.displayValue}}
<!--
Usually we'd want to set the label as an `aria-label` attribute, but VoiceOver where it
will read out both the `aria-label` and the cell content which is repetitive. We work
around the issue by rendering it as a hidden element and hiding the visual label with
`aria-hidden`. An alternative approach is to keep the `aria-label` and only set
`aria-hidden` on the visual content, but that causes VoiceOver to read out the
word "blank" after each cell.
-->
<span class="cdk-visually-hidden">{{item.ariaLabel}}</span>
<span
class="mat-calendar-body-cell-visual-label"
aria-hidden="true">{{item.displayValue}}</span>
</div>
<div class="mat-calendar-body-cell-preview"></div>
</td>
Expand Down
27 changes: 19 additions & 8 deletions src/material/datepicker/calendar-body.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,16 @@ describe('MatCalendarBody', () => {

it('highlights today', () => {
const todayCell = calendarBodyNativeElement.querySelector('.mat-calendar-body-today')!;
const todayContent = todayCell.querySelector('.mat-calendar-body-cell-visual-label')!;
expect(todayCell).not.toBeNull();
expect(todayCell.innerHTML.trim()).toBe('3');
expect(todayContent.textContent!.trim()).toBe('3');
});

it('highlights selected', () => {
const selectedCell = calendarBodyNativeElement.querySelector('.mat-calendar-body-selected')!;
const selectedContent = selectedCell.querySelector('.mat-calendar-body-cell-visual-label')!;
expect(selectedCell).not.toBeNull();
expect(selectedCell.innerHTML.trim()).toBe('4');
expect(selectedContent.textContent!.trim()).toBe('4');
});

it('should set aria-selected correctly', () => {
Expand Down Expand Up @@ -97,15 +99,24 @@ describe('MatCalendarBody', () => {
});

it('should mark active date', () => {
expect((cellEls[10] as HTMLElement).innerText.trim()).toBe('11');
expect(cellEls[10].classList).toContain('mat-calendar-body-active');
const cell = cellEls[10] as HTMLElement;
const content = cell.querySelector('.mat-calendar-body-cell-visual-label') as HTMLElement;

expect(content.innerText.trim()).toBe('11');
expect(cell.classList).toContain('mat-calendar-body-active');
});

it('should set a class on even dates', () => {
expect((cellEls[0] as HTMLElement).innerText.trim()).toBe('1');
expect((cellEls[1] as HTMLElement).innerText.trim()).toBe('2');
expect(cellEls[0].classList).not.toContain('even');
expect(cellEls[1].classList).toContain('even');
const labelClass = '.mat-calendar-body-cell-visual-label';
const firstCell = cellEls[0] as HTMLElement;
const secondCell = cellEls[1] as HTMLElement;
const firstContent = firstCell.querySelector(labelClass) as HTMLElement;
const secondContent = secondCell.querySelector(labelClass) as HTMLElement;

expect(firstContent.innerText.trim()).toBe('1');
expect(secondContent.innerText.trim()).toBe('2');
expect(firstCell.classList).not.toContain('even');
expect(secondCell.classList).toContain('even');
});

it('should have a focus indicator', () => {
Expand Down
14 changes: 8 additions & 6 deletions src/material/datepicker/calendar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,19 @@ describe('MatCalendar', () => {
calendarInstance.updateTodaysDate();
fixture.detectChanges();

let todayCell = calendarElement.querySelector('.mat-calendar-body-today')!;
expect(todayCell).not.toBeNull();
expect(todayCell.innerHTML.trim()).toBe('1');
let todayContent = calendarElement.querySelector(
'.mat-calendar-body-today .mat-calendar-body-cell-visual-label')!;
expect(todayContent).not.toBeNull();
expect(todayContent.innerHTML.trim()).toBe('1');

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

todayCell = calendarElement.querySelector('.mat-calendar-body-today')!;
expect(todayCell).not.toBeNull();
expect(todayCell.innerHTML.trim()).toBe('10');
todayContent = calendarElement.querySelector(
'.mat-calendar-body-today .mat-calendar-body-cell-visual-label')!;
expect(todayContent).not.toBeNull();
expect(todayContent.innerHTML.trim()).toBe('10');
}));

it('should be in month view with specified month active', () => {
Expand Down
10 changes: 8 additions & 2 deletions src/material/datepicker/date-range-input.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,10 @@ describe('MatDateRangeInput', () => {
'.mat-calendar-body-range-start',
'.mat-calendar-body-in-range',
'.mat-calendar-body-range-end'
].join(','))).map(cell => cell.textContent!.trim());
].join(','))).map(cell => {
const content = cell.querySelector('.mat-calendar-body-cell-visual-label') as HTMLElement;
return content.textContent!.trim();
});

expect(rangeTexts).toEqual(['2', '3', '4', '5']);
}));
Expand All @@ -556,7 +559,10 @@ describe('MatDateRangeInput', () => {
'.mat-calendar-body-comparison-start',
'.mat-calendar-body-in-comparison-range',
'.mat-calendar-body-comparison-end'
].join(','))).map(cell => cell.textContent!.trim());
].join(','))).map(cell => {
const content = cell.querySelector('.mat-calendar-body-cell-visual-label') as HTMLElement;
return content.textContent!.trim();
});

expect(rangeTexts).toEqual(['2', '3', '4', '5']);
}));
Expand Down
16 changes: 8 additions & 8 deletions src/material/datepicker/datepicker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -676,14 +676,14 @@ describe('MatDatepicker', () => {
testComponent.datepicker.open();
fixture.detectChanges();

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

// When the calendar is in year view, the first cell should be for a month rather than
// for a date.
// When the calendar is in year view, the first cell should be for a month rather than
// for a date.
expect(firstCalendarCell.textContent!.trim())
.withContext('Expected the calendar to be in year-view').toBe('JAN');
// for a date.
expect(firstCalendarCell.textContent!.trim())
.withContext('Expected the calendar to be in year-view').toBe('JAN');
});

it('should fire yearSelected when user selects calendar year in year view',
Expand Down Expand Up @@ -728,14 +728,14 @@ expect(firstCalendarCell.textContent!.trim())
testComponent.datepicker.open();
fixture.detectChanges();

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

// When the calendar is in year view, the first cell should be for a month rather than
// for a date.
// When the calendar is in year view, the first cell should be for a month rather than
// for a date.
expect(firstCalendarCell.textContent!.trim())
.withContext('Expected the calendar to be in multi-year-view').toBe('2016');
// for a date.
expect(firstCalendarCell.textContent!.trim())
.withContext('Expected the calendar to be in multi-year-view').toBe('2016');
});

it('should fire yearSelected when user selects calendar year in multiyear view',
Expand Down
21 changes: 13 additions & 8 deletions src/material/datepicker/month-view.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,9 @@ describe('MatMonthView', () => {
});

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

it('does not show selected date if in different month', () => {
Expand All @@ -97,18 +98,22 @@ describe('MatMonthView', () => {
});

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

let selectedEl = monthViewNativeElement.querySelector('.mat-calendar-body-selected')!;
expect(selectedEl.innerHTML.trim()).toBe('31');
const selectedContent = monthViewNativeElement.querySelector(
'.mat-calendar-body-selected .mat-calendar-body-cell-visual-label')!;
expect(selectedContent.textContent!.trim()).toBe('31');
});

it('should mark active date', () => {
let cellEls = monthViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
expect((cellEls[4] as HTMLElement).innerText.trim()).toBe('5');
expect(cellEls[4].classList).toContain('mat-calendar-body-active');
const cellEls = monthViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
const cell = cellEls[4] as HTMLElement;
const content = cell.querySelector('.mat-calendar-body-cell-visual-label') as HTMLElement;

expect(content.innerText.trim()).toBe('5');
expect(cell.classList).toContain('mat-calendar-body-active');
});

describe('a11y', () => {
Expand Down
33 changes: 23 additions & 10 deletions src/material/datepicker/multi-year-view.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ describe('MatMultiYearView', () => {
});

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

it('does not show selected year if in different range', () => {
Expand All @@ -81,8 +82,9 @@ describe('MatMultiYearView', () => {
(cellEls[cellEls.length - 1] as HTMLElement).click();
fixture.detectChanges();

let selectedEl = multiYearViewNativeElement.querySelector('.mat-calendar-body-selected')!;
expect(selectedEl.innerHTML.trim()).toBe('2039');
const selectedContent = multiYearViewNativeElement.querySelector(
'.mat-calendar-body-selected .mat-calendar-body-cell-visual-label')!;
expect(selectedContent.textContent!.trim()).toBe('2039');
});

it('should emit the selected year on cell clicked', () => {
Expand All @@ -96,9 +98,12 @@ describe('MatMultiYearView', () => {
});

it('should mark active date', () => {
let cellEls = multiYearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
expect((cellEls[1] as HTMLElement).innerText.trim()).toBe('2017');
expect(cellEls[1].classList).toContain('mat-calendar-body-active');
const cellEls = multiYearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
const cell = cellEls[1] as HTMLElement;
const content = cell.querySelector('.mat-calendar-body-cell-visual-label') as HTMLElement;

expect(content.innerText.trim()).toBe('2017');
expect(cell.classList).toContain('mat-calendar-body-active');
});

describe('a11y', () => {
Expand Down Expand Up @@ -284,7 +289,11 @@ describe('MatMultiYearView', () => {
fixture.detectChanges();

const cells = multiYearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
expect((cells[0] as HTMLElement).innerText.trim()).toBe('2014');
const firstCell = cells[0] as HTMLElement;
const content =
firstCell.querySelector('.mat-calendar-body-cell-visual-label') as HTMLElement;

expect(content.innerText.trim()).toBe('2014');
});
});

Expand All @@ -307,7 +316,9 @@ describe('MatMultiYearView', () => {
fixture.detectChanges();

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

Expand All @@ -330,7 +341,9 @@ describe('MatMultiYearView', () => {
fixture.detectChanges();

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

it('should disable dates before minDate', () => {
Expand Down
11 changes: 9 additions & 2 deletions src/material/datepicker/testing/calendar-cell-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export class MatCalendarCellHarness extends ComponentHarness {
/** Reference to the inner content element inside the cell. */
private _content = this.locatorFor('.mat-calendar-body-cell-content');

/** Inner element containing the visual label. */
private _visualLabel = this.locatorFor('.mat-calendar-body-cell-visual-label');

/**
* Gets a `HarnessPredicate` that can be used to search for a `MatCalendarCellHarness`
* that meets certain criteria.
Expand Down Expand Up @@ -53,10 +56,14 @@ export class MatCalendarCellHarness extends ComponentHarness {

/** Gets the text of the calendar cell. */
async getText(): Promise<string> {
return (await this._content()).text();
return (await this._visualLabel()).text();
}

/** Gets the aria-label of the calendar cell. */
/**
* Gets the aria-label of the calendar cell.
* @deprecated Calendar cells no longer have an `aria-label`. To be removed.
* @breaking-change 14.0.0
*/
async getAriaLabel(): Promise<string> {
// We're guaranteed for the `aria-label` to be defined
// since this is a private element that we control.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,15 +98,6 @@ export function runCalendarHarnessTests(
expect(await targetCell.isSelected()).toBe(true);
});

it('should get the aria-label of a cell', async () => {
const calendar = await loader.getHarness(calendarHarness.with({selector: '#single'}));
const cells = await calendar.getCells();

expect(await cells[0].getAriaLabel()).toBe('August 1, 2020');
expect(await cells[15].getAriaLabel()).toBe('August 16, 2020');
expect(await cells[30].getAriaLabel()).toBe('August 31, 2020');
});

it('should get the disabled state of a cell', async () => {
fixture.componentInstance.minDate =
new Date(calendarDate.getFullYear(), calendarDate.getMonth(), 20);
Expand Down
19 changes: 12 additions & 7 deletions src/material/datepicker/year-view.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@ describe('MatYearView', () => {
});

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

it('does not show selected month if in different year', () => {
Expand All @@ -85,8 +86,9 @@ describe('MatYearView', () => {
(cellEls[cellEls.length - 1] as HTMLElement).click();
fixture.detectChanges();

let selectedEl = yearViewNativeElement.querySelector('.mat-calendar-body-selected')!;
expect(selectedEl.innerHTML.trim()).toBe('DEC');
const selectedContent = yearViewNativeElement.querySelector(
'.mat-calendar-body-selected .mat-calendar-body-cell-visual-label')!;
expect(selectedContent.textContent!.trim()).toBe('DEC');
});

it('should emit the selected month on cell clicked', () => {
Expand All @@ -100,9 +102,12 @@ describe('MatYearView', () => {
});

it('should mark active date', () => {
let cellEls = yearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
expect((cellEls[0] as HTMLElement).innerText.trim()).toBe('JAN');
expect(cellEls[0].classList).toContain('mat-calendar-body-active');
const cellEls = yearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
const cell = cellEls[0] as HTMLElement;
const content = cell.querySelector('.mat-calendar-body-cell-visual-label') as HTMLElement;

expect(content.innerText.trim()).toBe('JAN');
expect(cell.classList).toContain('mat-calendar-body-active');
});

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

0 comments on commit db631a9

Please sign in to comment.