Skip to content

Commit 518c1fc

Browse files
authored
Feat/rule requires accessibility role (#34)
* feat: add rule require-accessibility-role-when-accessible * feat: add breaking example in example-app * docs: update README and add doc for rule * feat: add auto-fix suggestion
1 parent c57b4e1 commit 518c1fc

7 files changed

Lines changed: 241 additions & 8 deletions

File tree

example-app/.eslintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"@bam.tech/require-named-effect": "error",
1212
"@bam.tech/image-requires-accessible-prop": "error",
1313
"@bam.tech/do-not-use-role-on-image": "error",
14-
"@bam.tech/accessibility-props-require-accessible": "error"
14+
"@bam.tech/accessibility-props-require-accessible": "error",
15+
"@bam.tech/requires-accessibility-role-when-accessible": "error"
1516
}
1617
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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-role-when-accessible
7+
8+
import { View, Pressable, Image } from "react-native";
9+
10+
export const MyComponent = () => {
11+
return (
12+
<>
13+
<View accessible />
14+
<Pressable accessible onPress={() => {}} />
15+
<Image source={{ uri: "" }} accessible />
16+
</>
17+
);
18+
};

packages/eslint-plugin/README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,13 @@ This plugin exports some custom rules that you can optionally use in your projec
5757
🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
5858
💡 Manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
5959

60-
| Name                                   | Description | 🔧 | 💡 |
61-
| :--------------------------------------------------------------------------------------------- | :------------------------------------------------------------ | :-- | :-- |
62-
| [accessibility-props-require-accessible](docs/rules/accessibility-props-require-accessible.md) | Requires accessible prop when accessibility props are defined | 🔧 | |
63-
| [do-not-use-role-on-image](docs/rules/do-not-use-role-on-image.md) | Disallow role prop on Image component | 🔧 | |
64-
| [image-requires-accessible-prop](docs/rules/image-requires-accessible-prop.md) | Require accessible prop on image components | 🔧 | 💡 |
65-
| [require-named-effect](docs/rules/require-named-effect.md) | Enforces the use of named functions inside a useEffect | | |
60+
| Name                                        | Description | 🔧 | 💡 |
61+
| :------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------- | :-- | :-- |
62+
| [accessibility-props-require-accessible](docs/rules/accessibility-props-require-accessible.md) | Requires accessible prop when accessibility props are defined | 🔧 | |
63+
| [do-not-use-role-on-image](docs/rules/do-not-use-role-on-image.md) | Disallow role prop on Image component | 🔧 | |
64+
| [image-requires-accessible-prop](docs/rules/image-requires-accessible-prop.md) | Require accessible prop on image components | 🔧 | 💡 |
65+
| [require-named-effect](docs/rules/require-named-effect.md) | Enforces the use of named functions inside a useEffect | | |
66+
| [requires-accessibility-role-when-accessible](docs/rules/requires-accessibility-role-when-accessible.md) | Enforces accessibilityRole or role when component is accessible | | 💡 |
6667

6768
<!-- end auto-generated rules list -->
6869

@@ -76,7 +77,8 @@ To use a rule, just declare it in your `.eslintrc`:
7677
"@bam.tech/require-named-effect": "error",
7778
"@bam.tech/image-requires-accessible-prop": "error",
7879
"@bam.tech/do-not-use-role-on-image": "error",
79-
"@bam.tech/accessibility-props-require-accessible": "error"
80+
"@bam.tech/accessibility-props-require-accessible": "error",
81+
"@bam.tech/requires-accessibility-role-when-accessible": "error"
8082
}
8183
}
8284
```
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Enforces accessibilityRole or role when component is accessible (`@bam.tech/requires-accessibility-role-when-accessible`)
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+
Enforces accessibilityRole when component is accessible
8+
9+
## Rule Details
10+
11+
Examples of **incorrect** code for this rule:
12+
13+
```jsx
14+
<View accessible />
15+
```
16+
17+
```jsx
18+
<Pressable onPress={() => {}} accessible />
19+
```
20+
21+
```jsx
22+
<Image source={{ uri: "" }} accessible />
23+
```
24+
25+
Examples of **correct** code for this rule:
26+
27+
```jsx
28+
<View accessible accessibilityRole="button" />
29+
```
30+
31+
```jsx
32+
<Pressable accessible accessibilityRole="button" />
33+
```
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* @fileoverview Enforces accessibilityRole or role when component is accessible
3+
* @author Paul Briand
4+
*/
5+
"use strict";
6+
7+
//------------------------------------------------------------------------------
8+
// Rule Definition
9+
//------------------------------------------------------------------------------
10+
const isAccessible = require("../utils/isAccessible");
11+
const isButton = require("../utils/isButton");
12+
const isImage = require("../utils/isImage");
13+
const isPressable = require("../utils/isPressable");
14+
const isText = require("../utils/isText");
15+
const isTextInput = require("../utils/isTextInput");
16+
17+
/** @type {import('eslint').Rule.RuleModule} */
18+
module.exports = {
19+
meta: {
20+
hasSuggestions: true,
21+
type: "problem", // `problem`, `suggestion`, or `layout`
22+
docs: {
23+
description:
24+
"Enforces accessibilityRole or role when component is accessible",
25+
recommended: false,
26+
url: "https://github.com/bamlab/react-native-project-config/tree/main/packages/eslint-plugin/docs/rules/requires-accessibility-role-when-accessible.md", // URL to the documentation page for this rule
27+
},
28+
fixable: null, // Or `code` or `whitespace`
29+
schema: [], // Add a schema if the rule has options
30+
messages: {
31+
requiresAccessibilityRoleWhenAccessible:
32+
"Requires accessibilityRole when accessible",
33+
suggestAddingButtonRole: "Add accessibilityRole button to component",
34+
suggestAddingImageRole: "Add accessibilityRole image to component",
35+
},
36+
},
37+
38+
create(context) {
39+
return {
40+
JSXOpeningElement(node) {
41+
if (isAccessible(node)) {
42+
if (
43+
!node.attributes.some(
44+
(attribute) =>
45+
attribute.name.name === "accessibilityRole" ||
46+
attribute.name.name === "role"
47+
)
48+
) {
49+
if (
50+
!(
51+
isTextInput(node) ||
52+
isText(node) ||
53+
isButton(node) ||
54+
node.name.name === "ExpoImage"
55+
)
56+
) {
57+
if (isPressable(node)) {
58+
context.report({
59+
node,
60+
messageId: "requiresAccessibilityRoleWhenAccessible",
61+
suggest: [
62+
{
63+
messageId: "suggestAddingButtonRole",
64+
fix: (fixer) => {
65+
const openingTagEnd = node.range[1];
66+
return fixer.insertTextBeforeRange(
67+
[
68+
openingTagEnd - (node.selfClosing ? 2 : 1),
69+
openingTagEnd,
70+
],
71+
` accessibilityRole="button"`
72+
);
73+
},
74+
},
75+
],
76+
});
77+
} else if (isImage(node)) {
78+
context.report({
79+
node,
80+
messageId: "requiresAccessibilityRoleWhenAccessible",
81+
suggest: [
82+
{
83+
messageId: "suggestAddingImageRole",
84+
fix: (fixer) => {
85+
const openingTagEnd = node.range[1];
86+
return fixer.insertTextBeforeRange(
87+
[
88+
openingTagEnd - (node.selfClosing ? 2 : 1),
89+
openingTagEnd,
90+
],
91+
` accessibilityRole="image"`
92+
);
93+
},
94+
},
95+
],
96+
});
97+
} else {
98+
context.report({
99+
node,
100+
messageId: "requiresAccessibilityRoleWhenAccessible",
101+
});
102+
}
103+
}
104+
}
105+
}
106+
},
107+
};
108+
},
109+
};
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
module.exports = (element) => {
2+
if (element.type === "JSXOpeningElement") {
3+
if (
4+
element.name.type == "JSXIdentifier" &&
5+
element.name.name.endsWith("Button")
6+
) {
7+
return true;
8+
}
9+
10+
if (
11+
element.name.type === "JSXMemberExpression" &&
12+
element.name.object.name === "Button"
13+
) {
14+
return true;
15+
}
16+
}
17+
18+
return false;
19+
};
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* @fileoverview Enforces accessibilityRole or role when component is accessible
3+
* @author Paul Briand
4+
*/
5+
"use strict";
6+
7+
//------------------------------------------------------------------------------
8+
// Requirements
9+
//------------------------------------------------------------------------------
10+
11+
const rule = require("../../../lib/rules/requires-accessibility-role-when-accessible"),
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 accessibilityRole="button" />`,
31+
`<Pressable accessible accessibilityRole="button" />`,
32+
];
33+
34+
ruleTester.run("requires-accessibility-role-when-accessible", rule, {
35+
valid,
36+
37+
invalid: [
38+
{
39+
code: `<View accessible />`,
40+
errors: ["Requires accessibilityRole when accessible"],
41+
},
42+
{
43+
code: `<Pressable onPress={()=>{}} accessible />`,
44+
errors: ["Requires accessibilityRole when accessible"],
45+
},
46+
{
47+
code: `<Image source={{uri:""}} accessible />`,
48+
errors: ["Requires accessibilityRole when accessible"],
49+
},
50+
],
51+
});

0 commit comments

Comments
 (0)