Skip to content

Commit 2a7a3b0

Browse files
committed
Modified onclick-focus-test to only fail on elements with interactive roles
1 parent 1a98ac8 commit 2a7a3b0

File tree

3 files changed

+63
-21
lines changed

3 files changed

+63
-21
lines changed

__tests__/src/rules/onclick-has-focus-test.js

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ ruleTester.run('onclick-has-focus', rule, {
4343
{ code: '<div aria-hidden={1 <= 2} onClick={() => void 0} />', parserOptions },
4444
{ code: '<div aria-hidden={2 > 1} onClick={() => void 0} />', parserOptions },
4545
{ code: '<div aria-hidden={2 >= 1} onClick={() => void 0} />', parserOptions },
46+
{ code: '<div onClick={() => void 0} />;', parserOptions },
47+
{ code: '<div onClick={() => void 0} tabIndex={undefined} />;', parserOptions },
48+
{ code: '<div onClick={() => void 0} tabIndex="bad" />;', parserOptions },
49+
{ code: '<div onClick={() => void 0} role={undefined} />;', parserOptions },
50+
{ code: '<div onClick={() => void 0} aria-hidden={false} />;', parserOptions },
51+
{ code: '<div onClick={() => void 0} {...props} />;', parserOptions },
4652
{ code: '<input type="text" onClick={() => void 0} />', parserOptions },
4753
{ code: '<input type="hidden" onClick={() => void 0} tabIndex="-1" />', parserOptions },
4854
{ code: '<input type="hidden" onClick={() => void 0} tabIndex={-1} />', parserOptions },
@@ -51,7 +57,12 @@ ruleTester.run('onclick-has-focus', rule, {
5157
{ code: '<option onClick={() => void 0} className="foo" />', parserOptions },
5258
{ code: '<select onClick={() => void 0} className="foo" />', parserOptions },
5359
{ code: '<area href="#" onClick={() => void 0} className="foo" />', parserOptions },
60+
{ code: '<area onClick={() => void 0} className="foo" />', parserOptions },
5461
{ code: '<textarea onClick={() => void 0} className="foo" />', parserOptions },
62+
{ code: '<a onClick="showNextPage();">Next page</a>', parserOptions },
63+
{ code: '<a onClick="showNextPage();" tabIndex={undefined}>Next page</a>', parserOptions },
64+
{ code: '<a onClick="showNextPage();" tabIndex="bad">Next page</a>', parserOptions },
65+
{ code: '<a onClick={() => void 0} />', parserOptions },
5566
{ code: '<a tabIndex="0" onClick={() => void 0} />', parserOptions },
5667
{ code: '<a tabIndex={dynamicTabIndex} onClick={() => void 0} />', parserOptions },
5768
{ code: '<a tabIndex={0} onClick={() => void 0} />', parserOptions },
@@ -61,76 +72,101 @@ ruleTester.run('onclick-has-focus', rule, {
6172
{ code: '<a onClick={() => void 0} href="http://x.y.z" tabIndex={0} />', parserOptions },
6273
{ code: '<TestComponent onClick={doFoo} />', parserOptions },
6374
{ code: '<input onClick={() => void 0} type="hidden" />;', parserOptions },
75+
{ code: '<span onClick="submitForm();">Submit</span>', errors: [expectedError], parserOptions },
76+
{ code: '<span onClick="submitForm();" tabIndex={undefined}>Submit</span>', parserOptions },
77+
{ code: '<span onClick="submitForm();" tabIndex="bad">Submit</span>', parserOptions },
6478
{ code: '<span onClick="doSomething();" tabIndex="0">Click me!</span>', parserOptions },
6579
{ code: '<span onClick="doSomething();" tabIndex={0}>Click me!</span>', parserOptions },
6680
{ code: '<span onClick="doSomething();" tabIndex="-1">Click me too!</span>', parserOptions },
6781
{
6882
code: '<a href="javascript:void(0);" onClick="doSomething();">Click ALL the things!</a>',
6983
parserOptions,
7084
},
85+
{ code: '<section onClick={() => void 0} />;', parserOptions },
86+
{ code: '<main onClick={() => void 0} />;', parserOptions },
87+
{ code: '<article onClick={() => void 0} />;', parserOptions },
88+
{ code: '<header onClick={() => void 0} />;', parserOptions },
89+
{ code: '<footer onClick={() => void 0} />;', parserOptions },
90+
{ code: '<div role="button" tabIndex="0" onClick={() => void 0} />', parserOptions },
91+
{ code: '<div role="checkbox" tabIndex="0" onClick={() => void 0} />', parserOptions },
92+
{ code: '<div role="link" tabIndex="0" onClick={() => void 0} />', parserOptions },
93+
{ code: '<div role="menuitem" tabIndex="0" onClick={() => void 0} />', parserOptions },
94+
{ code: '<div role="menuitemcheckbox" tabIndex="0" onClick={() => void 0} />', parserOptions },
95+
{ code: '<div role="menuitemradio" tabIndex="0" onClick={() => void 0} />', parserOptions },
96+
{ code: '<div role="option" tabIndex="0" onClick={() => void 0} />', parserOptions },
97+
{ code: '<div role="radio" tabIndex="0" onClick={() => void 0} />', parserOptions },
98+
{ code: '<div role="spinbutton" tabIndex="0" onClick={() => void 0} />', parserOptions },
99+
{ code: '<div role="tab" tabIndex="0" onClick={() => void 0} />', parserOptions },
100+
{ code: '<div role="textbox" tabIndex="0" onClick={() => void 0} />', parserOptions },
71101
{ code: '<Foo.Bar onClick={() => void 0} aria-hidden={false} />;', parserOptions },
72102
{ code: '<Input onClick={() => void 0} type="hidden" />;', parserOptions },
73103
],
74104

75105
invalid: [
76-
{ code: '<span onClick="submitForm();">Submit</span>', errors: [expectedError], parserOptions },
77106
{
78-
code: '<span onClick="submitForm();" tabIndex={undefined}>Submit</span>',
107+
code: '<span role="button" onClick={() => void 0} />',
108+
errors: [expectedError],
109+
parserOptions,
110+
},
111+
{
112+
code: '<a role="button" onClick={() => void 0} />',
113+
errors: [expectedError],
114+
parserOptions,
115+
},
116+
{
117+
code: '<div role="button" onClick={() => void 0} />',
118+
errors: [expectedError],
119+
parserOptions,
120+
},
121+
{
122+
code: '<div role="checkbox" onClick={() => void 0} />',
79123
errors: [expectedError],
80124
parserOptions,
81125
},
82126
{
83-
code: '<span onClick="submitForm();" tabIndex="bad">Submit</span>',
127+
code: '<div role="link" onClick={() => void 0} />',
84128
errors: [expectedError],
85129
parserOptions,
86130
},
87-
{ code: '<a onClick="showNextPage();">Next page</a>', errors: [expectedError], parserOptions },
88131
{
89-
code: '<a onClick="showNextPage();" tabIndex={undefined}>Next page</a>',
132+
code: '<div role="menuitem" onClick={() => void 0} />',
90133
errors: [expectedError],
91134
parserOptions,
92135
},
93136
{
94-
code: '<a onClick="showNextPage();" tabIndex="bad">Next page</a>',
137+
code: '<div role="menuitemcheckbox" onClick={() => void 0} />',
95138
errors: [expectedError],
96139
parserOptions,
97140
},
98141
{
99-
code: '<a onClick={() => void 0} />',
142+
code: '<div role="menuitemradio" onClick={() => void 0} />',
100143
errors: [expectedError],
101144
parserOptions,
102145
},
103146
{
104-
code: '<area onClick={() => void 0} className="foo" />',
147+
code: '<div role="option" onClick={() => void 0} />',
105148
errors: [expectedError],
106149
parserOptions,
107150
},
108-
{ code: '<div onClick={() => void 0} />;', errors: [expectedError], parserOptions },
109151
{
110-
code: '<div onClick={() => void 0} tabIndex={undefined} />;',
152+
code: '<div role="radio" onClick={() => void 0} />',
111153
errors: [expectedError],
112154
parserOptions,
113155
},
114156
{
115-
code: '<div onClick={() => void 0} tabIndex="bad" />;',
157+
code: '<div role="spinbutton" onClick={() => void 0} />',
116158
errors: [expectedError],
117159
parserOptions,
118160
},
119161
{
120-
code: '<div onClick={() => void 0} role={undefined} />;',
162+
code: '<div role="tab" onClick={() => void 0} />',
121163
errors: [expectedError],
122164
parserOptions,
123165
},
124166
{
125-
code: '<div onClick={() => void 0} aria-hidden={false} />;',
167+
code: '<div role="textbox" onClick={() => void 0} />',
126168
errors: [expectedError],
127169
parserOptions,
128170
},
129-
{ code: '<div onClick={() => void 0} {...props} />;', errors: [expectedError], parserOptions },
130-
{ code: '<section onClick={() => void 0} />;', errors: [expectedError], parserOptions },
131-
{ code: '<main onClick={() => void 0} />;', errors: [expectedError], parserOptions },
132-
{ code: '<article onClick={() => void 0} />;', errors: [expectedError], parserOptions },
133-
{ code: '<header onClick={() => void 0} />;', errors: [expectedError], parserOptions },
134-
{ code: '<footer onClick={() => void 0} />;', errors: [expectedError], parserOptions },
135171
],
136172
});

src/rules/onclick-has-focus.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* @author Ethan Cohen
44
*/
55

6-
import { getProp, elementType } from 'jsx-ast-utils';
6+
import { getProp, getPropValue, elementType } from 'jsx-ast-utils';
77
import { generateObjSchema } from '../util/schemas';
88
import isHiddenFromScreenReader from '../util/isHiddenFromScreenReader';
99
import isInteractiveElement from '../util/isInteractiveElement';
@@ -39,7 +39,12 @@ module.exports = {
3939
return;
4040
} else if (isInteractiveElement(type, attributes)) {
4141
return;
42-
} else if (isInteractiveRole(type, attributes)) {
42+
} else if (!isInteractiveRole(type, attributes)) {
43+
// A non-interactive element or an element without an interactive
44+
// role might have a click hanlder attached to it in order to catch
45+
// bubbled click events. In this case, the author should apply a role
46+
// of presentation to the element to indicate that it is not meant to
47+
// be interactive.
4348
return;
4449
} else if (getTabIndex(getProp(attributes, 'tabIndex')) !== undefined) {
4550
return;

src/util/isInteractiveRole.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getProp, getPropValue } from 'jsx-ast-utils';
22
import DOMElements from './attributes/DOM.json';
3+
import isInteractiveElement from '../util/isInteractiveElement';
34

45
// ARIA roles that denote user interaction support.
56
export const interactiveRoles = [

0 commit comments

Comments
 (0)