Skip to content

Commit 0edae25

Browse files
committed
All tests passing on new is* utils
1 parent de75c0a commit 0edae25

10 files changed

+152
-71
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ ruleTester.run('no-noninteractive-element-handlers', rule, {
6060
/* End all flavors of input */
6161
{ code: '<input type="hidden" onClick={() => void 0} />' },
6262
{ code: '<button onClick={() => void 0} className="foo" />' },
63+
{ code: '<menuitem onClick={() => {}} />;' },
6364
{ code: '<option onClick={() => void 0} className="foo" />' },
6465
{ code: '<select onClick={() => void 0} className="foo" />' },
6566
{ code: '<textarea onClick={() => void 0} className="foo" />' },
@@ -164,7 +165,6 @@ ruleTester.run('no-noninteractive-element-handlers', rule, {
164165
{ code: '<mark onClick={() => {}} />;', errors: [expectedError] },
165166
{ code: '<marquee onClick={() => {}} />;', errors: [expectedError] },
166167
{ code: '<menu onClick={() => {}} />;', errors: [expectedError] },
167-
{ code: '<menuitem onClick={() => {}} />;', errors: [expectedError] },
168168
{ code: '<meta onClick={() => {}} />;', errors: [expectedError] },
169169
{ code: '<meter onClick={() => {}} />;', errors: [expectedError] },
170170
{ code: '<noembed onClick={() => {}} />;', errors: [expectedError] },

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ ruleTester.run('no-static-element-interactions', rule, {
6060
/* End all flavors of input */
6161
{ code: '<input type="hidden" onClick={() => void 0} />' },
6262
{ code: '<button onClick={() => void 0} className="foo" />' },
63+
{ code: '<menuitem onClick={() => {}} />;' },
6364
{ code: '<option onClick={() => void 0} className="foo" />' },
6465
{ code: '<select onClick={() => void 0} className="foo" />' },
6566
{ code: '<textarea onClick={() => void 0} className="foo" />' },
@@ -242,7 +243,6 @@ ruleTester.run('no-static-element-interactions', rule, {
242243
{ code: '<mark onClick={() => {}} />;', errors: [expectedError] },
243244
{ code: '<marquee onClick={() => {}} />;', errors: [expectedError] },
244245
{ code: '<menu onClick={() => {}} />;', errors: [expectedError] },
245-
{ code: '<menuitem onClick={() => {}} />;', errors: [expectedError] },
246246
{ code: '<meta onClick={() => {}} />;', errors: [expectedError] },
247247
{ code: '<meter onClick={() => {}} />;', errors: [expectedError] },
248248
{ code: '<noembed onClick={() => {}} />;', errors: [expectedError] },

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

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,76 @@ const expectedError = {
2828

2929
ruleTester.run('onclick-has-focus', rule, {
3030
valid: [
31+
{ code: '<div />' },
32+
{ code: '<div aria-hidden onClick={() => void 0} />' },
33+
{ code: '<div aria-hidden={true == true} onClick={() => void 0} />' },
34+
{ code: '<div aria-hidden={true === true} onClick={() => void 0} />' },
35+
{ code: '<div aria-hidden={hidden !== false} onClick={() => void 0} />' },
36+
{ code: '<div aria-hidden={hidden != false} onClick={() => void 0} />' },
37+
{ code: '<div aria-hidden={1 < 2} onClick={() => void 0} />' },
38+
{ code: '<div aria-hidden={1 <= 2} onClick={() => void 0} />' },
39+
{ code: '<div aria-hidden={2 > 1} onClick={() => void 0} />' },
40+
{ code: '<div aria-hidden={2 >= 1} onClick={() => void 0} />' },
41+
{ code: '<div onClick={() => void 0} />;' },
42+
{ code: '<div onClick={() => void 0} tabIndex={undefined} />;' },
43+
{ code: '<div onClick={() => void 0} tabIndex="bad" />;' },
44+
{ code: '<div onClick={() => void 0} role={undefined} />;' },
45+
{ code: '<div role="section" onClick={() => void 0} />' },
46+
{ code: '<div onClick={() => void 0} aria-hidden={false} />;' },
47+
{ code: '<div onClick={() => void 0} {...props} />;' },
48+
{ code: '<input type="text" onClick={() => void 0} />' },
49+
{ code: '<input type="hidden" onClick={() => void 0} tabIndex="-1" />' },
50+
{ code: '<input type="hidden" onClick={() => void 0} tabIndex={-1} />' },
51+
{ code: '<input onClick={() => void 0} />' },
52+
{ code: '<input onClick={() => void 0} role="combobox" />' },
53+
{ code: '<button onClick={() => void 0} className="foo" />' },
54+
{ code: '<option onClick={() => void 0} className="foo" />' },
55+
{ code: '<select onClick={() => void 0} className="foo" />' },
56+
{ code: '<area href="#" onClick={() => void 0} className="foo" />' },
57+
{ code: '<area onClick={() => void 0} className="foo" />' },
58+
{ code: '<textarea onClick={() => void 0} className="foo" />' },
59+
{ code: '<a onClick="showNextPage();">Next page</a>' },
60+
{ code: '<a onClick="showNextPage();" tabIndex={undefined}>Next page</a>' },
61+
{ code: '<a onClick="showNextPage();" tabIndex="bad">Next page</a>' },
62+
{ code: '<a onClick={() => void 0} />' },
63+
{ code: '<a tabIndex="0" onClick={() => void 0} />' },
64+
{ code: '<a tabIndex={dynamicTabIndex} onClick={() => void 0} />' },
65+
{ code: '<a tabIndex={0} onClick={() => void 0} />' },
66+
{ code: '<a role="button" href="#" onClick={() => void 0} />' },
67+
{ code: '<a onClick={() => void 0} href="http://x.y.z" />' },
68+
{ code: '<a onClick={() => void 0} href="http://x.y.z" tabIndex="0" />' },
69+
{ code: '<a onClick={() => void 0} href="http://x.y.z" tabIndex={0} />' },
70+
{ code: '<a onClick={() => void 0} href="http://x.y.z" role="button" />' },
71+
{ code: '<TestComponent onClick={doFoo} />' },
72+
{ code: '<input onClick={() => void 0} type="hidden" />;' },
73+
{ code: '<span onClick="submitForm();">Submit</span>', errors: [expectedError] },
74+
{ code: '<span onClick="submitForm();" tabIndex={undefined}>Submit</span>' },
75+
{ code: '<span onClick="submitForm();" tabIndex="bad">Submit</span>' },
76+
{ code: '<span onClick="doSomething();" tabIndex="0">Click me!</span>' },
77+
{ code: '<span onClick="doSomething();" tabIndex={0}>Click me!</span>' },
78+
{ code: '<span onClick="doSomething();" tabIndex="-1">Click me too!</span>' },
79+
{
80+
code: '<a href="javascript:void(0);" onClick="doSomething();">Click ALL the things!</a>',
81+
},
82+
{ code: '<section onClick={() => void 0} />;' },
83+
{ code: '<main onClick={() => void 0} />;' },
84+
{ code: '<article onClick={() => void 0} />;' },
85+
{ code: '<header onClick={() => void 0} />;' },
86+
{ code: '<footer onClick={() => void 0} />;' },
87+
{ code: '<div role="button" tabIndex="0" onClick={() => void 0} />' },
88+
{ code: '<div role="checkbox" tabIndex="0" onClick={() => void 0} />' },
89+
{ code: '<div role="link" tabIndex="0" onClick={() => void 0} />' },
90+
{ code: '<div role="menuitem" tabIndex="0" onClick={() => void 0} />' },
91+
{ code: '<div role="menuitemcheckbox" tabIndex="0" onClick={() => void 0} />' },
92+
{ code: '<div role="menuitemradio" tabIndex="0" onClick={() => void 0} />' },
93+
{ code: '<div role="option" tabIndex="0" onClick={() => void 0} />' },
94+
{ code: '<div role="radio" tabIndex="0" onClick={() => void 0} />' },
95+
{ code: '<div role="spinbutton" tabIndex="0" onClick={() => void 0} />' },
96+
{ code: '<div role="switch" tabIndex="0" onClick={() => void 0} />' },
97+
{ code: '<div role="tab" tabIndex="0" onClick={() => void 0} />' },
98+
{ code: '<div role="textbox" tabIndex="0" onClick={() => void 0} />' },
99+
{ code: '<Foo.Bar onClick={() => void 0} aria-hidden={false} />;' },
100+
{ code: '<Input onClick={() => void 0} type="hidden" />;' },
31101
].map(parserOptionsMapper),
32102

33103
invalid: [

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
// Rule Definition
88
// ----------------------------------------------------------------------------
99

10+
import {
11+
dom,
12+
} from 'aria-query';
1013
import { getProp, hasAnyProp, elementType } from 'jsx-ast-utils';
1114
import { generateObjSchema } from '../util/schemas';
1215
import isHiddenFromScreenReader from '../util/isHiddenFromScreenReader';
@@ -16,6 +19,7 @@ const errorMessage = 'Visible, non-interactive elements with click handlers' +
1619
' must have at least one keyboard listener.';
1720

1821
const schema = generateObjSchema();
22+
const domElements = [...dom.keys()];
1923

2024
module.exports = {
2125
meta: {
@@ -33,7 +37,11 @@ module.exports = {
3337
const type = elementType(node);
3438
const requiredProps = ['onkeydown', 'onkeyup', 'onkeypress'];
3539

36-
if (isHiddenFromScreenReader(type, props)) {
40+
if (!domElements.includes(type)) {
41+
// Do not test higher level JSX components, as we do not know what
42+
// low-level DOM element this maps to.
43+
return;
44+
} else if (isHiddenFromScreenReader(type, props)) {
3745
return;
3846
} else if (isInteractiveElement(type, props)) {
3947
return;

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

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ import {
1414
import {
1515
elementType,
1616
eventHandlersByType,
17-
getLiteralPropValue,
18-
getProp,
1917
hasAnyProp,
2018
} from 'jsx-ast-utils';
2119
import type { JSXOpeningElement } from 'ast-types-flow';
2220
import { generateObjSchema } from '../util/schemas';
2321
import isAbstractRole from '../util/isAbstractRole';
2422
import isHiddenFromScreenReader from '../util/isHiddenFromScreenReader';
2523
import isInteractiveElement from '../util/isInteractiveElement';
24+
import isInteractiveRole from '../util/isInteractiveRole';
25+
import isPresentationRole from '../util/isPresentationRole';
2626

2727
const errorMessage =
2828
'Visible, non-interactive elements should not have mouse or keyboard event listeners';
@@ -45,31 +45,28 @@ module.exports = {
4545
JSXOpeningElement: (
4646
node: JSXOpeningElement,
4747
) => {
48-
const props = node.attributes;
48+
const attributes = node.attributes;
4949
const type = elementType(node);
5050

51-
const hasInteractiveProps = hasAnyProp(props, interactiveProps);
51+
const hasInteractiveProps = hasAnyProp(attributes, interactiveProps);
5252

5353
if (!domElements.includes(type)) {
5454
// Do not test higher level JSX components, as we do not know what
5555
// low-level DOM element this maps to.
5656
return;
57-
} else if (isHiddenFromScreenReader(type, props)) {
58-
return;
59-
} else if (!hasInteractiveProps) {
60-
return;
6157
} else if (
62-
['presentation', 'none'].indexOf(
63-
getLiteralPropValue(getProp(props, 'role')),
64-
) > -1
58+
!hasInteractiveProps
59+
|| isHiddenFromScreenReader(type, attributes)
60+
|| isPresentationRole(type, attributes)
6561
) {
6662
// Presentation is an intentional signal from the author that this
6763
// element is not meant to be perceivable. For example, a click screen
6864
// to close a dialog .
6965
return;
7066
} else if (
71-
isInteractiveElement(type, props)
72-
|| isAbstractRole(type, props)
67+
isInteractiveElement(type, attributes)
68+
|| isInteractiveRole(type, attributes)
69+
|| isAbstractRole(type, attributes)
7370
) {
7471
// This rule has no opinion about abtract roles.
7572
return;

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

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,17 @@ import {
1414
import {
1515
elementType,
1616
eventHandlersByType,
17-
getLiteralPropValue,
18-
getProp,
1917
hasAnyProp,
2018
} from 'jsx-ast-utils';
2119
import type { JSXOpeningElement } from 'ast-types-flow';
2220
import { generateObjSchema } from '../util/schemas';
2321
import isAbstractRole from '../util/isAbstractRole';
2422
import isHiddenFromScreenReader from '../util/isHiddenFromScreenReader';
2523
import isInteractiveElement from '../util/isInteractiveElement';
24+
import isInteractiveRole from '../util/isInteractiveRole';
2625
import isNonInteractiveElement from '../util/isNonInteractiveElement';
26+
import isNonInteractiveRole from '../util/isNonInteractiveRole';
27+
import isPresentationRole from '../util/isPresentationRole';
2728

2829
const errorMessage =
2930
'Visible, static elements should not have mouse or keyboard event listeners';
@@ -46,32 +47,30 @@ module.exports = {
4647
JSXOpeningElement: (
4748
node: JSXOpeningElement,
4849
) => {
49-
const props = node.attributes;
50+
const attributes = node.attributes;
5051
const type = elementType(node);
5152

52-
const hasInteractiveProps = hasAnyProp(props, interactiveProps);
53+
const hasInteractiveProps = hasAnyProp(attributes, interactiveProps);
5354

5455
if (!domElements.includes(type)) {
5556
// Do not test higher level JSX components, as we do not know what
5657
// low-level DOM element this maps to.
5758
return;
58-
} else if (isHiddenFromScreenReader(type, props)) {
59-
return;
60-
} else if (!hasInteractiveProps) {
61-
return;
6259
} else if (
63-
['presentation', 'none'].indexOf(
64-
getLiteralPropValue(getProp(props, 'role')),
65-
) > -1
60+
!hasInteractiveProps
61+
|| isHiddenFromScreenReader(type, attributes)
62+
|| isPresentationRole(type, attributes)
6663
) {
6764
// Presentation is an intentional signal from the author that this
6865
// element is not meant to be perceivable. For example, a click screen
6966
// to close a dialog .
7067
return;
7168
} else if (
72-
isInteractiveElement(type, props)
73-
|| isNonInteractiveElement(type, props)
74-
|| isAbstractRole(type, props)
69+
isInteractiveElement(type, attributes)
70+
|| isInteractiveRole(type, attributes)
71+
|| isNonInteractiveElement(type, attributes)
72+
|| isNonInteractiveRole(type, attributes)
73+
|| isAbstractRole(type, attributes)
7574
) {
7675
// This rule has no opinion about abtract roles.
7776
return;

src/rules/onclick-has-focus.js

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
import { dom } from 'aria-query';
88
import {
9-
getLiteralPropValue,
109
getProp,
1110
elementType,
1211
} from 'jsx-ast-utils';
@@ -15,6 +14,7 @@ import { generateObjSchema } from '../util/schemas';
1514
import isHiddenFromScreenReader from '../util/isHiddenFromScreenReader';
1615
import isInteractiveElement from '../util/isInteractiveElement';
1716
import isInteractiveRole from '../util/isInteractiveRole';
17+
import isPresentationRole from '../util/isPresentationRole';
1818
import getTabIndex from '../util/getTabIndex';
1919

2020
// ----------------------------------------------------------------------------
@@ -46,35 +46,34 @@ module.exports = {
4646
}
4747

4848
const type = elementType(node);
49+
const hasTabindex = getTabIndex(
50+
getProp(attributes, 'tabIndex'),
51+
) !== undefined;
4952

5053
if (!domElements.includes(type)) {
5154
// Do not test higher level JSX components, as we do not know what
5255
// low-level DOM element this maps to.
5356
return;
54-
} else if (isHiddenFromScreenReader(type, attributes)) {
55-
return;
56-
} else if (isInteractiveElement(type, attributes)) {
57-
return;
5857
} else if (
59-
['presentation', 'none'].indexOf(
60-
getLiteralPropValue(getProp(attributes, 'role')),
61-
) > -1
58+
isHiddenFromScreenReader(type, attributes)
59+
|| isPresentationRole(type, attributes)
6260
) {
6361
// Presentation is an intentional signal from the author that this
6462
// element is not meant to be perceivable. For example, a click screen
6563
// to close a dialog .
6664
return;
67-
} else if (
68-
isInteractiveRole(type, attributes)
69-
&& getTabIndex(getProp(attributes, 'tabIndex')) !== undefined
70-
) {
71-
return;
7265
}
7366

74-
context.report({
75-
node,
76-
message: errorMessage,
77-
});
67+
if (
68+
!isInteractiveElement(type, attributes)
69+
&& isInteractiveRole(type, attributes)
70+
&& !hasTabindex
71+
) {
72+
context.report({
73+
node,
74+
message: errorMessage,
75+
});
76+
}
7877
},
7978
}),
8079
};

src/rules/onclick-has-role.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import { getProp, getPropValue, elementType } from 'jsx-ast-utils';
99
import { generateObjSchema } from '../util/schemas';
1010
import isHiddenFromScreenReader from '../util/isHiddenFromScreenReader';
1111
import isInteractiveElement from '../util/isInteractiveElement';
12+
import isInteractiveRole from '../util/isInteractiveRole';
13+
import isPresentationRole from '../util/isPresentationRole';
14+
1215

1316
// ----------------------------------------------------------------------------
1417
// Rule Definition
@@ -34,16 +37,28 @@ module.exports = {
3437
}
3538

3639
const type = elementType(node);
40+
const hasRole = getPropValue(getProp(attributes, 'role')) !== undefined;
3741

3842
if (!domElements.includes(type)) {
3943
// Do not test higher level JSX components, as we do not know what
4044
// low-level DOM element this maps to.
4145
return;
42-
} else if (isHiddenFromScreenReader(type, attributes)) {
46+
} else if (
47+
isHiddenFromScreenReader(type, attributes)
48+
|| isPresentationRole(type, attributes)
49+
) {
50+
// Presentation is an intentional signal from the author that this
51+
// element is not meant to be perceivable. For example, a click screen
52+
// to close a dialog .
4353
return;
44-
} else if (isInteractiveElement(type, attributes)) {
54+
} else if (
55+
isInteractiveElement(type, attributes)
56+
|| isInteractiveRole(type, attributes)
57+
) {
4558
return;
46-
} else if (getPropValue(getProp(attributes, 'role'))) {
59+
} else if (hasRole) {
60+
// The element has a value in a role attribute, but the literal value
61+
// cannot be determined, so we must assume it's valid.
4762
return;
4863
}
4964

0 commit comments

Comments
 (0)