-
-
Notifications
You must be signed in to change notification settings - Fork 636
/
Copy pathisInteractiveElement.js
134 lines (119 loc) · 4.49 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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
/**
* @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 iterFrom from 'es-iterator-helpers/Iterator.from';
// import iterFlatMap from 'es-iterator-helpers/Iterator.prototype.flatMap';
import filter from 'es-iterator-helpers/Iterator.prototype.filter';
import some from 'es-iterator-helpers/Iterator.prototype.some';
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'
&& !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'
&& 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',
));
// TODO: convert to use iterFlatMap and iterFrom
const interactiveElementRoleSchemas = flatMap(
elementRoleEntries,
([elementSchema, rolesArr]) => (rolesArr.some((role): boolean => interactiveRoles.has(role)) ? [elementSchema] : []),
);
// TODO: convert to use iterFlatMap and iterFrom
const nonInteractiveElementRoleSchemas = flatMap(
elementRoleEntries,
([elementSchema, rolesArr]) => (rolesArr.every((role): boolean => nonInteractiveRoles.has(role)) ? [elementSchema] : []),
);
const interactiveAXObjects = new Set(filter(iterFrom(AXObjects.keys()), (name) => AXObjects.get(name).type === 'widget'));
// TODO: convert to use iterFlatMap and iterFrom
const interactiveElementAXObjectSchemas = flatMap(
[...elementAXObjects],
([elementSchema, AXObjectsArr]) => (AXObjectsArr.every((role): boolean => interactiveAXObjects.has(role)) ? [elementSchema] : []),
);
function checkIsInteractiveElement(tagName, attributes): boolean {
function elementSchemaMatcher(elementSchema) {
return (
tagName === elementSchema.name
&& attributesComparator(elementSchema.attributes, attributes)
);
}
// TODO: remove this when aria-query and axobject-query are upgraded
if (tagName === 'summary') {
return false;
}
// Check in elementRoles for inherent interactive role associations for
// this element.
const isInherentInteractiveElement = some(iterFrom(interactiveElementRoleSchemas), elementSchemaMatcher);
if (isInherentInteractiveElement) {
return true;
}
// Check in elementRoles for inherent non-interactive role associations for
// this element.
const isInherentNonInteractiveElement = some(iterFrom(nonInteractiveElementRoleSchemas), elementSchemaMatcher);
if (isInherentNonInteractiveElement) {
return false;
}
// Check in elementAXObjects for AX Tree associations for this element.
const isInteractiveAXElement = some(iterFrom(interactiveElementAXObjectSchemas), elementSchemaMatcher);
if (
isInteractiveAXElement
|| tagName === 'summary' // TODO: Remove this hard-coded addition once axobject-query is updated.
) {
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 (!dom.has(tagName)) {
return false;
}
return checkIsInteractiveElement(tagName, attributes);
};
export default isInteractiveElement;