Skip to content

Commit

Permalink
[New] add getAccessibleChildText util
Browse files Browse the repository at this point in the history
  • Loading branch information
mattxwang authored and ljharb committed Jul 9, 2022
1 parent d678a98 commit 630116b
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 0 deletions.
96 changes: 96 additions & 0 deletions __tests__/src/util/getAccessibleChildText-test.js
@@ -0,0 +1,96 @@
import expect from 'expect';
import { elementType } from 'jsx-ast-utils';
import getAccessibleChildText from '../../../src/util/getAccessibleChildText';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
import JSXElementMock from '../../../__mocks__/JSXElementMock';

describe('getAccessibleChildText', () => {
it('returns the aria-label when present', () => {
expect(getAccessibleChildText(JSXElementMock(
'a',
[JSXAttributeMock('aria-label', 'foo')],
), elementType)).toBe('foo');
});

it('returns the aria-label instead of children', () => {
expect(getAccessibleChildText(JSXElementMock(
'a',
[JSXAttributeMock('aria-label', 'foo')],
[{ type: 'JSXText', value: 'bar' }],
), elementType)).toBe('foo');
});

it('skips elements with aria-hidden=true', () => {
expect(getAccessibleChildText(JSXElementMock(
'a',
[JSXAttributeMock('aria-hidden', 'true')],
), elementType)).toBe('');
});

it('returns literal value for JSXText child', () => {
expect(getAccessibleChildText(JSXElementMock(
'a',
[],
[{ type: 'JSXText', value: 'bar' }],
), elementType)).toBe('bar');
});

it('returns literal value for JSXText child', () => {
expect(getAccessibleChildText(JSXElementMock(
'a',
[],
[{ type: 'Literal', value: 'bar' }],
), elementType)).toBe('bar');
});

it('returns recursive value for JSXElement child', () => {
expect(getAccessibleChildText(JSXElementMock(
'a',
[],
[JSXElementMock(
'span',
[],
[{ type: 'Literal', value: 'bar' }],
)],
), elementType)).toBe('bar');
});

it('skips children with aria-hidden-true', () => {
expect(getAccessibleChildText(JSXElementMock(
'a',
[],
[JSXElementMock(
'span',
[],
[JSXElementMock(
'span',
[JSXAttributeMock('aria-hidden', 'true')],
)],
)],
), elementType)).toBe('');
});

it('joins multiple children properly - no spacing', () => {
expect(getAccessibleChildText(JSXElementMock(
'a',
[],
[{ type: 'Literal', value: 'foo' }, { type: 'Literal', value: 'bar' }],
), elementType)).toBe('foo bar');
});

it('joins multiple children properly - with spacing', () => {
expect(getAccessibleChildText(JSXElementMock(
'a',
[],
[{ type: 'Literal', value: ' foo ' }, { type: 'Literal', value: ' bar ' }],
), elementType)).toBe('foo bar');
});

it('skips unknown elements', () => {
expect(getAccessibleChildText(JSXElementMock(
'a',
[],
[{ type: 'Literal', value: 'foo' }, { type: 'Unknown' }, { type: 'Literal', value: 'bar' }],
), elementType)).toBe('foo bar');
});
});
55 changes: 55 additions & 0 deletions src/util/getAccessibleChildText.js
@@ -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);
}

0 comments on commit 630116b

Please sign in to comment.