Skip to content

Commit d605ecf

Browse files
code review feedback
1 parent 1dd64f6 commit d605ecf

File tree

2 files changed

+95
-50
lines changed

2 files changed

+95
-50
lines changed

src/rules/__tests__/non-interactive-tooltip-trigger.test.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ ruleTester.run('non-interactive-tooltip-trigger', rule, {
2222
],
2323
invalid: [
2424
{
25-
code: `import {Tooltip} from '@primer/react';<Tooltip type="description" text="supportive text" direction="e"><button>save</button><button>cancel</button></Tooltip>`,
25+
code: `import {Tooltip} from '@primer/react';<Tooltip type="description" text="supportive text" direction="e"><button>button1</button><button>button2</button></Tooltip>
26+
`,
2627
errors: [
2728
{
2829
messageId: 'singleChild'
@@ -62,7 +63,7 @@ ruleTester.run('non-interactive-tooltip-trigger', rule, {
6263
]
6364
},
6465
{
65-
code: `import {Tooltip, Button} from '@primer/react';<Tooltip aria-label="Supplementary text" direction="e"><heading><span>Save</span></heading></Tooltip>`,
66+
code: `import {Tooltip, Button} from '@primer/react';<Tooltip aria-label="Supplementary text" direction="e"><header><span>Save</span></header></Tooltip>`,
6667
errors: [
6768
{
6869
messageId: 'nonInteractiveTrigger'
@@ -73,7 +74,7 @@ ruleTester.run('non-interactive-tooltip-trigger', rule, {
7374
code: `import {Tooltip, Button} from '@primer/react';<Tooltip aria-label="Supplementary text" direction="e"><h1><a>Save</a></h1></Tooltip>`,
7475
errors: [
7576
{
76-
messageId: 'nonInteractiveTrigger'
77+
messageId: 'anchorTagWithoutHref'
7778
}
7879
]
7980
}

src/rules/non-interactive-tooltip-trigger.js

Lines changed: 91 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,65 +2,107 @@ const {isPrimerComponent} = require('../utils/is-primer-component')
22
const {getJSXOpeningElementName} = require('../utils/get-jsx-opening-element-name')
33
const {getJSXOpeningElementAttribute} = require('../utils/get-jsx-opening-element-attribute')
44

5-
isInteractive = child => {
5+
const isInteractive = child => {
66
const childName = getJSXOpeningElementName(child.openingElement)
7-
return ['button', 'summary', 'select', 'textarea', 'a', 'input', 'iconbutton'].includes(childName.toLowerCase())
7+
return ['button', 'summary', 'select', 'textarea', 'iconbutton', 'a', 'input'].includes(childName.toLowerCase())
88
}
99

10-
isInteractiveAnchor = child => {
10+
const isInteractiveAnchor = child => {
1111
const hasHref = getJSXOpeningElementAttribute(child.openingElement, 'href')
1212
if (!hasHref) return false
1313
const href = getJSXOpeningElementAttribute(child.openingElement, 'href').value.value
1414
const isAnchorInteractive = typeof href === 'string' && href !== ''
1515
return isAnchorInteractive
1616
}
1717

18-
isInteractiveInput = child => {
18+
const isInteractiveInput = child => {
1919
const hasHiddenType =
2020
getJSXOpeningElementAttribute(child.openingElement, 'type') &&
2121
getJSXOpeningElementAttribute(child.openingElement, 'type').value.value === 'hidden'
2222
return !hasHiddenType
2323
}
2424

25-
const checkTriggerElement = jsxNode => {
26-
let messageId = ''
27-
const child = jsxNode.children
28-
const childName = getJSXOpeningElementName(child.openingElement)
29-
30-
// First check specific requirements for anchor
31-
if (childName === 'a' && !isInteractiveAnchor(child)) {
32-
messageId = 'anchorTagWithoutHref'
33-
return {messageId, node: jsxNode}
25+
const getAllChildren = node => {
26+
if (Array.isArray(node.children)) {
27+
return node.children
28+
.filter(child => {
29+
return child.type === 'JSXElement'
30+
})
31+
.flatMap(child => {
32+
return [child, ...getAllChildren(child)]
33+
})
3434
}
35-
// Then check specific requirements input
36-
if (childName === 'input' && !isInteractiveInput(child)) {
37-
messageId = 'hiddenInput'
38-
return {messageId, node: jsxNode}
35+
return []
36+
}
37+
38+
const checks = [
39+
{
40+
id: 'anchorTagWithoutHref',
41+
filter: jsxElement => {
42+
return getJSXOpeningElementName(jsxElement.openingElement) === 'a'
43+
},
44+
check: isInteractiveAnchor
45+
},
46+
{
47+
id: 'hiddenInput',
48+
filter: jsxElement => {
49+
return getJSXOpeningElementName(jsxElement.openingElement) === 'input'
50+
},
51+
check: isInteractiveInput
52+
},
53+
{
54+
id: 'nonInteractiveTrigger',
55+
filter: jsxElement => {
56+
// filter elements that is not a or input
57+
return !(
58+
getJSXOpeningElementName(jsxElement.openingElement) === 'a' ||
59+
getJSXOpeningElementName(jsxElement.openingElement) === 'input'
60+
)
61+
},
62+
check: isInteractive
3963
}
40-
// Then check if the child is interactive
41-
if (!isInteractive(child)) {
42-
// If child is not interactive, check if there are any grandchildren that is interactive
43-
const hasJsxGrands =
44-
child.children.length > 0 && child.children.filter(gChild => gChild.type === 'JSXElement').length > 0 //.some(gChild => isInteractive(gChild))
64+
]
4565

46-
if (!hasJsxGrands) {
47-
messageId = 'nonInteractiveTrigger'
66+
const checkTriggerElement = jsxNode => {
67+
const elements = [...getAllChildren(jsxNode)]
68+
const hasInteractiveElement = elements.find(element => {
69+
if (getJSXOpeningElementName(element.openingElement) === 'a') {
70+
return isInteractiveAnchor(element)
71+
}
72+
if (getJSXOpeningElementName(element.openingElement) === 'input') {
73+
return isInteractiveInput(element)
4874
} else {
49-
const hasInteractiveGrands = child.children // is there any way I can access all child nodes? :/
50-
.filter(gChild => gChild.type === 'JSXElement')
51-
.some(gChild => {
52-
const gChildName = getJSXOpeningElementName(gChild.openingElement)
53-
// TODO: How can I check all child nodes?
54-
return checkTriggerElement(gChild).messageId === ''
55-
})
56-
if (!hasInteractiveGrands) messageId = 'nonInteractiveTrigger'
75+
return isInteractive(element)
5776
}
77+
})
5878

59-
return {messageId, node: jsxNode}
79+
// If the tooltip has interactive elements, return.
80+
if (hasInteractiveElement) return
81+
82+
const errors = new Set()
83+
84+
for (const element of elements) {
85+
for (const check of checks) {
86+
if (!check.filter(element)) {
87+
continue
88+
}
89+
90+
if (!check.check(element)) {
91+
errors.add(check.id)
92+
}
93+
}
94+
}
95+
// check the specificity of the errors. If there are multiple errors, only return the most specific one.
96+
if (errors.size > 1) {
97+
if (errors.has('anchorTagWithoutHref')) {
98+
errors.delete('nonInteractiveTrigger')
99+
}
100+
if (errors.has('hiddenInput')) {
101+
errors.delete('nonInteractiveTrigger')
102+
}
60103
}
61104

62-
// All good the element is interactive
63-
return {messageId, node: jsxNode}
105+
return errors
64106
}
65107

66108
module.exports = {
@@ -95,28 +137,30 @@ module.exports = {
95137
const {options} = context
96138
return {
97139
JSXElement(jsxNode) {
98-
// If `skipImportCheck` is true, this rule will check for direct slot children
99-
// in any components (not just ones that are imported from `@primer/react`).
140+
// If `skipImportCheck` is true, this rule will check for non-interactive element in any components (not just ones that are imported from `@primer/react`).
100141
const skipImportCheck = context.options[0] ? context.options[0].skipImportCheck : false
101-
102142
const name = getJSXOpeningElementName(jsxNode.openingElement)
103-
104-
if (name === 'Tooltip' && jsxNode.children) {
105-
// Check if there is a single child
143+
if (
144+
(skipImportCheck || isPrimerComponent(jsxNode.openingElement.name, context.getScope(jsxNode))) &&
145+
name === 'Tooltip' &&
146+
jsxNode.children
147+
) {
106148
if (jsxNode.children.length > 1) {
107149
context.report({
108150
node: jsxNode,
109151
messageId: 'singleChild'
110152
})
111153
} else {
112154
// Check if the child is interactive
113-
const {node, messageId} = checkTriggerElement(jsxNode)
155+
const errors = checkTriggerElement(jsxNode)
114156

115-
if (messageId !== '') {
116-
context.report({
117-
node,
118-
messageId
119-
})
157+
if (errors) {
158+
for (const [key, value] of errors.entries()) {
159+
context.report({
160+
node: jsxNode,
161+
messageId: value
162+
})
163+
}
120164
}
121165
}
122166
}

0 commit comments

Comments
 (0)