/
isInteractiveElement.ts
94 lines (78 loc) · 3.27 KB
/
isInteractiveElement.ts
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
import { dom, elementRoles, roles } from 'aria-query';
import { AXObjects, elementAXObjects } from 'axobject-query';
import { attributesComparator } from './attributesComparator';
import { ElementAst } from '@angular/compiler';
const domKeys = <string[]>Array.from(dom.keys());
const roleKeys: any = <string[]>Array.from(roles.keys());
const elementRoleEntries = Array.from(elementRoles);
const nonInteractiveRoles = new Set(
roleKeys.filter(name => {
const role = roles.get(name);
return !role.abstract && !role.superClass.some(classes => classes.indexOf('widget') !== 0);
})
);
const interactiveRoles: any = new Set(
[
...roleKeys,
// 'toolbar' does not descend from widget, but it does support
// aria-activedescendant, thus in practice we treat it as a widget.
'toolbar'
].filter(name => {
const role = roles.get(name);
return !role.abstract && role.superClass.some(classes => classes.indexOf('widget') !== 0);
})
);
const nonInteractiveElementRoleSchemas = elementRoleEntries.reduce((accumulator: any[], [elementSchema, roleSet]: any) => {
if (Array.from(roleSet).every((role): boolean => nonInteractiveRoles.has(role))) {
accumulator.push(elementSchema);
}
return accumulator;
}, []);
const interactiveElementRoleSchemas = elementRoleEntries.reduce((accumulator: any[], [elementSchema, roleSet]: any) => {
if (Array.from(roleSet).some((role): boolean => interactiveRoles.has(role))) {
accumulator.push(elementSchema);
}
return accumulator;
}, []);
const interactiveAXObjects = new Set(Array.from(AXObjects.keys()).filter(name => AXObjects.get(name).type === 'widget'));
const interactiveElementAXObjectSchemas = Array.from(elementAXObjects).reduce((accumulator: any[], [elementSchema, AXObjectSet]: any) => {
if (Array.from(AXObjectSet).every((role): boolean => interactiveAXObjects.has(role))) {
accumulator.push(elementSchema);
}
return accumulator;
}, []);
function checkIsInteractiveElement(el: ElementAst): boolean {
function elementSchemaMatcher(elementSchema) {
return el.name === elementSchema.name && attributesComparator(elementSchema.attributes, el);
}
// 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.
*/
export const isInteractiveElement = (el: ElementAst): boolean => {
if (domKeys.indexOf(el.name) === -1) {
return false;
}
return checkIsInteractiveElement(el);
};