Skip to content

Commit f673943

Browse files
jessebeachljharb
authored andcommitted
Introduce utils to get roles from elements
1 parent b0c582a commit f673943

8 files changed

+191
-17
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/* eslint-env jest */
2+
import getComputedRole from '../../../src/util/getComputedRole';
3+
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
4+
5+
describe('getComputedRole', () => {
6+
describe('explicit role', () => {
7+
describe('valid role', () => {
8+
it('should return the role', () => {
9+
expect(getComputedRole(
10+
'div',
11+
[JSXAttributeMock('role', 'button')],
12+
)).toBe('button');
13+
});
14+
});
15+
describe('invalid role', () => {
16+
describe('has implicit', () => {
17+
it('should return the implicit role', () => {
18+
expect(getComputedRole(
19+
'li',
20+
[JSXAttributeMock('role', 'beeswax')],
21+
)).toBe('listitem');
22+
});
23+
});
24+
describe('lacks implicit', () => {
25+
it('should return null', () => {
26+
expect(getComputedRole(
27+
'div',
28+
[JSXAttributeMock('role', 'beeswax')],
29+
)).toBeNull();
30+
});
31+
});
32+
});
33+
34+
describe('no role', () => {
35+
describe('has implicit', () => {
36+
it('should return the implicit role', () => {
37+
expect(getComputedRole(
38+
'li',
39+
[],
40+
)).toBe('listitem');
41+
});
42+
});
43+
describe('lacks implicit', () => {
44+
it('should return null', () => {
45+
expect(getComputedRole(
46+
'div',
47+
[],
48+
)).toBeNull();
49+
});
50+
});
51+
});
52+
});
53+
describe('implicit role', () => {
54+
describe('has implicit', () => {
55+
it('should return the implicit role', () => {
56+
expect(getComputedRole(
57+
'li',
58+
[JSXAttributeMock('role', 'beeswax')],
59+
)).toBe('listitem');
60+
});
61+
});
62+
describe('lacks implicit', () => {
63+
it('should return null', () => {
64+
expect(getComputedRole(
65+
'div',
66+
[],
67+
)).toBeNull();
68+
});
69+
});
70+
});
71+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/* eslint-env jest */
2+
import getExplicitRole from '../../../src/util/getExplicitRole';
3+
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
4+
5+
describe('getExplicitRole', () => {
6+
describe('valid role', () => {
7+
it('should return the role', () => {
8+
expect(getExplicitRole(
9+
'div',
10+
[JSXAttributeMock('role', 'button')],
11+
)).toBe('button');
12+
});
13+
});
14+
describe('invalid role', () => {
15+
it('should return null', () => {
16+
expect(getExplicitRole(
17+
'div',
18+
[JSXAttributeMock('role', 'beeswax')],
19+
)).toBeNull();
20+
});
21+
});
22+
describe('no role', () => {
23+
it('should return null', () => {
24+
expect(getExplicitRole(
25+
'div',
26+
[],
27+
)).toBeNull();
28+
});
29+
});
30+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/* eslint-env jest */
2+
import getImplicitRole from '../../../src/util/getImplicitRole';
3+
4+
describe('getImplicitRole', () => {
5+
describe('has implicit', () => {
6+
it('should return the implicit role', () => {
7+
expect(getImplicitRole(
8+
'li',
9+
[],
10+
)).toBe('listitem');
11+
});
12+
});
13+
describe('lacks implicit', () => {
14+
it('should return null', () => {
15+
expect(getImplicitRole(
16+
'div',
17+
[],
18+
)).toBeNull();
19+
});
20+
});
21+
});

src/rules/no-noninteractive-element-to-interactive-role.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ import {
1414
} from 'aria-query';
1515
import {
1616
elementType,
17-
getProp,
18-
getLiteralPropValue,
1917
propName,
2018
} from 'jsx-ast-utils';
2119
import type {
@@ -24,6 +22,7 @@ import type {
2422
import includes from 'array-includes';
2523
import type { ESLintContext } from '../../flow/eslint';
2624
import type { ESLintJSXAttribute } from '../../flow/eslint-jsx';
25+
import getExplicitRole from '../util/getExplicitRole';
2726
import isNonInteractiveElement from '../util/isNonInteractiveElement';
2827
import isInteractiveRole from '../util/isInteractiveRole';
2928

@@ -58,7 +57,7 @@ module.exports = {
5857
const node = attribute.parent;
5958
const { attributes } = node;
6059
const type = elementType(node);
61-
const role = getLiteralPropValue(getProp(node.attributes, 'role'));
60+
const role = getExplicitRole(type, node.attributes);
6261

6362
if (!includes(domElements, type)) {
6463
// Do not test higher level JSX components, as we do not know what

src/rules/no-redundant-roles.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
// Rule Definition
99
// ----------------------------------------------------------------------------
1010

11-
import { elementType, getProp, getLiteralPropValue } from 'jsx-ast-utils';
11+
import { elementType } from 'jsx-ast-utils';
1212
import { generateObjSchema } from '../util/schemas';
13+
import getExplicitRole from '../util/getExplicitRole';
1314
import getImplicitRole from '../util/getImplicitRole';
1415

1516
const errorMessage = (element, implicitRole) =>
@@ -27,15 +28,13 @@ module.exports = {
2728
JSXOpeningElement: (node) => {
2829
const type = elementType(node);
2930
const implicitRole = getImplicitRole(type, node.attributes);
31+
const explicitRole = getExplicitRole(type, node.attributes);
3032

31-
if (implicitRole === '') {
33+
if (!implicitRole || !explicitRole) {
3234
return;
3335
}
3436

35-
const role = getProp(node.attributes, 'role');
36-
const roleValue = getLiteralPropValue(role);
37-
38-
if (typeof roleValue === 'string' && roleValue.toUpperCase() === implicitRole.toUpperCase()) {
37+
if (implicitRole === explicitRole) {
3938
context.report({
4039
node,
4140
message: errorMessage(type, implicitRole.toLowerCase()),

src/util/getComputedRole.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// @flow
2+
import type { Node } from 'ast-types-flow';
3+
import getExplicitRole from './getExplicitRole';
4+
import getImplicitRole from './getImplicitRole';
5+
/**
6+
* Returns an element's computed role, which is
7+
*
8+
* 1. The valid value of its explicit role attribute; or
9+
* 2. The implicit value of its tag.
10+
*/
11+
export default function getComputedRole(
12+
tag: string,
13+
attributes: Array<Node>,
14+
): ?string {
15+
return getExplicitRole(tag, attributes) || getImplicitRole(tag, attributes);
16+
}

src/util/getExplicitRole.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// @flow
2+
import {
3+
roles as rolesMap,
4+
} from 'aria-query';
5+
import {
6+
getProp,
7+
getLiteralPropValue,
8+
} from 'jsx-ast-utils';
9+
import type { Node } from 'ast-types-flow';
10+
/**
11+
* Returns an element's computed role, which is
12+
*
13+
* 1. The valid value of its explicit role attribute; or
14+
* 2. The implicit value of its tag.
15+
*/
16+
export default function getExplicitRole(
17+
tag: string,
18+
attributes: Array<Node>,
19+
): ?string {
20+
const explicitRole = (function toLowerCase(role) {
21+
if (typeof role === 'string') {
22+
return role.toLowerCase();
23+
}
24+
return null;
25+
}(getLiteralPropValue(getProp(attributes, 'role'))));
26+
27+
if (rolesMap.has(explicitRole)) {
28+
return explicitRole;
29+
}
30+
return null;
31+
}

src/util/getImplicitRole.js

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
1+
// @flow
2+
import {
3+
roles as rolesMap,
4+
} from 'aria-query';
5+
import type { Node } from 'ast-types-flow';
16
import implicitRoles from './implicitRoles';
27

38
/**
49
* Returns an element's implicit role given its attributes and type.
510
* Some elements only have an implicit role when certain props are defined.
6-
*
7-
* @param type - The node's tagName.
8-
* @param attributes - The collection of attributes on the node.
9-
* @returns {String} - String representing the node's implicit role or '' if it doesn't exist.
1011
*/
11-
export default function getImplicitRole(type, attributes) {
12+
export default function getImplicitRole(
13+
type: string,
14+
attributes: Array<Node>,
15+
): ?string {
16+
let implicitRole;
1217
if (implicitRoles[type]) {
13-
return implicitRoles[type](attributes);
18+
implicitRole = implicitRoles[type](attributes);
1419
}
15-
16-
return '';
20+
if (rolesMap.has(implicitRole)) {
21+
return implicitRole;
22+
}
23+
return null;
1724
}

0 commit comments

Comments
 (0)