Skip to content

Commit 6be5eab

Browse files
authored
Merge pull request #218 from jessebeach/options-for-interactive-element-rules
Add options to element interaction rules
2 parents a36d8b0 + acb6f31 commit 6be5eab

7 files changed

+1019
-618
lines changed

__tests__/src/rules/no-noninteractive-element-interactions-test.js

Lines changed: 423 additions & 256 deletions
Large diffs are not rendered by default.

__tests__/src/rules/no-static-element-interactions-test.js

Lines changed: 423 additions & 255 deletions
Large diffs are not rendered by default.

docs/rules/no-noninteractive-element-interactions.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,25 @@ Headers often double as expand/collapse controls for the content they headline.
6464

6565
## Rule details
6666

67-
This rule takes no arguments.
67+
You may configure which handler props should be taken into account when applying this rule. The recommended configuration includes the following 6 handlers.
68+
69+
```javascript
70+
'jsx-a11y/no-noninteractive-element-interactions': [
71+
'error',
72+
{
73+
handlers: [
74+
'onClick',
75+
'onMouseDown',
76+
'onMouseUp',
77+
'onKeyPress',
78+
'onKeyDown',
79+
'onKeyUp',
80+
],
81+
},
82+
],
83+
```
84+
85+
Adjust the list of handler prop names in the handlers array to increase or decrease the coverage surface of this rule in your codebase.
6886

6987
### Succeed
7088
```jsx

docs/rules/no-static-element-interactions.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,25 @@ Marking an element with the role `presentation` indicates to assistive technolog
6060

6161
## Rule details
6262

63-
This rule takes no arguments.
63+
You may configure which handler props should be taken into account when applying this rule. The recommended configuration includes the following 6 handlers.
64+
65+
```javascript
66+
'jsx-a11y/no-static-element-interactions': [
67+
'error',
68+
{
69+
handlers: [
70+
'onClick',
71+
'onMouseDown',
72+
'onMouseUp',
73+
'onKeyPress',
74+
'onKeyDown',
75+
'onKeyUp',
76+
],
77+
},
78+
],
79+
```
80+
81+
Adjust the list of handler prop names in the handlers array to increase or decrease the coverage surface of this rule in your codebase.
6482

6583
### Succeed
6684
```jsx

src/index.js

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,19 @@ module.exports = {
7272
tr: ['none', 'presentation'],
7373
},
7474
],
75-
76-
'jsx-a11y/no-noninteractive-element-interactions': 'error',
77-
75+
'jsx-a11y/no-noninteractive-element-interactions': [
76+
'error',
77+
{
78+
handlers: [
79+
'onClick',
80+
'onMouseDown',
81+
'onMouseUp',
82+
'onKeyPress',
83+
'onKeyDown',
84+
'onKeyUp',
85+
],
86+
},
87+
],
7888
'jsx-a11y/no-noninteractive-element-to-interactive-role': [
7989
'error',
8090
{
@@ -110,7 +120,19 @@ module.exports = {
110120
],
111121
'jsx-a11y/no-onchange': 'error',
112122
'jsx-a11y/no-redundant-roles': 'error',
113-
'jsx-a11y/no-static-element-interactions': 'warn',
123+
'jsx-a11y/no-static-element-interactions': [
124+
'error',
125+
{
126+
handlers: [
127+
'onClick',
128+
'onMouseDown',
129+
'onMouseUp',
130+
'onKeyPress',
131+
'onKeyDown',
132+
'onKeyUp',
133+
],
134+
},
135+
],
114136
'jsx-a11y/role-has-required-aria-props': 'error',
115137
'jsx-a11y/role-supports-aria-props': 'error',
116138
'jsx-a11y/scope': 'error',
@@ -151,7 +173,7 @@ module.exports = {
151173
'jsx-a11y/no-noninteractive-tabindex': 'error',
152174
'jsx-a11y/no-onchange': 'error',
153175
'jsx-a11y/no-redundant-roles': 'error',
154-
'jsx-a11y/no-static-element-interactions': 'warn',
176+
'jsx-a11y/no-static-element-interactions': 'error',
155177
'jsx-a11y/role-has-required-aria-props': 'error',
156178
'jsx-a11y/role-supports-aria-props': 'error',
157179
'jsx-a11y/scope': 'error',

src/rules/no-noninteractive-element-interactions.js

Lines changed: 55 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ import {
1212
} from 'aria-query';
1313
import {
1414
elementType,
15-
eventHandlersByType,
15+
eventHandlers,
1616
getPropValue,
1717
getProp,
1818
hasProp,
1919
} from 'jsx-ast-utils';
2020
import type { JSXOpeningElement } from 'ast-types-flow';
21-
import { generateObjSchema } from '../util/schemas';
21+
import { arraySchema, generateObjSchema } from '../util/schemas';
2222
import isAbstractRole from '../util/isAbstractRole';
2323
import isHiddenFromScreenReader from '../util/isHiddenFromScreenReader';
2424
import isInteractiveElement from '../util/isInteractiveElement';
@@ -30,64 +30,68 @@ import isPresentationRole from '../util/isPresentationRole';
3030
const errorMessage =
3131
'Non-interactive elements should not be assigned mouse or keyboard event listeners.';
3232

33-
const schema = generateObjSchema();
34-
3533
const domElements = [...dom.keys()];
36-
const interactiveProps = [
37-
...eventHandlersByType.mouse,
38-
...eventHandlersByType.keyboard,
39-
];
34+
const defaultInteractiveProps = eventHandlers;
35+
const schema = generateObjSchema({
36+
handlers: arraySchema,
37+
});
4038

4139
module.exports = {
4240
meta: {
4341
docs: {},
4442
schema: [schema],
4543
},
4644

47-
create: (context: ESLintContext) => ({
48-
JSXOpeningElement: (
49-
node: JSXOpeningElement,
50-
) => {
51-
const attributes = node.attributes;
52-
const type = elementType(node);
45+
create: (context: ESLintContext) => {
46+
const options = context.options;
47+
return {
48+
JSXOpeningElement: (
49+
node: JSXOpeningElement,
50+
) => {
51+
const attributes = node.attributes;
52+
const type = elementType(node);
53+
const interactiveProps = options[0]
54+
? options[0].handlers
55+
: defaultInteractiveProps;
5356

54-
const hasInteractiveProps = interactiveProps
55-
.some(prop => (
56-
hasProp(attributes, prop)
57-
&& getPropValue(getProp(attributes, prop)) != null
58-
));
57+
const hasInteractiveProps = interactiveProps
58+
.some(prop => (
59+
hasProp(attributes, prop)
60+
&& getPropValue(getProp(attributes, prop)) != null
61+
));
5962

60-
if (!domElements.includes(type)) {
61-
// Do not test higher level JSX components, as we do not know what
62-
// low-level DOM element this maps to.
63-
return;
64-
} else if (
65-
!hasInteractiveProps
66-
|| isHiddenFromScreenReader(type, attributes)
67-
|| isPresentationRole(type, attributes)
68-
) {
69-
// Presentation is an intentional signal from the author that this
70-
// element is not meant to be perceivable. For example, a click screen
71-
// to close a dialog .
72-
return;
73-
} else if (
74-
isInteractiveElement(type, attributes)
75-
|| isInteractiveRole(type, attributes)
76-
|| (
77-
!isNonInteractiveElement(type, attributes)
78-
&& !isNonInteractiveRole(type, attributes)
79-
)
80-
|| isAbstractRole(type, attributes)
81-
) {
82-
// This rule has no opinion about abtract roles.
83-
return;
84-
}
63+
if (!domElements.includes(type)) {
64+
// Do not test higher level JSX components, as we do not know what
65+
// low-level DOM element this maps to.
66+
return;
67+
} else if (
68+
!hasInteractiveProps
69+
|| isHiddenFromScreenReader(type, attributes)
70+
|| isPresentationRole(type, attributes)
71+
) {
72+
// Presentation is an intentional signal from the author that this
73+
// element is not meant to be perceivable. For example, a click screen
74+
// to close a dialog .
75+
return;
76+
} else if (
77+
isInteractiveElement(type, attributes)
78+
|| isInteractiveRole(type, attributes)
79+
|| (
80+
!isNonInteractiveElement(type, attributes)
81+
&& !isNonInteractiveRole(type, attributes)
82+
)
83+
|| isAbstractRole(type, attributes)
84+
) {
85+
// This rule has no opinion about abtract roles.
86+
return;
87+
}
8588

86-
// Visible, non-interactive elements should not have an interactive handler.
87-
context.report({
88-
node,
89-
message: errorMessage,
90-
});
91-
},
92-
}),
89+
// Visible, non-interactive elements should not have an interactive handler.
90+
context.report({
91+
node,
92+
message: errorMessage,
93+
});
94+
},
95+
};
96+
},
9397
};

src/rules/no-static-element-interactions.js

Lines changed: 53 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ import {
1313
} from 'aria-query';
1414
import {
1515
elementType,
16-
eventHandlersByType,
16+
eventHandlers,
1717
getPropValue,
1818
getProp,
1919
hasProp,
2020
} from 'jsx-ast-utils';
2121
import type { JSXOpeningElement } from 'ast-types-flow';
22-
import { generateObjSchema } from '../util/schemas';
22+
import { arraySchema, generateObjSchema } from '../util/schemas';
2323
import isAbstractRole from '../util/isAbstractRole';
2424
import isHiddenFromScreenReader from '../util/isHiddenFromScreenReader';
2525
import isInteractiveElement from '../util/isInteractiveElement';
@@ -31,62 +31,66 @@ import isPresentationRole from '../util/isPresentationRole';
3131
const errorMessage =
3232
'Static HTML elements with event handlers require a role.';
3333

34-
const schema = generateObjSchema();
35-
3634
const domElements = [...dom.keys()];
37-
const interactiveProps = [
38-
...eventHandlersByType.mouse,
39-
...eventHandlersByType.keyboard,
40-
];
35+
const defaultInteractiveProps = eventHandlers;
36+
const schema = generateObjSchema({
37+
handlers: arraySchema,
38+
});
4139

4240
module.exports = {
4341
meta: {
4442
docs: {},
4543
schema: [schema],
4644
},
4745

48-
create: (context: ESLintContext) => ({
49-
JSXOpeningElement: (
50-
node: JSXOpeningElement,
51-
) => {
52-
const attributes = node.attributes;
53-
const type = elementType(node);
46+
create: (context: ESLintContext) => {
47+
const options = context.options;
48+
return {
49+
JSXOpeningElement: (
50+
node: JSXOpeningElement,
51+
) => {
52+
const attributes = node.attributes;
53+
const type = elementType(node);
54+
const interactiveProps = options[0]
55+
? options[0].handlers
56+
: defaultInteractiveProps;
5457

55-
const hasInteractiveProps = interactiveProps
56-
.some(prop => (
57-
hasProp(attributes, prop)
58-
&& getPropValue(getProp(attributes, prop)) != null
59-
));
58+
const hasInteractiveProps = interactiveProps
59+
.some(prop => (
60+
hasProp(attributes, prop)
61+
&& getPropValue(getProp(attributes, prop)) != null
62+
));
6063

61-
if (!domElements.includes(type)) {
62-
// Do not test higher level JSX components, as we do not know what
63-
// low-level DOM element this maps to.
64-
return;
65-
} else if (
66-
!hasInteractiveProps
67-
|| isHiddenFromScreenReader(type, attributes)
68-
|| isPresentationRole(type, attributes)
69-
) {
70-
// Presentation is an intentional signal from the author that this
71-
// element is not meant to be perceivable. For example, a click screen
72-
// to close a dialog .
73-
return;
74-
} else if (
75-
isInteractiveElement(type, attributes)
76-
|| isInteractiveRole(type, attributes)
77-
|| isNonInteractiveElement(type, attributes)
78-
|| isNonInteractiveRole(type, attributes)
79-
|| isAbstractRole(type, attributes)
80-
) {
81-
// This rule has no opinion about abstract roles.
82-
return;
83-
}
64+
if (!domElements.includes(type)) {
65+
// Do not test higher level JSX components, as we do not know what
66+
// low-level DOM element this maps to.
67+
return;
68+
} else if (
69+
!hasInteractiveProps
70+
|| isHiddenFromScreenReader(type, attributes)
71+
|| isPresentationRole(type, attributes)
72+
) {
73+
// Presentation is an intentional signal from the author that this
74+
// element is not meant to be perceivable. For example, a click screen
75+
// to close a dialog .
76+
return;
77+
} else if (
78+
isInteractiveElement(type, attributes)
79+
|| isInteractiveRole(type, attributes)
80+
|| isNonInteractiveElement(type, attributes)
81+
|| isNonInteractiveRole(type, attributes)
82+
|| isAbstractRole(type, attributes)
83+
) {
84+
// This rule has no opinion about abstract roles.
85+
return;
86+
}
8487

85-
// Visible, non-interactive elements should not have an interactive handler.
86-
context.report({
87-
node,
88-
message: errorMessage,
89-
});
90-
},
91-
}),
88+
// Visible, non-interactive elements should not have an interactive handler.
89+
context.report({
90+
node,
91+
message: errorMessage,
92+
});
93+
},
94+
};
95+
},
9296
};

0 commit comments

Comments
 (0)