diff --git a/src/material/datepicker/calendar-header.html b/src/material/datepicker/calendar-header.html
index 7570d82e9a02..9eca2d0bcd0e 100644
--- a/src/material/datepicker/calendar-header.html
+++ b/src/material/datepicker/calendar-header.html
@@ -2,11 +2,12 @@
diff --git a/src/material/datepicker/calendar-header.spec.ts b/src/material/datepicker/calendar-header.spec.ts
index 7c8ed1260d35..bdb08e8586c5 100644
--- a/src/material/datepicker/calendar-header.spec.ts
+++ b/src/material/datepicker/calendar-header.spec.ts
@@ -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);
});
});
diff --git a/src/material/datepicker/calendar.ts b/src/material/datepicker/calendar.ts
index 616c769d8c72..f6b219c212d6 100644
--- a/src/material/datepicker/calendar.ts
+++ b/src/material/datepicker/calendar.ts
@@ -71,7 +71,7 @@ export class MatCalendarHeader
{
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
@@ -82,28 +82,25 @@ export class MatCalendarHeader {
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
@@ -192,6 +189,30 @@ export class MatCalendarHeader {
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. */
diff --git a/src/material/datepicker/datepicker-intl.ts b/src/material/datepicker/datepicker-intl.ts
index 6e82b95d714f..7140b5750fd1 100644
--- a/src/material/datepicker/datepicker-intl.ts
+++ b/src/material/datepicker/datepicker-intl.ts
@@ -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}`;
+ }
}
diff --git a/tools/public_api_guard/material/datepicker.md b/tools/public_api_guard/material/datepicker.md
index 8ac9e6557fbd..9596f814e42a 100644
--- a/tools/public_api_guard/material/datepicker.md
+++ b/tools/public_api_guard/material/datepicker.md
@@ -290,6 +290,8 @@ export class MatCalendarHeader {
nextClicked(): void;
nextEnabled(): boolean;
// (undocumented)
+ get periodButtonDescription(): string;
+ // (undocumented)
get periodButtonLabel(): string;
get periodButtonText(): string;
get prevButtonLabel(): string;
@@ -525,7 +527,9 @@ export class MatDatepickerIntl {
calendarLabel: string;
readonly changes: Subject;
closeCalendarLabel: string;
+ // (undocumented)
formatYearRange(start: string, end: string): string;
+ formatYearRangeLabel(start: string, end: string): string;
nextMonthLabel: string;
nextMultiYearLabel: string;
nextYearLabel: string;