forked from angular/components
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
wip: giving aria-description on calendar UI to clarify start vs end date
Fixes angular#23442 and angular#23445
- Loading branch information
Showing
15 changed files
with
413 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
/** | ||
* @fileoverview A description of this module. What would someone | ||
* new to your team want to know about the code in this file? | ||
* (DO NOT SUBMIT as is; replace this comment.) | ||
*/ | ||
import {_computeAriaAccessibleName} from './aria-accessible-name'; | ||
|
||
fdescribe('_computeAriaAccessibleName', () => { | ||
let rootElement: HTMLSpanElement; | ||
|
||
beforeEach(() => { | ||
rootElement = document.createElement('span'); | ||
document.body.appendChild(rootElement); | ||
}); | ||
|
||
afterEach(() => { | ||
rootElement.innerHTML = ''; | ||
document.body.removeChild(rootElement); | ||
}); | ||
|
||
it('should use aria-labelledby over aria-label', () => { | ||
rootElement.innerHTML = ` | ||
<label id='test-label'>Aria Labelledby</label> | ||
<input id='test-el' aria-labelledby='test-label' aria-label='Aria Label'/> | ||
`; | ||
|
||
const input = rootElement.querySelector('#test-el')!; | ||
expect(_computeAriaAccessibleName(input)).toBe('Aria Labelledby'); | ||
}); | ||
|
||
it('should use aria-label over for/id', () => { | ||
rootElement.innerHTML = ` | ||
<label for='test-el'>For</label> | ||
<input id='test-el' aria-label='Aria Label'/> | ||
`; | ||
|
||
const input = rootElement.querySelector('#test-el')!; | ||
expect(_computeAriaAccessibleName(input)).toBe('Aria Label'); | ||
}); | ||
|
||
it('should use a label with for/id over a title attribute', () => { | ||
rootElement.innerHTML = ` | ||
<label for='test-el'>For</label> | ||
<input id='test-el' title='Title'/> | ||
`; | ||
|
||
const input = rootElement.querySelector('#test-el')!; | ||
expect(_computeAriaAccessibleName(input)).toBe('For'); | ||
}); | ||
|
||
it('should return the title with argument input that has only title attribute', () => { | ||
rootElement.innerHTML = `<input id="test-el" title='Title'/>`; | ||
|
||
const input = rootElement.querySelector('#test-el')!; | ||
expect(_computeAriaAccessibleName(input)).toBe('Title'); | ||
}); | ||
|
||
it('should return the placeholder with argument input that has both title and placeholder', () => { | ||
rootElement.innerHTML = `<input id="test-el" title='Title' placeholder='Placeholder'/>`; | ||
|
||
const input = rootElement.querySelector('#test-el')!; | ||
expect(_computeAriaAccessibleName(input)).toBe('Placeholder'); | ||
}); | ||
|
||
it('should return the aria-label with argument input that has an aria-label, title and placeholder', () => { | ||
rootElement.innerHTML = `<input id="test-el" title='Title' placeholder='Placeholder' | ||
aria-label="Aria Label"/>`; | ||
|
||
const input = rootElement.querySelector('#test-el')!; | ||
expect(_computeAriaAccessibleName(input)).toBe('Aria Label'); | ||
}); | ||
|
||
it('should include both textnode and element children', () => { | ||
rootElement.innerHTML = ` | ||
<label for="test-el"> | ||
Hello | ||
<span> | ||
Wo | ||
<span><span>r</span></span> | ||
<span> ld </span> | ||
</span> | ||
! | ||
</label> | ||
<input id='test-el'/> | ||
`; | ||
|
||
const input = rootElement.querySelector('#test-el')!; | ||
expect(_computeAriaAccessibleName(input)).toBe('Hello Wo r ld !'); | ||
}); | ||
|
||
it('return computed name of hiden label which has for/id', () => { | ||
rootElement.innerHTML = ` | ||
<label for="test-el" aria-hidden="true" style="display: none;">For</label> | ||
<input id='test-el'/> | ||
`; | ||
|
||
const input = rootElement.querySelector('#test-el')!; | ||
expect(_computeAriaAccessibleName(input)).toBe('For'); | ||
}); | ||
|
||
it('return computed names of existing elements when 2 of 3 targets of aria-labelledby exist', () => { | ||
rootElement.innerHTML = ` | ||
<label id="label-1-of-2" aria-hidden="true" style="display: none;">Label1</label> | ||
<label id="label-2-of-2" aria-hidden="true" style="display: none;">Label2</label> | ||
<input id="test-el" aria-labelledby="label-1-of-2 label-2-of-2 non-existant-label"/> | ||
`; | ||
|
||
const input = rootElement.querySelector('#test-el')!; | ||
expect(_computeAriaAccessibleName(input)).toBe('Label1 Label2'); | ||
}); | ||
|
||
it('return computed name twice when there are duplicate ids in aria-labelledby', () => { | ||
rootElement.innerHTML = ` | ||
<label id="label-1-of-1" aria-hidden="true" style="display: none;">Label1</label> | ||
<input id="test-el" aria-labelledby="label-1-of-1 label-1-of-1"/> | ||
`; | ||
|
||
const input = rootElement.querySelector('#test-el')!; | ||
expect(_computeAriaAccessibleName(input)).toBe('Label1 Label1'); | ||
}); | ||
|
||
it('returns empty string when passed `<input id="test-el"/>`', () => { | ||
rootElement.innerHTML = `<input id="test-el"/>`; | ||
|
||
const input = rootElement.querySelector('#test-el')!; | ||
expect(_computeAriaAccessibleName(input)).toBe(''); | ||
}); | ||
|
||
it('returns empty string when target of aria-labelledby also has an aria-labelledby', () => { | ||
rootElement.innerHTML = ` | ||
<label id="transitive-label">Label</label> | ||
<div id="transitive-div" aria-labelled-by="transitive-label"></div> | ||
<input id="test-el" aria-labelled-by="transitive-div"/> | ||
`; | ||
|
||
const input = rootElement.querySelector('#test-el')!; | ||
expect(_computeAriaAccessibleName(input)).toBe(''); | ||
}); | ||
|
||
it('returns empty string when when label with for/id has an aria-labelledby', () => { | ||
rootElement.innerHTML = ` | ||
<label for="transitive2-label" aria-labelledby="transitive2-div"></label> | ||
<div id="transitive2-div">Div</div> | ||
<input id="test-el" aria-labelled-by="transitive2-label"/> | ||
`; | ||
|
||
const input = rootElement.querySelector('#test-el')!; | ||
expect(_computeAriaAccessibleName(input)).toBe(''); | ||
}); | ||
|
||
it('does not crash when passed input that is aria-labelledby itself', () => { | ||
rootElement.innerHTML = ` | ||
<input id="test-el" aria-labelled-by="test-el"/> | ||
`; | ||
|
||
const input = rootElement.querySelector('#test-el')!; | ||
const computedName = _computeAriaAccessibleName(input); | ||
expect(typeof computedName) | ||
.withContext('should return value of type string') | ||
.toBe('string'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
/** | ||
* @fileoverview A description of this module. What would someone | ||
* new to your team want to know about the code in this file? | ||
* (DO NOT SUBMIT as is; replace this comment.) | ||
*/ | ||
|
||
export function _computeAriaAccessibleName(element: Element): string { | ||
return _computeAriaAccessibleNameInternal(element, true); | ||
} | ||
|
||
function _computeAriaAccessibleNameInternal( | ||
currentNode: Node, | ||
isDirectlyReferenced: boolean, | ||
): string { | ||
// FIXME: If `currentNode`'s role prohibits naming, return the empty string (""). This is not | ||
// relevant for Datepicker's use case. | ||
|
||
// Step 2A. | ||
// FIXME: return empty string if current element is hidden and not directly referenced by | ||
// aria-labelledby. This is not relevant for Datepicker's use case. | ||
|
||
// Step 2B. aria-labelledby | ||
if (currentNode instanceof Element && isDirectlyReferenced) { | ||
const labelledbyIds: string[] = | ||
currentNode.getAttribute?.('aria-labelledby')?.split(/\s+/g) || []; | ||
const validIdRefs: HTMLElement[] = labelledbyIds.reduce((accum, curr) => { | ||
const elem = document.getElementById(curr); | ||
if (elem) { | ||
accum.push(elem); | ||
} | ||
return accum; | ||
}, [] as HTMLElement[]); | ||
|
||
if (validIdRefs.length) { | ||
return validIdRefs | ||
.map(idRef => { | ||
return _computeAriaAccessibleNameInternal(idRef, false); | ||
}) | ||
.join(' '); | ||
} | ||
} | ||
|
||
// 2C. aria-label | ||
if (currentNode instanceof Element) { | ||
const ariaLabel = currentNode.getAttribute('aria-label')?.trim(); | ||
|
||
if (ariaLabel) { | ||
return ariaLabel; | ||
} | ||
} | ||
|
||
// 2D. attribute or element that defines a text alternative | ||
// only implemented for `<input/>` element | ||
// | ||
// FIXME: Implement for all elements that have an attribute or element that defines a text | ||
// alternative. Only `<label>` and `<input/>` elements are relevant for Datepicker's use case. | ||
if (currentNode instanceof HTMLInputElement) { | ||
// handle label with a for attribute referencing the current node | ||
// ``` | ||
// <label for="current-node">Label</label> | ||
// <input id="current-node"/> | ||
// ``` | ||
const id = currentNode.id; | ||
const fors = document.querySelectorAll(`[for="${id}"]`); | ||
if (fors.length) { | ||
return Array.from(fors) | ||
.map(x => _computeAriaAccessibleNameInternal(x, false)) | ||
.join(' '); | ||
} | ||
|
||
// use the input's placeholder if available `<input placeholder="06/03/1990"/>` | ||
const placeholder = currentNode.getAttribute('placeholder')?.trim(); | ||
if (placeholder) return placeholder; | ||
|
||
// use the input's title if available `<input title="Check-In"/>` | ||
const title = currentNode.getAttribute('title')?.trim(); | ||
if (title) return title; | ||
} | ||
|
||
// 2. E. embedded control's value | ||
// FIXME: implement embedded controls such as textbox, listbox, range, etc. This is not relevant | ||
// for Datepicker's use case. | ||
|
||
// FIXME: Implement check in step 2F. check if current role allows for text from content | ||
// FIXME: Implement step 2Fii. Include CSS generated textual content such as `:before` and | ||
// `:after` pseudo element. | ||
// FIXME: Implement Step 2I. If the curret node has a Tooltip attribute, return it's value. | ||
|
||
// Use textContent to accomplish steps 2F, 2G and 2H. | ||
return (currentNode.textContent || '').replace(/\s+/g, ' ').trim(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.