Skip to content

Commit d54d3e7

Browse files
authored
Feat/rule requires accessibility label (#35)
* feat: add rule requires-accessibility-label * feat: add breaking example * docs: update README and add specific docs * feat: add suggestion for auto-fix * fix: fix linting in plugin * refactor: use hasLabelProp * fix: rewrite hasOneLabeledChild
1 parent 518c1fc commit d54d3e7

7 files changed

Lines changed: 205 additions & 2 deletions

File tree

example-app/.eslintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"@bam.tech/image-requires-accessible-prop": "error",
1313
"@bam.tech/do-not-use-role-on-image": "error",
1414
"@bam.tech/accessibility-props-require-accessible": "error",
15-
"@bam.tech/requires-accessibility-role-when-accessible": "error"
15+
"@bam.tech/requires-accessibility-role-when-accessible": "error",
16+
"@bam.tech/requires-accessibility-label": "error"
1617
}
1718
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/* eslint-disable @typescript-eslint/no-empty-function */
2+
/* eslint-disable react-native-a11y/has-valid-accessibility-ignores-invert-colors */
3+
// Save without formatting: [⌘ + K] > [S]
4+
5+
// This should trigger an error breaking eslint-plugin-bam-custom-rules:
6+
// bam-custom-rules/requires-accessibility-label
7+
8+
import { Text, View } from "react-native";
9+
10+
export const MyComponent = () => {
11+
return (
12+
<>
13+
<View accessible />
14+
<View accessible>
15+
<View accessibilityLabel="" />
16+
<Text>this is a text</Text>
17+
</View>
18+
</>
19+
);
20+
};

packages/eslint-plugin/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ This plugin exports some custom rules that you can optionally use in your projec
6363
| [do-not-use-role-on-image](docs/rules/do-not-use-role-on-image.md) | Disallow role prop on Image component | 🔧 | |
6464
| [image-requires-accessible-prop](docs/rules/image-requires-accessible-prop.md) | Require accessible prop on image components | 🔧 | 💡 |
6565
| [require-named-effect](docs/rules/require-named-effect.md) | Enforces the use of named functions inside a useEffect | | |
66+
| [requires-accessibility-label](docs/rules/requires-accessibility-label.md) | Enforces label when component accessible | | 💡 |
6667
| [requires-accessibility-role-when-accessible](docs/rules/requires-accessibility-role-when-accessible.md) | Enforces accessibilityRole or role when component is accessible | | 💡 |
6768

6869
<!-- end auto-generated rules list -->
@@ -78,7 +79,8 @@ To use a rule, just declare it in your `.eslintrc`:
7879
"@bam.tech/image-requires-accessible-prop": "error",
7980
"@bam.tech/do-not-use-role-on-image": "error",
8081
"@bam.tech/accessibility-props-require-accessible": "error",
81-
"@bam.tech/requires-accessibility-role-when-accessible": "error"
82+
"@bam.tech/requires-accessibility-role-when-accessible": "error",
83+
"@bam.tech/requires-accessibility-label": "error"
8284
}
8385
}
8486
```
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Enforces label when component accessible (`@bam.tech/requires-accessibility-label`)
2+
3+
💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
4+
5+
<!-- end auto-generated rule header -->
6+
7+
Please describe the origin of the rule here.
8+
9+
## Rule Details
10+
11+
This rule aims to...
12+
13+
Examples of **incorrect** code for this rule:
14+
15+
```jsx
16+
<View accessible />
17+
```
18+
19+
```jsx
20+
<View accessible>
21+
<Text>foo</Text>
22+
<View accessible accessibilityLabel="foo" />
23+
</View>
24+
```
25+
26+
Examples of **correct** code for this rule:
27+
28+
```jsx
29+
<View accessible accessibilityLabel="foo" />
30+
```
31+
32+
```jsx
33+
<View accessible>
34+
<Text>foo</Text>
35+
</View>
36+
```
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* @fileoverview Enforces label when component accessible
3+
* @author Paul Briand
4+
*/
5+
"use strict";
6+
7+
const hasLabelProp = require("../utils/hasLabelProp");
8+
const isAccessible = require("../utils/isAccessible");
9+
const isText = require("../utils/isText");
10+
11+
//------------------------------------------------------------------------------
12+
// Rule Definition
13+
//------------------------------------------------------------------------------
14+
15+
/** @type {import('eslint').Rule.RuleModule} */
16+
module.exports = {
17+
meta: {
18+
hasSuggestions: true,
19+
type: "problem", // `problem`, `suggestion`, or `layout`
20+
docs: {
21+
description: "Enforces label when component accessible",
22+
recommended: false,
23+
url: "https://github.com/bamlab/react-native-project-config/tree/main/packages/eslint-plugin/docs/rules/requires-accessibility-label.md", // URL to the documentation page for this rule
24+
},
25+
fixable: null, // Or `code` or `whitespace`
26+
schema: [], // Add a schema if the rule has options
27+
messages: {
28+
requiresAccessibilityLabel: "Requires accessibilityLabel",
29+
suggestAddingAccessibilityLabel: "Add accessibilityLabel",
30+
},
31+
},
32+
33+
create(context) {
34+
return {
35+
JSXOpeningElement(node) {
36+
if (isAccessible(node) && !isText(node)) {
37+
if (!hasLabelProp(node) && !hasOneLabeledChild(node.parent)) {
38+
context.report({
39+
node,
40+
messageId: "requiresAccessibilityLabel",
41+
suggest: [
42+
{
43+
messageId: "suggestAddingAccessibilityLabel",
44+
fix: (fixer) => {
45+
const openingTagEnd = node.range[1];
46+
return fixer.insertTextBeforeRange(
47+
[
48+
openingTagEnd - (node.selfClosing ? 2 : 1),
49+
openingTagEnd,
50+
],
51+
` accessibilityLabel=""`
52+
);
53+
},
54+
},
55+
],
56+
});
57+
}
58+
}
59+
},
60+
};
61+
},
62+
};
63+
64+
const hasOneLabeledChild = (node) => {
65+
if (node.type === "JSXElement") {
66+
const JSXChildren = node.children.filter(
67+
(child) => child.type === "JSXElement"
68+
);
69+
if (JSXChildren.length === 1) {
70+
return (
71+
isLabeledComponent(JSXChildren[0].openingElement) ||
72+
hasOneLabeledChild(JSXChildren[0])
73+
);
74+
}
75+
}
76+
};
77+
78+
const isLabeledComponent = (element) =>
79+
isText(element) || hasLabelProp(element);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module.exports = (element) => {
2+
if (element.type === "JSXOpeningElement") {
3+
return element.attributes.some((attribute) => {
4+
return (
5+
attribute.type === "JSXAttribute" &&
6+
["accessibilityLabel", "aria-label", "alt"].includes(
7+
attribute.name.name
8+
)
9+
);
10+
});
11+
}
12+
return false;
13+
};
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* @fileoverview Enforces label when component accessible
3+
* @author Paul Briand
4+
*/
5+
"use strict";
6+
7+
//------------------------------------------------------------------------------
8+
// Requirements
9+
//------------------------------------------------------------------------------
10+
11+
const rule = require("../../../lib/rules/requires-accessibility-label"),
12+
RuleTester = require("eslint").RuleTester;
13+
14+
//------------------------------------------------------------------------------
15+
// Tests
16+
//------------------------------------------------------------------------------
17+
18+
const ruleTester = new RuleTester({
19+
parser: require.resolve("@typescript-eslint/parser"),
20+
parserOptions: {
21+
ecmaVersion: 2021,
22+
sourceType: "module",
23+
ecmaFeatures: {
24+
jsx: true,
25+
},
26+
},
27+
});
28+
29+
const valid = [
30+
`<View accessible accessibilityLabel="foo" />`,
31+
`<View accessible>
32+
<Text>foo</Text>
33+
</View>`,
34+
];
35+
36+
ruleTester.run("requires-accessibility-label", rule, {
37+
valid,
38+
39+
invalid: [
40+
{
41+
code: `<View accessible />`,
42+
errors: ["Requires accessibilityLabel"],
43+
},
44+
{
45+
code: `<View accessible>
46+
<Text>foo</Text>
47+
<View accessible accessibilityLabel="foo" />
48+
</View>`,
49+
errors: ["Requires accessibilityLabel"],
50+
},
51+
],
52+
});

0 commit comments

Comments
 (0)