eslint-plugin-primer-react 5.1.0 → 5.2.0-rc.dc08e82
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/workflows/release_tracking.yml +19 -0
- package/CHANGELOG.md +10 -0
- package/docs/rules/a11y-link-in-text-block.md +100 -0
- package/package-lock.json +14790 -0
- package/package.json +2 -2
- package/src/index.js +1 -0
- package/src/rules/__tests__/a11y-link-in-text-block.test.js +161 -0
- package/src/rules/a11y-explicit-heading.js +2 -1
- package/src/rules/a11y-link-in-text-block.js +104 -0
- package/src/rules/a11y-tooltip-interactive-trigger.js +2 -1
- package/src/rules/direct-slot-children.js +2 -1
- package/src/rules/no-system-props.js +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-primer-react",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0-rc.dc08e82",
|
|
4
4
|
"description": "ESLint rules for Primer React",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@styled-system/props": "^5.1.5",
|
|
32
|
-
"eslint-plugin-github": "^
|
|
32
|
+
"eslint-plugin-github": "^5.0.1",
|
|
33
33
|
"eslint-plugin-jsx-a11y": "^6.7.1",
|
|
34
34
|
"eslint-traverse": "^1.0.0",
|
|
35
35
|
"lodash": "^4.17.21",
|
package/src/index.js
CHANGED
|
@@ -7,6 +7,7 @@ module.exports = {
|
|
|
7
7
|
'new-color-css-vars': require('./rules/new-color-css-vars'),
|
|
8
8
|
'a11y-explicit-heading': require('./rules/a11y-explicit-heading'),
|
|
9
9
|
'no-deprecated-props': require('./rules/no-deprecated-props'),
|
|
10
|
+
'a11y-link-in-text-block': require('./rules/a11y-link-in-text-block'),
|
|
10
11
|
},
|
|
11
12
|
configs: {
|
|
12
13
|
recommended: require('./configs/recommended'),
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
const rule = require('../a11y-link-in-text-block')
|
|
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-link-in-text-block', rule, {
|
|
15
|
+
valid: [
|
|
16
|
+
`import {Link} from '@primer/react';
|
|
17
|
+
<Box>
|
|
18
|
+
|
|
19
|
+
<Link href="something">
|
|
20
|
+
Blah blah
|
|
21
|
+
</Link>{' '}
|
|
22
|
+
.
|
|
23
|
+
</Box>
|
|
24
|
+
`,
|
|
25
|
+
`import {Text, Link} from '@primer/react';
|
|
26
|
+
<Something>
|
|
27
|
+
<Link href='blah'>
|
|
28
|
+
blah
|
|
29
|
+
</Link>
|
|
30
|
+
</Something>
|
|
31
|
+
`,
|
|
32
|
+
`import {Link} from '@primer/react';
|
|
33
|
+
<p>bla blah <Link inline={true}>Link level 1</Link></p>;
|
|
34
|
+
`,
|
|
35
|
+
`import {Link} from '@primer/react';
|
|
36
|
+
<p>bla blah<Link inline>Link level 1</Link></p>;
|
|
37
|
+
`,
|
|
38
|
+
`import {Link} from '@primer/react';
|
|
39
|
+
<><span>something</span><Link inline={true}>Link level 1</Link></>;
|
|
40
|
+
`,
|
|
41
|
+
`import {Link} from '@primer/react';
|
|
42
|
+
<Link>Link level 1</Link>;
|
|
43
|
+
`,
|
|
44
|
+
`import {Heading, Link} from '@primer/react';
|
|
45
|
+
<Heading>
|
|
46
|
+
<Link>Link level 1</Link>
|
|
47
|
+
hello
|
|
48
|
+
</Heading>
|
|
49
|
+
`,
|
|
50
|
+
`import {Heading, Link} from '@primer/react';
|
|
51
|
+
<Heading as="h2">
|
|
52
|
+
<Link href={somePath}>
|
|
53
|
+
Breadcrumb
|
|
54
|
+
</Link>
|
|
55
|
+
Create a thing
|
|
56
|
+
</Heading>
|
|
57
|
+
`,
|
|
58
|
+
`import {Link} from '@primer/react';
|
|
59
|
+
<div>
|
|
60
|
+
<h2>
|
|
61
|
+
<Link href={somePath}>
|
|
62
|
+
Breadcrumb
|
|
63
|
+
</Link>
|
|
64
|
+
</h2>
|
|
65
|
+
Create a thing
|
|
66
|
+
</div>
|
|
67
|
+
`,
|
|
68
|
+
`import {Link} from '@primer/react';
|
|
69
|
+
<div>
|
|
70
|
+
<Link href={somePath}>
|
|
71
|
+
<GitHubAvatar />{owner}
|
|
72
|
+
</Link>{' '}
|
|
73
|
+
last edited{' '}
|
|
74
|
+
</div>
|
|
75
|
+
`,
|
|
76
|
+
`import {Link} from '@primer/react';
|
|
77
|
+
<span>
|
|
78
|
+
by
|
|
79
|
+
<Link href="something" sx={{p: 2, fontWeight: 'bold'}}>
|
|
80
|
+
Blah blah
|
|
81
|
+
</Link>
|
|
82
|
+
</span>
|
|
83
|
+
`,
|
|
84
|
+
`import {Link} from '@primer/react';
|
|
85
|
+
<span>
|
|
86
|
+
by
|
|
87
|
+
<Link href="something" sx={{fontWeight: 'bold'}}>
|
|
88
|
+
Blah blah
|
|
89
|
+
</Link>
|
|
90
|
+
</span>
|
|
91
|
+
`,
|
|
92
|
+
`import {Link} from '@primer/react';
|
|
93
|
+
<span>
|
|
94
|
+
by
|
|
95
|
+
<Link href="something" sx={{fontFamily: 'mono'}}>
|
|
96
|
+
Blah blah
|
|
97
|
+
</Link>
|
|
98
|
+
</span>
|
|
99
|
+
`,
|
|
100
|
+
`import {Link} from '@primer/react';
|
|
101
|
+
<Box>
|
|
102
|
+
|
|
103
|
+
<Link href="something">
|
|
104
|
+
Blah blah
|
|
105
|
+
</Link>{' '}
|
|
106
|
+
.
|
|
107
|
+
</Box>
|
|
108
|
+
`,
|
|
109
|
+
`import {Link} from '@primer/react';
|
|
110
|
+
<Heading sx={{fontSize: 1, mb: 3}} as="h3">
|
|
111
|
+
In addition,{' '}
|
|
112
|
+
<Link href="https://github.com/pricing" target="_blank">
|
|
113
|
+
GitHub Team
|
|
114
|
+
</Link>{' '}
|
|
115
|
+
includes:
|
|
116
|
+
</Heading>
|
|
117
|
+
`,
|
|
118
|
+
],
|
|
119
|
+
invalid: [
|
|
120
|
+
{
|
|
121
|
+
code: `import {Link} from '@primer/react';
|
|
122
|
+
<p>bla blah<Link>Link level 1</Link></p>
|
|
123
|
+
`,
|
|
124
|
+
errors: [{messageId: 'linkInTextBlock'}],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
code: `import {Link} from '@primer/react';
|
|
128
|
+
<p><Link>Link level 1</Link> something something</p>
|
|
129
|
+
`,
|
|
130
|
+
errors: [{messageId: 'linkInTextBlock'}],
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
code: `import {Link} from '@primer/react';
|
|
134
|
+
<p>bla blah<Link inline={false}>Link level 1</Link></p>
|
|
135
|
+
`,
|
|
136
|
+
errors: [{messageId: 'linkInTextBlock'}],
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
code: `import {Link} from '@primer/react';
|
|
140
|
+
<Box>Something something{' '}
|
|
141
|
+
<Link>Link level 1</Link>
|
|
142
|
+
</Box>
|
|
143
|
+
`,
|
|
144
|
+
errors: [{messageId: 'linkInTextBlock'}],
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
code: `import {Link} from '@primer/react';
|
|
148
|
+
<>blah blah blah{' '}
|
|
149
|
+
<Link>Link level 1</Link></>;
|
|
150
|
+
`,
|
|
151
|
+
errors: [{messageId: 'linkInTextBlock'}],
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
code: `import {Link} from '@primer/react';
|
|
155
|
+
<>blah blah blah{' '}
|
|
156
|
+
<Link underline>Link level 1</Link></>;
|
|
157
|
+
`,
|
|
158
|
+
errors: [{messageId: 'linkInTextBlock'}],
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
})
|
|
@@ -46,12 +46,13 @@ module.exports = {
|
|
|
46
46
|
},
|
|
47
47
|
},
|
|
48
48
|
create(context) {
|
|
49
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode()
|
|
49
50
|
return {
|
|
50
51
|
JSXOpeningElement(jsxNode) {
|
|
51
52
|
const skipImportCheck = context.options[0] ? context.options[0].skipImportCheck : false
|
|
52
53
|
|
|
53
54
|
if (
|
|
54
|
-
(skipImportCheck || isPrimerComponent(jsxNode.name,
|
|
55
|
+
(skipImportCheck || isPrimerComponent(jsxNode.name, sourceCode.getScope(jsxNode))) &&
|
|
55
56
|
isHeadingComponent(jsxNode)
|
|
56
57
|
) {
|
|
57
58
|
const error = isInvalid(jsxNode)
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
module.exports = {
|
|
6
|
+
meta: {
|
|
7
|
+
type: 'problem',
|
|
8
|
+
schema: [
|
|
9
|
+
{
|
|
10
|
+
properties: {
|
|
11
|
+
skipImportCheck: {
|
|
12
|
+
type: 'boolean',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
messages: {
|
|
18
|
+
linkInTextBlock: '<Link>s that are used within a text block should have the inline prop.',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
create(context) {
|
|
22
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode()
|
|
23
|
+
return {
|
|
24
|
+
JSXElement(node) {
|
|
25
|
+
const name = getJSXOpeningElementName(node.openingElement)
|
|
26
|
+
if (
|
|
27
|
+
isPrimerComponent(node.openingElement.name, sourceCode.getScope(node)) &&
|
|
28
|
+
name === 'Link' &&
|
|
29
|
+
node.parent.children
|
|
30
|
+
) {
|
|
31
|
+
let siblings = node.parent.children
|
|
32
|
+
const parentName = node.parent.openingElement?.name?.name
|
|
33
|
+
// Skip if Link is nested inside of a heading.
|
|
34
|
+
const parentsToSkip = ['Heading', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']
|
|
35
|
+
if (parentsToSkip.includes(parentName)) return
|
|
36
|
+
if (siblings.length > 0) {
|
|
37
|
+
siblings = siblings.filter(childNode => {
|
|
38
|
+
return (
|
|
39
|
+
!(childNode.type === 'JSXText' && /^\s+$/.test(childNode.value)) &&
|
|
40
|
+
!(
|
|
41
|
+
childNode.type === 'JSXExpressionContainer' &&
|
|
42
|
+
childNode.expression.type === 'Literal' &&
|
|
43
|
+
/^\s+$/.test(childNode.expression.value)
|
|
44
|
+
) &&
|
|
45
|
+
!(childNode.type === 'Literal' && /^\s+$/.test(childNode.value))
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
const index = siblings.findIndex(childNode => {
|
|
49
|
+
return childNode.range === node.range
|
|
50
|
+
})
|
|
51
|
+
const prevSibling = siblings[index - 1]
|
|
52
|
+
const nextSibling = siblings[index + 1]
|
|
53
|
+
|
|
54
|
+
const prevSiblingIsText = prevSibling && prevSibling.type === 'JSXText'
|
|
55
|
+
const nextSiblingIsText = nextSibling && nextSibling.type === 'JSXText'
|
|
56
|
+
if (prevSiblingIsText || nextSiblingIsText) {
|
|
57
|
+
// Skip if the only text adjacent to the link is a period, then skip it.
|
|
58
|
+
if (!prevSiblingIsText && /^\s*\.+\s*$/.test(nextSibling.value)) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
const sxAttribute = getJSXOpeningElementAttribute(node.openingElement, 'sx')
|
|
62
|
+
const inlineAttribute = getJSXOpeningElementAttribute(node.openingElement, 'inline')
|
|
63
|
+
|
|
64
|
+
// Skip if Link child is a JSX element.
|
|
65
|
+
const jsxElementChildren = node.children.filter(child => {
|
|
66
|
+
return child.type === 'JSXElement'
|
|
67
|
+
})
|
|
68
|
+
if (jsxElementChildren.length > 0) return
|
|
69
|
+
|
|
70
|
+
// Skip if fontWeight or fontFamily is set via the sx prop since these may technically be considered sufficiently distinguishing styles that don't use color.
|
|
71
|
+
if (
|
|
72
|
+
sxAttribute &&
|
|
73
|
+
sxAttribute?.value?.expression &&
|
|
74
|
+
sxAttribute.value.expression.type === 'ObjectExpression' &&
|
|
75
|
+
sxAttribute.value.expression.properties &&
|
|
76
|
+
sxAttribute.value.expression.properties.length > 0
|
|
77
|
+
) {
|
|
78
|
+
const fontStyleProperty = sxAttribute.value.expression.properties.filter(property => {
|
|
79
|
+
return property.key.name === 'fontWeight' || property.key.name === 'fontFamily'
|
|
80
|
+
})
|
|
81
|
+
if (fontStyleProperty.length > 0) return
|
|
82
|
+
}
|
|
83
|
+
if (inlineAttribute) {
|
|
84
|
+
if (!inlineAttribute.value) {
|
|
85
|
+
return
|
|
86
|
+
} else if (inlineAttribute.value.type === 'JSXExpressionContainer') {
|
|
87
|
+
if (inlineAttribute.value.expression.type === 'Literal') {
|
|
88
|
+
if (inlineAttribute.value.expression.value === true) {
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
context.report({
|
|
95
|
+
node,
|
|
96
|
+
messageId: 'linkInTextBlock',
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
}
|
|
@@ -152,13 +152,14 @@ module.exports = {
|
|
|
152
152
|
},
|
|
153
153
|
},
|
|
154
154
|
create(context) {
|
|
155
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode()
|
|
155
156
|
return {
|
|
156
157
|
JSXElement(jsxNode) {
|
|
157
158
|
// If `skipImportCheck` is true, this rule will check for non-interactive element in any components (not just ones that are imported from `@primer/react`).
|
|
158
159
|
const skipImportCheck = context.options[0] ? context.options[0].skipImportCheck : false
|
|
159
160
|
const name = getJSXOpeningElementName(jsxNode.openingElement)
|
|
160
161
|
if (
|
|
161
|
-
(skipImportCheck || isPrimerComponent(jsxNode.openingElement.name,
|
|
162
|
+
(skipImportCheck || isPrimerComponent(jsxNode.openingElement.name, sourceCode.getScope(jsxNode))) &&
|
|
162
163
|
name === 'Tooltip' &&
|
|
163
164
|
jsxNode.children
|
|
164
165
|
) {
|
|
@@ -45,6 +45,7 @@ module.exports = {
|
|
|
45
45
|
},
|
|
46
46
|
create(context) {
|
|
47
47
|
const stack = []
|
|
48
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode()
|
|
48
49
|
return {
|
|
49
50
|
JSXOpeningElement(jsxNode) {
|
|
50
51
|
const name = getJSXOpeningElementName(jsxNode)
|
|
@@ -56,7 +57,7 @@ module.exports = {
|
|
|
56
57
|
// If component is a Primer component and a slot child,
|
|
57
58
|
// check if it's a direct child of the slot parent
|
|
58
59
|
if (
|
|
59
|
-
(skipImportCheck || isPrimerComponent(jsxNode.name,
|
|
60
|
+
(skipImportCheck || isPrimerComponent(jsxNode.name, sourceCode.getScope(jsxNode))) &&
|
|
60
61
|
slotChildToParentMap[name]
|
|
61
62
|
) {
|
|
62
63
|
const expectedParentNames = slotChildToParentMap[name]
|
|
@@ -78,6 +78,8 @@ module.exports = {
|
|
|
78
78
|
...(includeUtilityComponents ? [] : utilityComponents),
|
|
79
79
|
])
|
|
80
80
|
|
|
81
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode()
|
|
82
|
+
|
|
81
83
|
return {
|
|
82
84
|
JSXOpeningElement(jsxNode) {
|
|
83
85
|
if (skipImportCheck) {
|
|
@@ -86,7 +88,7 @@ module.exports = {
|
|
|
86
88
|
if (isHTMLElement(jsxNode)) return
|
|
87
89
|
} else {
|
|
88
90
|
// skip if component is not imported from primer/react
|
|
89
|
-
if (!isPrimerComponent(jsxNode.name,
|
|
91
|
+
if (!isPrimerComponent(jsxNode.name, sourceCode.getScope(jsxNode))) return
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
const componentName = getJSXOpeningElementName(jsxNode)
|