@@ -2,65 +2,107 @@ const {isPrimerComponent} = require('../utils/is-primer-component')
2
2
const { getJSXOpeningElementName} = require ( '../utils/get-jsx-opening-element-name' )
3
3
const { getJSXOpeningElementAttribute} = require ( '../utils/get-jsx-opening-element-attribute' )
4
4
5
- isInteractive = child => {
5
+ const isInteractive = child => {
6
6
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 ( ) )
8
8
}
9
9
10
- isInteractiveAnchor = child => {
10
+ const isInteractiveAnchor = child => {
11
11
const hasHref = getJSXOpeningElementAttribute ( child . openingElement , 'href' )
12
12
if ( ! hasHref ) return false
13
13
const href = getJSXOpeningElementAttribute ( child . openingElement , 'href' ) . value . value
14
14
const isAnchorInteractive = typeof href === 'string' && href !== ''
15
15
return isAnchorInteractive
16
16
}
17
17
18
- isInteractiveInput = child => {
18
+ const isInteractiveInput = child => {
19
19
const hasHiddenType =
20
20
getJSXOpeningElementAttribute ( child . openingElement , 'type' ) &&
21
21
getJSXOpeningElementAttribute ( child . openingElement , 'type' ) . value . value === 'hidden'
22
22
return ! hasHiddenType
23
23
}
24
24
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
+ } )
34
34
}
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
39
63
}
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
+ ]
45
65
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 )
48
74
} 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 )
57
76
}
77
+ } )
58
78
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
+ }
60
103
}
61
104
62
- // All good the element is interactive
63
- return { messageId, node : jsxNode }
105
+ return errors
64
106
}
65
107
66
108
module . exports = {
@@ -95,28 +137,30 @@ module.exports = {
95
137
const { options} = context
96
138
return {
97
139
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`).
100
141
const skipImportCheck = context . options [ 0 ] ? context . options [ 0 ] . skipImportCheck : false
101
-
102
142
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
+ ) {
106
148
if ( jsxNode . children . length > 1 ) {
107
149
context . report ( {
108
150
node : jsxNode ,
109
151
messageId : 'singleChild'
110
152
} )
111
153
} else {
112
154
// Check if the child is interactive
113
- const { node , messageId } = checkTriggerElement ( jsxNode )
155
+ const errors = checkTriggerElement ( jsxNode )
114
156
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
+ }
120
164
}
121
165
}
122
166
}
0 commit comments