Skip to content

Commit 56d3b9a

Browse files
jessebeachbeefancohen
authored andcommitted
[484] Fix role-has-required-aria-props for semantic elements like input[checkbox]
1 parent 46e9abd commit 56d3b9a

File tree

5 files changed

+102
-4
lines changed

5 files changed

+102
-4
lines changed

__tests__/src/rules/role-has-required-aria-props-test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ ruleTester.run('role-has-required-aria-props', rule, {
5555
{ code: '<div role={role || "foobar"} />' },
5656
{ code: '<div role="row" />' },
5757
{ code: '<span role="checkbox" aria-checked="false" aria-labelledby="foo" tabindex="0"></span>' },
58+
{ code: '<input type="checkbox" role="switch" />' },
5859
].concat(basicValidityTests).map(parserOptionsMapper),
5960

6061
invalid: [
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/* eslint-env mocha */
2+
import expect from 'expect';
3+
import isSemanticRoleElement from '../../../src/util/isSemanticRoleElement';
4+
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
5+
6+
describe('isSemanticRoleElement', () => {
7+
it('should identify semantic role elements', () => {
8+
expect(isSemanticRoleElement('input', [
9+
JSXAttributeMock('type', 'checkbox'),
10+
JSXAttributeMock('role', 'switch'),
11+
])).toBe(true);
12+
});
13+
it('should reject non-semantic role elements', () => {
14+
expect(isSemanticRoleElement('input', [
15+
JSXAttributeMock('type', 'radio'),
16+
JSXAttributeMock('role', 'switch'),
17+
])).toBe(false);
18+
expect(isSemanticRoleElement('input', [
19+
JSXAttributeMock('type', 'text'),
20+
JSXAttributeMock('role', 'combobox'),
21+
])).toBe(false);
22+
expect(isSemanticRoleElement('button', [
23+
JSXAttributeMock('role', 'switch'),
24+
JSXAttributeMock('aria-pressed', 'true'),
25+
])).toBe(false);
26+
expect(isSemanticRoleElement('input', [
27+
JSXAttributeMock('role', 'switch'),
28+
])).toBe(false);
29+
});
30+
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"aria-query": "^3.0.0",
6565
"array-includes": "^3.0.3",
6666
"ast-types-flow": "^0.0.7",
67-
"axobject-query": "^2.0.1",
67+
"axobject-query": "^2.0.2",
6868
"damerau-levenshtein": "^1.0.4",
6969
"emoji-regex": "^6.5.1",
7070
"has": "^1.0.3",

src/rules/role-has-required-aria-props.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
propName,
1717
} from 'jsx-ast-utils';
1818
import { generateObjSchema } from '../util/schemas';
19+
import isSemanticRoleElement from '../util/isSemanticRoleElement';
1920

2021
const errorMessage = (role, requiredProps) => (
2122
`Elements with the ARIA role "${role}" must have the following attributes defined: ${String(requiredProps).toLowerCase()}`
@@ -44,19 +45,26 @@ module.exports = {
4445
return;
4546
}
4647

47-
const value = getLiteralPropValue(attribute);
48+
const roleAttrValue = getLiteralPropValue(attribute);
49+
const { attributes } = attribute.parent;
4850

4951
// If value is undefined, then the role attribute will be dropped in the DOM.
5052
// If value is null, then getLiteralAttributeValue is telling us
5153
// that the value isn't in the form of a literal.
52-
if (value === undefined || value === null) {
54+
if (roleAttrValue === undefined || roleAttrValue === null) {
5355
return;
5456
}
5557

56-
const normalizedValues = String(value).toLowerCase().split(' ');
58+
const normalizedValues = String(roleAttrValue).toLowerCase().split(' ');
5759
const validRoles = normalizedValues
5860
.filter(val => [...roles.keys()].indexOf(val) > -1);
5961

62+
// Check semantic DOM elements
63+
// For example, <input type="checkbox" role="switch" />
64+
if (isSemanticRoleElement(type, attributes)) {
65+
return;
66+
}
67+
// Check arbitrary DOM elements
6068
validRoles.forEach((role) => {
6169
const {
6270
requiredProps: requiredPropKeyValues,

src/util/isSemanticRoleElement.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* @flow
3+
*/
4+
5+
import type { JSXAttribute } from 'ast-types-flow';
6+
import { AXObjectRoles, elementAXObjects } from 'axobject-query';
7+
import { getLiteralPropValue, getProp, propName } from 'jsx-ast-utils';
8+
9+
const isSemanticRoleElement = (
10+
elementType: string,
11+
attributes: Array<JSXAttribute>,
12+
): boolean => {
13+
const roleAttr = getProp(attributes, 'role');
14+
let res = false;
15+
const roleAttrValue = getLiteralPropValue(roleAttr);
16+
elementAXObjects.forEach((axObjects, concept) => {
17+
if (res) {
18+
return;
19+
}
20+
if (
21+
concept.name === elementType
22+
&& (concept.attributes || []).every(
23+
cAttr => attributes.some(
24+
(attr) => {
25+
const namesMatch = cAttr.name === propName(attr);
26+
let valuesMatch;
27+
if (cAttr.value !== undefined) {
28+
valuesMatch = cAttr.value === getLiteralPropValue(attr);
29+
}
30+
if (!namesMatch) {
31+
return false;
32+
}
33+
return namesMatch && (valuesMatch !== undefined) ? valuesMatch : true;
34+
},
35+
),
36+
)
37+
) {
38+
axObjects.forEach((name) => {
39+
if (res) {
40+
return;
41+
}
42+
const roles = AXObjectRoles.get(name);
43+
if (roles) {
44+
roles.forEach((role) => {
45+
if (res === true) {
46+
return;
47+
}
48+
if (role.name === roleAttrValue) {
49+
res = true;
50+
}
51+
});
52+
}
53+
});
54+
}
55+
});
56+
return res;
57+
};
58+
59+
export default isSemanticRoleElement;

0 commit comments

Comments
 (0)