-
Notifications
You must be signed in to change notification settings - Fork 456
/
role-helpers.js
160 lines (133 loc) 路 4.26 KB
/
role-helpers.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import {elementRoles} from 'aria-query'
import {prettyDOM} from './pretty-dom'
const elementRoleList = buildElementRoleList(elementRoles)
/**
* Partial implementation https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion
* which should only be used for elements with a non-presentational role i.e.
* `role="none"` and `role="presentation"` will not be excluded.
*
* Implements aria-hidden semantics (i.e. parent overrides child)
* Ignores "Child Presentational: True" characteristics
*
* @param {Element} element -
* @returns {boolean} true if excluded, otherwise false
*/
function isInaccessible(element) {
const window = element.ownerDocument.defaultView
const computedStyle = window.getComputedStyle(element)
// since visibility is inherited we can exit early
if (computedStyle.visibility === 'hidden') {
return true
}
// Remove once https://github.com/jsdom/jsdom/issues/2616 is fixed
const supportsStyleInheritance = computedStyle.visibility !== ''
let visibility = computedStyle.visibility
let currentElement = element
while (currentElement) {
if (currentElement.hidden === true) {
return true
}
if (currentElement.getAttribute('aria-hidden') === 'true') {
return true
}
const currentComputedStyle = window.getComputedStyle(currentElement)
if (currentComputedStyle.display === 'none') {
return true
}
// this branch is temporary code until jsdom fixes a bug
// istanbul ignore else
if (supportsStyleInheritance === false) {
// we go bottom-up for an inheritable property so we can only set it
// if it wasn't set already i.e. the parent can't overwrite the child
if (visibility === '') visibility = currentComputedStyle.visibility
if (visibility === 'hidden') {
return true
}
}
currentElement = currentElement.parentElement
}
return false
}
function getImplicitAriaRoles(currentNode) {
// eslint bug here:
// eslint-disable-next-line no-unused-vars
for (const {selector, roles} of elementRoleList) {
if (currentNode.matches(selector)) {
return [...roles]
}
}
return []
}
function buildElementRoleList(elementRolesMap) {
function makeElementSelector({name, attributes = []}) {
return `${name}${attributes
.map(({name: attributeName, value}) =>
value ? `[${attributeName}=${value}]` : `[${attributeName}]`,
)
.join('')}`
}
function getSelectorSpecificity({attributes = []}) {
return attributes.length
}
function bySelectorSpecificity(
{specificity: leftSpecificity},
{specificity: rightSpecificity},
) {
return rightSpecificity - leftSpecificity
}
let result = []
// eslint bug here:
// eslint-disable-next-line no-unused-vars
for (const [element, roles] of elementRolesMap.entries()) {
result = [
...result,
{
selector: makeElementSelector(element),
roles: Array.from(roles),
specificity: getSelectorSpecificity(element),
},
]
}
return result.sort(bySelectorSpecificity)
}
function getRoles(container, {hidden = false} = {}) {
function flattenDOM(node) {
return [
node,
...Array.from(node.children).reduce(
(acc, child) => [...acc, ...flattenDOM(child)],
[],
),
]
}
return flattenDOM(container)
.filter(element => {
return hidden === false ? isInaccessible(element) === false : true
})
.reduce((acc, node) => {
const roles = getImplicitAriaRoles(node)
return roles.reduce(
(rolesAcc, role) =>
Array.isArray(rolesAcc[role])
? {...rolesAcc, [role]: [...rolesAcc[role], node]}
: {...rolesAcc, [role]: [node]},
acc,
)
}, {})
}
function prettyRoles(dom, {hidden}) {
const roles = getRoles(dom, {hidden})
return Object.entries(roles)
.map(([role, elements]) => {
const delimiterBar = '-'.repeat(50)
const elementsString = elements
.map(el => prettyDOM(el.cloneNode(false)))
.join('\n\n')
return `${role}:\n\n${elementsString}\n\n${delimiterBar}`
})
.join('\n')
}
const logRoles = (dom, {hidden = false} = {}) =>
console.log(prettyRoles(dom, {hidden}))
export {getRoles, logRoles, getImplicitAriaRoles, prettyRoles, isInaccessible}
/* eslint no-console:0 */