+
diff --git a/src/material/datepicker/calendar-header.spec.ts b/src/material/datepicker/calendar-header.spec.ts
index 7c8ed1260d35..e619b2ea9ccf 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.getAttribute('aria-label')).toMatch(/^[a-z0-9\s]+$/i);
expect(periodButton.hasAttribute('aria-describedby')).toBe(true);
- expect(periodButton.getAttribute('aria-describedby')).toBe(description?.getAttribute('id')!);
+ expect(periodButton.getAttribute('aria-describedby')).toMatch(/mat-calendar-header-[0-9]+/i);
});
});
diff --git a/src/material/datepicker/calendar.scss b/src/material/datepicker/calendar.scss
index dfc182b1a1e5..3c0e989e5784 100644
--- a/src/material/datepicker/calendar.scss
+++ b/src/material/datepicker/calendar.scss
@@ -139,3 +139,8 @@ $calendar-next-icon-transform: translateX(-2px) rotate(45deg);
.mat-calendar-body-cell:focus .mat-focus-indicator::before {
content: '';
}
+
+// Label that is not rendered and removed from the accessibility tree.
+.mat-calendar-hidden-label {
+ display: none;
+}
diff --git a/src/material/datepicker/calendar.ts b/src/material/datepicker/calendar.ts
index 1bf1423d5394..cad50e3721c8 100644
--- a/src/material/datepicker/calendar.ts
+++ b/src/material/datepicker/calendar.ts
@@ -41,15 +41,14 @@ import {
import {MatYearView} from './year-view';
import {MAT_SINGLE_DATE_SELECTION_MODEL_PROVIDER, DateRange} from './date-selection-model';
+let calendarHeaderId = 1;
+
/**
* Possible views for the calendar.
* @docs-private
*/
export type MatCalendarView = 'month' | 'year' | 'multi-year';
-/** Counter used to generate unique IDs. */
-let uniqueId = 0;
-
/** Default header for MatCalendar */
@Component({
selector: 'mat-calendar-header',
@@ -59,8 +58,6 @@ let uniqueId = 0;
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MatCalendarHeader {
- _buttonDescriptionId = `mat-calendar-button-${uniqueId++}`;
-
constructor(
private _intl: MatDatepickerIntl,
@Inject(forwardRef(() => MatCalendar)) public calendar: MatCalendar,
@@ -71,7 +68,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 +79,26 @@ 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),
- );
- return this._intl.formatYearRange(minYearName, maxYearName);
+ return this._intl.formatYearRange(...this._formatMinAndMaxYearLabels());
}
+ /** The aria description for 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);
+ }
+
+ // Format a label for the window of years displayed in the multi-year calendar view. Use
+ // `formatYearRangeLabel` because it is TTS friendly.
+ return this._intl.formatYearRangeLabel(...this._formatMinAndMaxYearLabels());
+ }
+
+ /** The `aria-label` for changing the calendar view. */
get periodButtonLabel(): string {
return this.calendar.currentView == 'month'
? this._intl.switchToMultiYearViewLabel
@@ -192,6 +187,39 @@ export class MatCalendarHeader {
this.calendar.maxDate,
);
}
+
+ /**
+ * Format two individual labels for the minimum year and maximum year available in the multi-year
+ * calendar view. Returns an array of two strings where the first string is the formatted label
+ * for the minimum year, and the second string is the formatted label for the maximum year.
+ */
+ private _formatMinAndMaxYearLabels(): [minYearLabel: string, maxYearLabel: 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 minYearLabel = this._dateAdapter.getYearName(
+ this._dateAdapter.createDate(minYearOfPage, 0, 1),
+ );
+ const maxYearLabel = this._dateAdapter.getYearName(
+ this._dateAdapter.createDate(maxYearOfPage, 0, 1),
+ );
+
+ return [minYearLabel, maxYearLabel];
+ }
+
+ private _id = `mat-calendar-header-${calendarHeaderId++}`;
+
+ _periodButtonLabelId = `${this._id}-period-label`;
}
/** A calendar that is used as part of the datepicker. */
diff --git a/tools/public_api_guard/material/datepicker.md b/tools/public_api_guard/material/datepicker.md
index f6e846d768b5..4db59c131111 100644
--- a/tools/public_api_guard/material/datepicker.md
+++ b/tools/public_api_guard/material/datepicker.md
@@ -291,15 +291,15 @@ export type MatCalendarCellCssClasses = string | string[] | Set | {
export class MatCalendarHeader {
constructor(_intl: MatDatepickerIntl, calendar: MatCalendar, _dateAdapter: DateAdapter, _dateFormats: MatDateFormats, changeDetectorRef: ChangeDetectorRef);
// (undocumented)
- _buttonDescriptionId: string;
- // (undocumented)
calendar: MatCalendar;
currentPeriodClicked(): void;
get nextButtonLabel(): string;
nextClicked(): void;
nextEnabled(): boolean;
- // (undocumented)
+ get periodButtonDescription(): string;
get periodButtonLabel(): string;
+ // (undocumented)
+ _periodButtonLabelId: string;
get periodButtonText(): string;
get prevButtonLabel(): string;
previousClicked(): void;