Skip to content

Commit ea2fc64

Browse files
eps1lonKent C. Dodds
authored andcommitted
feat(ByRole): Improve perf if multiple elements are found (#395)
* Add benchmmark Still has +-20% in median runtime * Extract isSubtreeInaccessible Useful to inject more performant implementations * Cache isSubtreeInaccessible calls * Remove benchmark
1 parent 4903a82 commit ea2fc64

File tree

4 files changed

+61
-37
lines changed

4 files changed

+61
-37
lines changed

src/__tests__/role-helpers.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ test.each([
171171
['<div style="visibility: visible" />', false],
172172
['<div hidden />', true],
173173
['<div style="display: none;"/>', true],
174-
['<div style="visibility: hidden;"/>', true],
174+
['<div style="visibility: hidden;"/>', false], // bug in jsdom < 15.2
175175
['<div aria-hidden="true" />', true],
176176
])('shouldExcludeFromA11yTree for %s returns %p', (html, expected) => {
177177
const {container} = render(html)

src/__tests__/role.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,18 +115,20 @@ There are no accessible roles. But there might be some inaccessible roles. If yo
115115
})
116116

117117
test('by default excludes elements which have visibility hidden', () => {
118-
const {getByRole} = render('<div style="visibility: hidden;"><ul /></div>')
118+
// works in jsdom < 15.2 only when the actual element in question has this
119+
// css property. only jsdom@^15.2 implements inheritance for `visibility`
120+
const {getByRole} = render('<div><ul style="visibility: hidden;" /></div>')
119121

120122
expect(() => getByRole('list')).toThrowErrorMatchingInlineSnapshot(`
121123
"Unable to find an accessible element with the role "list"
122124
123125
There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the \`hidden\` option to \`true\`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole
124126
125127
<div>
126-
<div
127-
style="visibility: hidden;"
128-
>
129-
<ul />
128+
<div>
129+
<ul
130+
style="visibility: hidden;"
131+
/>
130132
</div>
131133
</div>"
132134
`)

src/queries/role.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
getImplicitAriaRoles,
33
prettyRoles,
44
isInaccessible,
5+
isSubtreeInaccessible,
56
} from '../role-helpers'
67
import {buildQueries, fuzzyMatches, makeNormalizer, matches} from './all-utils'
78

@@ -13,6 +14,15 @@ function queryAllByRole(
1314
const matcher = exact ? matches : fuzzyMatches
1415
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
1516

17+
const subtreeIsInaccessibleCache = new WeakMap()
18+
function cachedIsSubtreeInaccessible(element) {
19+
if (!subtreeIsInaccessibleCache.has(element)) {
20+
subtreeIsInaccessibleCache.set(element, isSubtreeInaccessible(element))
21+
}
22+
23+
return subtreeIsInaccessibleCache.get(element)
24+
}
25+
1626
return Array.from(container.querySelectorAll('*'))
1727
.filter(node => {
1828
const isRoleSpecifiedExplicitly = node.hasAttribute('role')
@@ -28,7 +38,11 @@ function queryAllByRole(
2838
)
2939
})
3040
.filter(element => {
31-
return hidden === false ? isInaccessible(element) === false : true
41+
return hidden === false
42+
? isInaccessible(element, {
43+
isSubtreeInaccessible: cachedIsSubtreeInaccessible,
44+
}) === false
45+
: true
3246
})
3347
}
3448

src/role-helpers.js

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,27 @@ import {prettyDOM} from './pretty-dom'
33

44
const elementRoleList = buildElementRoleList(elementRoles)
55

6+
/**
7+
* @param {Element} element -
8+
* @returns {boolean} - `true` if `element` and its subtree are inaccessible
9+
*/
10+
function isSubtreeInaccessible(element) {
11+
if (element.hidden === true) {
12+
return true
13+
}
14+
15+
if (element.getAttribute('aria-hidden') === 'true') {
16+
return true
17+
}
18+
19+
const window = element.ownerDocument.defaultView
20+
if (window.getComputedStyle(element).display === 'none') {
21+
return true
22+
}
23+
24+
return false
25+
}
26+
627
/**
728
* Partial implementation https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion
829
* which should only be used for elements with a non-presentational role i.e.
@@ -12,47 +33,27 @@ const elementRoleList = buildElementRoleList(elementRoles)
1233
* Ignores "Child Presentational: True" characteristics
1334
*
1435
* @param {Element} element -
36+
* @param {object} [options] -
37+
* @param {function (element: Element): boolean} options.isSubtreeInaccessible -
38+
* can be used to return cached results from previous isSubtreeInaccessible calls
1539
* @returns {boolean} true if excluded, otherwise false
1640
*/
17-
function isInaccessible(element) {
41+
function isInaccessible(element, options = {}) {
42+
const {
43+
isSubtreeInaccessible: isSubtreeInaccessibleImpl = isSubtreeInaccessible,
44+
} = options
1845
const window = element.ownerDocument.defaultView
19-
const computedStyle = window.getComputedStyle(element)
2046
// since visibility is inherited we can exit early
21-
if (computedStyle.visibility === 'hidden') {
47+
if (window.getComputedStyle(element).visibility === 'hidden') {
2248
return true
2349
}
2450

25-
// Remove once https://github.com/jsdom/jsdom/issues/2616 is fixed
26-
const supportsStyleInheritance = computedStyle.visibility !== ''
27-
let visibility = computedStyle.visibility
28-
2951
let currentElement = element
3052
while (currentElement) {
31-
if (currentElement.hidden === true) {
53+
if (isSubtreeInaccessibleImpl(currentElement)) {
3254
return true
3355
}
3456

35-
if (currentElement.getAttribute('aria-hidden') === 'true') {
36-
return true
37-
}
38-
39-
const currentComputedStyle = window.getComputedStyle(currentElement)
40-
41-
if (currentComputedStyle.display === 'none') {
42-
return true
43-
}
44-
45-
// this branch is temporary code until jsdom fixes a bug
46-
// istanbul ignore else
47-
if (supportsStyleInheritance === false) {
48-
// we go bottom-up for an inheritable property so we can only set it
49-
// if it wasn't set already i.e. the parent can't overwrite the child
50-
if (visibility === '') visibility = currentComputedStyle.visibility
51-
if (visibility === 'hidden') {
52-
return true
53-
}
54-
}
55-
5657
currentElement = currentElement.parentElement
5758
}
5859

@@ -155,6 +156,13 @@ function prettyRoles(dom, {hidden}) {
155156
const logRoles = (dom, {hidden = false} = {}) =>
156157
console.log(prettyRoles(dom, {hidden}))
157158

158-
export {getRoles, logRoles, getImplicitAriaRoles, prettyRoles, isInaccessible}
159+
export {
160+
getRoles,
161+
logRoles,
162+
getImplicitAriaRoles,
163+
isSubtreeInaccessible,
164+
prettyRoles,
165+
isInaccessible,
166+
}
159167

160168
/* eslint no-console:0 */

0 commit comments

Comments
 (0)