forked from jsx-eslint/eslint-plugin-jsx-a11y
-
-
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.
[New] add
getAccessibleChildText
util
- Loading branch information
Showing
2 changed files
with
151 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'); | ||
}); | ||
}); |
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,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); | ||
} |