-
-
Notifications
You must be signed in to change notification settings - Fork 636
/
Copy pathisInteractiveElement.js
119 lines (107 loc) · 3.93 KB
/
isInteractiveElement.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
/**
* @flow
*/
import {
dom,
elementRoles,
roles,
} from 'aria-query';
import type { Node } from 'ast-types-flow';
import {
AXObjects,
elementAXObjects,
} from 'axobject-query';
import includes from 'array-includes';
import flatMap from 'array.prototype.flatmap';
import attributesComparator from './attributesComparator';
const domKeys = [...dom.keys()];
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'
&& !role.superClass.some((classes) => includes(classes, 'widget') || includes(classes, 'window'))
);
}).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'
&& role.superClass.some((classes) => includes(classes, 'widget') || includes(classes, 'window'))
);
}).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 interactiveAXObjects = new Set([...AXObjects.keys()]
.filter((name) => AXObjects.get(name).type === 'widget'));
const interactiveElementAXObjectSchemas = flatMap(
[...elementAXObjects],
([elementSchema, AXObjectSet]) => ([...AXObjectSet].every((role): boolean => interactiveAXObjects.has(role)) ? [elementSchema] : []),
);
function checkIsInteractiveElement(tagName, attributes): boolean {
function elementSchemaMatcher(elementSchema) {
return (
tagName === elementSchema.name
&& attributesComparator(elementSchema.attributes, attributes)
);
}
// Check in elementRoles for inherent interactive role associations for
// this element.
const isInherentInteractiveElement = interactiveElementRoleSchemas.some(elementSchemaMatcher);
if (isInherentInteractiveElement) {
return true;
}
// Check in elementRoles for inherent non-interactive role associations for
// this element.
const isInherentNonInteractiveElement = nonInteractiveElementRoleSchemas.some(elementSchemaMatcher);
if (isInherentNonInteractiveElement) {
return false;
}
// Check in elementAXObjects for AX Tree associations for this element.
const isInteractiveAXElement = interactiveElementAXObjectSchemas.some(elementSchemaMatcher);
if (isInteractiveAXElement) {
return true;
}
return false;
}
/**
* Returns boolean indicating whether the given element is
* interactive on the DOM or not. Usually used when an element
* has a dynamic handler on it and we need to discern whether or not
* it's intention is to be interacted with on the DOM.
*/
const isInteractiveElement = (
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 (!includes(domKeys, tagName)) {
return false;
}
return checkIsInteractiveElement(tagName, attributes);
};
export default isInteractiveElement;