@elastic/eslint-plugin-eui 3.0.0 → 3.1.0
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/README.md +64 -1
- package/lib/cjs/index.js +3 -0
- package/lib/cjs/rules/a11y/no_nested_interactive_element.d.ts +3 -0
- package/lib/cjs/rules/a11y/no_nested_interactive_element.d.ts.map +1 -0
- package/lib/cjs/rules/a11y/no_nested_interactive_element.js +202 -0
- package/lib/cjs/rules/a11y/tooltip_no_interactive_content.d.ts.map +1 -1
- package/lib/cjs/rules/a11y/tooltip_no_interactive_content.js +32 -13
- package/lib/cjs/utils/constants.d.ts +42 -5
- package/lib/cjs/utils/constants.d.ts.map +1 -1
- package/lib/cjs/utils/constants.js +60 -7
- package/lib/cjs/utils/is_interactive_element.d.ts +23 -0
- package/lib/cjs/utils/is_interactive_element.d.ts.map +1 -0
- package/lib/cjs/utils/is_interactive_element.js +71 -0
- package/lib/cjs/utils/walk_jsx_children.d.ts +5 -0
- package/lib/cjs/utils/walk_jsx_children.d.ts.map +1 -1
- package/lib/cjs/utils/walk_jsx_children.js +8 -4
- package/lib/esm/index.js +3 -0
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/rules/a11y/no_nested_interactive_element.d.ts +2 -0
- package/lib/esm/rules/a11y/no_nested_interactive_element.js +225 -0
- package/lib/esm/rules/a11y/no_nested_interactive_element.js.map +1 -0
- package/lib/esm/rules/a11y/tooltip_no_interactive_content.js +33 -22
- package/lib/esm/rules/a11y/tooltip_no_interactive_content.js.map +1 -1
- package/lib/esm/utils/constants.d.ts +42 -5
- package/lib/esm/utils/constants.js +75 -10
- package/lib/esm/utils/constants.js.map +1 -1
- package/lib/esm/utils/is_interactive_element.d.ts +22 -0
- package/lib/esm/utils/is_interactive_element.js +71 -0
- package/lib/esm/utils/is_interactive_element.js.map +1 -0
- package/lib/esm/utils/walk_jsx_children.d.ts +5 -0
- package/lib/esm/utils/walk_jsx_children.js +8 -4
- package/lib/esm/utils/walk_jsx_children.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.hasAnyProp = hasAnyProp;
|
|
7
|
+
exports.hasInteractivityProp = hasInteractivityProp;
|
|
8
|
+
exports.isInteractiveElement = isInteractiveElement;
|
|
9
|
+
var _constants = require("./constants");
|
|
10
|
+
var _get_element_name = require("./get_element_name");
|
|
11
|
+
var _has_meaningful_attr = require("./has_meaningful_attr");
|
|
12
|
+
/*
|
|
13
|
+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
14
|
+
* or more contributor license agreements. Licensed under the Elastic License
|
|
15
|
+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
|
16
|
+
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
|
17
|
+
* Side Public License, v 1.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Elements focusable only when given one of the listed props. */
|
|
21
|
+
const CONDITIONALLY_INTERACTIVE = {
|
|
22
|
+
..._constants.CONDITIONALLY_INTERACTIVE_HTML_ELEMENTS,
|
|
23
|
+
..._constants.CONDITIONALLY_INTERACTIVE_EUI_COMPONENTS
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Elements that are focusable regardless of the props they receive. */
|
|
27
|
+
const ALWAYS_INTERACTIVE = new Set([..._constants.INTERACTIVE_HTML_ELEMENTS, ..._constants.INTERACTIVE_EUI_COMPONENTS].filter(name => !CONDITIONALLY_INTERACTIVE[name]));
|
|
28
|
+
|
|
29
|
+
/** Props that make an element respond to user input. */
|
|
30
|
+
const INTERACTIVITY_PROPS = ['onClick', 'href'];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Whether any of `propNames` is present on the element with a statically
|
|
34
|
+
* non-empty value. Dynamic values (`prop={maybe}`) count as present, since
|
|
35
|
+
* their runtime value is unknown.
|
|
36
|
+
*/
|
|
37
|
+
function hasAnyProp(openingElement, propNames, presenceOnlyProps = new Set()) {
|
|
38
|
+
return propNames.some(prop => {
|
|
39
|
+
const attr = openingElement.attributes.find(attr => attr.type === 'JSXAttribute' && attr.name.type === 'JSXIdentifier' && attr.name.name === prop);
|
|
40
|
+
if (!attr) return false;
|
|
41
|
+
return (0, _has_meaningful_attr.hasMeaningfulAttr)(openingElement, prop) || attr.value?.type === 'Literal' && attr.value.value === '' || attr.value?.type === 'JSXExpressionContainer' && attr.value.expression.type === 'Literal' && attr.value.expression.value === '';
|
|
42
|
+
return (0, _has_meaningful_attr.hasMeaningfulAttr)(openingElement, prop);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Whether an element has a prop that makes it respond to user input
|
|
48
|
+
* (`onClick` or `href`).
|
|
49
|
+
*/
|
|
50
|
+
function hasInteractivityProp(openingElement) {
|
|
51
|
+
return hasAnyProp(openingElement, INTERACTIVITY_PROPS);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Whether a JSX element renders a focusable control.
|
|
56
|
+
*
|
|
57
|
+
* Native interactive HTML elements and unconditionally interactive EUI
|
|
58
|
+
* components always qualify. Everything in
|
|
59
|
+
* `CONDITIONALLY_INTERACTIVE_EUI_COMPONENTS` and
|
|
60
|
+
* `CONDITIONALLY_INTERACTIVE_HTML_ELEMENTS` — `EuiBadge`, `EuiListGroupItem`,
|
|
61
|
+
* a bare `<a>`, … — qualifies only when given a prop that makes it focusable.
|
|
62
|
+
*/
|
|
63
|
+
function isInteractiveElement(element) {
|
|
64
|
+
const name = (0, _get_element_name.getElementName)(element.openingElement);
|
|
65
|
+
if (!name) return false;
|
|
66
|
+
const conditionalProps = CONDITIONALLY_INTERACTIVE[name];
|
|
67
|
+
if (conditionalProps) {
|
|
68
|
+
return hasAnyProp(element.openingElement, conditionalProps, new Set(_constants.PRESENCE_INTERACTIVE_PROPS[name] ?? []));
|
|
69
|
+
}
|
|
70
|
+
return ALWAYS_INTERACTIVE.has(name);
|
|
71
|
+
}
|
|
@@ -20,11 +20,16 @@ import { type TSESTree, type TSESLint } from '@typescript-eslint/utils';
|
|
|
20
20
|
* element itself and recurses into its children instead. Use this to make
|
|
21
21
|
* layout wrappers or non-interactive shells transparent to the visitor.
|
|
22
22
|
*
|
|
23
|
+
* `options.getChildren` - when provided, returns the rendered content roots to
|
|
24
|
+
* recurse into for JSX elements skipped by `shouldSkip`. Defaults to
|
|
25
|
+
* `node.children`.
|
|
26
|
+
*
|
|
23
27
|
* `visit` is called for leaf nodes: JSXElement, JSXText, Literal,
|
|
24
28
|
* TemplateLiteral, MemberExpression.
|
|
25
29
|
*/
|
|
26
30
|
export declare function walkJsxChildren(node: TSESTree.Node, visit: (node: TSESTree.Node) => void, options?: {
|
|
27
31
|
sourceCode?: TSESLint.SourceCode;
|
|
28
32
|
shouldSkip?: (el: TSESTree.JSXElement) => boolean;
|
|
33
|
+
getChildren?: (el: TSESTree.JSXElement) => TSESTree.Node[] | undefined;
|
|
29
34
|
}): void;
|
|
30
35
|
//# sourceMappingURL=walk_jsx_children.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"walk_jsx_children.d.ts","sourceRoot":"","sources":["../../../src/utils/walk_jsx_children.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,QAAQ,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"walk_jsx_children.d.ts","sourceRoot":"","sources":["../../../src/utils/walk_jsx_children.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAsMxE;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,QAAQ,CAAC,IAAI,EACnB,KAAK,EAAE,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,KAAK,IAAI,EACpC,OAAO,GAAE;IACP,UAAU,CAAC,EAAE,QAAQ,CAAC,UAAU,CAAC;IACjC,UAAU,CAAC,EAAE,CAAC,EAAE,EAAE,QAAQ,CAAC,UAAU,KAAK,OAAO,CAAC;IAClD,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,QAAQ,CAAC,UAAU,KAAK,QAAQ,CAAC,IAAI,EAAE,GAAG,SAAS,CAAC;CACnE,GACL,IAAI,CASN"}
|
|
@@ -89,8 +89,8 @@ function resolveLocalComponent(sourceCode, name, refNode) {
|
|
|
89
89
|
}
|
|
90
90
|
return null;
|
|
91
91
|
}
|
|
92
|
-
function walkJsxChild(node, visit, sourceCode, visited, shouldSkip) {
|
|
93
|
-
const walk = n => walkJsxChild(n, visit, sourceCode, visited, shouldSkip);
|
|
92
|
+
function walkJsxChild(node, visit, sourceCode, visited, shouldSkip, getChildren) {
|
|
93
|
+
const walk = n => walkJsxChild(n, visit, sourceCode, visited, shouldSkip, getChildren);
|
|
94
94
|
switch (node.type) {
|
|
95
95
|
case 'JSXElement':
|
|
96
96
|
{
|
|
@@ -113,7 +113,7 @@ function walkJsxChild(node, visit, sourceCode, visited, shouldSkip) {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
if (shouldSkip?.(node)) {
|
|
116
|
-
node.children.forEach(walk);
|
|
116
|
+
(getChildren?.(node) ?? node.children).forEach(walk);
|
|
117
117
|
return;
|
|
118
118
|
}
|
|
119
119
|
visit(node);
|
|
@@ -202,9 +202,13 @@ function walkJsxChild(node, visit, sourceCode, visited, shouldSkip) {
|
|
|
202
202
|
* element itself and recurses into its children instead. Use this to make
|
|
203
203
|
* layout wrappers or non-interactive shells transparent to the visitor.
|
|
204
204
|
*
|
|
205
|
+
* `options.getChildren` - when provided, returns the rendered content roots to
|
|
206
|
+
* recurse into for JSX elements skipped by `shouldSkip`. Defaults to
|
|
207
|
+
* `node.children`.
|
|
208
|
+
*
|
|
205
209
|
* `visit` is called for leaf nodes: JSXElement, JSXText, Literal,
|
|
206
210
|
* TemplateLiteral, MemberExpression.
|
|
207
211
|
*/
|
|
208
212
|
function walkJsxChildren(node, visit, options = {}) {
|
|
209
|
-
walkJsxChild(node, visit, options.sourceCode, new Set(), options.shouldSkip);
|
|
213
|
+
walkJsxChild(node, visit, options.sourceCode, new Set(), options.shouldSkip, options.getChildren);
|
|
210
214
|
}
|
package/lib/esm/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const href_or_on_click_1 = require("./rules/href_or_on_click");
|
|
|
15
15
|
const require_href_for_link_1 = require("./rules/require_href_for_link");
|
|
16
16
|
const no_css_color_1 = require("./rules/no_css_color");
|
|
17
17
|
const no_nested_copy_tooltip_1 = require("./rules/a11y/no_nested_copy_tooltip");
|
|
18
|
+
const no_nested_interactive_element_1 = require("./rules/a11y/no_nested_interactive_element");
|
|
18
19
|
const no_restricted_eui_imports_1 = require("./rules/no_restricted_eui_imports");
|
|
19
20
|
const no_static_z_index_1 = require("./rules/no_static_z_index");
|
|
20
21
|
const no_unnamed_interactive_element_1 = require("./rules/a11y/no_unnamed_interactive_element");
|
|
@@ -42,6 +43,7 @@ const config = {
|
|
|
42
43
|
'no-css-color': no_css_color_1.NoCssColor,
|
|
43
44
|
'no-deprecated-icon-aliases': no_deprecated_icon_aliases_1.NoDeprecatedIconAliases,
|
|
44
45
|
'no-nested-copy-tooltip': no_nested_copy_tooltip_1.NoNestedCopyTooltip,
|
|
46
|
+
'no-nested-interactive-element': no_nested_interactive_element_1.NoNestedInteractiveElement,
|
|
45
47
|
'no-restricted-eui-imports': no_restricted_eui_imports_1.NoRestrictedEuiImports,
|
|
46
48
|
'no-static-z-index': no_static_z_index_1.NoStaticZIndex,
|
|
47
49
|
'no-unnamed-interactive-element': no_unnamed_interactive_element_1.NoUnnamedInteractiveElement,
|
|
@@ -74,6 +76,7 @@ const config = {
|
|
|
74
76
|
'@elastic/eui/no-css-color': 'warn',
|
|
75
77
|
'@elastic/eui/no-deprecated-icon-aliases': 'warn',
|
|
76
78
|
'@elastic/eui/no-nested-copy-tooltip': 'warn',
|
|
79
|
+
'@elastic/eui/no-nested-interactive-element': 'warn',
|
|
77
80
|
'@elastic/eui/no-restricted-eui-imports': 'warn',
|
|
78
81
|
'@elastic/eui/no-static-z-index': 'warn',
|
|
79
82
|
'@elastic/eui/no-unnamed-interactive-element': 'warn',
|
package/lib/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;AAEH,gGAA4F;AAC5F,sFAAgF;AAChF,+FAAwF;AACxF,0FAAoF;AACpF,+DAAuD;AACvD,yEAAmE;AACnE,uDAAkD;AAClD,gFAA0E;AAC1E,iFAA2E;AAC3E,iEAA2D;AAC3D,gGAA0F;AAC1F,gFAA0E;AAC1E,0EAAoE;AACpE,8FAAuF;AACvF,8EAAyE;AACzE,wFAA4F;AAC5F,oFAA+E;AAC/E,iHAAyG;AACzG,sFAAoF;AACpF,oFAAkF;AAClF,gGAA0F;AAC1F,oFAA8E;AAC9E,mFAA6E;AAC7E,+FAAwF;AACxF,iGAA0F;AAE1F,MAAM,MAAM,GAAG;IACb,KAAK,EAAE;QACL,gCAAgC,EAAE,8DAA6B;QAC/D,2BAA2B,EAAE,kDAAsB;QACnD,kCAAkC,EAAE,+DAA4B;QAChE,6BAA6B,EAAE,sDAAwB;QACvD,kBAAkB,EAAE,8BAAW;QAC/B,cAAc,EAAE,yBAAU;QAC1B,4BAA4B,EAAE,oDAAuB;QACrD,wBAAwB,EAAE,4CAAmB;QAC7C,2BAA2B,EAAE,kDAAsB;QACnD,mBAAmB,EAAE,kCAAc;QACnC,gCAAgC,EAAE,4DAA2B;QAC7D,wBAAwB,EAAE,4CAAmB;QAC7C,qBAAqB,EAAE,sCAAgB;QACvC,+BAA+B,EAAE,yDAAyB;QAC1D,uBAAuB,EAAE,2CAAmB;QAC5C,4BAA4B,EAAE,8DAAiC;QAC/D,0BAA0B,EAAE,iDAAsB;QAClD,2CAA2C,EACzC,gFAAoC;QACtC,2BAA2B,EAAE,sDAA0B;QACvD,0BAA0B,EAAE,oDAAyB;QACrD,uBAAuB,EAAE,0CAAkB;QAC3C,gCAAgC,EAAE,4DAA2B;QAC7D,0BAA0B,EAAE,gDAAqB;QACjD,kCAAkC,EAAE,+DAA4B;QAChE,mCAAmC,EAAE,iEAA6B;KACnE;IACD,OAAO,EAAE;QACP,WAAW,EAAE;YACX,OAAO,EAAE,CAAC,4BAA4B,CAAC;YACvC,KAAK,EAAE;gBACL,6CAA6C,EAAE,MAAM;gBACrD,+CAA+C,EAAE,OAAO;gBACxD,gDAAgD,EAAE,OAAO;gBACzD,wCAAwC,EAAE,MAAM;gBAChD,+CAA+C,EAAE,MAAM;gBACvD,0CAA0C,EAAE,MAAM;gBAClD,+BAA+B,EAAE,MAAM;gBACvC,2BAA2B,EAAE,MAAM;gBACnC,yCAAyC,EAAE,MAAM;gBACjD,qCAAqC,EAAE,MAAM;gBAC7C,wCAAwC,EAAE,MAAM;gBAChD,gCAAgC,EAAE,MAAM;gBACxC,6CAA6C,EAAE,MAAM;gBACrD,qCAAqC,EAAE,MAAM;gBAC7C,kCAAkC,EAAE,MAAM;gBAC1C,4CAA4C,EAAE,MAAM;gBACpD,oCAAoC,EAAE,MAAM;gBAC5C,yCAAyC,EAAE,MAAM;gBACjD,uCAAuC,EAAE,MAAM;gBAC/C,wDAAwD,EAAE,MAAM;gBAChE,wCAAwC,EAAE,MAAM;gBAChD,uCAAuC,EAAE,MAAM;gBAC/C,oCAAoC,EAAE,MAAM;gBAC5C,6CAA6C,EAAE,MAAM;gBACrD,uCAAuC,EAAE,MAAM;aAChD;SACF;KACF;CACF,CAAC;AAEF,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;AAEH,gGAA4F;AAC5F,sFAAgF;AAChF,+FAAwF;AACxF,0FAAoF;AACpF,+DAAuD;AACvD,yEAAmE;AACnE,uDAAkD;AAClD,gFAA0E;AAC1E,8FAAwF;AACxF,iFAA2E;AAC3E,iEAA2D;AAC3D,gGAA0F;AAC1F,gFAA0E;AAC1E,0EAAoE;AACpE,8FAAuF;AACvF,8EAAyE;AACzE,wFAA4F;AAC5F,oFAA+E;AAC/E,iHAAyG;AACzG,sFAAoF;AACpF,oFAAkF;AAClF,gGAA0F;AAC1F,oFAA8E;AAC9E,mFAA6E;AAC7E,+FAAwF;AACxF,iGAA0F;AAE1F,MAAM,MAAM,GAAG;IACb,KAAK,EAAE;QACL,gCAAgC,EAAE,8DAA6B;QAC/D,2BAA2B,EAAE,kDAAsB;QACnD,kCAAkC,EAAE,+DAA4B;QAChE,6BAA6B,EAAE,sDAAwB;QACvD,kBAAkB,EAAE,8BAAW;QAC/B,cAAc,EAAE,yBAAU;QAC1B,4BAA4B,EAAE,oDAAuB;QACrD,wBAAwB,EAAE,4CAAmB;QAC7C,+BAA+B,EAAE,0DAA0B;QAC3D,2BAA2B,EAAE,kDAAsB;QACnD,mBAAmB,EAAE,kCAAc;QACnC,gCAAgC,EAAE,4DAA2B;QAC7D,wBAAwB,EAAE,4CAAmB;QAC7C,qBAAqB,EAAE,sCAAgB;QACvC,+BAA+B,EAAE,yDAAyB;QAC1D,uBAAuB,EAAE,2CAAmB;QAC5C,4BAA4B,EAAE,8DAAiC;QAC/D,0BAA0B,EAAE,iDAAsB;QAClD,2CAA2C,EACzC,gFAAoC;QACtC,2BAA2B,EAAE,sDAA0B;QACvD,0BAA0B,EAAE,oDAAyB;QACrD,uBAAuB,EAAE,0CAAkB;QAC3C,gCAAgC,EAAE,4DAA2B;QAC7D,0BAA0B,EAAE,gDAAqB;QACjD,kCAAkC,EAAE,+DAA4B;QAChE,mCAAmC,EAAE,iEAA6B;KACnE;IACD,OAAO,EAAE;QACP,WAAW,EAAE;YACX,OAAO,EAAE,CAAC,4BAA4B,CAAC;YACvC,KAAK,EAAE;gBACL,6CAA6C,EAAE,MAAM;gBACrD,+CAA+C,EAAE,OAAO;gBACxD,gDAAgD,EAAE,OAAO;gBACzD,wCAAwC,EAAE,MAAM;gBAChD,+CAA+C,EAAE,MAAM;gBACvD,0CAA0C,EAAE,MAAM;gBAClD,+BAA+B,EAAE,MAAM;gBACvC,2BAA2B,EAAE,MAAM;gBACnC,yCAAyC,EAAE,MAAM;gBACjD,qCAAqC,EAAE,MAAM;gBAC7C,4CAA4C,EAAE,MAAM;gBACpD,wCAAwC,EAAE,MAAM;gBAChD,gCAAgC,EAAE,MAAM;gBACxC,6CAA6C,EAAE,MAAM;gBACrD,qCAAqC,EAAE,MAAM;gBAC7C,kCAAkC,EAAE,MAAM;gBAC1C,4CAA4C,EAAE,MAAM;gBACpD,oCAAoC,EAAE,MAAM;gBAC5C,yCAAyC,EAAE,MAAM;gBACjD,uCAAuC,EAAE,MAAM;gBAC/C,wDAAwD,EAAE,MAAM;gBAChE,wCAAwC,EAAE,MAAM;gBAChD,uCAAuC,EAAE,MAAM;gBAC/C,oCAAoC,EAAE,MAAM;gBAC5C,6CAA6C,EAAE,MAAM;gBACrD,uCAAuC,EAAE,MAAM;aAChD;SACF;KACF;CACF,CAAC;AAEF,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC"}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
4
|
+
* or more contributor license agreements. Licensed under the Elastic License
|
|
5
|
+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
|
6
|
+
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
|
7
|
+
* Side Public License, v 1.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.NoNestedInteractiveElement = void 0;
|
|
11
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
12
|
+
const constants_1 = require("../../utils/constants");
|
|
13
|
+
const get_element_name_1 = require("../../utils/get_element_name");
|
|
14
|
+
const has_meaningful_attr_1 = require("../../utils/has_meaningful_attr");
|
|
15
|
+
const is_interactive_element_1 = require("../../utils/is_interactive_element");
|
|
16
|
+
const walk_jsx_children_1 = require("../../utils/walk_jsx_children");
|
|
17
|
+
const CARD = 'EuiCard';
|
|
18
|
+
const LIST_GROUP_ITEM = 'EuiListGroupItem';
|
|
19
|
+
const LIST_GROUP_ITEM_EXTRA_ACTION = 'EuiButtonIcon';
|
|
20
|
+
/**
|
|
21
|
+
* Leaf-control props whose value renders *inside* the focusable element, in
|
|
22
|
+
* addition to children. Props that render a **sibling** control (e.g.
|
|
23
|
+
* `EuiListGroupItem` `extraAction`) are intentionally absent, as are props
|
|
24
|
+
* rendered into a portal (e.g. `EuiContextMenuItem` `toolTipContent`).
|
|
25
|
+
*/
|
|
26
|
+
const LEAF_CONTENT_PROPS = {
|
|
27
|
+
EuiContextMenuItem: ['icon'],
|
|
28
|
+
EuiFacetButton: ['icon'],
|
|
29
|
+
EuiHeaderSectionItemButton: ['notification'],
|
|
30
|
+
EuiKeyPadMenuItem: ['label'],
|
|
31
|
+
EuiListGroupItem: ['icon', 'label'],
|
|
32
|
+
EuiStepHorizontal: ['title'],
|
|
33
|
+
EuiTab: ['append', 'prepend'],
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* `EuiCard` props rendered inside the card wrapper, which is what carries the
|
|
37
|
+
* forwarding click handler. `betaBadgeProps` can also produce a control, but it
|
|
38
|
+
* is configured through an object rather than JSX and is not analysed here.
|
|
39
|
+
*/
|
|
40
|
+
const CARD_CONTENT_PROPS = ['description', 'footer', 'image', 'title'];
|
|
41
|
+
const LEAF_COMPONENTS = new Set(constants_1.LEAF_INTERACTIVE_EUI_COMPONENTS);
|
|
42
|
+
const LEAF_HTML_ELEMENTS = new Set(['a', 'button']);
|
|
43
|
+
const COMPONENTS_WITH_CHILDREN_PROP = new Set([
|
|
44
|
+
'a',
|
|
45
|
+
CARD,
|
|
46
|
+
'button',
|
|
47
|
+
'EuiBadge',
|
|
48
|
+
'EuiBetaBadge',
|
|
49
|
+
'EuiButton',
|
|
50
|
+
'EuiButtonEmpty',
|
|
51
|
+
'EuiContextMenuItem',
|
|
52
|
+
'EuiFacetButton',
|
|
53
|
+
'EuiFilterButton',
|
|
54
|
+
'EuiHeaderLink',
|
|
55
|
+
'EuiHeaderSectionItemButton',
|
|
56
|
+
'EuiKeyPadMenuItem',
|
|
57
|
+
'EuiLink',
|
|
58
|
+
'EuiTab',
|
|
59
|
+
]);
|
|
60
|
+
/**
|
|
61
|
+
* Whether a boolean prop is set to a statically-known `true` (`<El prop />` or
|
|
62
|
+
* `<El prop={true} />`). Dynamic values are treated as *not* set, so that a
|
|
63
|
+
* card gated on e.g. `isDisabled={isLoading}` is still checked.
|
|
64
|
+
*/
|
|
65
|
+
function isStaticallyTrue(openingElement, propName) {
|
|
66
|
+
const attr = openingElement.attributes.find((a) => a.type === 'JSXAttribute' &&
|
|
67
|
+
a.name.type === 'JSXIdentifier' &&
|
|
68
|
+
a.name.name === propName);
|
|
69
|
+
if (!attr)
|
|
70
|
+
return false;
|
|
71
|
+
if (attr.value == null)
|
|
72
|
+
return true;
|
|
73
|
+
return (attr.value.type === 'JSXExpressionContainer' &&
|
|
74
|
+
attr.value.expression.type === 'Literal' &&
|
|
75
|
+
attr.value.expression.value === true);
|
|
76
|
+
}
|
|
77
|
+
function getTarget(componentName, node) {
|
|
78
|
+
const { openingElement } = node;
|
|
79
|
+
if (componentName === CARD) {
|
|
80
|
+
// A card with `onClick`/`href` attaches a wrapper click handler that
|
|
81
|
+
// forwards to the title link, so a control anywhere in its content fires
|
|
82
|
+
// both its own action and the card's. A disabled card attaches no handler.
|
|
83
|
+
//
|
|
84
|
+
// `selectable` cards are deliberately not a target: pairing the select
|
|
85
|
+
// button with a footer action is a pattern EUI itself ships. It is only
|
|
86
|
+
// safe when the footer control stops propagation, which this rule cannot
|
|
87
|
+
// verify statically — see the PR discussion.
|
|
88
|
+
if ((0, has_meaningful_attr_1.hasMeaningfulAttr)(openingElement, 'selectable')) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
return (0, is_interactive_element_1.hasInteractivityProp)(openingElement) &&
|
|
92
|
+
!isStaticallyTrue(openingElement, 'isDisabled')
|
|
93
|
+
? {
|
|
94
|
+
messageId: 'clickableCardContent',
|
|
95
|
+
contentProps: getComponentContentProps(componentName),
|
|
96
|
+
}
|
|
97
|
+
: null;
|
|
98
|
+
}
|
|
99
|
+
if (LEAF_HTML_ELEMENTS.has(componentName) && (0, is_interactive_element_1.isInteractiveElement)(node)) {
|
|
100
|
+
return {
|
|
101
|
+
messageId: 'nestedInteractive',
|
|
102
|
+
contentProps: [],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// Several leaf components only render a control when given the right props
|
|
106
|
+
// (`EuiListGroupItem` is an `<li>` without an action, `EuiContextMenuItem` a
|
|
107
|
+
// `<div>`), so the name alone is not enough to make it a target.
|
|
108
|
+
if (LEAF_COMPONENTS.has(componentName) && (0, is_interactive_element_1.isInteractiveElement)(node)) {
|
|
109
|
+
return {
|
|
110
|
+
messageId: 'nestedInteractive',
|
|
111
|
+
contentProps: getComponentContentProps(componentName),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
/** The element's children plus the expression value of each content prop. */
|
|
117
|
+
function getContentRoots(node, componentName, contentProps) {
|
|
118
|
+
const roots = [...node.children];
|
|
119
|
+
for (const attr of node.openingElement.attributes) {
|
|
120
|
+
if (attr.type !== 'JSXAttribute' || attr.name.type !== 'JSXIdentifier') {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const isChildrenProp = attr.name.name === 'children' &&
|
|
124
|
+
componentName != null &&
|
|
125
|
+
COMPONENTS_WITH_CHILDREN_PROP.has(componentName);
|
|
126
|
+
if (!isChildrenProp && !contentProps.includes(attr.name.name))
|
|
127
|
+
continue;
|
|
128
|
+
// Only `prop={<JSX />}` can hold an element; a string literal cannot.
|
|
129
|
+
if (attr.value?.type !== 'JSXExpressionContainer')
|
|
130
|
+
continue;
|
|
131
|
+
if (attr.value.expression.type === 'JSXEmptyExpression')
|
|
132
|
+
continue;
|
|
133
|
+
roots.push(attr.value.expression);
|
|
134
|
+
}
|
|
135
|
+
return roots;
|
|
136
|
+
}
|
|
137
|
+
function getComponentContentProps(componentName) {
|
|
138
|
+
if (componentName === CARD) {
|
|
139
|
+
return CARD_CONTENT_PROPS;
|
|
140
|
+
}
|
|
141
|
+
return LEAF_CONTENT_PROPS[componentName ?? ''] ?? [];
|
|
142
|
+
}
|
|
143
|
+
function getSyntheticInteractiveElementName(element) {
|
|
144
|
+
const componentName = (0, get_element_name_1.getElementName)(element.openingElement);
|
|
145
|
+
if (componentName === LIST_GROUP_ITEM &&
|
|
146
|
+
(0, has_meaningful_attr_1.hasMeaningfulAttr)(element.openingElement, 'extraAction')) {
|
|
147
|
+
return LIST_GROUP_ITEM_EXTRA_ACTION;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
function getDescendantRoots(node) {
|
|
152
|
+
const componentName = (0, get_element_name_1.getElementName)(node.openingElement);
|
|
153
|
+
// `getContentRoots` also adds supported `children={...}` content based on
|
|
154
|
+
// the component name, so descendants are traversed the same way as targets.
|
|
155
|
+
return getContentRoots(node, componentName, getComponentContentProps(componentName));
|
|
156
|
+
}
|
|
157
|
+
function shouldSkipNestedScanElement(element) {
|
|
158
|
+
const elementName = (0, get_element_name_1.getElementName)(element.openingElement);
|
|
159
|
+
if (!elementName)
|
|
160
|
+
return true;
|
|
161
|
+
if (getSyntheticInteractiveElementName(element))
|
|
162
|
+
return false;
|
|
163
|
+
// Custom components stay traversable here; local ones are resolved by
|
|
164
|
+
// `walkJsxChildren` before this guard runs, and unresolved ones remain opaque.
|
|
165
|
+
if (/^[A-Z]/.test(elementName) && !elementName.startsWith('Eui')) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
return !(0, is_interactive_element_1.isInteractiveElement)(element);
|
|
169
|
+
}
|
|
170
|
+
exports.NoNestedInteractiveElement = utils_1.ESLintUtils.RuleCreator.withoutDocs({
|
|
171
|
+
create(context) {
|
|
172
|
+
return {
|
|
173
|
+
JSXElement(node) {
|
|
174
|
+
const componentName = (0, get_element_name_1.getElementName)(node.openingElement);
|
|
175
|
+
if (!componentName)
|
|
176
|
+
return;
|
|
177
|
+
const target = getTarget(componentName, node);
|
|
178
|
+
if (!target)
|
|
179
|
+
return;
|
|
180
|
+
// Non-interactive elements (layout wrappers, `EuiToolTip`, text, …) are
|
|
181
|
+
// walked through; interactive ones are reported and not descended into,
|
|
182
|
+
// so this scan flags only the outermost control of a subtree. Anything
|
|
183
|
+
// deeper is reported against its own nearest target, when that target
|
|
184
|
+
// is itself scanned.
|
|
185
|
+
const reported = new Set();
|
|
186
|
+
for (const root of getContentRoots(node, componentName, target.contentProps)) {
|
|
187
|
+
(0, walk_jsx_children_1.walkJsxChildren)(root, (leaf) => {
|
|
188
|
+
if (leaf.type !== 'JSXElement' || reported.has(leaf))
|
|
189
|
+
return;
|
|
190
|
+
const elementName = getSyntheticInteractiveElementName(leaf) ??
|
|
191
|
+
(0, get_element_name_1.getElementName)(leaf.openingElement);
|
|
192
|
+
if (!elementName)
|
|
193
|
+
return;
|
|
194
|
+
reported.add(leaf);
|
|
195
|
+
context.report({
|
|
196
|
+
node: leaf.openingElement,
|
|
197
|
+
messageId: target.messageId,
|
|
198
|
+
data: {
|
|
199
|
+
componentName,
|
|
200
|
+
elementName,
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}, {
|
|
204
|
+
sourceCode: context.sourceCode,
|
|
205
|
+
shouldSkip: shouldSkipNestedScanElement,
|
|
206
|
+
getChildren: getDescendantRoots,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
},
|
|
212
|
+
meta: {
|
|
213
|
+
type: 'problem',
|
|
214
|
+
docs: {
|
|
215
|
+
description: 'Disallow interactive elements inside other interactive elements',
|
|
216
|
+
},
|
|
217
|
+
schema: [],
|
|
218
|
+
messages: {
|
|
219
|
+
nestedInteractive: '{{ elementName }} is nested inside {{ componentName }}, which renders a single focusable element. Nested controls are invalid HTML and not reliably reachable by keyboard or screen reader. Render {{ elementName }} as a sibling instead.',
|
|
220
|
+
clickableCardContent: '{{ elementName }} inside a clickable {{ componentName }} also triggers the card `onClick`/`href` when activated. Use one action per card: remove the card `onClick`/`href`, or move {{ elementName }} outside the card.',
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
defaultOptions: [],
|
|
224
|
+
});
|
|
225
|
+
//# sourceMappingURL=no_nested_interactive_element.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no_nested_interactive_element.js","sourceRoot":"","sources":["../../../../src/rules/a11y/no_nested_interactive_element.ts"],"names":[],"mappings":"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"}
|
|
@@ -9,23 +9,35 @@
|
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
exports.TooltipNoInteractiveContent = void 0;
|
|
11
11
|
const utils_1 = require("@typescript-eslint/utils");
|
|
12
|
-
const constants_1 = require("../../utils/constants");
|
|
13
12
|
const walk_jsx_children_1 = require("../../utils/walk_jsx_children");
|
|
14
13
|
const get_element_name_1 = require("../../utils/get_element_name");
|
|
14
|
+
const has_meaningful_attr_1 = require("../../utils/has_meaningful_attr");
|
|
15
|
+
const is_interactive_element_1 = require("../../utils/is_interactive_element");
|
|
15
16
|
const TOOLTIP_COMPONENTS = ['EuiToolTip', 'EuiIconTip'];
|
|
16
17
|
const TOOLTIP_CONTENT_PROPS = ['content', 'title'];
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
18
|
+
const LIST_GROUP_ITEM = 'EuiListGroupItem';
|
|
19
|
+
const LIST_GROUP_ITEM_EXTRA_ACTION = 'EuiButtonIcon';
|
|
20
|
+
function getSyntheticInteractiveElementName(element) {
|
|
21
|
+
const componentName = (0, get_element_name_1.getElementName)(element.openingElement);
|
|
22
|
+
if (componentName === LIST_GROUP_ITEM &&
|
|
23
|
+
(0, has_meaningful_attr_1.hasMeaningfulAttr)(element.openingElement, 'extraAction')) {
|
|
24
|
+
return LIST_GROUP_ITEM_EXTRA_ACTION;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
function shouldSkipTooltipContentElement(element) {
|
|
29
|
+
const elementName = (0, get_element_name_1.getElementName)(element.openingElement);
|
|
30
|
+
if (!elementName)
|
|
31
|
+
return true;
|
|
32
|
+
if (getSyntheticInteractiveElementName(element))
|
|
33
|
+
return false;
|
|
34
|
+
// Custom components stay traversable here; local ones are resolved by
|
|
35
|
+
// `walkJsxChildren` before this guard runs, and unresolved ones remain opaque.
|
|
36
|
+
if (/^[A-Z]/.test(elementName) && !elementName.startsWith('Eui')) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
return !(0, is_interactive_element_1.isInteractiveElement)(element);
|
|
40
|
+
}
|
|
29
41
|
exports.TooltipNoInteractiveContent = utils_1.ESLintUtils.RuleCreator.withoutDocs({
|
|
30
42
|
create(context) {
|
|
31
43
|
return {
|
|
@@ -53,25 +65,24 @@ exports.TooltipNoInteractiveContent = utils_1.ESLintUtils.RuleCreator.withoutDoc
|
|
|
53
65
|
(0, walk_jsx_children_1.walkJsxChildren)(expression, (leaf) => {
|
|
54
66
|
if (found || leaf.type !== 'JSXElement')
|
|
55
67
|
return;
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
if (!
|
|
68
|
+
const syntheticElementName = getSyntheticInteractiveElementName(leaf);
|
|
69
|
+
const elementName = syntheticElementName ?? (0, get_element_name_1.getElementName)(leaf.openingElement);
|
|
70
|
+
if (!elementName || (!syntheticElementName && !(0, is_interactive_element_1.isInteractiveElement)(leaf))) {
|
|
59
71
|
return;
|
|
72
|
+
}
|
|
60
73
|
context.report({
|
|
61
|
-
node:
|
|
74
|
+
node: leaf.openingElement,
|
|
62
75
|
messageId: 'noInteractiveContent',
|
|
63
76
|
data: {
|
|
64
77
|
propName: attr.name.name,
|
|
65
78
|
componentName,
|
|
66
|
-
elementName
|
|
79
|
+
elementName,
|
|
67
80
|
},
|
|
68
81
|
});
|
|
69
82
|
found = true;
|
|
70
83
|
}, {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return !name || !INTERACTIVE_ELEMENTS.has(name);
|
|
74
|
-
},
|
|
84
|
+
sourceCode: context.sourceCode,
|
|
85
|
+
shouldSkip: shouldSkipTooltipContentElement,
|
|
75
86
|
});
|
|
76
87
|
}
|
|
77
88
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tooltip_no_interactive_content.js","sourceRoot":"","sources":["../../../../src/rules/a11y/tooltip_no_interactive_content.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;AAEH,oDAAsE;AACtE,
|
|
1
|
+
{"version":3,"file":"tooltip_no_interactive_content.js","sourceRoot":"","sources":["../../../../src/rules/a11y/tooltip_no_interactive_content.ts"],"names":[],"mappings":"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"}
|
|
@@ -25,15 +25,39 @@ export declare const NON_INTERACTIVE_HTML_TAGS: string[];
|
|
|
25
25
|
*/
|
|
26
26
|
export declare const INTERACTIVE_EUI_COMPONENTS: string[];
|
|
27
27
|
/**
|
|
28
|
-
* EUI components that
|
|
29
|
-
* Without
|
|
30
|
-
* so rules that need
|
|
31
|
-
*
|
|
28
|
+
* EUI components that render a focusable element only when given one of the
|
|
29
|
+
* listed props. Without them they render plain, non-focusable markup — a
|
|
30
|
+
* `<span>`, `<div>`, or `<li>` — so rules that need an unconditionally
|
|
31
|
+
* interactive element must check the props rather than the name alone.
|
|
32
|
+
*
|
|
33
|
+
* - `EuiBadge` / `EuiBetaBadge` render a `<span>` unless clickable.
|
|
34
|
+
* - `EuiCard` renders a plain panel; `selectable` also makes it clickable by
|
|
35
|
+
* rendering an `EuiCardSelect` button.
|
|
36
|
+
* - `EuiContextMenuItem` renders a `<div>` unless it has an action, and is a
|
|
37
|
+
* `<button>` when given `toolTipContent`.
|
|
38
|
+
* - `EuiHeaderLogo` renders an `<a>` without an `href` attribute, which is not
|
|
39
|
+
* focusable.
|
|
40
|
+
* - `EuiListGroupItem` renders an `<li>` unless it has an action.
|
|
41
|
+
*/
|
|
42
|
+
export declare const CONDITIONALLY_INTERACTIVE_EUI_COMPONENTS: Record<string, string[]>;
|
|
43
|
+
/**
|
|
44
|
+
* Native HTML elements that are focusable only when given one of the listed
|
|
45
|
+
* attributes. An `<a>` without `href` is not a link and is not focusable.
|
|
46
|
+
*/
|
|
47
|
+
export declare const CONDITIONALLY_INTERACTIVE_HTML_ELEMENTS: Record<string, string[]>;
|
|
48
|
+
/**
|
|
49
|
+
* Conditional interactive props whose presence alone is enough to make the
|
|
50
|
+
* rendered element focusable, even when the statically-known value is `""`.
|
|
51
|
+
*
|
|
52
|
+
* Native anchors and `EuiHeaderLogo` both forward `href=""` to the DOM, which
|
|
53
|
+
* still creates a focusable same-document link.
|
|
32
54
|
*/
|
|
33
|
-
export declare const
|
|
55
|
+
export declare const PRESENCE_INTERACTIVE_PROPS: Record<string, string[]>;
|
|
34
56
|
export declare const HTML_TEXT_ELEMENTS: Set<string>;
|
|
35
57
|
export declare const EUI_TEXT_COMPONENTS: Set<string>;
|
|
36
58
|
export declare const HTML_ACTION_ELEMENTS: Set<string>;
|
|
59
|
+
/** Native HTML elements that are focusable and respond to user input. */
|
|
60
|
+
export declare const INTERACTIVE_HTML_ELEMENTS: string[];
|
|
37
61
|
/**
|
|
38
62
|
* Transparent layout wrappers inside `EuiCallOut` children that the rule should
|
|
39
63
|
* traverse rather than treat as opaque custom components.
|
|
@@ -45,3 +69,16 @@ export declare const CALLOUT_LAYOUT_CONTAINERS: Set<string>;
|
|
|
45
69
|
* Rules treat these the same as `HTML_TEXT_ELEMENTS` / `EUI_TEXT_COMPONENTS`.
|
|
46
70
|
*/
|
|
47
71
|
export declare const I18N_TEXT_COMPONENTS: Set<string>;
|
|
72
|
+
/**
|
|
73
|
+
* EUI components that render their content **inside** a single focusable
|
|
74
|
+
* `<button>` or `<a>`.
|
|
75
|
+
*
|
|
76
|
+
* Nesting another interactive element in their content produces invalid HTML
|
|
77
|
+
* (e.g. `<button>` inside `<button>`) and leaves the inner control unreachable
|
|
78
|
+
* or ambiguous for keyboard and screen-reader users.
|
|
79
|
+
*
|
|
80
|
+
* This is deliberately narrower than `INTERACTIVE_EUI_COMPONENTS`, which also
|
|
81
|
+
* contains composite components (`EuiBasicTable`, `EuiSelectable`, `EuiSideNav`,
|
|
82
|
+
* …) whose entire purpose is to host controls.
|
|
83
|
+
*/
|
|
84
|
+
export declare const LEAF_INTERACTIVE_EUI_COMPONENTS: string[];
|