Skip to content

Commit cbdfeb8

Browse files
committed
Rename onclick-has-focus to interactive-supports-focus and expand scope to key and mouse events
1 parent 18c9b71 commit cbdfeb8

File tree

4 files changed

+61
-84
lines changed

4 files changed

+61
-84
lines changed

__tests__/src/rules/onclick-has-focus-test.js renamed to __tests__/src/rules/interactive-supports-focus-test.js

Lines changed: 45 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import { RuleTester } from 'eslint';
1212
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
13-
import rule from '../../../src/rules/onclick-has-focus';
13+
import rule from '../../../src/rules/interactive-supports-focus';
1414

1515
// -----------------------------------------------------------------------------
1616
// Tests
@@ -26,7 +26,7 @@ const expectedError = {
2626
type: 'JSXOpeningElement',
2727
};
2828

29-
ruleTester.run('onclick-has-focus', rule, {
29+
ruleTester.run('interactive-supports-focus', rule, {
3030
valid: [
3131
{ code: '<div />' },
3232
{ code: '<div aria-hidden onClick={() => void 0} />' },
@@ -101,77 +101,48 @@ ruleTester.run('onclick-has-focus', rule, {
101101
].map(parserOptionsMapper),
102102

103103
invalid: [
104-
{
105-
code: '<span role="button" onClick={() => void 0} />',
106-
errors: [expectedError],
107-
},
108-
{
109-
code: '<a role="button" onClick={() => void 0} />',
110-
errors: [expectedError],
111-
},
112-
{
113-
code: '<div role="button" onClick={() => void 0} />',
114-
errors: [expectedError],
115-
},
116-
{
117-
code: '<div role="checkbox" onClick={() => void 0} />',
118-
errors: [expectedError],
119-
},
120-
{
121-
code: '<div role="link" onClick={() => void 0} />',
122-
errors: [expectedError],
123-
},
124-
{
125-
code: '<div role="gridcell" onClick={() => void 0} />',
126-
errors: [expectedError],
127-
},
128-
{
129-
code: '<div role="menuitem" onClick={() => void 0} />',
130-
errors: [expectedError],
131-
},
132-
{
133-
code: '<div role="menuitemcheckbox" onClick={() => void 0} />',
134-
errors: [expectedError],
135-
},
136-
{
137-
code: '<div role="menuitemradio" onClick={() => void 0} />',
138-
errors: [expectedError],
139-
},
140-
{
141-
code: '<div role="option" onClick={() => void 0} />',
142-
errors: [expectedError],
143-
},
144-
{
145-
code: '<div role="radio" onClick={() => void 0} />',
146-
errors: [expectedError],
147-
},
148-
{
149-
code: '<div role="searchbox" onClick={() => void 0} />',
150-
errors: [expectedError],
151-
},
152-
{
153-
code: '<div role="slider" onClick={() => void 0} />',
154-
errors: [expectedError],
155-
},
156-
{
157-
code: '<div role="spinbutton" onClick={() => void 0} />',
158-
errors: [expectedError],
159-
},
160-
{
161-
code: '<div role="switch" onClick={() => void 0} />',
162-
errors: [expectedError],
163-
},
164-
{
165-
code: '<div role="tab" onClick={() => void 0} />',
166-
errors: [expectedError],
167-
},
168-
{
169-
code: '<div role="textbox" onClick={() => void 0} />',
170-
errors: [expectedError],
171-
},
172-
{
173-
code: '<div role="treeitem" onClick={() => void 0} />',
174-
errors: [expectedError],
175-
},
104+
// onClick
105+
{ code: '<span role="button" onClick={() => void 0} />', errors: [expectedError] },
106+
{ code: '<a role="button" onClick={() => void 0} />', errors: [expectedError] },
107+
{ code: '<div role="button" onClick={() => void 0} />', errors: [expectedError] },
108+
{ code: '<div role="checkbox" onClick={() => void 0} />', errors: [expectedError] },
109+
{ code: '<div role="link" onClick={() => void 0} />', errors: [expectedError] },
110+
{ code: '<div role="gridcell" onClick={() => void 0} />', errors: [expectedError] },
111+
{ code: '<div role="menuitem" onClick={() => void 0} />', errors: [expectedError] },
112+
{ code: '<div role="menuitemcheckbox" onClick={() => void 0} />', errors: [expectedError] },
113+
{ code: '<div role="menuitemradio" onClick={() => void 0} />', errors: [expectedError] },
114+
{ code: '<div role="option" onClick={() => void 0} />', errors: [expectedError] },
115+
{ code: '<div role="radio" onClick={() => void 0} />', errors: [expectedError] },
116+
{ code: '<div role="searchbox" onClick={() => void 0} />', errors: [expectedError] },
117+
{ code: '<div role="slider" onClick={() => void 0} />', errors: [expectedError] },
118+
{ code: '<div role="spinbutton" onClick={() => void 0} />', errors: [expectedError] },
119+
{ code: '<div role="switch" onClick={() => void 0} />', errors: [expectedError] },
120+
{ code: '<div role="tab" onClick={() => void 0} />', errors: [expectedError] },
121+
{ code: '<div role="textbox" onClick={() => void 0} />', errors: [expectedError] },
122+
{ code: '<div role="treeitem" onClick={() => void 0} />', errors: [expectedError] },
123+
// onKeyPress
124+
{ code: '<span role="button" onKeyPress={() => void 0} />', errors: [expectedError] },
125+
{ code: '<a role="button" onKeyPress={() => void 0} />', errors: [expectedError] },
126+
{ code: '<div role="button" onKeyPress={() => void 0} />', errors: [expectedError] },
127+
{ code: '<div role="checkbox" onKeyPress={() => void 0} />', errors: [expectedError] },
128+
{ code: '<div role="link" onKeyPress={() => void 0} />', errors: [expectedError] },
129+
{ code: '<div role="gridcell" onKeyPress={() => void 0} />', errors: [expectedError] },
130+
{ code: '<div role="menuitem" onKeyPress={() => void 0} />', errors: [expectedError] },
131+
{ code: '<div role="menuitemcheckbox" onKeyPress={() => void 0} />', errors: [expectedError] },
132+
{ code: '<div role="menuitemradio" onKeyPress={() => void 0} />', errors: [expectedError] },
133+
{ code: '<div role="option" onKeyPress={() => void 0} />', errors: [expectedError] },
134+
{ code: '<div role="radio" onKeyPress={() => void 0} />', errors: [expectedError] },
135+
{ code: '<div role="searchbox" onKeyPress={() => void 0} />', errors: [expectedError] },
136+
{ code: '<div role="slider" onKeyPress={() => void 0} />', errors: [expectedError] },
137+
{ code: '<div role="spinbutton" onKeyPress={() => void 0} />', errors: [expectedError] },
138+
{ code: '<div role="switch" onKeyPress={() => void 0} />', errors: [expectedError] },
139+
{ code: '<div role="tab" onKeyPress={() => void 0} />', errors: [expectedError] },
140+
{ code: '<div role="textbox" onKeyPress={() => void 0} />', errors: [expectedError] },
141+
{ code: '<div role="treeitem" onKeyPress={() => void 0} />', errors: [expectedError] },
142+
// Other interactive handlers
143+
{ code: '<div role="button" onKeyDown={() => void 0} />', errors: [expectedError] },
144+
{ code: '<div role="button" onKeyUp={() => void 0} />', errors: [expectedError] },
145+
{ code: '<div role="button" onMouseDown={() => void 0} />', errors: [expectedError] },
146+
{ code: '<div role="button" onMouseUp={() => void 0} />', errors: [expectedError] },
176147
].map(parserOptionsMapper),
177148
});

docs/rules/onclick-has-focus.md renamed to docs/rules/interactive-supports-focus.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# onclick-has-focus
1+
# interactive-supports-focus
22

33
Enforce that visible elements with onClick handlers must be focusable. Visible means that it is not hidden from a screen reader. Examples of non-interactive elements are `div`, `section`, and `a` elements without a href prop. Elements which have click handlers but are not focusable can not be used by keyboard-only users.
44

src/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ module.exports = {
2828
'no-onchange': require('./rules/no-onchange'),
2929
'no-redundant-roles': require('./rules/no-redundant-roles'),
3030
'no-static-element-interactions': require('./rules/no-static-element-interactions'),
31-
'onclick-has-focus': require('./rules/onclick-has-focus'),
31+
'interactive-supports-focus': require('./rules/interactive-supports-focus'),
3232
'role-has-required-aria-props': require('./rules/role-has-required-aria-props'),
3333
'role-supports-aria-props': require('./rules/role-supports-aria-props'),
3434
scope: require('./rules/scope'),
@@ -67,7 +67,7 @@ module.exports = {
6767
'jsx-a11y/no-onchange': 'error',
6868
'jsx-a11y/no-redundant-roles': 'error',
6969
'jsx-a11y/no-static-element-interactions': 'warn',
70-
'jsx-a11y/onclick-has-focus': 'error',
70+
'jsx-a11y/interactive-supports-focus': 'error',
7171
'jsx-a11y/role-has-required-aria-props': 'error',
7272
'jsx-a11y/role-supports-aria-props': 'error',
7373
'jsx-a11y/scope': 'error',

src/rules/onclick-has-focus.js renamed to src/rules/interactive-supports-focus.js

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { dom } from 'aria-query';
88
import {
99
getProp,
1010
elementType,
11+
eventHandlersByType,
12+
hasAnyProp,
1113
} from 'jsx-ast-utils';
1214
import type { JSXOpeningElement } from 'ast-types-flow';
1315
import { generateObjSchema } from '../util/schemas';
@@ -30,6 +32,11 @@ const errorMessage =
3032
const schema = generateObjSchema();
3133
const domElements = [...dom.keys()];
3234

35+
const interactiveProps = [
36+
...eventHandlersByType.mouse,
37+
...eventHandlersByType.keyboard,
38+
];
39+
3340
module.exports = {
3441
meta: {
3542
docs: {},
@@ -40,12 +47,9 @@ module.exports = {
4047
JSXOpeningElement: (
4148
node: JSXOpeningElement,
4249
) => {
43-
const { attributes } = node;
44-
if (getProp(attributes, 'onClick') === undefined) {
45-
return;
46-
}
47-
50+
const attributes = node.attributes
4851
const type = elementType(node);
52+
const hasInteractiveProps = hasAnyProp(attributes, interactiveProps);
4953
const hasTabindex = getTabIndex(
5054
getProp(attributes, 'tabIndex'),
5155
) !== undefined;
@@ -55,7 +59,8 @@ module.exports = {
5559
// low-level DOM element this maps to.
5660
return;
5761
} else if (
58-
isHiddenFromScreenReader(type, attributes)
62+
!hasInteractiveProps
63+
|| isHiddenFromScreenReader(type, attributes)
5964
|| isPresentationRole(type, attributes)
6065
) {
6166
// Presentation is an intentional signal from the author that this
@@ -65,7 +70,8 @@ module.exports = {
6570
}
6671

6772
if (
68-
!isInteractiveElement(type, attributes)
73+
hasInteractiveProps
74+
&& !isInteractiveElement(type, attributes)
6975
&& isInteractiveRole(type, attributes)
7076
&& !hasTabindex
7177
) {

0 commit comments

Comments
 (0)