Skip to content

Commit c0a7ad7

Browse files
committed
Merge branch 'main' into tylerjdev/update-eslint-package
2 parents f429c1b + dbb58f2 commit c0a7ad7

File tree

6 files changed

+147
-2
lines changed

6 files changed

+147
-2
lines changed

.changeset/lovely-toys-attend.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-primer-react': major
3+
---
4+
5+
Add `a11y-explicit-heading` rule

docs/rules/explicit-heading.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
## Require explicit heading level on `<Heading>` component
2+
3+
The `Heading` component does not require you to use `as` to specify the heading level, as it will default to an `h2` if one isn't specified. This may lead to inaccessible usage if the default is out of order in the existing heading hierarchy.
4+
5+
## Rule Details
6+
7+
This rule enforces using `as` on the `<Heading>` component to specify a heading level (`h1`-`h6`). In addition, it enforces `as` usage to only be used for headings.
8+
9+
👎 Examples of **incorrect** code for this rule
10+
11+
```jsx
12+
import {Heading} from '@primer/react'
13+
14+
<Heading>Heading without explicit heading level</Heading>
15+
```
16+
17+
`as` must only be for headings (`h1`-`h6`)
18+
19+
```jsx
20+
import {Heading} from '@primer/react'
21+
22+
<Heading as="span">Heading component used as "span"</Heading>
23+
```
24+
25+
👍 Examples of **correct** code for this rule:
26+
27+
```jsx
28+
import {Heading} from '@primer/react';
29+
30+
<Heading as="h2">Heading level 2</Heading>
31+
```
32+
33+
## Options
34+
35+
- `skipImportCheck` (default: `false`)
36+
37+
By default, the `a11y-explicit-heading` rule will only check for `<Heading>` components imported directly from `@primer/react`. You can disable this behavior by setting `skipImportCheck` to `true`.

src/configs/recommended.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ module.exports = {
1111
'primer-react/direct-slot-children': 'error',
1212
'primer-react/no-deprecated-colors': 'warn',
1313
'primer-react/no-system-props': 'warn',
14-
'primer-react/a11y-tooltip-interactive-trigger': 'error'
14+
'primer-react/a11y-tooltip-interactive-trigger': 'error',
15+
'primer-react/a11y-explicit-heading': 'error'
1516
},
1617
settings: {
1718
'jsx-a11y': {

src/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ module.exports = {
33
'direct-slot-children': require('./rules/direct-slot-children'),
44
'no-deprecated-colors': require('./rules/no-deprecated-colors'),
55
'no-system-props': require('./rules/no-system-props'),
6-
'a11y-tooltip-interactive-trigger': require('./rules/a11y-tooltip-interactive-trigger')
6+
'a11y-tooltip-interactive-trigger': require('./rules/a11y-tooltip-interactive-trigger'),
7+
'a11y-explicit-heading': require('./rules/a11y-explicit-heading')
78
},
89
configs: {
910
recommended: require('./configs/recommended')
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
const rule = require('../a11y-explicit-heading')
2+
const {RuleTester} = require('eslint')
3+
4+
const ruleTester = new RuleTester({
5+
parserOptions: {
6+
ecmaVersion: 'latest',
7+
sourceType: 'module',
8+
ecmaFeatures: {
9+
jsx: true
10+
}
11+
}
12+
})
13+
14+
ruleTester.run('a11y-explicit-heading', rule, {
15+
valid: [
16+
`import {Heading} from '@primer/react';
17+
<Heading as="h1">Heading level 1</Heading>
18+
`,
19+
`import {Heading} from '@primer/react';
20+
<Heading as="h2">Heading level 2</Heading>
21+
`,
22+
`import {Heading} from '@primer/react';
23+
<Heading as="H3">Heading level 3</Heading>
24+
`,
25+
],
26+
invalid: [
27+
{
28+
code:
29+
`import {Heading} from '@primer/react';
30+
<Heading>Heading without "as"</Heading>`,
31+
errors: [{ messageId: 'nonExplicitHeadingLevel' }]
32+
},
33+
{
34+
code:
35+
`import {Heading} from '@primer/react';
36+
<Heading as="span">Heading component used as "span"</Heading>
37+
`,
38+
errors: [{ messageId: 'invalidAsValue' }]
39+
},
40+
]
41+
})

src/rules/a11y-explicit-heading.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
const {isPrimerComponent} = require('../utils/is-primer-component')
2+
const {getJSXOpeningElementName} = require('../utils/get-jsx-opening-element-name')
3+
const {getJSXOpeningElementAttribute} = require('../utils/get-jsx-opening-element-attribute')
4+
5+
const validHeadings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']
6+
7+
const isHeadingComponent = elem => getJSXOpeningElementName(elem) === 'Heading'
8+
const isUsingAsProp = elem => {
9+
const componentAs = getJSXOpeningElementAttribute(elem, 'as')
10+
11+
if (!componentAs) return
12+
13+
return componentAs.value
14+
}
15+
16+
const isValidAsUsage = value => validHeadings.includes(value.toLowerCase())
17+
const isInvalid = elem => {
18+
const elemAs = isUsingAsProp(elem)
19+
20+
if (!elemAs) return 'nonExplicitHeadingLevel'
21+
if (!isValidAsUsage(elemAs.value)) return 'invalidAsValue'
22+
23+
return false
24+
}
25+
26+
module.exports = {
27+
meta: {
28+
schema: [
29+
{
30+
properties: {
31+
skipImportCheck: {
32+
type: 'boolean'
33+
}
34+
}
35+
}
36+
],
37+
messages: {
38+
nonExplicitHeadingLevel: 'Heading must have an explicit heading level applied through the `as` prop.',
39+
invalidAsValue: 'Usage of `as` must only be used for heading elements (h1-h6).'
40+
}
41+
},
42+
create: function(context) {
43+
return {
44+
JSXOpeningElement(jsxNode) {
45+
const skipImportCheck = context.options[0] ? context.options[0].skipImportCheck : false
46+
47+
if ((skipImportCheck || isPrimerComponent(jsxNode.name, context.getScope(jsxNode))) && isHeadingComponent(jsxNode)) {
48+
const error = isInvalid(jsxNode)
49+
50+
if (error) {
51+
context.report({
52+
node: jsxNode,
53+
messageId: error
54+
})
55+
}
56+
}
57+
}
58+
}
59+
}
60+
}

0 commit comments

Comments
 (0)