Skip to content

Commit bbcc1a2

Browse files
sohkaibeefancohen
authored andcommitted
[fix] Fix checks involving the tabIndex attribute that do not account for integer literals (#48)
* Add failing tests to onclick-has-focus when tabIndexes are 0 * Reorganize onclick-has-focus failing tests * Coerce tabIndex attributes to a number to check for their validity
1 parent c2ccd06 commit bbcc1a2

File tree

3 files changed

+21
-13
lines changed

3 files changed

+21
-13
lines changed

src/rules/onclick-has-focus.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import getAttributeValue from '../util/getAttributeValue';
1515
// ----------------------------------------------------------------------------
1616

1717
const errorMessage = 'Elements with onClick handlers must be focusable. ' +
18-
'Either set the tabIndex property (usually 0), or use an element type which ' +
19-
'is inherently focusable such as `button`.';
18+
'Either set the tabIndex property to a valid value (usually 0), or use ' +
19+
'an element type which is inherently focusable such as `button`.';
2020

2121
module.exports = context => ({
2222
JSXOpeningElement: node => {
@@ -31,7 +31,7 @@ module.exports = context => ({
3131
return;
3232
} else if (isInteractiveElement(type, attributes)) {
3333
return;
34-
} else if (getAttributeValue(getAttribute(attributes, 'tabIndex'))) {
34+
} else if (!isNaN(Number(getAttributeValue(getAttribute(attributes, 'tabIndex'))))) {
3535
return;
3636
}
3737

src/util/isInteractiveElement.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
'use strict';
22

33
import getAttribute from './getAttribute';
4-
import { getLiteralAttributeValue } from './getAttributeValue';
4+
import getAttributeValue, { getLiteralAttributeValue } from './getAttributeValue';
55
import DOMElements from './attributes/DOM';
66

77

88

99
// Map of tagNames to functions that return whether that element is interactive or not.
1010
const interactiveMap = {
1111
a: attributes => {
12-
const href = getAttribute(attributes, 'href');
13-
const tabIndex = getAttribute(attributes, 'tabIndex');
14-
return (Boolean(href) || (!href && Boolean(tabIndex)));
12+
const href = getAttributeValue(getAttribute(attributes, 'href'));
13+
const tabIndex = getAttributeValue(getAttribute(attributes, 'tabIndex'));
14+
return Boolean(href) || !isNaN(Number(tabIndex));
1515
},
1616
// This is same as `a` interactivity function
1717
area: attributes => interactiveMap.a(attributes),

tests/src/rules/onclick-has-focus.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ const ruleTester = new RuleTester();
2727

2828
const expectedError = {
2929
message: 'Elements with onClick handlers must be focusable. ' +
30-
'Either set the tabIndex property (usually 0), or use an element type which ' +
31-
'is inherently focusable such as `button`.',
30+
'Either set the tabIndex property to a valid value (usually 0), ' +
31+
'or use an element type which is inherently focusable such as `button`.',
3232
type: 'JSXOpeningElement'
3333
};
3434

@@ -46,20 +46,23 @@ ruleTester.run('onclick-has-focus', rule, {
4646
{ code: '<div aria-hidden={2 >= 1} onClick={() => void 0} />', parserOptions },
4747
{ code: '<input type="text" onClick={() => void 0} />', parserOptions },
4848
{ code: '<input type="hidden" onClick={() => void 0} tabIndex="-1" />', parserOptions },
49+
{ code: '<input type="hidden" onClick={() => void 0} tabIndex={-1} />', parserOptions },
4950
{ code: '<input onClick={() => void 0} />', parserOptions },
5051
{ code: '<button onClick={() => void 0} className="foo" />', parserOptions },
5152
{ code: '<option onClick={() => void 0} className="foo" />', parserOptions },
5253
{ code: '<select onClick={() => void 0} className="foo" />', parserOptions },
5354
{ code: '<area href="#" onClick={() => void 0} className="foo" />', parserOptions },
5455
{ code: '<textarea onClick={() => void 0} className="foo" />', parserOptions },
5556
{ code: '<a tabIndex="0" onClick={() => void 0} />', parserOptions },
57+
{ code: '<a tabIndex={0} onClick={() => void 0} />', parserOptions },
5658
{ code: '<a role="button" href="#" onClick={() => void 0} />', parserOptions },
5759
{ code: '<a onClick={() => void 0} href="http://x.y.z" />', parserOptions },
5860
{ code: '<a onClick={() => void 0} href="http://x.y.z" tabIndex="0" />', parserOptions },
61+
{ code: '<a onClick={() => void 0} href="http://x.y.z" tabIndex={0} />', parserOptions },
5962
{ code: '<TestComponent onClick={doFoo} />', parserOptions },
6063
{ code: '<input onClick={() => void 0} type="hidden" />;', parserOptions },
6164
{ code: '<span onClick="doSomething();" tabIndex="0">Click me!</span>', parserOptions },
62-
{ code: '<span onClick="doSomething();" tabIndex="0">Click me!</span>', parserOptions },
65+
{ code: '<span onClick="doSomething();" tabIndex={0}>Click me!</span>', parserOptions },
6366
{ code: '<span onClick="doSomething();" tabIndex="-1">Click me too!</span>', parserOptions },
6467
{ code: '<a href="javascript:void(0);" onClick="doSomething();">Click ALL the things!</a>', parserOptions },
6568
{ code: '<Foo.Bar onClick={() => void 0} aria-hidden={false} />;', parserOptions },
@@ -69,17 +72,22 @@ ruleTester.run('onclick-has-focus', rule, {
6972
invalid: [
7073
{ code: '<span onClick="submitForm();">Submit</span>', errors: [ expectedError ], parserOptions },
7174
{ code: '<span onClick="submitForm();" tabIndex={undefined}>Submit</span>', errors: [ expectedError ], parserOptions },
75+
{ code: '<span onClick="submitForm();" tabIndex="bad">Submit</span>', errors: [ expectedError ], parserOptions },
7276
{ code: '<a onClick="showNextPage();">Next page</a>', errors: [ expectedError ], parserOptions },
77+
{ code: '<a onClick="showNextPage();" tabIndex={undefined}>Next page</a>', errors: [ expectedError ], parserOptions },
78+
{ code: '<a onClick="showNextPage();" tabIndex="bad">Next page</a>', errors: [ expectedError ], parserOptions },
79+
{ code: '<a onClick={() => void 0} />', errors: [ expectedError ], parserOptions },
7380
{ code: '<area onClick={() => void 0} className="foo" />', errors: [ expectedError ], parserOptions },
7481
{ code: '<div onClick={() => void 0} />;', errors: [ expectedError ], parserOptions },
82+
{ code: '<div onClick={() => void 0} tabIndex={undefined} />;', errors: [ expectedError ], parserOptions },
83+
{ code: '<div onClick={() => void 0} tabIndex="bad" />;', errors: [ expectedError ], parserOptions },
7584
{ code: '<div onClick={() => void 0} role={undefined} />;', errors: [ expectedError ], parserOptions },
85+
{ code: '<div onClick={() => void 0} aria-hidden={false} />;', errors: [ expectedError ], parserOptions },
7686
{ code: '<div onClick={() => void 0} {...props} />;', errors: [ expectedError ], parserOptions },
7787
{ code: '<section onClick={() => void 0} />;', errors: [ expectedError ], parserOptions },
7888
{ code: '<main onClick={() => void 0} />;', errors: [ expectedError ], parserOptions },
7989
{ code: '<article onClick={() => void 0} />;', errors: [ expectedError ], parserOptions },
8090
{ code: '<header onClick={() => void 0} />;', errors: [ expectedError ], parserOptions },
81-
{ code: '<footer onClick={() => void 0} />;', errors: [ expectedError ], parserOptions },
82-
{ code: '<div onClick={() => void 0} aria-hidden={false} />;', errors: [ expectedError ], parserOptions },
83-
{ code: '<a onClick={() => void 0} />', errors: [ expectedError ], parserOptions }
91+
{ code: '<footer onClick={() => void 0} />;', errors: [ expectedError ], parserOptions }
8492
]
8593
});

0 commit comments

Comments
 (0)