55 changes: 55 additions & 0 deletions src/util/getAccessibleChildText.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// @flow

import type { JSXElement, JSXOpeningElement, Node } from 'ast-types-flow';

import { getProp, getLiteralPropValue } from 'jsx-ast-utils';

import isHiddenFromScreenReader from './isHiddenFromScreenReader';

/**
* Returns a new "standardized" string: all whitespace is collapsed to one space,
* and the string is lowercase
* @param {string} input
* @returns lowercase, single-spaced, trimmed string
*/
function standardizeSpaceAndCase(input: string): string {
return input.trim().replace(/\s\s+/g, ' ').toLowerCase();
}

/**
* Returns the (recursively-defined) accessible child text of a node, which (in-order) is:
* 1. The element's aria-label
* 2. If the element is a direct literal, the literal value
* 3. Otherwise, merge all of its children
* @param {JSXElement} node - node to traverse
* @returns child text as a string
*/
export default function getAccessibleChildText(node: JSXElement, elementType: (JSXOpeningElement) => string): string {
const ariaLabel = getLiteralPropValue(getProp(node.openingElement.attributes, 'aria-label'));
// early escape-hatch when aria-label is applied
if (ariaLabel) return standardizeSpaceAndCase(ariaLabel);

// skip if aria-hidden is true
if (
isHiddenFromScreenReader(
elementType(node.openingElement),
node.openingElement.attributes,
)
) {
return '';
}

const rawChildText = node.children
.map((currentNode: Node): string => {
// $FlowFixMe JSXText is missing in ast-types-flow
if (currentNode.type === 'Literal' || currentNode.type === 'JSXText') {
return String(currentNode.value);
}
if (currentNode.type === 'JSXElement') {
return getAccessibleChildText(currentNode, elementType);
}
return '';
}).join(' ');

return standardizeSpaceAndCase(rawChildText);
}