Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ This config will be interpreted in the following way:
| [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. | ⚛️ | | |
| [a11y-no-generic-link-text](docs/rules/a11y-no-generic-link-text.md) | disallow generic link text | | | ❌ |
| [a11y-no-visually-hidden-interactive-element](docs/rules/a11y-no-visually-hidden-interactive-element.md) | Ensures that interactive elements are not visually hidden | ⚛️ | | |
| [a11y-svg-has-accessible-name](docs/rules/a11y-svg-has-accessible-name.md) | SVGs must have an accessible name | ⚛️ | | |
| [array-foreach](docs/rules/array-foreach.md) | enforce `for..of` loops over `Array.forEach` | ✅ | | |
| [async-currenttarget](docs/rules/async-currenttarget.md) | disallow `event.currentTarget` calls inside of async functions | 🔍 | | |
| [async-preventdefault](docs/rules/async-preventdefault.md) | disallow `event.preventDefault` calls inside of async functions | 🔍 | | |
Expand Down
73 changes: 73 additions & 0 deletions docs/rules/a11y-svg-has-accessible-name.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# SVGs must have an accessible name (`github/a11y-svg-has-accessible-name`)

💼 This rule is enabled in the ⚛️ `react` config.

<!-- end auto-generated rule header -->

## Rule Details

An `<svg>` must have an accessible name. Set `aria-label` or `aria-labelledby`, or nest a `<title>` element as the first child of the `<svg>` element.

However, if the `<svg>` is purely decorative, hide it with `aria-hidden="true"` or `role="presentation"`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhh I didn't consider role="presentation"! TIL!


## Resources

- [Accessible SVGs](https://css-tricks.com/accessible-svgs/)

## Examples

### **Incorrect** code for this rule 👎

```html
<svg height='100' width='100'>
<circle cx='50' cy='50' r='40' stroke='black' stroke-width='3' fill='red'/>
</svg>
```

```html
<svg height='100' width='100' title='Circle with a black outline and red fill'>
<circle cx='50' cy='50' r='40' stroke='black' stroke-width='3' fill='red'/>
</svg>
```

```html
<svg height='100' width='100'>
<circle cx='50' cy='50' r='40' stroke='black' stroke-width='3' fill='red'/>
<title>Circle with a black outline and red fill</title>
</svg>
```

### **Correct** code for this rule 👍

```html
<svg height='100' width='100'>
<title>Circle with a black outline and red fill</title>
<circle cx='50' cy='50' r='40' stroke='black' stroke-width='3' fill='red'/>
</svg>
```

```html
<svg aria-label='Circle with a black outline and red fill' height='100' width='100'>
<circle cx='50' cy='50' r='40' stroke='black' stroke-width='3' fill='red'/>
</svg>
```

```html
<svg aria-labelledby='circle_text' height='100' width='100'>
<circle cx='50' cy='50' r='40' stroke='black' stroke-width='3' fill='red'/>
</svg>
```

```html
<svg aria-hidden='true' height='100' width='100'>
<circle cx='50' cy='50' r='40' stroke='black' stroke-width='3' fill='red'/>
</svg>
```

```html
<svg role='presentation' height='100' width='100'>
<circle cx='50' cy='50' r='40' stroke='black' stroke-width='3' fill='red'/>
</svg>
```

## Version
1 change: 1 addition & 0 deletions lib/configs/react.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module.exports = {
'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
'github/a11y-aria-label-is-well-formatted': 'error',
'github/a11y-no-visually-hidden-interactive-element': 'error',
'github/a11y-svg-has-accessible-name': 'error',
'github/role-supports-aria-props': 'error',
'jsx-a11y/no-aria-hidden-on-focusable': 'error',
'jsx-a11y/no-autofocus': 'off',
Expand Down
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module.exports = {
'a11y-no-visually-hidden-interactive-element': require('./rules/a11y-no-visually-hidden-interactive-element'),
'a11y-no-generic-link-text': require('./rules/a11y-no-generic-link-text'),
'a11y-aria-label-is-well-formatted': require('./rules/a11y-aria-label-is-well-formatted'),
'a11y-svg-has-accessible-name': require('./rules/a11y-svg-has-accessible-name'),
'array-foreach': require('./rules/array-foreach'),
'async-currenttarget': require('./rules/async-currenttarget'),
'async-preventdefault': require('./rules/async-preventdefault'),
Expand Down
41 changes: 41 additions & 0 deletions lib/rules/a11y-svg-has-accessible-name.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const {hasProp} = require('jsx-ast-utils')
const {getElementType} = require('../utils/get-element-type')

module.exports = {
meta: {
docs: {
description: 'SVGs must have an accessible name',
url: require('../url')(module),
},
schema: [],
},

create(context) {
return {
JSXOpeningElement: node => {
const elementType = getElementType(context, node)
Comment thread
lindseywild marked this conversation as resolved.

// Check if there is a nested title element that is the first child of the `<svg>`
const hasNestedTitleAsFirstChild =
node.parent.children.length &&
node.parent.children[0].type === 'JSXElement' &&
node.parent.children[0].openingElement.name.name === 'title'
Comment thread
lindseywild marked this conversation as resolved.
Outdated

// Check if `aria-label` or `aria-labelledby` is set
const hasAccessibleName = hasProp(node.attributes, 'aria-label') || hasProp(node.attributes, 'aria-labelledby')

// Check if SVG is decorative
const isDecorative =
hasProp(node.attributes, 'role', 'presentation') || hasProp(node.attributes, 'aria-hidden', 'true')

if (elementType === 'svg' && !hasAccessibleName && !isDecorative && !hasNestedTitleAsFirstChild) {
context.report({
node,
message:
'`<svg>` must have an accessible name. Set `aria-label` or `aria-labelledby`, or nest a `<title>` element. However, if the `<svg>` is purely decorative, hide it with `aria-hidden="true"` or `role="presentation"`.',
})
}
},
}
},
}
49 changes: 49 additions & 0 deletions tests/a11y-svg-has-accessible-name.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const rule = require('../lib/rules/a11y-svg-has-accessible-name')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice tests!! thank you!!

const RuleTester = require('eslint').RuleTester

const ruleTester = new RuleTester({
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
})

const errorMessage =
'`<svg>` must have an accessible name. Set `aria-label` or `aria-labelledby`, or nest a `<title>` element. However, if the `<svg>` is purely decorative, hide it with `aria-hidden="true"` or `role="presentation"`.'

ruleTester.run('a11y-svg-has-accessible-name', rule, {
valid: [
{
code: "<svg height='100' width='100'><title>Circle with a black outline and red fill</title><circle cx='50' cy='50' r='40' stroke='black' stroke-width='3' fill='red'/></svg>",
},
{
code: "<svg aria-label='Circle with a black outline and red fill' height='100' width='100'><circle cx='50' cy='50' r='40' stroke='black' stroke-width='3' fill='red'/></svg>",
},
{
code: "<svg aria-labelledby='circle_text' height='100' width='100'><circle cx='50' cy='50' r='40' stroke='black' stroke-width='3' fill='red'/></svg>",
},
{
code: "<svg aria-hidden='true' height='100' width='100'><circle cx='50' cy='50' r='40' stroke='black' stroke-width='3' fill='red'/></svg>",
},
{
code: "<svg role='presentation' height='100' width='100'><circle cx='50' cy='50' r='40' stroke='black' stroke-width='3' fill='red'/></svg>",
},
],
invalid: [
{
code: "<svg height='100' width='100'><circle cx='50' cy='50' r='40' stroke='black' stroke-width='3' fill='red'/></svg>",
errors: [{message: errorMessage}],
},
{
code: "<svg height='100' width='100' title='Circle with a black outline and red fill'><circle cx='50' cy='50' r='40' stroke='black' stroke-width='3' fill='red'/></svg>",
errors: [{message: errorMessage}],
},
{
code: "<svg height='100' width='100'><circle cx='50' cy='50' r='40' stroke='black' stroke-width='3' fill='red'/><title>Circle with a black outline and red fill</title></svg>",
errors: [{message: errorMessage}],
},
],
})