From 67212ba4f4f0af4520ff7b3cf74521a5c429f5b6 Mon Sep 17 00:00:00 2001 From: Zach Arend Date: Fri, 26 Aug 2022 16:39:52 -0700 Subject: [PATCH] fix(material/datepicker): calendar aria-descriptions start/end date (#25457) For date ranges, add aria-descriptions to the cell of the current start date and also for end date. Popuplate aria descriptions with the expected value of the ARIA accessible name of the `matStartDate` and `matEndDate` inputs. Introduces `_computeAriaAccessibleName` function to implement ARIA acc-name-1.2 specificiation. Fixes angular#23442 and angular#23445 --- .../datepicker/aria-accessible-name.spec.ts | 159 ++++++++++++++++ .../datepicker/aria-accessible-name.ts | 179 ++++++++++++++++++ src/material/datepicker/calendar-body.html | 8 + src/material/datepicker/calendar-body.scss | 5 + src/material/datepicker/calendar-body.ts | 30 +++ src/material/datepicker/calendar.html | 2 + src/material/datepicker/calendar.ts | 6 + .../datepicker/date-range-input-parts.ts | 7 +- .../datepicker/date-range-input.spec.ts | 85 +++++++-- src/material/datepicker/date-range-input.ts | 12 +- src/material/datepicker/date-range-picker.ts | 4 + src/material/datepicker/datepicker-base.ts | 6 + .../datepicker/datepicker-content.html | 2 + src/material/datepicker/month-view.html | 2 + src/material/datepicker/month-view.ts | 6 + tools/public_api_guard/material/datepicker.md | 27 ++- 16 files changed, 523 insertions(+), 17 deletions(-) create mode 100644 src/material/datepicker/aria-accessible-name.spec.ts create mode 100644 src/material/datepicker/aria-accessible-name.ts diff --git a/src/material/datepicker/aria-accessible-name.spec.ts b/src/material/datepicker/aria-accessible-name.spec.ts new file mode 100644 index 000000000000..260164b2cddc --- /dev/null +++ b/src/material/datepicker/aria-accessible-name.spec.ts @@ -0,0 +1,159 @@ +import {_computeAriaAccessibleName} from './aria-accessible-name'; + +describe('_computeAriaAccessibleName', () => { + let rootElement: HTMLSpanElement; + + beforeEach(() => { + rootElement = document.createElement('span'); + document.body.appendChild(rootElement); + }); + + afterEach(() => { + rootElement.remove(); + }); + + it('uses aria-labelledby over aria-label', () => { + rootElement.innerHTML = ` + + + `; + + const input = rootElement.querySelector('#test-el')!; + expect(_computeAriaAccessibleName(input as HTMLInputElement)).toBe('Aria Labelledby'); + }); + + it('uses aria-label over for/id', () => { + rootElement.innerHTML = ` + + + `; + + const input = rootElement.querySelector('#test-el')!; + expect(_computeAriaAccessibleName(input as HTMLInputElement)).toBe('Aria Label'); + }); + + it('uses a label with for/id over a title attribute', () => { + rootElement.innerHTML = ` + + + `; + + const input = rootElement.querySelector('#test-el')!; + expect(_computeAriaAccessibleName(input as HTMLInputElement)).toBe('For'); + }); + + it('returns title when argument has a specified title', () => { + rootElement.innerHTML = ``; + + const input = rootElement.querySelector('#test-el')!; + expect(_computeAriaAccessibleName(input as HTMLInputElement)).toBe('Title'); + }); + + // match browser behavior of giving placeholder attribute preference over title attribute + it('uses placeholder over title', () => { + rootElement.innerHTML = ``; + + const input = rootElement.querySelector('#test-el')!; + expect(_computeAriaAccessibleName(input as HTMLInputElement)).toBe('Placeholder'); + }); + + it('uses aria-label over title and placeholder', () => { + rootElement.innerHTML = ``; + + const input = rootElement.querySelector('#test-el')!; + expect(_computeAriaAccessibleName(input as HTMLInputElement)).toBe('Aria Label'); + }); + + it('includes both textnode and element children of label with for/id', () => { + rootElement.innerHTML = ` + + + `; + + const input = rootElement.querySelector('#test-el')!; + expect(_computeAriaAccessibleName(input as HTMLInputElement)).toBe('Hello Wo r ld !'); + }); + + it('return computed name of hidden label which has for/id', () => { + rootElement.innerHTML = ` + + + `; + + const input = rootElement.querySelector('#test-el')!; + expect(_computeAriaAccessibleName(input as HTMLInputElement)).toBe('For'); + }); + + it('returns computed names of existing elements when 2 of 3 targets of aria-labelledby exist', () => { + rootElement.innerHTML = ` + + + + `; + + const input = rootElement.querySelector('#test-el')!; + expect(_computeAriaAccessibleName(input as HTMLInputElement)).toBe('Label1 Label2'); + }); + + it('returns repeated label when there are duplicate ids in aria-labelledby', () => { + rootElement.innerHTML = ` + + + `; + + const input = rootElement.querySelector('#test-el')!; + expect(_computeAriaAccessibleName(input as HTMLInputElement)).toBe('Label1 Label1'); + }); + + it('returns empty string when passed ``', () => { + rootElement.innerHTML = ``; + + const input = rootElement.querySelector('#test-el')!; + expect(_computeAriaAccessibleName(input as HTMLInputElement)).toBe(''); + }); + + it('ignores the aria-labelledby of an aria-labelledby', () => { + rootElement.innerHTML = ` + +