Skip to content

Commit 1ae1089

Browse files
More specific tag formatting
1 parent d404c2c commit 1ae1089

File tree

2 files changed

+96
-59
lines changed

2 files changed

+96
-59
lines changed
Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,61 @@
1-
import { RuleTester } from "eslint";
2-
import parserOptionsMapper from "../../__util__/parserOptionsMapper";
3-
import rule from "../../../src/rules/prefer-tag-over-role";
1+
import { RuleTester } from 'eslint';
2+
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
3+
import rule from '../../../src/rules/prefer-tag-over-role';
44

55
const ruleTester = new RuleTester();
66

77
const expectedError = (role, tag) => ({
88
message: `Use ${tag} instead of the "${role}" role to ensure accessibility across all devices.`,
9-
type: "JSXOpeningElement",
9+
type: 'JSXOpeningElement',
1010
});
1111

12-
ruleTester.run("element-rol", rule, {
12+
ruleTester.run('element-role', rule, {
1313
valid: [
14-
{ code: "<div />;" },
14+
{ code: '<div />;' },
1515
{ code: '<div role="unknown" />;' },
16-
{ code: "<other />" },
16+
{ code: '<div role="also unknown" />;' },
17+
{ code: '<other />' },
1718
{ code: '<img role="img" />' },
1819
{ code: '<input role="checkbox" />' },
1920
].map(parserOptionsMapper),
2021
invalid: [
2122
{
2223
code: '<div role="checkbox" />',
23-
errors: [expectedError("checkbox", "<input>")],
24+
errors: [expectedError('checkbox', '<input type="checkbox">')],
25+
},
26+
{
27+
code: '<div role="button checkbox" />',
28+
errors: [expectedError('checkbox', '<input type="checkbox">')],
2429
},
2530
{
2631
code: '<div role="heading" />',
2732
errors: [
28-
expectedError("heading", "<h1>, <h2>, <h3>, <h4>, <h5>, or <h6>"),
33+
expectedError('heading', '<h1>, <h2>, <h3>, <h4>, <h5>, or <h6>'),
2934
],
3035
},
36+
{
37+
code: '<div role="link" />',
38+
errors: [expectedError('link', '<a href=...>, <area href=...>, or <link href=...>')],
39+
},
3140
{
3241
code: '<div role="rowgroup" />',
33-
errors: [expectedError("rowgroup", "<tbody>, <tfoot>, or <thead>")],
42+
errors: [expectedError('rowgroup', '<tbody>, <tfoot>, or <thead>')],
3443
},
3544
{
3645
code: '<span role="checkbox" />',
37-
errors: [expectedError("checkbox", "<input>")],
46+
errors: [expectedError('checkbox', '<input type="checkbox">')],
3847
},
3948
{
4049
code: '<other role="checkbox" />',
41-
errors: [expectedError("checkbox", "<input>")],
50+
errors: [expectedError('checkbox', '<input type="checkbox">')],
4251
},
4352
{
4453
code: '<other role="checkbox" />',
45-
errors: [expectedError("checkbox", "<input>")],
54+
errors: [expectedError('checkbox', '<input type="checkbox">')],
4655
},
4756
{
4857
code: '<div role="banner" />',
49-
errors: [expectedError("banner", "<header>")],
58+
errors: [expectedError('banner', '<header>')],
5059
},
5160
].map(parserOptionsMapper),
5261
});

src/rules/prefer-tag-over-role.js

Lines changed: 73 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,87 @@
1-
import { roleElements } from "aria-query";
2-
import { getProp, getPropValue } from "jsx-ast-utils";
1+
import { roleElements } from 'aria-query';
2+
import { getProp, getPropValue } from 'jsx-ast-utils';
33

4-
import { generateObjSchema } from "../util/schemas";
4+
import getElementType from '../util/getElementType';
5+
import { generateObjSchema } from '../util/schemas';
56

6-
const errorMessage =
7-
'Use {{tag}} instead of the "{{role}}" role to ensure accessibility across all devices.';
7+
const errorMessage = 'Use {{tag}} instead of the "{{role}}" role to ensure accessibility across all devices.';
88

99
const schema = generateObjSchema();
1010

11-
const tag = (input) => `<${input}>`;
11+
const formatTag = (tag) => {
12+
if (!tag.attributes) {
13+
return `<${tag.name}>`;
14+
}
15+
16+
const [attribute] = tag.attributes;
17+
const value = attribute.value ? `"${attribute.value}"` : '...';
18+
19+
return `<${tag.name} ${attribute.name}=${value}>`;
20+
};
21+
22+
const getLastPropValue = (rawProp) => {
23+
const propValue = getPropValue(rawProp);
24+
if (!propValue) {
25+
return propValue;
26+
}
27+
28+
const lastSpaceIndex = propValue.lastIndexOf(' ');
29+
30+
return lastSpaceIndex === -1
31+
? propValue
32+
: propValue.substring(lastSpaceIndex + 1);
33+
};
1234

1335
export default {
1436
meta: {
1537
docs: {
16-
url: "https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/prefer-tag-over-role.md",
38+
url: 'https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/prefer-tag-over-role.md',
1739
},
1840
schema: [schema],
1941
},
2042

21-
create: (context) => ({
22-
JSXOpeningElement: (node) => {
23-
const role = getPropValue(getProp(node.attributes, "role"));
24-
if (!role) {
25-
return;
26-
}
27-
28-
const matchedTags = roleElements.get(role);
29-
if (!matchedTags) {
30-
return;
31-
}
32-
33-
const matchedNames = Array.from(
34-
new Set(Array.from(matchedTags).map((matchedTag) => matchedTag.name))
35-
);
36-
if (matchedNames.some((matchedTag) => matchedTag === node.name.name)) {
37-
return;
38-
}
39-
40-
context.report({
41-
data: {
42-
tag:
43-
matchedNames.length === 1
44-
? `<${matchedNames[0]}>`
45-
: [
46-
matchedNames
47-
.slice(0, matchedNames.length - 1)
48-
.map(tag)
49-
.join(", "),
50-
tag(matchedNames[matchedNames.length - 1]),
51-
].join(", or "),
52-
role,
53-
},
54-
node,
55-
message: errorMessage,
56-
});
57-
},
58-
}),
43+
create: (context) => {
44+
const elementType = getElementType(context);
45+
46+
return {
47+
JSXOpeningElement: (node) => {
48+
const role = getLastPropValue(getProp(node.attributes, 'role'));
49+
if (!role) {
50+
return;
51+
}
52+
53+
const matchedTagsSet = roleElements.get(role);
54+
if (!matchedTagsSet) {
55+
return;
56+
}
57+
58+
const matchedTags = Array.from(matchedTagsSet);
59+
if (
60+
matchedTags.some(
61+
(matchedTag) => matchedTag.name === elementType(node),
62+
)
63+
) {
64+
return;
65+
}
66+
67+
context.report({
68+
data: {
69+
tag:
70+
matchedTags.length === 1
71+
? formatTag(matchedTags[0])
72+
: [
73+
matchedTags
74+
.slice(0, matchedTags.length - 1)
75+
.map(formatTag)
76+
.join(', '),
77+
formatTag(matchedTags[matchedTags.length - 1]),
78+
].join(', or '),
79+
role,
80+
},
81+
node,
82+
message: errorMessage,
83+
});
84+
},
85+
};
86+
},
5987
};

0 commit comments

Comments
 (0)