|
| 1 | +const {isPrimerComponent} = require('../utils/is-primer-component') |
| 2 | +const {getJSXOpeningElementName} = require('../utils/get-jsx-opening-element-name') |
| 3 | +const {getJSXOpeningElementAttribute} = require('../utils/get-attribute') |
| 4 | + |
| 5 | +isInteractive = child => { |
| 6 | + const childName = getJSXOpeningElementName(child.openingElement) |
| 7 | + return ['button', 'summary', 'select', 'textarea', 'a', 'input', 'iconbutton'].includes(childName.toLowerCase()) |
| 8 | +} |
| 9 | + |
| 10 | +isInteractiveAnchor = child => { |
| 11 | + const hasHref = getJSXOpeningElementAttribute(child.openingElement, 'href') |
| 12 | + if (!hasHref) return false |
| 13 | + const href = getJSXOpeningElementAttribute(child.openingElement, 'href').value.value |
| 14 | + const isAnchorInteractive = typeof href === 'string' && href !== '' |
| 15 | + return isAnchorInteractive |
| 16 | +} |
| 17 | + |
| 18 | +isInteractiveInput = child => { |
| 19 | + const hasHiddenType = |
| 20 | + getJSXOpeningElementAttribute(child.openingElement, 'type') && |
| 21 | + getJSXOpeningElementAttribute(child.openingElement, 'type').value.value === 'hidden' |
| 22 | + return !hasHiddenType |
| 23 | +} |
| 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} |
| 34 | + } |
| 35 | + // Then check specific requirements input |
| 36 | + if (childName === 'input' && !isInteractiveInput(child)) { |
| 37 | + messageId = 'hiddenInput' |
| 38 | + return {messageId, node: jsxNode} |
| 39 | + } |
| 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)) |
| 45 | + |
| 46 | + if (!hasJsxGrands) { |
| 47 | + messageId = 'nonInteractiveTrigger' |
| 48 | + } 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' |
| 57 | + } |
| 58 | + |
| 59 | + return {messageId, node: jsxNode} |
| 60 | + } |
| 61 | + |
| 62 | + // All good the element is interactive |
| 63 | + return {messageId, node: jsxNode} |
| 64 | +} |
| 65 | + |
| 66 | +module.exports = { |
| 67 | + meta: { |
| 68 | + type: 'problem', |
| 69 | + docs: { |
| 70 | + url: |
| 71 | + 'https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/no-noninteractive-element-interactions.md', |
| 72 | + description: 'Non-interactive elements should not be assigned mouse or keyboard event listeners.' |
| 73 | + }, |
| 74 | + schema: [ |
| 75 | + { |
| 76 | + properties: { |
| 77 | + skipImportCheck: { |
| 78 | + type: 'boolean' |
| 79 | + } |
| 80 | + } |
| 81 | + } |
| 82 | + ], |
| 83 | + messages: { |
| 84 | + nonInteractiveTrigger: |
| 85 | + 'The `Tooltip` component expects a single React element that contains interactive content. Consider using a `<button>` or equivalent interactive element instead.', |
| 86 | + anchorTagWithoutHref: |
| 87 | + 'Anchor tags without an href attribute are not be considered interactive, therefore can not be used as a trigger for a tooltip.', |
| 88 | + hiddenInput: |
| 89 | + 'Hidden inputs are not be considered interactive, therefore can not be used as a trigger for a tooltip.', |
| 90 | + singleChild: 'The `Tooltip` component expects a single React element as a child.' |
| 91 | + } |
| 92 | + }, |
| 93 | + create(context) { |
| 94 | + const stack = [] |
| 95 | + const {options} = context |
| 96 | + return { |
| 97 | + 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`). |
| 100 | + const skipImportCheck = context.options[0] ? context.options[0].skipImportCheck : false |
| 101 | + |
| 102 | + const name = getJSXOpeningElementName(jsxNode.openingElement) |
| 103 | + |
| 104 | + if (name === 'Tooltip' && jsxNode.children) { |
| 105 | + // Check if there is a single child |
| 106 | + if (jsxNode.children.length > 1) { |
| 107 | + context.report({ |
| 108 | + node: jsxNode, |
| 109 | + messageId: 'singleChild' |
| 110 | + }) |
| 111 | + } else { |
| 112 | + // Check if the child is interactive |
| 113 | + const {node, messageId} = checkTriggerElement(jsxNode) |
| 114 | + |
| 115 | + if (messageId !== '') { |
| 116 | + context.report({ |
| 117 | + node, |
| 118 | + messageId |
| 119 | + }) |
| 120 | + } |
| 121 | + } |
| 122 | + } |
| 123 | + } |
| 124 | + } |
| 125 | + } |
| 126 | +} |
0 commit comments