Skip to content

Commit bb2bff6

Browse files
Update the tooltip eslint rule as we now export the new one from main bundle and the old one from deprecated
1 parent c860192 commit bb2bff6

File tree

8 files changed

+227
-196
lines changed

8 files changed

+227
-196
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,4 @@ ESLint rules for Primer React
3939
- [a11y-explicit-heading](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/a11y-explicit-heading.md)
4040
- [a11y-link-in-text-block](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/a11y-link-in-text-block.md)
4141
- [a11y-remove-disable-tooltip](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/a11y-remove-disable-tooltip.md)
42-
- [a11y-use-next-tooltip](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/a11y-use-next-tooltip.md)
42+
- [a11y-use-accessible-tooltip](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/a11y-use-accessible-tooltip.md)

docs/rules/a11y-use-next-tooltip.md renamed to docs/rules/a11y-use-accessible-tooltip.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,21 @@ This rule recommends using the tooltip that is imported from `@primer/react/next
66
👎 Examples of **incorrect** code for this rule:
77

88
```tsx
9-
import {Tooltip} from '@primer/react'
9+
import {Tooltip} from '@primer/react/deprecated'
1010
```
1111

1212
👍 Examples of **correct** code for this rule:
1313

1414
```tsx
15-
import {Tooltip} from '@primer/react/next'
15+
import {Tooltip} from '@primer/react'
1616
```
1717

1818
## Icon buttons and tooltips
1919

2020
Even though the below code is perfectly valid, since icon buttons now come with tooltips by default, it is not required to explicitly use the Tooltip component on icon buttons.
2121

2222
```jsx
23-
import {IconButton} from '@primer/react'
24-
import {Tooltip} from '@primer/react/next'
23+
import {IconButton, Tooltip} from '@primer/react'
2524

2625
function ExampleComponent() {
2726
return (

src/configs/recommended.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ module.exports = {
1717
'primer-react/a11y-explicit-heading': 'error',
1818
'primer-react/no-deprecated-props': 'warn',
1919
'primer-react/a11y-remove-disable-tooltip': 'error',
20-
'primer-react/a11y-use-next-tooltip': 'error',
20+
'primer-react/a11y-use-accessible-tooltip': 'error',
2121
'primer-react/no-unnecessary-components': 'error',
2222
'primer-react/prefer-action-list-item-onselect': 'error',
2323
},

src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ module.exports = {
99
'no-deprecated-props': require('./rules/no-deprecated-props'),
1010
'a11y-link-in-text-block': require('./rules/a11y-link-in-text-block'),
1111
'a11y-remove-disable-tooltip': require('./rules/a11y-remove-disable-tooltip'),
12-
'a11y-use-next-tooltip': require('./rules/a11y-use-next-tooltip'),
12+
'a11y-use-accessible-tooltip': require('./rules/a11y-use-accessible-tooltip'),
1313
'use-deprecated-from-deprecated': require('./rules/use-deprecated-from-deprecated'),
1414
'no-wildcard-imports': require('./rules/no-wildcard-imports'),
1515
'no-unnecessary-components': require('./rules/no-unnecessary-components'),
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
const rule = require('../a11y-use-accessible-tooltip')
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-use-accessible-tooltip', rule, {
15+
valid: [`import {Tooltip} from '@primer/react';`],
16+
invalid: [
17+
// Single import from deprecated
18+
{
19+
code: `import {Tooltip} from '@primer/react/deprecated';`,
20+
errors: [{messageId: 'useAccessibleTooltip'}],
21+
output: `import {Tooltip} from '@primer/react';`,
22+
},
23+
// Multiple imports from deprecated
24+
{
25+
code: `import {Tooltip, Button} from '@primer/react/deprecated';`,
26+
errors: [{messageId: 'useAccessibleTooltip'}],
27+
output: `import {Button} from '@primer/react/deprecated';\nimport {Tooltip} from '@primer/react';`,
28+
},
29+
// Multiple imports from deprecated
30+
{
31+
code: `import {Button, Tooltip, Stack} from '@primer/react/deprecated';`,
32+
errors: [{messageId: 'useAccessibleTooltip'}],
33+
output: `import {Button, Stack} from '@primer/react/deprecated';\nimport {Tooltip} from '@primer/react';`,
34+
},
35+
// Single import from deprecated with an existing import from @primer/react
36+
{
37+
code: `import {Tooltip} from '@primer/react/deprecated';import {ActionList, ActionMenu} from '@primer/react';`,
38+
errors: [{messageId: 'useAccessibleTooltip', line: 1}],
39+
output: `import {ActionList, ActionMenu, Tooltip} from '@primer/react';`,
40+
},
41+
// Multiple imports from deprecated with an existing import from @primer/react
42+
{
43+
code: `import {Dialog, Tooltip} from '@primer/react/deprecated';import {ActionList, ActionMenu} from '@primer/react';`,
44+
errors: [{messageId: 'useAccessibleTooltip', line: 1}],
45+
output: `import {Dialog} from '@primer/react/deprecated';import {ActionList, ActionMenu, Tooltip} from '@primer/react';`,
46+
},
47+
{
48+
code: `import {ActionList, ActionMenu, Tooltip, Button} from '@primer/react/deprecated';\n<Tooltip aria-label="tooltip text" noDelay={true} wrap={true} align="left"><Button>Button</Button></Tooltip>`,
49+
errors: [
50+
{messageId: 'useAccessibleTooltip', line: 1},
51+
{messageId: 'useTextProp', line: 2},
52+
{messageId: 'noDelayRemoved', line: 2},
53+
{messageId: 'wrapRemoved', line: 2},
54+
{messageId: 'alignRemoved', line: 2},
55+
],
56+
output: `import {ActionList, ActionMenu, Button} from '@primer/react/deprecated';\nimport {Tooltip} from '@primer/react';\n<Tooltip text="tooltip text" ><Button>Button</Button></Tooltip>`,
57+
},
58+
],
59+
})

src/rules/__tests__/a11y-use-next-tooltip.test.js

Lines changed: 0 additions & 61 deletions
This file was deleted.
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
'use strict'
2+
const url = require('../url')
3+
const {getJSXOpeningElementAttribute} = require('../utils/get-jsx-opening-element-attribute')
4+
const {getJSXOpeningElementName} = require('../utils/get-jsx-opening-element-name')
5+
6+
module.exports = {
7+
meta: {
8+
type: 'suggestion',
9+
docs: {
10+
description: 'recommends the use of @primer/react Tooltip component',
11+
category: 'Best Practices',
12+
recommended: true,
13+
url: url(module),
14+
},
15+
fixable: true,
16+
schema: [],
17+
messages: {
18+
useAccessibleTooltip: 'Please use @primer/react Tooltip component that has accessibility improvements',
19+
useTextProp: 'Please use the text prop instead of aria-label',
20+
noDelayRemoved: 'noDelay prop is removed. Tooltip now has no delay by default',
21+
wrapRemoved: 'wrap prop is removed. Tooltip now wraps by default',
22+
alignRemoved: 'align prop is removed. Please use the direction prop instead.',
23+
},
24+
},
25+
create(context) {
26+
return {
27+
ImportDeclaration(node) {
28+
if (node.source.value !== '@primer/react/deprecated') {
29+
return
30+
}
31+
const hasTooltip = node.specifiers.some(
32+
specifier => specifier.imported && specifier.imported.name === 'Tooltip',
33+
)
34+
35+
if (!hasTooltip) {
36+
return
37+
}
38+
39+
const hasOtherImports = node.specifiers.length > 1
40+
41+
const sourceCode = context.getSourceCode()
42+
// Checking to see if there is an existing root (@primer/react) import
43+
const rootImport = sourceCode.ast.body.filter(statement => {
44+
return statement.type === 'ImportDeclaration' && statement.source.value === '@primer/react'
45+
})
46+
47+
const tooltipSpecifier = node.specifiers.find(
48+
specifier => specifier.imported && specifier.imported.name === 'Tooltip',
49+
)
50+
51+
const hasRootImport = rootImport.length >= 1
52+
53+
context.report({
54+
node,
55+
messageId: 'useAccessibleTooltip',
56+
fix(fixer) {
57+
const fixes = []
58+
// If Tooltip is the only import and no existing @primer/react import, replace the whole import statement
59+
if (!hasOtherImports && !hasRootImport) {
60+
fixes.push(fixer.replaceText(node.source, `'@primer/react'`))
61+
} else if (hasOtherImports && !hasRootImport) {
62+
// There are other imports from the deprecated bundle but no existing @primer/react import, so remove the Tooltip import and add a new import statement with the correct path.
63+
const previousToken = sourceCode.getTokenBefore(tooltipSpecifier)
64+
const nextToken = sourceCode.getTokenAfter(tooltipSpecifier)
65+
const hasTrailingComma = nextToken && nextToken.value === ','
66+
const hasLeadingComma = previousToken && previousToken.value === ','
67+
68+
let rangeToRemove
69+
70+
if (hasTrailingComma) {
71+
rangeToRemove = [tooltipSpecifier.range[0], nextToken.range[1] + 1]
72+
} else if (hasLeadingComma) {
73+
rangeToRemove = [previousToken.range[0], tooltipSpecifier.range[1]]
74+
} else {
75+
rangeToRemove = [tooltipSpecifier.range[0], tooltipSpecifier.range[1]]
76+
}
77+
78+
fixes.push(fixer.removeRange(rangeToRemove))
79+
fixes.push(fixer.insertTextAfter(node, `\nimport {Tooltip} from '@primer/react';`))
80+
} else {
81+
if (!hasOtherImports) {
82+
// remove the entire import statement
83+
fixes.push(fixer.remove(node))
84+
} else {
85+
const previousToken = sourceCode.getTokenBefore(tooltipSpecifier)
86+
const nextToken = sourceCode.getTokenAfter(tooltipSpecifier)
87+
const hasTrailingComma = nextToken && nextToken.value === ','
88+
const hasLeadingComma = previousToken && previousToken.value === ','
89+
90+
let rangeToRemove
91+
92+
if (hasTrailingComma) {
93+
rangeToRemove = [tooltipSpecifier.range[0], nextToken.range[1] + 1]
94+
} else if (hasLeadingComma) {
95+
rangeToRemove = [previousToken.range[0], tooltipSpecifier.range[1]]
96+
} else {
97+
rangeToRemove = [tooltipSpecifier.range[0], tooltipSpecifier.range[1]]
98+
}
99+
100+
fixes.push(fixer.removeRange(rangeToRemove))
101+
}
102+
// find the last specifier in the existing @primer/react import and insert Tooltip after that
103+
// const rootImport = sourceCode.ast.body.find(statement => {
104+
// return statement.type === 'ImportDeclaration' && statement.source.value === '@primer/react'
105+
// })
106+
const lastSpecifier = rootImport.specifiers[rootImport.specifiers.length - 1]
107+
fixes.push(fixer.insertTextAfter(lastSpecifier, `, Tooltip`))
108+
}
109+
110+
return fixes
111+
},
112+
})
113+
},
114+
JSXOpeningElement(node) {
115+
const openingElName = getJSXOpeningElementName(node)
116+
if (openingElName !== 'Tooltip') {
117+
return
118+
}
119+
const ariaLabel = getJSXOpeningElementAttribute(node, 'aria-label')
120+
if (ariaLabel !== undefined) {
121+
context.report({
122+
node,
123+
messageId: 'useTextProp',
124+
fix(fixer) {
125+
return fixer.replaceText(ariaLabel.name, 'text')
126+
},
127+
})
128+
}
129+
const noDelay = getJSXOpeningElementAttribute(node, 'noDelay')
130+
if (noDelay !== undefined) {
131+
context.report({
132+
node,
133+
messageId: 'noDelayRemoved',
134+
fix(fixer) {
135+
return fixer.remove(noDelay)
136+
},
137+
})
138+
}
139+
const wrap = getJSXOpeningElementAttribute(node, 'wrap')
140+
if (wrap !== undefined) {
141+
context.report({
142+
node,
143+
messageId: 'wrapRemoved',
144+
fix(fixer) {
145+
return fixer.remove(wrap)
146+
},
147+
})
148+
}
149+
const align = getJSXOpeningElementAttribute(node, 'align')
150+
if (align !== undefined) {
151+
context.report({
152+
node,
153+
messageId: 'alignRemoved',
154+
fix(fixer) {
155+
return fixer.remove(align)
156+
},
157+
})
158+
}
159+
},
160+
}
161+
},
162+
}

0 commit comments

Comments
 (0)