/**
* @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
// ```
//
//
//
// ```
// 2. `aria-label` attribute (e.g. ``)
// 3. Label with `for`/`id`
// ```
//
//
//
// ```
// 4. `placeholder` attribute (e.g. ``)
// 5. `title` attribute (e.g. ``)
// 6. text content
// ```
//
//
//
// ```
/**
* 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 `