180 lines
6.9 KiB
TypeScript
180 lines
6.9 KiB
TypeScript
|
|
/**
|
||
|
|
* @license
|
||
|
|
* Copyright Google LLC All Rights Reserved.
|
||
|
|
*
|
||
|
|
* Use of this source code is governed by an MIT-style license that can be
|
||
|
|
* found in the LICENSE file at https://angular.dev/license
|
||
|
|
*/
|
||
|
|
|
||
|
|
// This file contains the `_computeAriaAccessibleName` function, which computes what the *expected*
|
||
|
|
// ARIA accessible name would be for a given element. Implements a subset of ARIA specification
|
||
|
|
// [Accessible Name and Description Computation 1.2](https://www.w3.org/TR/accname-1.2/).
|
||
|
|
//
|
||
|
|
// Specification accname-1.2 can be summarized by returning the result of the first method
|
||
|
|
// available.
|
||
|
|
//
|
||
|
|
// 1. `aria-labelledby` attribute
|
||
|
|
// ```
|
||
|
|
// <!-- example using aria-labelledby-->
|
||
|
|
// <label id='label-id'>Start Date</label>
|
||
|
|
// <input aria-labelledby='label-id'/>
|
||
|
|
// ```
|
||
|
|
// 2. `aria-label` attribute (e.g. `<input aria-label="Departure"/>`)
|
||
|
|
// 3. Label with `for`/`id`
|
||
|
|
// ```
|
||
|
|
// <!-- example using for/id -->
|
||
|
|
// <label for="current-node">Label</label>
|
||
|
|
// <input id="current-node"/>
|
||
|
|
// ```
|
||
|
|
// 4. `placeholder` attribute (e.g. `<input placeholder="06/03/1990"/>`)
|
||
|
|
// 5. `title` attribute (e.g. `<input title="Check-In"/>`)
|
||
|
|
// 6. text content
|
||
|
|
// ```
|
||
|
|
// <!-- example using text content -->
|
||
|
|
// <label for="current-node"><span>Departure</span> Date</label>
|
||
|
|
// <input id="current-node"/>
|
||
|
|
// ```
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Computes the *expected* ARIA accessible name for argument element based on [accname-1.2
|
||
|
|
* specification](https://www.w3.org/TR/accname-1.2/). Implements a subset of accname-1.2,
|
||
|
|
* and should only be used for the Datepicker's specific use case.
|
||
|
|
*
|
||
|
|
* Intended use:
|
||
|
|
* This is not a general use implementation. Only implements the parts of accname-1.2 that are
|
||
|
|
* required for the Datepicker's specific use case. This function is not intended for any other
|
||
|
|
* use.
|
||
|
|
*
|
||
|
|
* Limitations:
|
||
|
|
* - Only covers the needs of `matStartDate` and `matEndDate`. Does not support other use cases.
|
||
|
|
* - See NOTES's in implementation for specific details on what parts of the accname-1.2
|
||
|
|
* specification are not implemented.
|
||
|
|
*
|
||
|
|
* @param element {HTMLInputElement} native <input/> element of `matStartDate` or
|
||
|
|
* `matEndDate` component. Corresponds to the 'Root Element' from accname-1.2
|
||
|
|
*
|
||
|
|
* @return expected ARIA accessible name of argument <input/>
|
||
|
|
*/
|
||
|
|
export function _computeAriaAccessibleName(
|
||
|
|
element: HTMLInputElement | HTMLTextAreaElement,
|
||
|
|
): string {
|
||
|
|
return _computeAriaAccessibleNameInternal(element, true);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Determine if argument node is an Element based on `nodeType` property. This function is safe to
|
||
|
|
* use with server-side rendering.
|
||
|
|
*/
|
||
|
|
function ssrSafeIsElement(node: Node): node is Element {
|
||
|
|
return node.nodeType === Node.ELEMENT_NODE;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Determine if argument node is an HTMLInputElement based on `nodeName` property. This funciton is
|
||
|
|
* safe to use with server-side rendering.
|
||
|
|
*/
|
||
|
|
function ssrSafeIsHTMLInputElement(node: Node): node is HTMLInputElement {
|
||
|
|
return node.nodeName === 'INPUT';
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Determine if argument node is an HTMLTextAreaElement based on `nodeName` property. This
|
||
|
|
* funciton is safe to use with server-side rendering.
|
||
|
|
*/
|
||
|
|
function ssrSafeIsHTMLTextAreaElement(node: Node): node is HTMLTextAreaElement {
|
||
|
|
return node.nodeName === 'TEXTAREA';
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Calculate the expected ARIA accessible name for given DOM Node. Given DOM Node may be either the
|
||
|
|
* "Root node" passed to `_computeAriaAccessibleName` or "Current node" as result of recursion.
|
||
|
|
*
|
||
|
|
* @return the accessible name of argument DOM Node
|
||
|
|
*
|
||
|
|
* @param currentNode node to determine accessible name of
|
||
|
|
* @param isDirectlyReferenced true if `currentNode` is the root node to calculate ARIA accessible
|
||
|
|
* name of. False if it is a result of recursion.
|
||
|
|
*/
|
||
|
|
function _computeAriaAccessibleNameInternal(
|
||
|
|
currentNode: Node,
|
||
|
|
isDirectlyReferenced: boolean,
|
||
|
|
): string {
|
||
|
|
// NOTE: this differs from accname-1.2 specification.
|
||
|
|
// - Does not implement Step 1. of accname-1.2: '''If `currentNode`'s role prohibits naming,
|
||
|
|
// return the empty string ("")'''.
|
||
|
|
// - Does not implement Step 2.A. of accname-1.2: '''if current node is hidden and not directly
|
||
|
|
// referenced by aria-labelledby... return the empty string.'''
|
||
|
|
|
||
|
|
// acc-name-1.2 Step 2.B.: aria-labelledby
|
||
|
|
if (ssrSafeIsElement(currentNode) && isDirectlyReferenced) {
|
||
|
|
const labelledbyIds: string[] =
|
||
|
|
currentNode.getAttribute?.('aria-labelledby')?.split(/\s+/g) || [];
|
||
|
|
const validIdRefs: HTMLElement[] = labelledbyIds.reduce((validIds, id) => {
|
||
|
|
const elem = document.getElementById(id);
|
||
|
|
if (elem) {
|
||
|
|
validIds.push(elem);
|
||
|
|
}
|
||
|
|
return validIds;
|
||
|
|
}, [] as HTMLElement[]);
|
||
|
|
|
||
|
|
if (validIdRefs.length) {
|
||
|
|
return validIdRefs
|
||
|
|
.map(idRef => {
|
||
|
|
return _computeAriaAccessibleNameInternal(idRef, false);
|
||
|
|
})
|
||
|
|
.join(' ');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// acc-name-1.2 Step 2.C.: aria-label
|
||
|
|
if (ssrSafeIsElement(currentNode)) {
|
||
|
|
const ariaLabel = currentNode.getAttribute('aria-label')?.trim();
|
||
|
|
|
||
|
|
if (ariaLabel) {
|
||
|
|
return ariaLabel;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// acc-name-1.2 Step 2.D. attribute or element that defines a text alternative
|
||
|
|
//
|
||
|
|
// NOTE: this differs from accname-1.2 specification.
|
||
|
|
// Only implements Step 2.D. for `<label>`,`<input/>`, and `<textarea/>` element. Does not
|
||
|
|
// implement other elements that have an attribute or element that defines a text alternative.
|
||
|
|
if (ssrSafeIsHTMLInputElement(currentNode) || ssrSafeIsHTMLTextAreaElement(currentNode)) {
|
||
|
|
// use label with a `for` attribute referencing the current node
|
||
|
|
if (currentNode.labels?.length) {
|
||
|
|
return Array.from(currentNode.labels)
|
||
|
|
.map(x => _computeAriaAccessibleNameInternal(x, false))
|
||
|
|
.join(' ');
|
||
|
|
}
|
||
|
|
|
||
|
|
// use placeholder if available
|
||
|
|
const placeholder = currentNode.getAttribute('placeholder')?.trim();
|
||
|
|
if (placeholder) {
|
||
|
|
return placeholder;
|
||
|
|
}
|
||
|
|
|
||
|
|
// use title if available
|
||
|
|
const title = currentNode.getAttribute('title')?.trim();
|
||
|
|
if (title) {
|
||
|
|
return title;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// NOTE: this differs from accname-1.2 specification.
|
||
|
|
// - does not implement acc-name-1.2 Step 2.E.: '''if the current node is a control embedded
|
||
|
|
// within the label... then include the embedded control as part of the text alternative in
|
||
|
|
// the following manner...'''. Step 2E applies to embedded controls such as textbox, listbox,
|
||
|
|
// range, etc.
|
||
|
|
// - does not implement acc-name-1.2 step 2.F.: check that '''role allows name from content''',
|
||
|
|
// which applies to `currentNode` and its children.
|
||
|
|
// - does not implement acc-name-1.2 Step 2.F.ii.: '''Check for CSS generated textual content'''
|
||
|
|
// (e.g. :before and :after).
|
||
|
|
// - does not implement acc-name-1.2 Step 2.I.: '''if the current node has a Tooltip attribute,
|
||
|
|
// return its value'''
|
||
|
|
|
||
|
|
// Return text content with whitespace collapsed into a single space character. Accomplish
|
||
|
|
// acc-name-1.2 steps 2F, 2G, and 2H.
|
||
|
|
return (currentNode.textContent || '').replace(/\s+/g, ' ').trim();
|
||
|
|
}
|