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;