eslint-plugin-primer-react 4.0.0-rc.b9f7dc6 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/dependabot.yml +10 -0
- package/CHANGELOG.md +15 -0
- package/README.md +1 -0
- package/docs/rules/a11y-explicit-heading.md +37 -0
- package/package.json +4 -4
- package/src/configs/components.js +23 -0
- package/src/configs/recommended.js +6 -9
- package/src/index.js +2 -1
- package/src/rules/__tests__/a11y-explicit-heading.test.js +63 -0
- package/src/rules/__tests__/a11y-tooltip-interactive-trigger.test.js +57 -6
- package/src/rules/a11y-explicit-heading.js +66 -0
- package/src/rules/a11y-tooltip-interactive-trigger.js +37 -28
- package/src/url.js +10 -0
- package/src/utils/__tests__/flatten-components.test.js +62 -0
- package/src/utils/flatten-components.js +17 -0
- package/src/utils/get-jsx-opening-element-attribute.js +1 -1
- package/package-lock.json +0 -14601
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,21 @@
|
|
|
6
6
|
|
|
7
7
|
- [#51](https://github.com/primer/eslint-plugin-primer-react/pull/51) [`a65aa32`](https://github.com/primer/eslint-plugin-primer-react/commit/a65aa32c612c7fe952ec47bb3d926cf8adae9fab) Thanks [@broccolinisoup](https://github.com/broccolinisoup)! - Add `a11y-tooltip-interactive-trigger`
|
|
8
8
|
|
|
9
|
+
* [#66](https://github.com/primer/eslint-plugin-primer-react/pull/66) [`d1df609`](https://github.com/primer/eslint-plugin-primer-react/commit/d1df609b260505ee9dea1740bc96a3187355d727) Thanks [@TylerJDev](https://github.com/TylerJDev)! - Add `a11y-explicit-heading` rule
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [#67](https://github.com/primer/eslint-plugin-primer-react/pull/67) [`4dfdb47`](https://github.com/primer/eslint-plugin-primer-react/commit/4dfdb47b40e7f187573b8203830541b86cc7a953) Thanks [@TylerJDev](https://github.com/TylerJDev)! - \* Updates component mapping, adds `components.js`
|
|
14
|
+
- Bumps `eslint-plugin-github` and `eslint-plugin-jsx-a11y`
|
|
15
|
+
- Fixes bug in `a11y-explicit-heading` when using spread props, or variable for `as`
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- [#72](https://github.com/primer/eslint-plugin-primer-react/pull/72) [`522b9cc`](https://github.com/primer/eslint-plugin-primer-react/commit/522b9ccbcfb26d18f2ea8b2514a6a7975480aaa7) Thanks [@TylerJDev](https://github.com/TylerJDev)! - Removes `Link`, `Spinner` and `TabNav.Link` from component mapping
|
|
20
|
+
|
|
21
|
+
* [#73](https://github.com/primer/eslint-plugin-primer-react/pull/73) [`974d9e8`](https://github.com/primer/eslint-plugin-primer-react/commit/974d9e85c7460a05eb0345086b340b650700d24d) Thanks [@TylerJDev](https://github.com/TylerJDev)! - \* Fixes `nonInteractiveLink` rule for links that pass values through JSX rather than a string
|
|
22
|
+
- Adds optional chaining to `getJSXOpeningElementAttribute` to avoid error when no `name` is present
|
|
23
|
+
|
|
9
24
|
## 3.0.0
|
|
10
25
|
|
|
11
26
|
### Major Changes
|
package/README.md
CHANGED
|
@@ -33,3 +33,4 @@ ESLint rules for Primer React
|
|
|
33
33
|
- [no-deprecated-colors](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/no-deprecated-colors.md)
|
|
34
34
|
- [no-system-props](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/no-system-props.md)
|
|
35
35
|
- [a11y-tooltip-interactive-trigger](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/a11y-tooltip-interactive-trigger.md)
|
|
36
|
+
- [a11y-explicit-heading](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/a11y-explicit-heading.md)
|
|
@@ -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`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-primer-react",
|
|
3
|
-
"version": "4.0.0
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "ESLint rules for Primer React",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"@changesets/changelog-github": "^0.4.0",
|
|
25
25
|
"@changesets/cli": "^2.16.0",
|
|
26
26
|
"@github/prettier-config": "0.0.4",
|
|
27
|
-
"@primer/primitives": "^
|
|
27
|
+
"@primer/primitives": "^7.11.14",
|
|
28
28
|
"eslint": "^8.0.1",
|
|
29
29
|
"jest": "^27.0.6"
|
|
30
30
|
},
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"prettier": "@github/prettier-config",
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@styled-system/props": "^5.1.5",
|
|
38
|
-
"eslint-plugin-github": "^4.
|
|
39
|
-
"eslint-plugin-jsx-a11y": "^6.
|
|
38
|
+
"eslint-plugin-github": "^4.9.2",
|
|
39
|
+
"eslint-plugin-jsx-a11y": "^6.7.1",
|
|
40
40
|
"eslint-traverse": "^1.0.0",
|
|
41
41
|
"lodash": "^4.17.21",
|
|
42
42
|
"styled-system": "^5.1.5"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const { flattenComponents } = require('../utils/flatten-components');
|
|
2
|
+
|
|
3
|
+
const components = flattenComponents({
|
|
4
|
+
Button: 'button',
|
|
5
|
+
IconButton: 'button',
|
|
6
|
+
ToggleSwitch: 'button',
|
|
7
|
+
Radio: 'input',
|
|
8
|
+
Checkbox: 'input',
|
|
9
|
+
Text: 'span',
|
|
10
|
+
TextInput: {
|
|
11
|
+
Action: 'button',
|
|
12
|
+
self: 'input',
|
|
13
|
+
},
|
|
14
|
+
Select: {
|
|
15
|
+
Option: 'option',
|
|
16
|
+
self: 'select',
|
|
17
|
+
},
|
|
18
|
+
TabNav: {
|
|
19
|
+
self: 'nav',
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
module.exports = components;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const components = require('./components');
|
|
2
|
+
|
|
1
3
|
module.exports = {
|
|
2
4
|
parserOptions: {
|
|
3
5
|
sourceType: 'module',
|
|
@@ -11,20 +13,15 @@ module.exports = {
|
|
|
11
13
|
'primer-react/direct-slot-children': 'error',
|
|
12
14
|
'primer-react/no-deprecated-colors': 'warn',
|
|
13
15
|
'primer-react/no-system-props': 'warn',
|
|
14
|
-
'primer-react/a11y-tooltip-interactive-trigger': 'error'
|
|
16
|
+
'primer-react/a11y-tooltip-interactive-trigger': 'error',
|
|
17
|
+
'primer-react/a11y-explicit-heading': 'error'
|
|
15
18
|
},
|
|
16
19
|
settings: {
|
|
17
20
|
github: {
|
|
18
|
-
components:
|
|
19
|
-
Link: {props: {as: {undefined: 'a', a: 'a', button: 'button'}}},
|
|
20
|
-
Button: {default: 'button'}
|
|
21
|
-
}
|
|
21
|
+
components: components
|
|
22
22
|
},
|
|
23
23
|
'jsx-a11y': {
|
|
24
|
-
components:
|
|
25
|
-
Button: 'button',
|
|
26
|
-
IconButton: 'button'
|
|
27
|
-
}
|
|
24
|
+
components: components
|
|
28
25
|
}
|
|
29
26
|
}
|
|
30
27
|
}
|
package/src/index.js
CHANGED
|
@@ -3,7 +3,8 @@ module.exports = {
|
|
|
3
3
|
'direct-slot-children': require('./rules/direct-slot-children'),
|
|
4
4
|
'no-deprecated-colors': require('./rules/no-deprecated-colors'),
|
|
5
5
|
'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')
|
|
7
8
|
},
|
|
8
9
|
configs: {
|
|
9
10
|
recommended: require('./configs/recommended')
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
`import {Heading} from '@primer/react';
|
|
26
|
+
const args = {};
|
|
27
|
+
<Heading {...args}>A heading with spread props</Heading>
|
|
28
|
+
`,
|
|
29
|
+
`import {Heading} from '@primer/react';
|
|
30
|
+
const as = 'h3';
|
|
31
|
+
<Heading as={as}>Heading as passed prop</Heading>
|
|
32
|
+
`,
|
|
33
|
+
`import {Heading} from '@primer/react';
|
|
34
|
+
const args = {};
|
|
35
|
+
<Heading as="h2" {...args}>Heading level 2</Heading>
|
|
36
|
+
`,
|
|
37
|
+
`
|
|
38
|
+
import {Heading} from '@primer/react';
|
|
39
|
+
<Heading
|
|
40
|
+
{...someProps}
|
|
41
|
+
as="h3"
|
|
42
|
+
>
|
|
43
|
+
Passed spread props
|
|
44
|
+
</Heading>
|
|
45
|
+
`
|
|
46
|
+
|
|
47
|
+
],
|
|
48
|
+
invalid: [
|
|
49
|
+
{
|
|
50
|
+
code:
|
|
51
|
+
`import {Heading} from '@primer/react';
|
|
52
|
+
<Heading>Heading without "as"</Heading>`,
|
|
53
|
+
errors: [{ messageId: 'nonExplicitHeadingLevel' }]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
code:
|
|
57
|
+
`import {Heading} from '@primer/react';
|
|
58
|
+
<Heading as="span">Heading component used as "span"</Heading>
|
|
59
|
+
`,
|
|
60
|
+
errors: [{ messageId: 'invalidAsValue' }]
|
|
61
|
+
},
|
|
62
|
+
]
|
|
63
|
+
})
|
|
@@ -51,7 +51,22 @@ ruleTester.run('non-interactive-tooltip-trigger', rule, {
|
|
|
51
51
|
`import {Tooltip, Link} from '@primer/react';
|
|
52
52
|
<Tooltip aria-label="Supplementary text" direction="e">
|
|
53
53
|
<Link href="https://github.com">Link</Link>
|
|
54
|
-
</Tooltip
|
|
54
|
+
</Tooltip>`,
|
|
55
|
+
`
|
|
56
|
+
import {Tooltip, Link} from '@primer/react';
|
|
57
|
+
<Tooltip aria-label={avatar.avatarName} direction="e">
|
|
58
|
+
<Link href={avatar.avatarLink} underline={true}>
|
|
59
|
+
User avatar
|
|
60
|
+
</Link>
|
|
61
|
+
</Tooltip>` ,
|
|
62
|
+
`
|
|
63
|
+
import {Tooltip, Link} from '@primer/react';
|
|
64
|
+
<Tooltip aria-label="product" direction="e">
|
|
65
|
+
<Link href={productLink}>
|
|
66
|
+
Product
|
|
67
|
+
</Link>
|
|
68
|
+
</Tooltip>
|
|
69
|
+
`
|
|
55
70
|
],
|
|
56
71
|
invalid: [
|
|
57
72
|
{
|
|
@@ -96,7 +111,7 @@ ruleTester.run('non-interactive-tooltip-trigger', rule, {
|
|
|
96
111
|
</Tooltip>`,
|
|
97
112
|
errors: [
|
|
98
113
|
{
|
|
99
|
-
messageId: '
|
|
114
|
+
messageId: 'nonInteractiveLink'
|
|
100
115
|
}
|
|
101
116
|
]
|
|
102
117
|
},
|
|
@@ -108,7 +123,7 @@ ruleTester.run('non-interactive-tooltip-trigger', rule, {
|
|
|
108
123
|
</Tooltip>`,
|
|
109
124
|
errors: [
|
|
110
125
|
{
|
|
111
|
-
messageId: '
|
|
126
|
+
messageId: 'nonInteractiveLink'
|
|
112
127
|
}
|
|
113
128
|
]
|
|
114
129
|
},
|
|
@@ -120,7 +135,7 @@ ruleTester.run('non-interactive-tooltip-trigger', rule, {
|
|
|
120
135
|
</Tooltip>`,
|
|
121
136
|
errors: [
|
|
122
137
|
{
|
|
123
|
-
messageId: '
|
|
138
|
+
messageId: 'nonInteractiveInput'
|
|
124
139
|
}
|
|
125
140
|
]
|
|
126
141
|
},
|
|
@@ -132,7 +147,43 @@ ruleTester.run('non-interactive-tooltip-trigger', rule, {
|
|
|
132
147
|
</Tooltip>`,
|
|
133
148
|
errors: [
|
|
134
149
|
{
|
|
135
|
-
messageId: '
|
|
150
|
+
messageId: 'nonInteractiveInput'
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
code: `
|
|
156
|
+
import {Tooltip, Button} from '@primer/react';
|
|
157
|
+
<Tooltip aria-label="Supplementary text" direction="e">
|
|
158
|
+
<Button disabled>Save</Button>
|
|
159
|
+
</Tooltip>`,
|
|
160
|
+
errors: [
|
|
161
|
+
{
|
|
162
|
+
messageId: 'nonInteractiveTrigger'
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
code: `
|
|
168
|
+
import {Tooltip, Button} from '@primer/react';
|
|
169
|
+
<Tooltip aria-label="Supplementary text" direction="e">
|
|
170
|
+
<IconButton disabled>Save</IconButton>
|
|
171
|
+
</Tooltip>`,
|
|
172
|
+
errors: [
|
|
173
|
+
{
|
|
174
|
+
messageId: 'nonInteractiveTrigger'
|
|
175
|
+
}
|
|
176
|
+
]
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
code: `
|
|
180
|
+
import {Tooltip, Button} from '@primer/react';
|
|
181
|
+
<Tooltip aria-label="Supplementary text" direction="e">
|
|
182
|
+
<input disabled>Save</input>
|
|
183
|
+
</Tooltip>`,
|
|
184
|
+
errors: [
|
|
185
|
+
{
|
|
186
|
+
messageId: 'nonInteractiveInput'
|
|
136
187
|
}
|
|
137
188
|
]
|
|
138
189
|
},
|
|
@@ -159,7 +210,7 @@ ruleTester.run('non-interactive-tooltip-trigger', rule, {
|
|
|
159
210
|
</Tooltip>`,
|
|
160
211
|
errors: [
|
|
161
212
|
{
|
|
162
|
-
messageId: '
|
|
213
|
+
messageId: 'nonInteractiveLink'
|
|
163
214
|
}
|
|
164
215
|
]
|
|
165
216
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
if (elem.attributes.length === 1 && elem.attributes[0].type === 'JSXSpreadAttribute') return;
|
|
19
|
+
|
|
20
|
+
const elemAs = isUsingAsProp(elem)
|
|
21
|
+
|
|
22
|
+
if (!elemAs) return 'nonExplicitHeadingLevel'
|
|
23
|
+
if (elemAs.value && !isValidAsUsage(elemAs.value)) return 'invalidAsValue'
|
|
24
|
+
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
meta: {
|
|
30
|
+
docs: {
|
|
31
|
+
description: 'Heading component must have explicit heading level, and specific `as` usage.',
|
|
32
|
+
url: require('../url')(module),
|
|
33
|
+
},
|
|
34
|
+
schema: [
|
|
35
|
+
{
|
|
36
|
+
properties: {
|
|
37
|
+
skipImportCheck: {
|
|
38
|
+
type: 'boolean'
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
messages: {
|
|
44
|
+
nonExplicitHeadingLevel: 'Heading must have an explicit heading level applied through the `as` prop.',
|
|
45
|
+
invalidAsValue: 'Usage of `as` must only be used for heading elements (h1-h6).'
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
create: function(context) {
|
|
49
|
+
return {
|
|
50
|
+
JSXOpeningElement(jsxNode) {
|
|
51
|
+
const skipImportCheck = context.options[0] ? context.options[0].skipImportCheck : false
|
|
52
|
+
|
|
53
|
+
if ((skipImportCheck || isPrimerComponent(jsxNode.name, context.getScope(jsxNode))) && isHeadingComponent(jsxNode)) {
|
|
54
|
+
const error = isInvalid(jsxNode)
|
|
55
|
+
|
|
56
|
+
if (error) {
|
|
57
|
+
context.report({
|
|
58
|
+
node: jsxNode,
|
|
59
|
+
messageId: error
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -1,41 +1,55 @@
|
|
|
1
|
+
const {getPropValue, propName} = require('jsx-ast-utils')
|
|
1
2
|
const {isPrimerComponent} = require('../utils/is-primer-component')
|
|
2
3
|
const {getJSXOpeningElementName} = require('../utils/get-jsx-opening-element-name')
|
|
3
4
|
const {getJSXOpeningElementAttribute} = require('../utils/get-jsx-opening-element-attribute')
|
|
4
5
|
|
|
5
6
|
const isInteractive = child => {
|
|
6
7
|
const childName = getJSXOpeningElementName(child.openingElement)
|
|
7
|
-
return
|
|
8
|
-
|
|
8
|
+
return (
|
|
9
|
+
['button', 'summary', 'select', 'textarea', 'a', 'input', 'link', 'iconbutton', 'textinput'].includes(
|
|
10
|
+
childName.toLowerCase()
|
|
11
|
+
) && !hasDisabledAttr(child)
|
|
9
12
|
)
|
|
10
13
|
}
|
|
11
14
|
|
|
15
|
+
const hasDisabledAttr = child => {
|
|
16
|
+
const hasDisabledAttr = getJSXOpeningElementAttribute(child.openingElement, 'disabled')
|
|
17
|
+
return hasDisabledAttr
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
const isAnchorTag = el => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
const openingEl = getJSXOpeningElementName(el.openingElement)
|
|
22
|
+
return openingEl === 'a' || openingEl.toLowerCase() === 'link'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const isJSXValue = (attributes) => {
|
|
26
|
+
const node = attributes.find(attribute => propName(attribute) === 'href');
|
|
27
|
+
const isJSXExpression = node.value.type === 'JSXExpressionContainer' && node
|
|
28
|
+
&& typeof getPropValue(node) === 'string';
|
|
29
|
+
|
|
30
|
+
return isJSXExpression
|
|
17
31
|
}
|
|
18
32
|
|
|
19
33
|
const isInteractiveAnchor = child => {
|
|
20
34
|
const hasHref = getJSXOpeningElementAttribute(child.openingElement, 'href')
|
|
21
35
|
if (!hasHref) return false
|
|
22
36
|
const href = getJSXOpeningElementAttribute(child.openingElement, 'href').value.value
|
|
23
|
-
const
|
|
37
|
+
const hasJSXValue = isJSXValue(child.openingElement.attributes);
|
|
38
|
+
const isAnchorInteractive = (typeof href === 'string' && href !== '' || hasJSXValue)
|
|
39
|
+
|
|
24
40
|
return isAnchorInteractive
|
|
25
41
|
}
|
|
26
42
|
|
|
27
43
|
const isInputTag = el => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
getJSXOpeningElementName(el.openingElement).toLowerCase() === 'textinput'
|
|
31
|
-
)
|
|
44
|
+
const openingEl = getJSXOpeningElementName(el.openingElement)
|
|
45
|
+
return openingEl === 'input' || openingEl.toLowerCase() === 'textinput'
|
|
32
46
|
}
|
|
33
47
|
|
|
34
48
|
const isInteractiveInput = child => {
|
|
35
49
|
const hasHiddenType =
|
|
36
50
|
getJSXOpeningElementAttribute(child.openingElement, 'type') &&
|
|
37
51
|
getJSXOpeningElementAttribute(child.openingElement, 'type').value.value === 'hidden'
|
|
38
|
-
return !hasHiddenType
|
|
52
|
+
return !hasHiddenType && !hasDisabledAttr(child)
|
|
39
53
|
}
|
|
40
54
|
|
|
41
55
|
const isOtherThanAnchorOrInput = el => {
|
|
@@ -57,12 +71,12 @@ const getAllChildren = node => {
|
|
|
57
71
|
|
|
58
72
|
const checks = [
|
|
59
73
|
{
|
|
60
|
-
id: '
|
|
74
|
+
id: 'nonInteractiveLink',
|
|
61
75
|
filter: jsxElement => isAnchorTag(jsxElement),
|
|
62
76
|
check: isInteractiveAnchor
|
|
63
77
|
},
|
|
64
78
|
{
|
|
65
|
-
id: '
|
|
79
|
+
id: 'nonInteractiveInput',
|
|
66
80
|
filter: jsxElement => isInputTag(jsxElement),
|
|
67
81
|
check: isInteractiveInput
|
|
68
82
|
},
|
|
@@ -76,16 +90,11 @@ const checks = [
|
|
|
76
90
|
const checkTriggerElement = jsxNode => {
|
|
77
91
|
const elements = [...getAllChildren(jsxNode)]
|
|
78
92
|
const hasInteractiveElement = elements.find(element => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
getJSXOpeningElementName(element.openingElement) === 'Link'
|
|
82
|
-
) {
|
|
93
|
+
const openingEl = getJSXOpeningElementName(element.openingElement)
|
|
94
|
+
if (openingEl === 'a' || openingEl === 'Link') {
|
|
83
95
|
return isInteractiveAnchor(element)
|
|
84
96
|
}
|
|
85
|
-
if (
|
|
86
|
-
getJSXOpeningElementName(element.openingElement) === 'input' ||
|
|
87
|
-
getJSXOpeningElementName(element.openingElement) === 'TextInput'
|
|
88
|
-
) {
|
|
97
|
+
if (openingEl === 'input' || openingEl === 'TextInput') {
|
|
89
98
|
return isInteractiveInput(element)
|
|
90
99
|
} else {
|
|
91
100
|
return isInteractive(element)
|
|
@@ -110,10 +119,10 @@ const checkTriggerElement = jsxNode => {
|
|
|
110
119
|
}
|
|
111
120
|
// check the specificity of the errors. If there are multiple errors, only return the most specific one.
|
|
112
121
|
if (errors.size > 1) {
|
|
113
|
-
if (errors.has('
|
|
122
|
+
if (errors.has('nonInteractiveLink')) {
|
|
114
123
|
errors.delete('nonInteractiveTrigger')
|
|
115
124
|
}
|
|
116
|
-
if (errors.has('
|
|
125
|
+
if (errors.has('nonInteractiveInput')) {
|
|
117
126
|
errors.delete('nonInteractiveTrigger')
|
|
118
127
|
}
|
|
119
128
|
}
|
|
@@ -135,11 +144,11 @@ module.exports = {
|
|
|
135
144
|
],
|
|
136
145
|
messages: {
|
|
137
146
|
nonInteractiveTrigger:
|
|
138
|
-
'
|
|
139
|
-
|
|
147
|
+
'Tooltips should only be applied to interactive elements that are not disabled. Consider using a `<button>` or equivalent interactive element instead.',
|
|
148
|
+
nonInteractiveLink:
|
|
140
149
|
'Anchor tags without an href attribute are not interactive, therefore they cannot be used as a trigger for a tooltip. Please add an href attribute or use an alternative interactive element instead',
|
|
141
|
-
|
|
142
|
-
'Hidden inputs are not interactive and cannot be used as a trigger for a tooltip. Please use an alternate input type or use a different interactive element instead',
|
|
150
|
+
nonInteractiveInput:
|
|
151
|
+
'Hidden or disabled inputs are not interactive and cannot be used as a trigger for a tooltip. Please use an alternate input type or use a different interactive element instead',
|
|
143
152
|
singleChild: 'The `Tooltip` component expects a single React element as a child.'
|
|
144
153
|
}
|
|
145
154
|
},
|
package/src/url.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const {homepage, version} = require('../package.json')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
|
|
4
|
+
module.exports = ({id}) => {
|
|
5
|
+
const url = new URL(homepage)
|
|
6
|
+
const rule = path.basename(id, '.js')
|
|
7
|
+
url.hash = ''
|
|
8
|
+
url.pathname += `/blob/v${version}/docs/rules/${rule}.md`
|
|
9
|
+
return url.toString()
|
|
10
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const {flattenComponents} = require('../flatten-components')
|
|
2
|
+
|
|
3
|
+
const mockComponents = function(passedObj) {
|
|
4
|
+
return {
|
|
5
|
+
Button: 'button',
|
|
6
|
+
Link: 'a',
|
|
7
|
+
Spinner: 'svg',
|
|
8
|
+
Radio: 'input',
|
|
9
|
+
TextInput: {
|
|
10
|
+
Action: 'button',
|
|
11
|
+
self: 'input'
|
|
12
|
+
},
|
|
13
|
+
...passedObj
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('getElementType', function() {
|
|
18
|
+
it('flattens passed object 1-level deep', function() {
|
|
19
|
+
const result = flattenComponents(mockComponents())
|
|
20
|
+
|
|
21
|
+
const expectedResult = {
|
|
22
|
+
Button: 'button',
|
|
23
|
+
Link: 'a',
|
|
24
|
+
Spinner: 'svg',
|
|
25
|
+
Radio: 'input',
|
|
26
|
+
TextInput: 'input',
|
|
27
|
+
'TextInput.Action': 'button'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
expect(result).toEqual(expectedResult)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('ignores objects nested deeper than 1-level', function() {
|
|
34
|
+
const result = flattenComponents(
|
|
35
|
+
mockComponents({
|
|
36
|
+
Select: {
|
|
37
|
+
Items: {
|
|
38
|
+
self: 'div'
|
|
39
|
+
},
|
|
40
|
+
Option: 'option',
|
|
41
|
+
self: 'select'
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const expectedResult = {
|
|
47
|
+
Button: 'button',
|
|
48
|
+
Link: 'a',
|
|
49
|
+
Spinner: 'svg',
|
|
50
|
+
Radio: 'input',
|
|
51
|
+
TextInput: 'input',
|
|
52
|
+
'TextInput.Action': 'button',
|
|
53
|
+
'Select.Items': {
|
|
54
|
+
self: 'div'
|
|
55
|
+
},
|
|
56
|
+
Select: 'select',
|
|
57
|
+
'Select.Option': 'option'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
expect(result).toEqual(expectedResult)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
function flattenComponents(componentObj) {
|
|
2
|
+
let result = {};
|
|
3
|
+
|
|
4
|
+
Object.keys(componentObj).forEach((key) => {
|
|
5
|
+
if (typeof componentObj[key] === 'object') {
|
|
6
|
+
const test = Object.keys(componentObj[key]).forEach((item) => {
|
|
7
|
+
result = { ...result, [`${key}${item !== 'self' ? `.${item}` : ''}`]: componentObj[key][item] };
|
|
8
|
+
});
|
|
9
|
+
} else {
|
|
10
|
+
result = {...result, [key]: componentObj[key]}
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
exports.flattenComponents = flattenComponents
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
function getJSXOpeningElementAttribute(openingEl, name) {
|
|
2
2
|
const attributes = openingEl.attributes
|
|
3
3
|
const attribute = attributes.find(attribute => {
|
|
4
|
-
return attribute.name.name === name
|
|
4
|
+
return attribute.name && attribute.name.name === name
|
|
5
5
|
})
|
|
6
6
|
|
|
7
7
|
return attribute
|