Skip to content

Commit

Permalink
fix(material/datepicker): announce the "to" when reading year range
Browse files Browse the repository at this point in the history
For the period button, announce the "to" when reading year range. When
in multi-year view, some screen readers would announce the period button
as "2019 2020". Add `formatYearRangeLabel` intl method to announce
period button description as "2019 to 2020".

Fixes #23467.
  • Loading branch information
zarend committed May 23, 2022
1 parent 64b6506 commit a30631e
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 28 deletions.
9 changes: 5 additions & 4 deletions src/material/datepicker/calendar-header.html
Expand Up @@ -2,11 +2,12 @@
<div class="mat-calendar-controls">
<button mat-button type="button" class="mat-calendar-period-button"
(click)="currentPeriodClicked()" [attr.aria-label]="periodButtonLabel"
[attr.aria-describedby]="_buttonDescriptionId"
aria-live="polite">
<span [attr.id]="_buttonDescriptionId">{{periodButtonText}}</span>
[attr.aria-description]="periodButtonDescription" aria-live="polite">
<span [attr.id]="_buttonDescriptionId" aria-hidden="true">
{{periodButtonText}}
</span>
<svg class="mat-calendar-arrow" [class.mat-calendar-invert]="calendar.currentView !== 'month'"
viewBox="0 0 10 5" focusable="false">
viewBox="0 0 10 5" focusable="false" aria-hidden="true">
<polygon points="0,0 5,5 10,0"/>
</svg>
</button>
Expand Down
12 changes: 9 additions & 3 deletions src/material/datepicker/calendar-header.spec.ts
Expand Up @@ -191,10 +191,16 @@ describe('MatCalendarHeader', () => {
});

it('should label and describe period button for assistive technology', () => {
const description = periodButton.querySelector('span[id]');
expect(calendarInstance.currentView).toBe('month');

periodButton.click();
fixture.detectChanges();

expect(calendarInstance.currentView).toBe('multi-year');
expect(periodButton.hasAttribute('aria-label')).toBe(true);
expect(periodButton.hasAttribute('aria-describedby')).toBe(true);
expect(periodButton.getAttribute('aria-describedby')).toBe(description?.getAttribute('id')!);
expect(periodButton.getAttribute('aria-label')).toMatch(/^[a-z0-9\s]+$/i);
expect(periodButton.hasAttribute('aria-description')).toBe(true);
expect(periodButton.getAttribute('aria-description')).toMatch(/^[a-z0-9\s]+$/i);
});
});

Expand Down
61 changes: 41 additions & 20 deletions src/material/datepicker/calendar.ts
Expand Up @@ -71,7 +71,7 @@ export class MatCalendarHeader<D> {
this.calendar.stateChanges.subscribe(() => changeDetectorRef.markForCheck());
}

/** The label for the current calendar view. */
/** The display text for the current calendar view. */
get periodButtonText(): string {
if (this.calendar.currentView == 'month') {
return this._dateAdapter
Expand All @@ -82,28 +82,25 @@ export class MatCalendarHeader<D> {
return this._dateAdapter.getYearName(this.calendar.activeDate);
}

// The offset from the active year to the "slot" for the starting year is the
// *actual* first rendered year in the multi-year view, and the last year is
// just yearsPerPage - 1 away.
const activeYear = this._dateAdapter.getYear(this.calendar.activeDate);
const minYearOfPage =
activeYear -
getActiveOffset(
this._dateAdapter,
this.calendar.activeDate,
this.calendar.minDate,
this.calendar.maxDate,
);
const maxYearOfPage = minYearOfPage + yearsPerPage - 1;
const minYearName = this._dateAdapter.getYearName(
this._dateAdapter.createDate(minYearOfPage, 0, 1),
);
const maxYearName = this._dateAdapter.getYearName(
this._dateAdapter.createDate(maxYearOfPage, 0, 1),
);
const [minYearName, maxYearName] = this._getMinMaxYearNames();
return this._intl.formatYearRange(minYearName, maxYearName);
}

/* The aria desciprtion of the current calendar view. */
get periodButtonDescription(): string {
if (this.calendar.currentView == 'month') {
return this._dateAdapter
.format(this.calendar.activeDate, this._dateFormats.display.monthYearLabel)
.toLocaleUpperCase();
}
if (this.calendar.currentView == 'year') {
return this._dateAdapter.getYearName(this.calendar.activeDate);
}

const [minYearName, maxYearName] = this._getMinMaxYearNames();
return this._intl.formatYearRangeLabel(minYearName, maxYearName);
}

get periodButtonLabel(): string {
return this.calendar.currentView == 'month'
? this._intl.switchToMultiYearViewLabel
Expand Down Expand Up @@ -192,6 +189,30 @@ export class MatCalendarHeader<D> {
this.calendar.maxDate,
);
}

private _getMinMaxYearNames(): [string, string] {
// The offset from the active year to the "slot" for the starting year is the
// *actual* first rendered year in the multi-year view, and the last year is
// just yearsPerPage - 1 away.
const activeYear = this._dateAdapter.getYear(this.calendar.activeDate);
const minYearOfPage =
activeYear -
getActiveOffset(
this._dateAdapter,
this.calendar.activeDate,
this.calendar.minDate,
this.calendar.maxDate,
);
const maxYearOfPage = minYearOfPage + yearsPerPage - 1;
const minYearName = this._dateAdapter.getYearName(
this._dateAdapter.createDate(minYearOfPage, 0, 1),
);
const maxYearName = this._dateAdapter.getYearName(
this._dateAdapter.createDate(maxYearOfPage, 0, 1),
);

return [minYearName, maxYearName];
}
}

/** A calendar that is used as part of the datepicker. */
Expand Down
7 changes: 6 additions & 1 deletion src/material/datepicker/datepicker-intl.ts
Expand Up @@ -51,8 +51,13 @@ export class MatDatepickerIntl {
/** A label for the 'switch to year view' button (used by screen readers). */
switchToMultiYearViewLabel: string = 'Choose month and year';

/** Formats a range of years. */
/* Formats a range of years (used only for visuals). */
formatYearRange(start: string, end: string): string {
return `${start} \u2013 ${end}`;
}

/** Formats a range of years (used by screen readers). */
formatYearRangeLabel(start: string, end: string): string {
return `${start} to ${end}`;
}
}
4 changes: 4 additions & 0 deletions tools/public_api_guard/material/datepicker.md
Expand Up @@ -290,6 +290,8 @@ export class MatCalendarHeader<D> {
nextClicked(): void;
nextEnabled(): boolean;
// (undocumented)
get periodButtonDescription(): string;
// (undocumented)
get periodButtonLabel(): string;
get periodButtonText(): string;
get prevButtonLabel(): string;
Expand Down Expand Up @@ -525,7 +527,9 @@ export class MatDatepickerIntl {
calendarLabel: string;
readonly changes: Subject<void>;
closeCalendarLabel: string;
// (undocumented)
formatYearRange(start: string, end: string): string;
formatYearRangeLabel(start: string, end: string): string;
nextMonthLabel: string;
nextMultiYearLabel: string;
nextYearLabel: string;
Expand Down

0 comments on commit a30631e

Please sign in to comment.