Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(material/datepicker): VoiceOver reading out cell content twice #23563

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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