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 Jan 11, 2022
1 parent 5fcc634 commit bed96ed
Show file tree
Hide file tree
Showing 11 changed files with 117 additions and 91 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)"
[attr.aria-current]="todayValue === item.compareValue ? 'date' : null"
Expand All @@ -58,7 +57,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" aria-hidden="true"></div>
</td>
Expand Down
63 changes: 20 additions & 43 deletions src/material/datepicker/calendar-body.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,49 +47,17 @@ describe('MatCalendarBody', () => {
});

it('highlights today', () => {
const todayCells = calendarBodyNativeElement.querySelectorAll('.mat-calendar-body-today')!;
expect(todayCells.length).toBe(1);

const todayCell = todayCells[0];

expect(todayCell).not.toBeNull();
expect(todayCell.textContent!.trim()).toBe('3');
});

it('sets aria-current="date" on today', () => {
const todayCells = calendarBodyNativeElement.querySelectorAll(
'[aria-current="date"] .mat-calendar-body-today',
)!;
expect(todayCells.length).toBe(1);

const todayCell = todayCells[0];

expect(todayCell).not.toBeNull();
expect(todayCell.textContent!.trim()).toBe('3');
});

it('does not highlight today if today is not within the scope', () => {
testComponent.todayValue = 100000;
fixture.detectChanges();

const todayCell = calendarBodyNativeElement.querySelector('.mat-calendar-body-today')!;
expect(todayCell).toBeNull();
});

it('does not set aria-current="date" on any cell if today is not ' + 'the scope', () => {
testComponent.todayValue = 100000;
fixture.detectChanges();

const todayCell = calendarBodyNativeElement.querySelector(
'[aria-current="date"] .mat-calendar-body-today',
)!;
expect(todayCell).toBeNull();
const todayContent = todayCell.querySelector('.mat-calendar-body-cell-visual-label')!;
expect(todayCell).not.toBeNull();
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 @@ -132,15 +100,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
16 changes: 10 additions & 6 deletions src/material/datepicker/calendar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,21 @@ 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 @@ -536,7 +536,10 @@ describe('MatDateRangeInput', () => {
'.mat-calendar-body-range-end',
].join(','),
),
).map(cell => cell.textContent!.trim());
).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 Expand Up @@ -569,7 +572,10 @@ describe('MatDateRangeInput', () => {
'.mat-calendar-body-comparison-end',
].join(','),
),
).map(cell => cell.textContent!.trim());
).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
4 changes: 2 additions & 2 deletions src/material/datepicker/datepicker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,7 @@ 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.
Expand Down Expand Up @@ -732,7 +732,7 @@ 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.
Expand Down
23 changes: 15 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,10 @@ 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 +99,23 @@ 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
36 changes: 26 additions & 10 deletions src/material/datepicker/multi-year-view.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ 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 @@ -79,8 +81,10 @@ 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 @@ -94,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 @@ -278,7 +285,12 @@ 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 @@ -301,7 +313,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 @@ -324,7 +338,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 @@ -56,10 +59,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 @@ -101,15 +101,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(),
Expand Down
21 changes: 14 additions & 7 deletions src/material/datepicker/year-view.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,10 @@ 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 @@ -83,8 +85,10 @@ 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 @@ -98,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
1 change: 1 addition & 0 deletions tools/public_api_guard/material/datepicker-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface DateRangeInputHarnessFilters extends BaseHarnessFilters {
export class MatCalendarCellHarness extends ComponentHarness {
blur(): Promise<void>;
focus(): Promise<void>;
// @deprecated
getAriaLabel(): Promise<string>;
getText(): Promise<string>;
// (undocumented)
Expand Down

0 comments on commit bed96ed

Please sign in to comment.