Skip to content

Commit

Permalink
[484] Fix role-has-required-aria-props for semantic elements like inp…
Browse files Browse the repository at this point in the history
…ut[checkbox]
  • Loading branch information
jessebeach authored and beefancohen committed Oct 16, 2018
1 parent 46e9abd commit 56d3b9a
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 4 deletions.
1 change: 1 addition & 0 deletions __tests__/src/rules/role-has-required-aria-props-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ ruleTester.run('role-has-required-aria-props', rule, {
{ code: '<div role={role || "foobar"} />' },
{ code: '<div role="row" />' },
{ code: '<span role="checkbox" aria-checked="false" aria-labelledby="foo" tabindex="0"></span>' },
{ code: '<input type="checkbox" role="switch" />' },
].concat(basicValidityTests).map(parserOptionsMapper),

invalid: [
Expand Down
30 changes: 30 additions & 0 deletions __tests__/src/util/isSemanticRoleElement-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* eslint-env mocha */
import expect from 'expect';
import isSemanticRoleElement from '../../../src/util/isSemanticRoleElement';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';

describe('isSemanticRoleElement', () => {
it('should identify semantic role elements', () => {
expect(isSemanticRoleElement('input', [
JSXAttributeMock('type', 'checkbox'),
JSXAttributeMock('role', 'switch'),
])).toBe(true);
});
it('should reject non-semantic role elements', () => {
expect(isSemanticRoleElement('input', [
JSXAttributeMock('type', 'radio'),
JSXAttributeMock('role', 'switch'),
])).toBe(false);
expect(isSemanticRoleElement('input', [
JSXAttributeMock('type', 'text'),
JSXAttributeMock('role', 'combobox'),
])).toBe(false);
expect(isSemanticRoleElement('button', [
JSXAttributeMock('role', 'switch'),
JSXAttributeMock('aria-pressed', 'true'),
])).toBe(false);
expect(isSemanticRoleElement('input', [
JSXAttributeMock('role', 'switch'),
])).toBe(false);
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"aria-query": "^3.0.0",
"array-includes": "^3.0.3",
"ast-types-flow": "^0.0.7",
"axobject-query": "^2.0.1",
"axobject-query": "^2.0.2",
"damerau-levenshtein": "^1.0.4",
"emoji-regex": "^6.5.1",
"has": "^1.0.3",
Expand Down
14 changes: 11 additions & 3 deletions src/rules/role-has-required-aria-props.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
propName,
} from 'jsx-ast-utils';
import { generateObjSchema } from '../util/schemas';
import isSemanticRoleElement from '../util/isSemanticRoleElement';

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

const value = getLiteralPropValue(attribute);
const roleAttrValue = getLiteralPropValue(attribute);
const { attributes } = attribute.parent;

// If value is undefined, then the role attribute will be dropped in the DOM.
// If value is null, then getLiteralAttributeValue is telling us
// that the value isn't in the form of a literal.
if (value === undefined || value === null) {
if (roleAttrValue === undefined || roleAttrValue === null) {
return;
}

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

// Check semantic DOM elements
// For example, <input type="checkbox" role="switch" />
if (isSemanticRoleElement(type, attributes)) {
return;
}
// Check arbitrary DOM elements
validRoles.forEach((role) => {
const {
requiredProps: requiredPropKeyValues,
Expand Down
59 changes: 59 additions & 0 deletions src/util/isSemanticRoleElement.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* @flow
*/

import type { JSXAttribute } from 'ast-types-flow';
import { AXObjectRoles, elementAXObjects } from 'axobject-query';
import { getLiteralPropValue, getProp, propName } from 'jsx-ast-utils';

const isSemanticRoleElement = (
elementType: string,
attributes: Array<JSXAttribute>,
): boolean => {
const roleAttr = getProp(attributes, 'role');
let res = false;
const roleAttrValue = getLiteralPropValue(roleAttr);
elementAXObjects.forEach((axObjects, concept) => {
if (res) {
return;
}
if (
concept.name === elementType
&& (concept.attributes || []).every(
cAttr => attributes.some(
(attr) => {
const namesMatch = cAttr.name === propName(attr);
let valuesMatch;
if (cAttr.value !== undefined) {
valuesMatch = cAttr.value === getLiteralPropValue(attr);
}
if (!namesMatch) {
return false;
}
return namesMatch && (valuesMatch !== undefined) ? valuesMatch : true;
},
),
)
) {
axObjects.forEach((name) => {
if (res) {
return;
}
const roles = AXObjectRoles.get(name);
if (roles) {
roles.forEach((role) => {
if (res === true) {
return;
}
if (role.name === roleAttrValue) {
res = true;
}
});
}
});
}
});
return res;
};

export default isSemanticRoleElement;

0 comments on commit 56d3b9a

Please sign in to comment.