Skip to content

Commit 3126329

Browse files
authored
New rule to flag invalid aria-label format (#418)
1 parent aadb4ce commit 3126329

6 files changed

+126
-22
lines changed

README.md

+23-22
Original file line numberDiff line numberDiff line change
@@ -82,27 +82,28 @@ This config will be interpreted in the following way:
8282
🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
8383
❌ Deprecated.
8484

85-
| Name                      | Description | 💼 | 🔧 ||
86-
| :------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------- | :- | :- | :- |
87-
| [a11y-no-generic-link-text](docs/rules/a11y-no-generic-link-text.md) | disallow generic link text | | ||
88-
| [array-foreach](docs/rules/array-foreach.md) | enforce `for..of` loops over `Array.forEach` || | |
89-
| [async-currenttarget](docs/rules/async-currenttarget.md) | disallow `event.currentTarget` calls inside of async functions | 🔍 | | |
90-
| [async-preventdefault](docs/rules/async-preventdefault.md) | disallow `event.preventDefault` calls inside of async functions | 🔍 | | |
91-
| [authenticity-token](docs/rules/authenticity-token.md) | disallow usage of CSRF tokens in JavaScript | 🔐 | | |
92-
| [get-attribute](docs/rules/get-attribute.md) | disallow wrong usage of attribute names | 🔍 | 🔧 | |
93-
| [js-class-name](docs/rules/js-class-name.md) | enforce a naming convention for js- prefixed classes | 🔐 | | |
94-
| [no-blur](docs/rules/no-blur.md) | disallow usage of `Element.prototype.blur()` | 🔍 | | |
95-
| [no-d-none](docs/rules/no-d-none.md) | disallow usage the `d-none` CSS class | 🔐 | | |
96-
| [no-dataset](docs/rules/no-dataset.md) | enforce usage of `Element.prototype.getAttribute` instead of `Element.prototype.datalist` | 🔍 | | |
97-
| [no-dynamic-script-tag](docs/rules/no-dynamic-script-tag.md) | disallow creating dynamic script tags || | |
98-
| [no-implicit-buggy-globals](docs/rules/no-implicit-buggy-globals.md) | disallow implicit global variables || | |
99-
| [no-inner-html](docs/rules/no-inner-html.md) | disallow `Element.prototype.innerHTML` in favor of `Element.prototype.textContent` | 🔍 | | |
100-
| [no-innerText](docs/rules/no-innerText.md) | disallow `Element.prototype.innerText` in favor of `Element.prototype.textContent` | 🔍 | 🔧 | |
101-
| [no-then](docs/rules/no-then.md) | enforce using `async/await` syntax over Promises || | |
102-
| [no-useless-passive](docs/rules/no-useless-passive.md) | disallow marking a event handler as passive when it has no effect | 🔍 | 🔧 | |
103-
| [prefer-observers](docs/rules/prefer-observers.md) | disallow poorly performing event listeners | 🔍 | | |
104-
| [require-passive-events](docs/rules/require-passive-events.md) | enforce marking high frequency event handlers as passive | 🔍 | | |
105-
| [role-supports-aria-props](docs/rules/role-supports-aria-props.md) | Enforce that elements with explicit or implicit roles defined contain only `aria-*` properties supported by that `role`. | ⚛️ | | |
106-
| [unescaped-html-literal](docs/rules/unescaped-html-literal.md) | disallow unescaped HTML literals | 🔍 | | |
85+
| Name                              | Description | 💼 | 🔧 ||
86+
| :----------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------- | :- | :- | :- |
87+
| [a11y-aria-label-is-well-formatted](docs/rules/a11y-aria-label-is-well-formatted.md) | [aria-label] text should be formatted as you would visual text. | ⚛️ | | |
88+
| [a11y-no-generic-link-text](docs/rules/a11y-no-generic-link-text.md) | disallow generic link text | | ||
89+
| [array-foreach](docs/rules/array-foreach.md) | enforce `for..of` loops over `Array.forEach` || | |
90+
| [async-currenttarget](docs/rules/async-currenttarget.md) | disallow `event.currentTarget` calls inside of async functions | 🔍 | | |
91+
| [async-preventdefault](docs/rules/async-preventdefault.md) | disallow `event.preventDefault` calls inside of async functions | 🔍 | | |
92+
| [authenticity-token](docs/rules/authenticity-token.md) | disallow usage of CSRF tokens in JavaScript | 🔐 | | |
93+
| [get-attribute](docs/rules/get-attribute.md) | disallow wrong usage of attribute names | 🔍 | 🔧 | |
94+
| [js-class-name](docs/rules/js-class-name.md) | enforce a naming convention for js- prefixed classes | 🔐 | | |
95+
| [no-blur](docs/rules/no-blur.md) | disallow usage of `Element.prototype.blur()` | 🔍 | | |
96+
| [no-d-none](docs/rules/no-d-none.md) | disallow usage the `d-none` CSS class | 🔐 | | |
97+
| [no-dataset](docs/rules/no-dataset.md) | enforce usage of `Element.prototype.getAttribute` instead of `Element.prototype.datalist` | 🔍 | | |
98+
| [no-dynamic-script-tag](docs/rules/no-dynamic-script-tag.md) | disallow creating dynamic script tags || | |
99+
| [no-implicit-buggy-globals](docs/rules/no-implicit-buggy-globals.md) | disallow implicit global variables || | |
100+
| [no-inner-html](docs/rules/no-inner-html.md) | disallow `Element.prototype.innerHTML` in favor of `Element.prototype.textContent` | 🔍 | | |
101+
| [no-innerText](docs/rules/no-innerText.md) | disallow `Element.prototype.innerText` in favor of `Element.prototype.textContent` | 🔍 | 🔧 | |
102+
| [no-then](docs/rules/no-then.md) | enforce using `async/await` syntax over Promises || | |
103+
| [no-useless-passive](docs/rules/no-useless-passive.md) | disallow marking a event handler as passive when it has no effect | 🔍 | 🔧 | |
104+
| [prefer-observers](docs/rules/prefer-observers.md) | disallow poorly performing event listeners | 🔍 | | |
105+
| [require-passive-events](docs/rules/require-passive-events.md) | enforce marking high frequency event handlers as passive | 🔍 | | |
106+
| [role-supports-aria-props](docs/rules/role-supports-aria-props.md) | Enforce that elements with explicit or implicit roles defined contain only `aria-*` properties supported by that `role`. | ⚛️ | | |
107+
| [unescaped-html-literal](docs/rules/unescaped-html-literal.md) | disallow unescaped HTML literals | 🔍 | | |
107108

108109
<!-- end auto-generated rules list -->
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# [aria-label] text should be formatted as you would visual text (`github/a11y-aria-label-is-well-formatted`)
2+
3+
💼 This rule is enabled in the ⚛️ `react` config.
4+
5+
<!-- end auto-generated rule header -->
6+
7+
## Rule Details
8+
9+
`[aria-label]` content should be formatted in the same way you would visual text. Please use sentence case.
10+
11+
Do not connect the words like you would an ID. An `aria-label` is not an ID, and should be formatted as human-friendly text.
12+
13+
## Resources
14+
15+
- [Using aria-label](https://www.w3.org/WAI/tutorials/forms/labels/#using-aria-label)
16+
17+
## Examples
18+
19+
### **Incorrect** code for this rule 👎
20+
21+
```html
22+
<a href="..." aria-label="learn more"></a>
23+
```
24+
25+
```html
26+
<a href="..." aria-label="go-to-link"></a>
27+
```
28+
29+
### **Correct** code for this rule 👍
30+
31+
```html
32+
<a href="..." aria-label="Learn more"></a>
33+
```
34+
35+
```html
36+
<a href="..." aria-label="Homepage"></a>
37+
```
38+
39+
## Version

lib/configs/react.js

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ module.exports = {
99
extends: ['plugin:jsx-a11y/recommended'],
1010
rules: {
1111
'jsx-a11y/role-supports-aria-props': 'off', // Override with github/role-supports-aria-props until https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/issues/910 is resolved
12+
'github/a11y-aria-label-is-well-formatted': 'error',
1213
'github/role-supports-aria-props': 'error',
1314
'jsx-a11y/no-aria-hidden-on-focusable': 'error',
1415
'jsx-a11y/anchor-ambiguous-text': [

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module.exports = {
22
rules: {
33
'a11y-no-generic-link-text': require('./rules/a11y-no-generic-link-text'),
4+
'a11y-aria-label-is-well-formatted': require('./rules/a11y-aria-label-is-well-formatted'),
45
'array-foreach': require('./rules/array-foreach'),
56
'async-currenttarget': require('./rules/async-currenttarget'),
67
'async-preventdefault': require('./rules/async-preventdefault'),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const {getProp} = require('jsx-ast-utils')
2+
3+
module.exports = {
4+
meta: {
5+
docs: {
6+
description: '[aria-label] text should be formatted as you would visual text.',
7+
url: require('../url')(module),
8+
},
9+
schema: [],
10+
},
11+
12+
create(context) {
13+
return {
14+
JSXOpeningElement: node => {
15+
const prop = getProp(node.attributes, 'aria-label')
16+
if (!prop) return
17+
18+
const propValue = prop.value
19+
if (propValue.type !== 'Literal') return
20+
21+
const ariaLabel = propValue.value
22+
if (ariaLabel.match(/^[a-z]+.*$/)) {
23+
context.report({
24+
node,
25+
message: '[aria-label] text should be formatted the same as you would visual text. Use sentence case.',
26+
})
27+
}
28+
},
29+
}
30+
},
31+
}
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const rule = require('../lib/rules/a11y-aria-label-is-well-formatted')
2+
const RuleTester = require('eslint').RuleTester
3+
4+
const ruleTester = new RuleTester({
5+
parserOptions: {
6+
ecmaVersion: 'latest',
7+
sourceType: 'module',
8+
ecmaFeatures: {
9+
jsx: true,
10+
},
11+
},
12+
})
13+
14+
const errorMessage = '[aria-label] text should be formatted the same as you would visual text. Use sentence case.'
15+
16+
ruleTester.run('a11y-aria-label-is-well-formatted', rule, {
17+
valid: [
18+
{code: "<a aria-labelledby='someId' href='#'>Read more</a>;"},
19+
{code: "<a aria-label={someName} href='#'>Read more</a>;"},
20+
{code: "<a aria-label='This is a label'></a>;"},
21+
{code: "<a aria-label='Valid'></a>;"},
22+
{code: "<a aria-label='VALID'></a>;"},
23+
{code: '<Link aria-label="Valid" href="#">Read more</Link>'},
24+
],
25+
invalid: [
26+
{code: "<a aria-label='close modal'></a>;", errors: [{message: errorMessage}]},
27+
{code: "<a aria-label='submit'></a>;", errors: [{message: errorMessage}]},
28+
{code: "<a aria-label='submit.yml'></a>;", errors: [{message: errorMessage}]},
29+
{code: "<a aria-label='this-is-not-an-id'></a>;", errors: [{message: errorMessage}]},
30+
],
31+
})

0 commit comments

Comments
 (0)