-
Notifications
You must be signed in to change notification settings - Fork 3
/
isNonInteractiveElement.js
138 lines (124 loc) · 4.68 KB
/
isNonInteractiveElement.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
/**
* @flow
*/
import {
dom,
elementRoles,
roles,
} from 'aria-query';
import {
AXObjects,
elementAXObjects,
} from 'axobject-query';
import type { Node } from 'ast-types-flow';
import includes from 'array-includes';
import flatMap from 'array.prototype.flatmap';
import attributesComparator from './attributesComparator';
const roleKeys = [...roles.keys()];
const elementRoleEntries = [...elementRoles];
const nonInteractiveRoles = new Set(roleKeys
.filter((name) => {
const role = roles.get(name);
return (
!role.abstract
// 'toolbar' does not descend from widget, but it does support
// aria-activedescendant, thus in practice we treat it as a widget.
&& name !== 'toolbar'
// This role is meant to have no semantic value.
// @see https://www.w3.org/TR/wai-aria-1.2/#generic
&& name !== 'generic'
&& !role.superClass.some((classes) => includes(classes, 'widget'))
);
}).concat(
// The `progressbar` is descended from `widget`, but in practice, its
// value is always `readonly`, so we treat it as a non-interactive role.
'progressbar',
));
const interactiveRoles = new Set(roleKeys
.filter((name) => {
const role = roles.get(name);
return (
!role.abstract
// The `progressbar` is descended from `widget`, but in practice, its
// value is always `readonly`, so we treat it as a non-interactive role.
&& name !== 'progressbar'
// This role is meant to have no semantic value.
// @see https://www.w3.org/TR/wai-aria-1.2/#generic
&& name !== 'generic'
&& role.superClass.some((classes) => includes(classes, 'widget'))
);
}).concat(
// 'toolbar' does not descend from widget, but it does support
// aria-activedescendant, thus in practice we treat it as a widget.
'toolbar',
));
const nonInteractiveElementRoleSchemas = flatMap(
elementRoleEntries,
([elementSchema, roleSet]) => ([...roleSet].every((role): boolean => nonInteractiveRoles.has(role)) ? [elementSchema] : []),
);
const interactiveElementRoleSchemas = flatMap(
elementRoleEntries,
([elementSchema, roleSet]) => ([...roleSet].some((role): boolean => interactiveRoles.has(role)) ? [elementSchema] : []),
);
const nonInteractiveAXObjects = new Set([...AXObjects.keys()]
.filter((name) => includes(['window', 'structure'], AXObjects.get(name).type)));
const nonInteractiveElementAXObjectSchemas = flatMap(
[...elementAXObjects],
([elementSchema, AXObjectSet]) => ([...AXObjectSet].every((role): boolean => nonInteractiveAXObjects.has(role)) ? [elementSchema] : []),
);
function checkIsNonInteractiveElement(tagName, attributes): boolean {
function elementSchemaMatcher(elementSchema) {
return (
tagName === elementSchema.name
&& attributesComparator(elementSchema.attributes, attributes)
);
}
// Check in elementRoles for inherent non-interactive role associations for
// this element.
const isInherentNonInteractiveElement = nonInteractiveElementRoleSchemas
.some(elementSchemaMatcher);
if (isInherentNonInteractiveElement) {
return true;
}
// Check in elementRoles for inherent interactive role associations for
// this element.
const isInherentInteractiveElement = interactiveElementRoleSchemas
.some(elementSchemaMatcher);
if (isInherentInteractiveElement) {
return false;
}
// Check in elementAXObjects for AX Tree associations for this element.
const isNonInteractiveAXElement = nonInteractiveElementAXObjectSchemas
.some(elementSchemaMatcher);
if (isNonInteractiveAXElement) {
return true;
}
return false;
}
/**
* Returns boolean indicating whether the given element is a non-interactive
* element. If the element has either a non-interactive role assigned or it
* is an element with an inherently non-interactive role, then this utility
* returns true. Elements that lack either an explicitly assigned role or
* an inherent role are not considered. For those, this utility returns false
* because a positive determination of interactiveness cannot be determined.
*/
const isNonInteractiveElement = (
tagName: string,
attributes: Array<Node>,
): boolean => {
// Do not test higher level JSX components, as we do not know what
// low-level DOM element this maps to.
if (!dom.has(tagName)) {
return false;
}
// <header> elements do not technically have semantics, unless the
// element is a direct descendant of <body>, and this plugin cannot
// reliably test that.
// @see https://www.w3.org/TR/wai-aria-practices/examples/landmarks/banner.html
if (tagName === 'header') {
return false;
}
return checkIsNonInteractiveElement(tagName, attributes);
};
export default isNonInteractiveElement;