Skip to content

Commit fa8e6dc

Browse files
committed
Add isInteractiveRole util module
1 parent 8b6d82a commit fa8e6dc

File tree

4 files changed

+141
-0
lines changed

4 files changed

+141
-0
lines changed

__tests__/__mocks__/attrMock.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export default function attrMock (prop, value) {
2+
return {
3+
type: 'JSXAttribute',
4+
name: {
5+
type: 'JSXIdentifier',
6+
name: prop,
7+
},
8+
value: {
9+
type: 'Literal',
10+
value: value,
11+
}
12+
};
13+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/* eslint-env mocha */
2+
import assert from 'assert';
3+
import isInteractiveRole from '../../../src/util/isInteractiveRole';
4+
import attrMock from '../../__mocks__/attrMock';
5+
6+
describe('isInteractiveRole', () => {
7+
describe('JSX Components (no tagName)', () => {
8+
it('should identify them as interactive elements', () => {
9+
expect(isInteractiveRole(undefined, [])).toBe(true);
10+
});
11+
});
12+
describe('elements with an interactive role', () => {
13+
it('should identify them as interactive elements', () => {
14+
[
15+
'button',
16+
'checkbox',
17+
'link',
18+
'menuitem',
19+
'menuitemcheckbox',
20+
'menuitemradio',
21+
'option',
22+
'radio',
23+
'spinbutton',
24+
'tab',
25+
'textbox',
26+
].forEach(role => expect(isInteractiveRole('div', [
27+
attrMock('role', role)
28+
])).toBe(true));
29+
});
30+
});
31+
describe('elements with a non-interactive role', () => {
32+
it('should not identify them as interactive elements', () => {
33+
[
34+
'alert',
35+
'alertdialog',
36+
'dialog',
37+
'gridcell',
38+
'log',
39+
'marquee',
40+
'progressbar',
41+
'scrollbar',
42+
'slider',
43+
'status',
44+
'tabpanel',
45+
'timer',
46+
'tooltip',
47+
'treeitem',
48+
'combobox',
49+
'grid',
50+
'listbox',
51+
'menu',
52+
'menubar',
53+
'radiogroup',
54+
'tablist',
55+
'tree',
56+
'treegrid',
57+
'article',
58+
'columnheader',
59+
'definition',
60+
'directory',
61+
'document',
62+
'group',
63+
'heading',
64+
'img',
65+
'list',
66+
'listitem',
67+
'math',
68+
'note',
69+
'presentation',
70+
'region',
71+
'row',
72+
'rowgroup',
73+
'rowheader',
74+
'separator',
75+
'toolbar',
76+
'application',
77+
'banner',
78+
'complementary',
79+
'contentinfo',
80+
'form',
81+
'main',
82+
'navigation',
83+
'search',
84+
].forEach(role => expect(isInteractiveRole('div', [
85+
attrMock('role', role)
86+
])).toBe(false));
87+
});
88+
});
89+
});

src/rules/onclick-has-focus.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { getProp, elementType } from 'jsx-ast-utils';
77
import { generateObjSchema } from '../util/schemas';
88
import isHiddenFromScreenReader from '../util/isHiddenFromScreenReader';
99
import isInteractiveElement from '../util/isInteractiveElement';
10+
import isInteractiveRole from '../util/isInteractiveRole';
1011
import getTabIndex from '../util/getTabIndex';
1112

1213
// ----------------------------------------------------------------------------
@@ -38,6 +39,8 @@ module.exports = {
3839
return;
3940
} else if (isInteractiveElement(type, attributes)) {
4041
return;
42+
} else if (isInteractiveRole(type, attributes)) {
43+
return;
4144
} else if (getTabIndex(getProp(attributes, 'tabIndex')) !== undefined) {
4245
return;
4346
}

src/util/isInteractiveRole.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { getProp, getLiteralPropValue } from 'jsx-ast-utils';
2+
import DOMElements from './attributes/DOM.json';
3+
4+
// Map of tagNames to functions that return whether that element is interactive or not.
5+
const interactiveSet = [
6+
'button',
7+
'checkbox',
8+
'link',
9+
'menuitem',
10+
'menuitemcheckbox',
11+
'menuitemradio',
12+
'option',
13+
'radio',
14+
'spinbutton',
15+
'tab',
16+
'textbox',
17+
];
18+
19+
/**
20+
* Returns boolean indicating whether the given element has a role
21+
* that is associated with an interactive component. Used when an element
22+
* has a dynamic handler on it and we need to discern whether or not
23+
* it's intention is to be interacted with in the DOM.
24+
*/
25+
const isInteractiveRole = (tagName, attributes) => {
26+
// Do not test higher level JSX components, as we do not know what
27+
// low-level DOM element this maps to.
28+
if (Object.keys(DOMElements).indexOf(tagName) === -1) {
29+
return true;
30+
}
31+
return (interactiveSet.indexOf(
32+
(getLiteralPropValue(getProp(attributes, 'role')) || '').toLowerCase(),
33+
) > -1);
34+
};
35+
36+
export default isInteractiveRole;

0 commit comments

Comments
 (0)