Skip to content

Commit 9aa878b

Browse files
JoshuaKGoldbergljharb
authored andcommitted
[Fix] no-noninteractive-element-interactions: Ignore contenteditable elements in no-noninteractive-element-interactions
1 parent 3d77c84 commit 9aa878b

5 files changed

+65
-0
lines changed

__mocks__/JSXAttributeMock.js

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export default function JSXAttributeMock(prop: string, value: mixed, isExpressio
2424
let attributeValue = astValue;
2525
if (isExpressionContainer || astValue.type !== 'Literal') {
2626
attributeValue = JSXExpressionContainerMock(astValue);
27+
} else if (attributeValue.type === 'Literal' && !('raw' in (attributeValue: any))) {
28+
(attributeValue: any).raw = JSON.stringify((attributeValue: any).value);
2729
}
2830

2931
return {

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

+3
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,9 @@ const neverValid = [
337337
{ code: '<time onClick={() => {}} />;', errors: [expectedError] },
338338
{ code: '<ol onClick={() => {}} />;', errors: [expectedError] },
339339
{ code: '<ul onClick={() => {}} />;', errors: [expectedError] },
340+
{ code: '<ul contentEditable="false" onClick={() => {}} />;', errors: [expectedError] },
341+
{ code: '<article contentEditable onClick={() => {}} />;', errors: [expectedError] },
342+
{ code: '<div contentEditable role="article" onKeyDown={() => {}} />;', errors: [expectedError] },
340343
/* HTML elements attributed with a non-interactive role */
341344
{ code: '<div role="alert" onClick={() => {}} />;', errors: [expectedError] },
342345
{ code: '<div role="alertdialog" onClick={() => {}} />;', errors: [expectedError] },
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import expect from 'expect';
2+
import isContentEditable from '../../../src/util/isContentEditable';
3+
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
4+
5+
describe('isContentEditable', () => {
6+
describe('HTML5', () => {
7+
describe('content editable', () => {
8+
it('should identify HTML5 contentEditable elements', () => {
9+
const attributes = [
10+
JSXAttributeMock('contentEditable', 'true'),
11+
];
12+
expect(isContentEditable('some tag', attributes))
13+
.toBe(true);
14+
});
15+
});
16+
17+
describe('not content editable', () => {
18+
it('should not identify HTML5 content editable elements with null as the value', () => {
19+
const attributes = [
20+
JSXAttributeMock('contentEditable', null),
21+
];
22+
expect(isContentEditable('some tag', attributes))
23+
.toBe(false);
24+
});
25+
26+
it('should not identify HTML5 content editable elements with undefined as the value', () => {
27+
const attributes = [
28+
JSXAttributeMock('contentEditable', undefined),
29+
];
30+
expect(isContentEditable('some tag', attributes))
31+
.toBe(false);
32+
});
33+
34+
it('should not identify HTML5 content editable elements with true as the value', () => {
35+
const attributes = [
36+
JSXAttributeMock('contentEditable', true),
37+
];
38+
expect(isContentEditable('some tag', attributes))
39+
.toBe(false);
40+
});
41+
42+
it('should not identify HTML5 content editable elements with "false" as the value', () => {
43+
const attributes = [
44+
JSXAttributeMock('contentEditable', 'false'),
45+
];
46+
expect(isContentEditable('some tag', attributes))
47+
.toBe(false);
48+
});
49+
});
50+
});
51+
});

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

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type { ESLintConfig, ESLintContext, ESLintVisitorSelectorConfig } from '.
2222
import { arraySchema, generateObjSchema } from '../util/schemas';
2323
import getElementType from '../util/getElementType';
2424
import isAbstractRole from '../util/isAbstractRole';
25+
import isContentEditable from '../util/isContentEditable';
2526
import isHiddenFromScreenReader from '../util/isHiddenFromScreenReader';
2627
import isInteractiveElement from '../util/isInteractiveElement';
2728
import isInteractiveRole from '../util/isInteractiveRole';
@@ -78,6 +79,7 @@ export default ({
7879
}
7980
if (
8081
!hasInteractiveProps
82+
|| isContentEditable(type, attributes)
8183
|| isHiddenFromScreenReader(type, attributes)
8284
|| isPresentationRole(type, attributes)
8385
) {

src/util/isContentEditable.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { getProp } from 'jsx-ast-utils';
2+
3+
export default function isContentEditable(tagName, attributes) {
4+
const prop = getProp(attributes, 'contentEditable');
5+
6+
return prop?.value?.raw === '"true"';
7+
}

0 commit comments

Comments
 (0)