Skip to content

Commit 2143514

Browse files
committed
Added isNonInteractiveElement util
1 parent 5a9a7b9 commit 2143514

File tree

3 files changed

+207
-8
lines changed

3 files changed

+207
-8
lines changed

__mocks__/genInteractives.js

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
/**
2+
* @flow
3+
*/
4+
15
import {
26
dom,
37
roles,
@@ -6,6 +10,7 @@ import JSXAttributeMock from './JSXAttributeMock';
610
import JSXElementMock from './JSXElementMock';
711

812
const domElements = [...dom.keys()];
13+
const roleNames = [...roles.keys()];
914

1015
const pureInteractiveElements = domElements
1116
.filter(name => dom.get(name).interactive === true)
@@ -27,12 +32,36 @@ const interactiveElementsMap = {
2732
],
2833
};
2934

30-
const pureNonInteractiveElementsMap = domElements
31-
.filter(name => !dom.get(name).interactive)
32-
.reduce((nonInteractiveElements, name) => {
33-
nonInteractiveElements[name] = [];
34-
return nonInteractiveElements;
35-
}, {});
35+
const pureNonInteractiveElementsMap = {
36+
a: [],
37+
area: [],
38+
article: [],
39+
dd: [],
40+
dfn: [],
41+
dt: [],
42+
fieldset: [],
43+
figure: [],
44+
form: [],
45+
frame: [],
46+
h1: [],
47+
h2: [],
48+
h3: [],
49+
h4: [],
50+
h5: [],
51+
h6: [],
52+
hr: [],
53+
img: [],
54+
input: [],
55+
li: [],
56+
nav: [],
57+
ol: [],
58+
table: [],
59+
tbody: [],
60+
tfoot: [],
61+
thead: [],
62+
tr: [],
63+
ul: [],
64+
};
3665

3766
const nonInteractiveElementsMap = {
3867
...pureNonInteractiveElementsMap,
@@ -41,10 +70,26 @@ const nonInteractiveElementsMap = {
4170
],
4271
};
4372

44-
const roleNames = [...roles.keys()];
73+
const indeterminantInteractiveElementsMap = domElements
74+
.reduce(
75+
(
76+
accumulator: {[key: string]: Array<any>},
77+
name: string
78+
): {[key: string]: Array<any>} => {
79+
accumulator[name] = [];
80+
return accumulator;
81+
},
82+
{},
83+
);
84+
85+
Object.keys(interactiveElementsMap)
86+
.concat(Object.keys(nonInteractiveElementsMap))
87+
.forEach(
88+
(name: string) => delete indeterminantInteractiveElementsMap[name]
89+
);
4590

4691
const interactiveRoles = roleNames.filter(
47-
role => roles.get(role).interactive === true
92+
role => roles[role].interactive === true
4893
);
4994

5095
const nonInteractiveRoles = roleNames.filter(
@@ -86,3 +131,13 @@ export function genNonInteractiveRoleElements () {
86131
])
87132
);
88133
}
134+
135+
export function genIndeterminantInteractiveElements () {
136+
return Object.keys(indeterminantInteractiveElementsMap)
137+
.map(name => {
138+
const attributes = indeterminantInteractiveElementsMap[name].map(
139+
({prop, value}) => JSXAttributeMock(prop, value)
140+
);
141+
return JSXElementMock(name, attributes);
142+
});
143+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/* eslint-env mocha */
2+
import expect from 'expect';
3+
import { elementType } from 'jsx-ast-utils';
4+
import isNonInteractiveElement from '../../../src/util/isNonInteractiveElement';
5+
import {
6+
genIndeterminantInteractiveElements,
7+
genInteractiveElements,
8+
genNonInteractiveElements,
9+
} from '../../../__mocks__/genInteractives';
10+
11+
describe('isNonInteractiveElement', () => {
12+
describe('JSX Components (no tagName)', () => {
13+
it('should identify them as interactive elements', () => {
14+
expect(isNonInteractiveElement(undefined, []))
15+
.toBe(false);
16+
});
17+
});
18+
describe('non-interactive elements', () => {
19+
genNonInteractiveElements().forEach(
20+
({ openingElement }) => {
21+
it(`should identify \`${openingElement.name.name}\` as a non-interactive element`, () => {
22+
expect(isNonInteractiveElement(
23+
elementType(openingElement),
24+
openingElement.attributes,
25+
)).toBe(true);
26+
});
27+
},
28+
);
29+
});
30+
describe('interactive elements', () => {
31+
genInteractiveElements().forEach(
32+
({ openingElement }) => {
33+
it(`should not identify \`${openingElement.name.name}\` as a non-interactive element`, () => {
34+
expect(isNonInteractiveElement(
35+
elementType(openingElement),
36+
openingElement.attributes,
37+
)).toBe(false);
38+
});
39+
},
40+
);
41+
});
42+
describe('indeterminate elements', () => {
43+
genIndeterminantInteractiveElements().forEach(
44+
({ openingElement }) => {
45+
it(`should not identify \`${openingElement.name.name}\` as a non-interactive element`, () => {
46+
expect(isNonInteractiveElement(
47+
elementType(openingElement),
48+
openingElement.attributes,
49+
)).toBe(false);
50+
});
51+
},
52+
);
53+
});
54+
});

src/util/isNonInteractiveElement.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* @flow
3+
*/
4+
5+
import {
6+
roles,
7+
elementRoles,
8+
roleElements,
9+
} from 'aria-query';
10+
import { getProp, getPropValue, getLiteralPropValue } from 'jsx-ast-utils';
11+
import getTabIndex from './getTabIndex';
12+
import DOMElements from './attributes/DOM.json';
13+
14+
const nonInteractiveRoles = new Set(
15+
[...roles.keys()].filter(name => !roles.get(name).interactive),
16+
);
17+
18+
const pureNonInteractiveElements = [...elementRoles.entries()]
19+
.reduce((accumulator, [elementSchemaJSON, roleSet]): {
20+
[elementName: string]: (attributes: Array<Object>) => boolean,
21+
} => {
22+
const nonInteractiveElements = accumulator;
23+
const elementSchema = JSON.parse(elementSchemaJSON);
24+
const elementName = elementSchema.name;
25+
const elementAttributes = elementSchema.attributes || [];
26+
nonInteractiveElements[elementName] = (attributes: Array<Object>): boolean => {
27+
const passedAttrCheck =
28+
elementAttributes.length === 0 ||
29+
elementAttributes.every(
30+
(controlAttr): boolean => attributes.some(
31+
(attr): boolean => (
32+
controlAttr.name === propName(attr).toLowerCase()
33+
&& controlAttr.value === getLiteralPropValue(attr)
34+
),
35+
),
36+
);
37+
return passedAttrCheck && [...roleSet.keys()].every(
38+
(roleName): boolean => nonInteractiveRoles.has(roleName),
39+
);
40+
};
41+
return nonInteractiveElements;
42+
}, {});
43+
44+
const isNotLink = function isNotLink(attributes) {
45+
const href = getPropValue(getProp(attributes, 'href'));
46+
const tabIndex = getTabIndex(getProp(attributes, 'tabIndex'));
47+
return href === undefined && tabIndex === undefined;
48+
};
49+
50+
export const nonInteractiveElementsMap = {
51+
...pureNonInteractiveElements,
52+
a: isNotLink,
53+
area: isNotLink,
54+
input: (attributes) => {
55+
const typeAttr = getLiteralPropValue(getProp(attributes, 'type'));
56+
return typeAttr ? typeAttr.toUpperCase() === 'HIDDEN' : false;
57+
},
58+
};
59+
60+
/**
61+
* Returns boolean indicating whether the given element is a non-interactive
62+
* element. If the element has either a non-interactive role assigned or it
63+
* is an element with an inherently non-interactive role, then this utility
64+
* returns true. Elements that lack either an explicitly assigned role or
65+
* an inherent role are not considered. For those, this utility returns false
66+
* because a positive determination of interactiveness cannot be determined.
67+
*/
68+
const isNonInteractiveElement = (tagName, attributes) => {
69+
// Do not test higher level JSX components, as we do not know what
70+
// low-level DOM element this maps to.
71+
if (Object.keys(DOMElements).indexOf(tagName) === -1) {
72+
return false;
73+
}
74+
75+
// The element has a role.
76+
const role = getLiteralPropValue(getProp(attributes, 'role'));
77+
if (role) {
78+
return nonInteractiveRoles.has(role);
79+
}
80+
81+
// The element does not have an explicit role, determine if it has an
82+
// inherently non-interactive role.
83+
if ({}.hasOwnProperty.call(nonInteractiveElementsMap, tagName) === false) {
84+
return false;
85+
}
86+
87+
return nonInteractiveElementsMap[tagName](attributes);
88+
};
89+
90+
export default isNonInteractiveElement;

0 commit comments

Comments
 (0)