Skip to content

Commit 750b894

Browse files
authored
Merge pull request #194 from jessebeach/master
Introduce static/noninteractive/interactive role concepts to the plugin
2 parents 5a9a7b9 + 595169c commit 750b894

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+3451
-853
lines changed

__mocks__/genInteractives.js

Lines changed: 170 additions & 41 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,63 +10,153 @@ import JSXAttributeMock from './JSXAttributeMock';
610
import JSXElementMock from './JSXElementMock';
711

812
const domElements = [...dom.keys()];
9-
10-
const pureInteractiveElements = domElements
11-
.filter(name => dom.get(name).interactive === true)
12-
.reduce((interactiveElements, name) => {
13-
interactiveElements[name] = [];
14-
return interactiveElements;
15-
}, {});
13+
const roleNames = [...roles.keys()];
1614

1715
const interactiveElementsMap = {
18-
...pureInteractiveElements,
19-
a: [
20-
{prop: 'href', value: '#'}
21-
],
22-
area: [
23-
{prop: 'href', value: '#'}
24-
],
25-
input: [
26-
{prop: 'type', value: 'text'}
27-
],
16+
a: [{prop: 'href', value: '#'}],
17+
area: [{prop: 'href', value: '#'}],
18+
button: [],
19+
input: [],
20+
'input[type=\"button\"]': [{prop: 'type', value: 'button'}],
21+
'input[type=\"checkbox\"]': [{prop: 'type', value: 'checkbox'}],
22+
'input[type=\"color\"]': [{prop: 'type', value: 'color'}],
23+
'input[type=\"date\"]': [{prop: 'type', value: 'date'}],
24+
'input[type=\"datetime\"]': [{prop: 'type', value: 'datetime'}],
25+
'input[type=\"datetime\"]': [{prop: 'type', value: 'datetime'}],
26+
'input[type=\"email\"]': [{prop: 'type', value: 'email'}],
27+
'input[type=\"file\"]': [{prop: 'type', value: 'file'}],
28+
'input[type=\"image\"]': [{prop: 'type', value: 'image'}],
29+
'input[type=\"month\"]': [{prop: 'type', value: 'month'}],
30+
'input[type=\"number\"]': [{prop: 'type', value: 'number'}],
31+
'input[type=\"password\"]': [{prop: 'type', value: 'password'}],
32+
'input[type=\"radio\"]': [{prop: 'type', value: 'radio'}],
33+
'input[type=\"range\"]': [{prop: 'type', value: 'range'}],
34+
'input[type=\"reset\"]': [{prop: 'type', value: 'reset'}],
35+
'input[type=\"search\"]': [{prop: 'type', value: 'search'}],
36+
'input[type=\"submit\"]': [{prop: 'type', value: 'submit'}],
37+
'input[type=\"tel\"]': [{prop: 'type', value: 'tel'}],
38+
'input[type=\"text\"]': [{prop: 'type', value: 'text'}],
39+
'input[type=\"time\"]': [{prop: 'type', value: 'time'}],
40+
'input[type=\"url\"]': [{prop: 'type', value: 'url'}],
41+
'input[type=\"week\"]': [{prop: 'type', value: 'week'}],
42+
menuitem:[],
43+
option: [],
44+
select: [],
45+
'table[role="grid"]': [{prop: 'role', value: 'grid'}],
46+
'td[role="gridcell"]': [{prop: 'role', value: 'gridcell'}],
47+
tr: [],
48+
textarea: [],
2849
};
2950

30-
const pureNonInteractiveElementsMap = domElements
31-
.filter(name => !dom.get(name).interactive)
32-
.reduce((nonInteractiveElements, name) => {
33-
nonInteractiveElements[name] = [];
34-
return nonInteractiveElements;
35-
}, {});
36-
3751
const nonInteractiveElementsMap = {
38-
...pureNonInteractiveElementsMap,
39-
input: [
40-
{prop: 'type', value: 'hidden'}
41-
],
52+
area: [],
53+
article: [],
54+
dd: [],
55+
dfn: [],
56+
dt: [],
57+
fieldset: [],
58+
figure: [],
59+
form: [],
60+
frame: [],
61+
h1: [],
62+
h2: [],
63+
h3: [],
64+
h4: [],
65+
h5: [],
66+
h6: [],
67+
hr: [],
68+
img: [],
69+
'input[type=\"hidden\"]': [{prop: 'type', value: 'hidden'}],
70+
li: [],
71+
main: [],
72+
nav: [],
73+
ol: [],
74+
table: [],
75+
td: [],
76+
tbody: [],
77+
tfoot: [],
78+
thead: [],
79+
ul: [],
4280
};
4381

44-
const roleNames = [...roles.keys()];
82+
const indeterminantInteractiveElementsMap = domElements
83+
.reduce(
84+
(
85+
accumulator: {[key: string]: Array<any>},
86+
name: string
87+
): {[key: string]: Array<any>} => {
88+
accumulator[name] = [];
89+
return accumulator;
90+
},
91+
{},
92+
);
93+
94+
Object.keys(interactiveElementsMap)
95+
.concat(Object.keys(nonInteractiveElementsMap))
96+
.forEach(
97+
(name: string) => delete indeterminantInteractiveElementsMap[name]
98+
);
99+
100+
const abstractRoles = roleNames
101+
.filter(role => roles.get(role).abstract);
45102

46-
const interactiveRoles = roleNames.filter(
47-
role => roles.get(role).interactive === true
48-
);
103+
const nonAbstractRoles = roleNames
104+
.filter(role => !roles.get(role).abstract);
49105

50-
const nonInteractiveRoles = roleNames.filter(
51-
role => roles.get(role).interactive === false
52-
);
106+
const interactiveRoles = []
107+
.concat(
108+
roleNames,
109+
// 'toolbar' does not descend from widget, but it does support
110+
// aria-activedescendant, thus in practice we treat it as a widget.
111+
'toolbar',
112+
)
113+
.filter(role => !roles.get(role).abstract)
114+
.filter(role => roles.get(role).superClass.some(
115+
klasses => klasses.includes('widget')),
116+
);
117+
118+
const nonInteractiveRoles = roleNames
119+
.filter(role => !roles.get(role).abstract)
120+
.filter(role => !roles.get(role).superClass.some(
121+
klasses => klasses.includes('widget')),
122+
)
123+
// 'toolbar' does not descend from widget, but it does support
124+
// aria-activedescendant, thus in practice we treat it as a widget.
125+
.filter(role => !['toolbar'].includes(role));
126+
127+
export function genElementSymbol (
128+
openingElement: Object,
129+
) {
130+
return openingElement.name.name + (
131+
(openingElement.attributes.length > 0)
132+
? `${openingElement.attributes.map(
133+
attr => `[${attr.name.name}=\"${attr.value.value}\"]` ).join('')
134+
}`
135+
: ''
136+
);
137+
};
53138

54139
export function genInteractiveElements () {
55140
return Object.keys(interactiveElementsMap)
56-
.map(name => {
57-
const attributes = interactiveElementsMap[name].map(
141+
.map(elementSymbol => {
142+
const bracketIndex = elementSymbol.indexOf('[');
143+
let name = elementSymbol;
144+
if (bracketIndex > -1) {
145+
name = elementSymbol.slice(0, bracketIndex);
146+
}
147+
const attributes = interactiveElementsMap[elementSymbol].map(
58148
({prop, value}) => JSXAttributeMock(prop, value)
59149
);
60150
return JSXElementMock(name, attributes);
61151
});
62152
}
63153

64154
export function genInteractiveRoleElements () {
65-
return interactiveRoles.map(
155+
return [
156+
...interactiveRoles,
157+
'button article',
158+
'fakerole button article',
159+
].map(
66160
value => JSXElementMock('div', [
67161
JSXAttributeMock('role', value)
68162
])
@@ -71,18 +165,53 @@ export function genInteractiveRoleElements () {
71165

72166
export function genNonInteractiveElements () {
73167
return Object.keys(nonInteractiveElementsMap)
74-
.map(name => {
75-
const attributes = nonInteractiveElementsMap[name].map(
168+
.map(elementSymbol => {
169+
const bracketIndex = elementSymbol.indexOf('[');
170+
let name = elementSymbol;
171+
if (bracketIndex > -1) {
172+
name = elementSymbol.slice(0, bracketIndex);
173+
}
174+
const attributes = nonInteractiveElementsMap[elementSymbol].map(
76175
({prop, value}) => JSXAttributeMock(prop, value)
77176
);
78177
return JSXElementMock(name, attributes);
79178
});
80179
}
81180

82181
export function genNonInteractiveRoleElements () {
83-
return nonInteractiveRoles.map(
182+
return [
183+
...nonInteractiveRoles,
184+
'article button',
185+
'fakerole article button',
186+
].map(
84187
value => JSXElementMock('div', [
85188
JSXAttributeMock('role', value)
86189
])
87190
);
88191
}
192+
193+
export function genAbstractRoleElements () {
194+
return abstractRoles.map(
195+
value => JSXElementMock('div', [
196+
JSXAttributeMock('role', value)
197+
])
198+
);
199+
};
200+
201+
export function genNonAbstractRoleElements () {
202+
return nonAbstractRoles.map(
203+
value => JSXElementMock('div', [
204+
JSXAttributeMock('role', value)
205+
])
206+
);
207+
};
208+
209+
export function genIndeterminantInteractiveElements () {
210+
return Object.keys(indeterminantInteractiveElementsMap)
211+
.map(name => {
212+
const attributes = indeterminantInteractiveElementsMap[name].map(
213+
({prop, value}) => JSXAttributeMock(prop, value)
214+
);
215+
return JSXElementMock(name, attributes);
216+
});
217+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* @flow
3+
*/
4+
5+
type ESLintTestRunnerTestCase = {
6+
code: string,
7+
errors: ?Array<{
8+
message: string,
9+
type: string,
10+
}>,
11+
options: ?Array<mixed>,
12+
parserOptions: ?Array<mixed>,
13+
};
14+
15+
export default function ruleOptionsMapperFactory(
16+
ruleOptions: Array<mixed> = [],
17+
) {
18+
return ({
19+
code,
20+
errors,
21+
options,
22+
parserOptions,
23+
}: ESLintTestRunnerTestCase): ESLintTestRunnerTestCase => ({
24+
code,
25+
errors,
26+
options: (options || []).concat(ruleOptions),
27+
parserOptions,
28+
});
29+
}

__tests__/src/rules/click-events-have-key-events-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const expectedError = {
2626
type: 'JSXOpeningElement',
2727
};
2828

29-
ruleTester.run('onclick-has-role', rule, {
29+
ruleTester.run('click-events-have-key-events', rule, {
3030
valid: [
3131
{ code: '<div onClick={() => void 0} onKeyDown={foo}/>;' },
3232
{ code: '<div onClick={() => void 0} onKeyUp={foo} />;' },

0 commit comments

Comments
 (0)