@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
package/README.md
CHANGED
|
@@ -549,6 +549,69 @@ The rule only fires when `variant="selection"` is set as a static string. Dynami
|
|
|
549
549
|
</EuiButtonGroup>
|
|
550
550
|
```
|
|
551
551
|
|
|
552
|
+
### `@elastic/eui/no-nested-interactive-element`
|
|
553
|
+
|
|
554
|
+
Disallow interactive elements nested inside other interactive elements.
|
|
555
|
+
|
|
556
|
+
Nesting a control inside another control produces invalid HTML (`<button>` inside `<button>`, `<a>` inside `<a>`) and leaves the inner control unreachable or ambiguous for keyboard and screen-reader users.
|
|
557
|
+
|
|
558
|
+
The rule checks three groups of outer elements:
|
|
559
|
+
|
|
560
|
+
- **Native leaf controls** — `button` and `a` with `href`.
|
|
561
|
+
- **Leaf controls** — components that render their content inside a single focusable element: `EuiButton`, `EuiButtonEmpty`, `EuiButtonIcon`, `EuiContextMenuItem`, `EuiFacetButton`, `EuiFilterButton`, `EuiHeaderLink`, `EuiHeaderSectionItemButton`, `EuiKeyPadMenuItem`, `EuiLink`, `EuiListGroupItem`, `EuiStepHorizontal`, and `EuiTab`. Some of these only render a control when given the right props — `EuiListGroupItem` is an `<li>` without `onClick`/`href`, `EuiContextMenuItem` a `<div>` without `onClick`/`href`/`toolTipContent` — and are checked accordingly. Composite components whose purpose is to host controls (`EuiBasicTable`, `EuiSelectable`, `EuiSideNav`, `EuiButtonGroup`, …) are intentionally not checked.
|
|
562
|
+
- **Clickable `EuiCard`** — a card with `onClick` or `href` forwards clicks anywhere in the card to its title link, so a control in `title`, `description`, `footer`, `image`, or the card's children fires both its own action and the card's. Statically disabled cards (`isDisabled`) attach no handler and are skipped, as are `selectable` cards: pairing the select button with a footer action is a pattern EUI itself ships.
|
|
563
|
+
|
|
564
|
+
Beyond children, the rule also checks props whose value renders inside the focusable element: `label` and `icon` on `EuiListGroupItem`, `label` on `EuiKeyPadMenuItem`, `title` on `EuiStepHorizontal`, `prepend`/`append` on `EuiTab`, `icon` on `EuiFacetButton` and `EuiContextMenuItem`, and `notification` on `EuiHeaderSectionItemButton`. Props that render a **sibling** control (`EuiListGroupItem`'s `extraAction`) or render into a portal (`EuiContextMenuItem`'s `toolTipContent`) are not reported.
|
|
565
|
+
|
|
566
|
+
What counts as nested content:
|
|
567
|
+
|
|
568
|
+
- Non-interactive wrappers (`EuiFlexGroup`, `EuiText`, `EuiToolTip`, …) are traversed, so a control behind them is still found.
|
|
569
|
+
- Controls referenced through a local variable or a local arrow-function component are resolved and checked.
|
|
570
|
+
- Conditionally-interactive elements only count as interactive content when the relevant prop is present:
|
|
571
|
+
- `EuiBadge`: `onClick`, `href`, or `iconOnClick`
|
|
572
|
+
- `EuiBetaBadge`: `onClick`, `href`, or `tooltipContent`
|
|
573
|
+
- `EuiCard`: `onClick`, `href`, or `selectable`
|
|
574
|
+
- `EuiContextMenuItem`: `onClick`, `href`, or `toolTipContent`
|
|
575
|
+
- `EuiListGroupItem`: `onClick` or `href` (`extraAction` is checked separately as a sibling `EuiButtonIcon`)
|
|
576
|
+
- Native `<a>` and `EuiHeaderLogo`: `href` (including `href=""`)
|
|
577
|
+
- Note the exact prop casing: `EuiBetaBadge` uses `tooltipContent`, while `EuiContextMenuItem` uses `toolTipContent`.
|
|
578
|
+
- Dynamic content (e.g. `{renderLabel()}`) cannot be statically analyzed and is skipped.
|
|
579
|
+
|
|
580
|
+
#### Examples
|
|
581
|
+
|
|
582
|
+
```tsx
|
|
583
|
+
// ✗ Bad - link inside a button
|
|
584
|
+
<EuiButton onClick={onClick}>
|
|
585
|
+
Read the <EuiLink href="/docs">docs</EuiLink>
|
|
586
|
+
</EuiButton>
|
|
587
|
+
|
|
588
|
+
// ✓ Render the controls as siblings
|
|
589
|
+
<EuiFlexGroup>
|
|
590
|
+
<EuiButton onClick={onClick}>Save</EuiButton>
|
|
591
|
+
<EuiLink href="/docs">Read the docs</EuiLink>
|
|
592
|
+
</EuiFlexGroup>
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
```tsx
|
|
596
|
+
// ✗ Bad - the footer button also triggers the card's `onClick`
|
|
597
|
+
<EuiCard
|
|
598
|
+
title="Dashboard"
|
|
599
|
+
description="View metrics"
|
|
600
|
+
onClick={onClick}
|
|
601
|
+
footer={<EuiButton>Open</EuiButton>}
|
|
602
|
+
/>
|
|
603
|
+
|
|
604
|
+
// ✓ One action per card - either a clickable card…
|
|
605
|
+
<EuiCard title="Dashboard" description="View metrics" onClick={onClick} />
|
|
606
|
+
|
|
607
|
+
// ✓ …or a footer action
|
|
608
|
+
<EuiCard
|
|
609
|
+
title="Dashboard"
|
|
610
|
+
description="View metrics"
|
|
611
|
+
footer={<EuiButton>Open</EuiButton>}
|
|
612
|
+
/>
|
|
613
|
+
```
|
|
614
|
+
|
|
552
615
|
## Testing
|
|
553
616
|
|
|
554
617
|
### Running unit tests
|
|
@@ -570,7 +633,7 @@ To test the local changes to the plugin, you must:
|
|
|
570
633
|
3. Build the package: `yarn build`
|
|
571
634
|
4. Run `yalc publish` in the plugin's directory to publish it locally.
|
|
572
635
|
5. In your project's directory, run `yalc add @elastic/eslint-plugin-eui` to link the locally published package.
|
|
573
|
-
6. Install dependencies
|
|
636
|
+
6. Install dependencies using your package manager.
|
|
574
637
|
7. After making further changes to the plugin, repeat the steps from 3.
|
|
575
638
|
|
|
576
639
|
## Publishing
|
package/lib/cjs/index.js
CHANGED
|
@@ -8,6 +8,7 @@ var _href_or_on_click = require("./rules/href_or_on_click");
|
|
|
8
8
|
var _require_href_for_link = require("./rules/require_href_for_link");
|
|
9
9
|
var _no_css_color = require("./rules/no_css_color");
|
|
10
10
|
var _no_nested_copy_tooltip = require("./rules/a11y/no_nested_copy_tooltip");
|
|
11
|
+
var _no_nested_interactive_element = require("./rules/a11y/no_nested_interactive_element");
|
|
11
12
|
var _no_restricted_eui_imports = require("./rules/no_restricted_eui_imports");
|
|
12
13
|
var _no_static_z_index = require("./rules/no_static_z_index");
|
|
13
14
|
var _no_unnamed_interactive_element = require("./rules/a11y/no_unnamed_interactive_element");
|
|
@@ -43,6 +44,7 @@ const config = {
|
|
|
43
44
|
'no-css-color': _no_css_color.NoCssColor,
|
|
44
45
|
'no-deprecated-icon-aliases': _no_deprecated_icon_aliases.NoDeprecatedIconAliases,
|
|
45
46
|
'no-nested-copy-tooltip': _no_nested_copy_tooltip.NoNestedCopyTooltip,
|
|
47
|
+
'no-nested-interactive-element': _no_nested_interactive_element.NoNestedInteractiveElement,
|
|
46
48
|
'no-restricted-eui-imports': _no_restricted_eui_imports.NoRestrictedEuiImports,
|
|
47
49
|
'no-static-z-index': _no_static_z_index.NoStaticZIndex,
|
|
48
50
|
'no-unnamed-interactive-element': _no_unnamed_interactive_element.NoUnnamedInteractiveElement,
|
|
@@ -75,6 +77,7 @@ const config = {
|
|
|
75
77
|
'@elastic/eui/no-css-color': 'warn',
|
|
76
78
|
'@elastic/eui/no-deprecated-icon-aliases': 'warn',
|
|
77
79
|
'@elastic/eui/no-nested-copy-tooltip': 'warn',
|
|
80
|
+
'@elastic/eui/no-nested-interactive-element': 'warn',
|
|
78
81
|
'@elastic/eui/no-restricted-eui-imports': 'warn',
|
|
79
82
|
'@elastic/eui/no-static-z-index': 'warn',
|
|
80
83
|
'@elastic/eui/no-unnamed-interactive-element': 'warn',
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
export declare const NoNestedInteractiveElement: ESLintUtils.RuleModule<"nestedInteractive" | "clickableCardContent", [], unknown, ESLintUtils.RuleListener>;
|
|
3
|
+
//# sourceMappingURL=no_nested_interactive_element.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no_nested_interactive_element.d.ts","sourceRoot":"","sources":["../../../../src/rules/a11y/no_nested_interactive_element.ts"],"names":[],"mappings":"AAQA,OAAO,EAAiB,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAqNtE,eAAO,MAAM,0BAA0B,6GAgErC,CAAC"}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.NoNestedInteractiveElement = void 0;
|
|
7
|
+
var _utils = require("@typescript-eslint/utils");
|
|
8
|
+
var _constants = require("../../utils/constants");
|
|
9
|
+
var _get_element_name = require("../../utils/get_element_name");
|
|
10
|
+
var _has_meaningful_attr = require("../../utils/has_meaningful_attr");
|
|
11
|
+
var _is_interactive_element = require("../../utils/is_interactive_element");
|
|
12
|
+
var _walk_jsx_children = require("../../utils/walk_jsx_children");
|
|
13
|
+
/*
|
|
14
|
+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
15
|
+
* or more contributor license agreements. Licensed under the Elastic License
|
|
16
|
+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
|
17
|
+
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
|
18
|
+
* Side Public License, v 1.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Describes why, and where, an element must not contain other controls. */
|
|
22
|
+
|
|
23
|
+
const CARD = 'EuiCard';
|
|
24
|
+
const LIST_GROUP_ITEM = 'EuiListGroupItem';
|
|
25
|
+
const LIST_GROUP_ITEM_EXTRA_ACTION = 'EuiButtonIcon';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Leaf-control props whose value renders *inside* the focusable element, in
|
|
29
|
+
* addition to children. Props that render a **sibling** control (e.g.
|
|
30
|
+
* `EuiListGroupItem` `extraAction`) are intentionally absent, as are props
|
|
31
|
+
* rendered into a portal (e.g. `EuiContextMenuItem` `toolTipContent`).
|
|
32
|
+
*/
|
|
33
|
+
const LEAF_CONTENT_PROPS = {
|
|
34
|
+
EuiContextMenuItem: ['icon'],
|
|
35
|
+
EuiFacetButton: ['icon'],
|
|
36
|
+
EuiHeaderSectionItemButton: ['notification'],
|
|
37
|
+
EuiKeyPadMenuItem: ['label'],
|
|
38
|
+
EuiListGroupItem: ['icon', 'label'],
|
|
39
|
+
EuiStepHorizontal: ['title'],
|
|
40
|
+
EuiTab: ['append', 'prepend']
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* `EuiCard` props rendered inside the card wrapper, which is what carries the
|
|
45
|
+
* forwarding click handler. `betaBadgeProps` can also produce a control, but it
|
|
46
|
+
* is configured through an object rather than JSX and is not analysed here.
|
|
47
|
+
*/
|
|
48
|
+
const CARD_CONTENT_PROPS = ['description', 'footer', 'image', 'title'];
|
|
49
|
+
const LEAF_COMPONENTS = new Set(_constants.LEAF_INTERACTIVE_EUI_COMPONENTS);
|
|
50
|
+
const LEAF_HTML_ELEMENTS = new Set(['a', 'button']);
|
|
51
|
+
const COMPONENTS_WITH_CHILDREN_PROP = new Set(['a', CARD, 'button', 'EuiBadge', 'EuiBetaBadge', 'EuiButton', 'EuiButtonEmpty', 'EuiContextMenuItem', 'EuiFacetButton', 'EuiFilterButton', 'EuiHeaderLink', 'EuiHeaderSectionItemButton', 'EuiKeyPadMenuItem', 'EuiLink', 'EuiTab']);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Whether a boolean prop is set to a statically-known `true` (`<El prop />` or
|
|
55
|
+
* `<El prop={true} />`). Dynamic values are treated as *not* set, so that a
|
|
56
|
+
* card gated on e.g. `isDisabled={isLoading}` is still checked.
|
|
57
|
+
*/
|
|
58
|
+
function isStaticallyTrue(openingElement, propName) {
|
|
59
|
+
const attr = openingElement.attributes.find(a => a.type === 'JSXAttribute' && a.name.type === 'JSXIdentifier' && a.name.name === propName);
|
|
60
|
+
if (!attr) return false;
|
|
61
|
+
if (attr.value == null) return true;
|
|
62
|
+
return attr.value.type === 'JSXExpressionContainer' && attr.value.expression.type === 'Literal' && attr.value.expression.value === true;
|
|
63
|
+
}
|
|
64
|
+
function getTarget(componentName, node) {
|
|
65
|
+
const {
|
|
66
|
+
openingElement
|
|
67
|
+
} = node;
|
|
68
|
+
if (componentName === CARD) {
|
|
69
|
+
// A card with `onClick`/`href` attaches a wrapper click handler that
|
|
70
|
+
// forwards to the title link, so a control anywhere in its content fires
|
|
71
|
+
// both its own action and the card's. A disabled card attaches no handler.
|
|
72
|
+
//
|
|
73
|
+
// `selectable` cards are deliberately not a target: pairing the select
|
|
74
|
+
// button with a footer action is a pattern EUI itself ships. It is only
|
|
75
|
+
// safe when the footer control stops propagation, which this rule cannot
|
|
76
|
+
// verify statically — see the PR discussion.
|
|
77
|
+
if ((0, _has_meaningful_attr.hasMeaningfulAttr)(openingElement, 'selectable')) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return (0, _is_interactive_element.hasInteractivityProp)(openingElement) && !isStaticallyTrue(openingElement, 'isDisabled') ? {
|
|
81
|
+
messageId: 'clickableCardContent',
|
|
82
|
+
contentProps: getComponentContentProps(componentName)
|
|
83
|
+
} : null;
|
|
84
|
+
}
|
|
85
|
+
if (LEAF_HTML_ELEMENTS.has(componentName) && (0, _is_interactive_element.isInteractiveElement)(node)) {
|
|
86
|
+
return {
|
|
87
|
+
messageId: 'nestedInteractive',
|
|
88
|
+
contentProps: []
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Several leaf components only render a control when given the right props
|
|
93
|
+
// (`EuiListGroupItem` is an `<li>` without an action, `EuiContextMenuItem` a
|
|
94
|
+
// `<div>`), so the name alone is not enough to make it a target.
|
|
95
|
+
if (LEAF_COMPONENTS.has(componentName) && (0, _is_interactive_element.isInteractiveElement)(node)) {
|
|
96
|
+
return {
|
|
97
|
+
messageId: 'nestedInteractive',
|
|
98
|
+
contentProps: getComponentContentProps(componentName)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The element's children plus the expression value of each content prop. */
|
|
105
|
+
function getContentRoots(node, componentName, contentProps) {
|
|
106
|
+
const roots = [...node.children];
|
|
107
|
+
for (const attr of node.openingElement.attributes) {
|
|
108
|
+
if (attr.type !== 'JSXAttribute' || attr.name.type !== 'JSXIdentifier') {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const isChildrenProp = attr.name.name === 'children' && componentName != null && COMPONENTS_WITH_CHILDREN_PROP.has(componentName);
|
|
112
|
+
if (!isChildrenProp && !contentProps.includes(attr.name.name)) continue;
|
|
113
|
+
// Only `prop={<JSX />}` can hold an element; a string literal cannot.
|
|
114
|
+
if (attr.value?.type !== 'JSXExpressionContainer') continue;
|
|
115
|
+
if (attr.value.expression.type === 'JSXEmptyExpression') continue;
|
|
116
|
+
roots.push(attr.value.expression);
|
|
117
|
+
}
|
|
118
|
+
return roots;
|
|
119
|
+
}
|
|
120
|
+
function getComponentContentProps(componentName) {
|
|
121
|
+
if (componentName === CARD) {
|
|
122
|
+
return CARD_CONTENT_PROPS;
|
|
123
|
+
}
|
|
124
|
+
return LEAF_CONTENT_PROPS[componentName ?? ''] ?? [];
|
|
125
|
+
}
|
|
126
|
+
function getSyntheticInteractiveElementName(element) {
|
|
127
|
+
const componentName = (0, _get_element_name.getElementName)(element.openingElement);
|
|
128
|
+
if (componentName === LIST_GROUP_ITEM && (0, _has_meaningful_attr.hasMeaningfulAttr)(element.openingElement, 'extraAction')) {
|
|
129
|
+
return LIST_GROUP_ITEM_EXTRA_ACTION;
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
function getDescendantRoots(node) {
|
|
134
|
+
const componentName = (0, _get_element_name.getElementName)(node.openingElement);
|
|
135
|
+
|
|
136
|
+
// `getContentRoots` also adds supported `children={...}` content based on
|
|
137
|
+
// the component name, so descendants are traversed the same way as targets.
|
|
138
|
+
return getContentRoots(node, componentName, getComponentContentProps(componentName));
|
|
139
|
+
}
|
|
140
|
+
function shouldSkipNestedScanElement(element) {
|
|
141
|
+
const elementName = (0, _get_element_name.getElementName)(element.openingElement);
|
|
142
|
+
if (!elementName) return true;
|
|
143
|
+
if (getSyntheticInteractiveElementName(element)) return false;
|
|
144
|
+
|
|
145
|
+
// Custom components stay traversable here; local ones are resolved by
|
|
146
|
+
// `walkJsxChildren` before this guard runs, and unresolved ones remain opaque.
|
|
147
|
+
if (/^[A-Z]/.test(elementName) && !elementName.startsWith('Eui')) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
return !(0, _is_interactive_element.isInteractiveElement)(element);
|
|
151
|
+
}
|
|
152
|
+
const NoNestedInteractiveElement = exports.NoNestedInteractiveElement = _utils.ESLintUtils.RuleCreator.withoutDocs({
|
|
153
|
+
create(context) {
|
|
154
|
+
return {
|
|
155
|
+
JSXElement(node) {
|
|
156
|
+
const componentName = (0, _get_element_name.getElementName)(node.openingElement);
|
|
157
|
+
if (!componentName) return;
|
|
158
|
+
const target = getTarget(componentName, node);
|
|
159
|
+
if (!target) return;
|
|
160
|
+
|
|
161
|
+
// Non-interactive elements (layout wrappers, `EuiToolTip`, text, …) are
|
|
162
|
+
// walked through; interactive ones are reported and not descended into,
|
|
163
|
+
// so this scan flags only the outermost control of a subtree. Anything
|
|
164
|
+
// deeper is reported against its own nearest target, when that target
|
|
165
|
+
// is itself scanned.
|
|
166
|
+
const reported = new Set();
|
|
167
|
+
for (const root of getContentRoots(node, componentName, target.contentProps)) {
|
|
168
|
+
(0, _walk_jsx_children.walkJsxChildren)(root, leaf => {
|
|
169
|
+
if (leaf.type !== 'JSXElement' || reported.has(leaf)) return;
|
|
170
|
+
const elementName = getSyntheticInteractiveElementName(leaf) ?? (0, _get_element_name.getElementName)(leaf.openingElement);
|
|
171
|
+
if (!elementName) return;
|
|
172
|
+
reported.add(leaf);
|
|
173
|
+
context.report({
|
|
174
|
+
node: leaf.openingElement,
|
|
175
|
+
messageId: target.messageId,
|
|
176
|
+
data: {
|
|
177
|
+
componentName,
|
|
178
|
+
elementName
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}, {
|
|
182
|
+
sourceCode: context.sourceCode,
|
|
183
|
+
shouldSkip: shouldSkipNestedScanElement,
|
|
184
|
+
getChildren: getDescendantRoots
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
},
|
|
190
|
+
meta: {
|
|
191
|
+
type: 'problem',
|
|
192
|
+
docs: {
|
|
193
|
+
description: 'Disallow interactive elements inside other interactive elements'
|
|
194
|
+
},
|
|
195
|
+
schema: [],
|
|
196
|
+
messages: {
|
|
197
|
+
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.',
|
|
198
|
+
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.'
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
defaultOptions: []
|
|
202
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tooltip_no_interactive_content.d.ts","sourceRoot":"","sources":["../../../../src/rules/a11y/tooltip_no_interactive_content.ts"],"names":[],"mappings":"AAQA,OAAO,EAAiB,WAAW,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"tooltip_no_interactive_content.d.ts","sourceRoot":"","sources":["../../../../src/rules/a11y/tooltip_no_interactive_content.ts"],"names":[],"mappings":"AAQA,OAAO,EAAiB,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAyCtE,eAAO,MAAM,2BAA2B,uFA8EtC,CAAC"}
|
|
@@ -5,9 +5,10 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.TooltipNoInteractiveContent = void 0;
|
|
7
7
|
var _utils = require("@typescript-eslint/utils");
|
|
8
|
-
var _constants = require("../../utils/constants");
|
|
9
8
|
var _walk_jsx_children = require("../../utils/walk_jsx_children");
|
|
10
9
|
var _get_element_name = require("../../utils/get_element_name");
|
|
10
|
+
var _has_meaningful_attr = require("../../utils/has_meaningful_attr");
|
|
11
|
+
var _is_interactive_element = require("../../utils/is_interactive_element");
|
|
11
12
|
/*
|
|
12
13
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
13
14
|
* or more contributor license agreements. Licensed under the Elastic License
|
|
@@ -18,9 +19,27 @@ var _get_element_name = require("../../utils/get_element_name");
|
|
|
18
19
|
|
|
19
20
|
const TOOLTIP_COMPONENTS = ['EuiToolTip', 'EuiIconTip'];
|
|
20
21
|
const TOOLTIP_CONTENT_PROPS = ['content', 'title'];
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
22
|
+
const LIST_GROUP_ITEM = 'EuiListGroupItem';
|
|
23
|
+
const LIST_GROUP_ITEM_EXTRA_ACTION = 'EuiButtonIcon';
|
|
24
|
+
function getSyntheticInteractiveElementName(element) {
|
|
25
|
+
const componentName = (0, _get_element_name.getElementName)(element.openingElement);
|
|
26
|
+
if (componentName === LIST_GROUP_ITEM && (0, _has_meaningful_attr.hasMeaningfulAttr)(element.openingElement, 'extraAction')) {
|
|
27
|
+
return LIST_GROUP_ITEM_EXTRA_ACTION;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
function shouldSkipTooltipContentElement(element) {
|
|
32
|
+
const elementName = (0, _get_element_name.getElementName)(element.openingElement);
|
|
33
|
+
if (!elementName) return true;
|
|
34
|
+
if (getSyntheticInteractiveElementName(element)) return false;
|
|
35
|
+
|
|
36
|
+
// Custom components stay traversable here; local ones are resolved by
|
|
37
|
+
// `walkJsxChildren` before this guard runs, and unresolved ones remain opaque.
|
|
38
|
+
if (/^[A-Z]/.test(elementName) && !elementName.startsWith('Eui')) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return !(0, _is_interactive_element.isInteractiveElement)(element);
|
|
42
|
+
}
|
|
24
43
|
const TooltipNoInteractiveContent = exports.TooltipNoInteractiveContent = _utils.ESLintUtils.RuleCreator.withoutDocs({
|
|
25
44
|
create(context) {
|
|
26
45
|
return {
|
|
@@ -48,24 +67,24 @@ const TooltipNoInteractiveContent = exports.TooltipNoInteractiveContent = _utils
|
|
|
48
67
|
let found = false;
|
|
49
68
|
(0, _walk_jsx_children.walkJsxChildren)(expression, leaf => {
|
|
50
69
|
if (found || leaf.type !== 'JSXElement') return;
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
if (!
|
|
70
|
+
const syntheticElementName = getSyntheticInteractiveElementName(leaf);
|
|
71
|
+
const elementName = syntheticElementName ?? (0, _get_element_name.getElementName)(leaf.openingElement);
|
|
72
|
+
if (!elementName || !syntheticElementName && !(0, _is_interactive_element.isInteractiveElement)(leaf)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
54
75
|
context.report({
|
|
55
|
-
node:
|
|
76
|
+
node: leaf.openingElement,
|
|
56
77
|
messageId: 'noInteractiveContent',
|
|
57
78
|
data: {
|
|
58
79
|
propName: attr.name.name,
|
|
59
80
|
componentName,
|
|
60
|
-
elementName
|
|
81
|
+
elementName
|
|
61
82
|
}
|
|
62
83
|
});
|
|
63
84
|
found = true;
|
|
64
85
|
}, {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return !name || !INTERACTIVE_ELEMENTS.has(name);
|
|
68
|
-
}
|
|
86
|
+
sourceCode: context.sourceCode,
|
|
87
|
+
shouldSkip: shouldSkipTooltipContentElement
|
|
69
88
|
});
|
|
70
89
|
}
|
|
71
90
|
}
|
|
@@ -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,4 +69,17 @@ 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[];
|
|
48
85
|
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/utils/constants.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,yBAAyB,UAyCrC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,0BAA0B,UA8CtC,CAAC;AAEF
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/utils/constants.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,yBAAyB,UAyCrC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,0BAA0B,UA8CtC,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,wCAAwC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAQ3E,CAAC;AAEJ;;;GAGG;AACH,eAAO,MAAM,uCAAuC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAG1E,CAAC;AAEJ;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAG/D,CAAC;AAEF,eAAO,MAAM,kBAAkB,aAS7B,CAAC;AAEH,eAAO,MAAM,mBAAmB,aAO9B,CAAC;AAEH,eAAO,MAAM,oBAAoB,aAA2B,CAAC;AAE7D,yEAAyE;AACzE,eAAO,MAAM,yBAAyB,UAMrC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,yBAAyB,aAMpC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,aAAgC,CAAC;AAElE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,+BAA+B,UAc3C,CAAC"}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.NON_INTERACTIVE_HTML_TAGS = exports.INTERACTIVE_EUI_COMPONENTS = exports.I18N_TEXT_COMPONENTS = exports.HTML_TEXT_ELEMENTS = exports.HTML_ACTION_ELEMENTS = exports.EUI_TEXT_COMPONENTS = exports.CONDITIONALLY_INTERACTIVE_EUI_COMPONENTS = exports.CALLOUT_LAYOUT_CONTAINERS = void 0;
|
|
6
|
+
exports.PRESENCE_INTERACTIVE_PROPS = exports.NON_INTERACTIVE_HTML_TAGS = exports.LEAF_INTERACTIVE_EUI_COMPONENTS = exports.INTERACTIVE_HTML_ELEMENTS = exports.INTERACTIVE_EUI_COMPONENTS = exports.I18N_TEXT_COMPONENTS = exports.HTML_TEXT_ELEMENTS = exports.HTML_ACTION_ELEMENTS = exports.EUI_TEXT_COMPONENTS = exports.CONDITIONALLY_INTERACTIVE_HTML_ELEMENTS = exports.CONDITIONALLY_INTERACTIVE_EUI_COMPONENTS = exports.CALLOUT_LAYOUT_CONTAINERS = void 0;
|
|
7
7
|
/*
|
|
8
8
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
9
9
|
* or more contributor license agreements. Licensed under the Elastic License
|
|
@@ -41,16 +41,55 @@ const NON_INTERACTIVE_HTML_TAGS = exports.NON_INTERACTIVE_HTML_TAGS = ['div', 's
|
|
|
41
41
|
const INTERACTIVE_EUI_COMPONENTS = exports.INTERACTIVE_EUI_COMPONENTS = ['EuiBadge', 'EuiBasicTable', 'EuiBetaBadge', 'EuiBreadcrumbs', 'EuiButton', 'EuiButtonEmpty', 'EuiButtonGroup', 'EuiButtonIcon', 'EuiCard', 'EuiCheckableCard', 'EuiCheckbox', 'EuiColorPicker', 'EuiComboBox', 'EuiContextMenuItem', 'EuiDatePicker', 'EuiDualRange', 'EuiFacetButton', 'EuiFieldNumber', 'EuiFieldPassword', 'EuiFieldSearch', 'EuiFieldText', 'EuiFilterButton', 'EuiFilterSelectItem', 'EuiFilterSelectable', 'EuiHeaderLink', 'EuiHeaderLogo', 'EuiHeaderSectionItemButton', 'EuiInMemoryTable', 'EuiKeyPadMenuItem', 'EuiLink', 'EuiListGroupItem', 'EuiPagination', 'EuiPinnableListGroup', 'EuiRadio', 'EuiRange', 'EuiSelect', 'EuiSelectable', 'EuiSideNav', 'EuiStepHorizontal', 'EuiSuperDatePicker', 'EuiSuperSelect', 'EuiSwitch', 'EuiTab', 'EuiTextArea', 'EuiTreeView'];
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* EUI components that
|
|
45
|
-
* Without
|
|
46
|
-
* so rules that need
|
|
47
|
-
*
|
|
44
|
+
* EUI components that render a focusable element only when given one of the
|
|
45
|
+
* listed props. Without them they render plain, non-focusable markup — a
|
|
46
|
+
* `<span>`, `<div>`, or `<li>` — so rules that need an unconditionally
|
|
47
|
+
* interactive element must check the props rather than the name alone.
|
|
48
|
+
*
|
|
49
|
+
* - `EuiBadge` / `EuiBetaBadge` render a `<span>` unless clickable.
|
|
50
|
+
* - `EuiCard` renders a plain panel; `selectable` also makes it clickable by
|
|
51
|
+
* rendering an `EuiCardSelect` button.
|
|
52
|
+
* - `EuiContextMenuItem` renders a `<div>` unless it has an action, and is a
|
|
53
|
+
* `<button>` when given `toolTipContent`.
|
|
54
|
+
* - `EuiHeaderLogo` renders an `<a>` without an `href` attribute, which is not
|
|
55
|
+
* focusable.
|
|
56
|
+
* - `EuiListGroupItem` renders an `<li>` unless it has an action.
|
|
57
|
+
*/
|
|
58
|
+
const CONDITIONALLY_INTERACTIVE_EUI_COMPONENTS = exports.CONDITIONALLY_INTERACTIVE_EUI_COMPONENTS = {
|
|
59
|
+
EuiBadge: ['iconOnClick', 'onClick', 'href'],
|
|
60
|
+
EuiBetaBadge: ['tooltipContent', 'onClick', 'href'],
|
|
61
|
+
EuiCard: ['onClick', 'href', 'selectable'],
|
|
62
|
+
EuiContextMenuItem: ['onClick', 'href', 'toolTipContent'],
|
|
63
|
+
EuiHeaderLogo: ['href'],
|
|
64
|
+
EuiListGroupItem: ['onClick', 'href']
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Native HTML elements that are focusable only when given one of the listed
|
|
69
|
+
* attributes. An `<a>` without `href` is not a link and is not focusable.
|
|
70
|
+
*/
|
|
71
|
+
const CONDITIONALLY_INTERACTIVE_HTML_ELEMENTS = exports.CONDITIONALLY_INTERACTIVE_HTML_ELEMENTS = {
|
|
72
|
+
a: ['href']
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Conditional interactive props whose presence alone is enough to make the
|
|
77
|
+
* rendered element focusable, even when the statically-known value is `""`.
|
|
78
|
+
*
|
|
79
|
+
* Native anchors and `EuiHeaderLogo` both forward `href=""` to the DOM, which
|
|
80
|
+
* still creates a focusable same-document link.
|
|
48
81
|
*/
|
|
49
|
-
const
|
|
82
|
+
const PRESENCE_INTERACTIVE_PROPS = exports.PRESENCE_INTERACTIVE_PROPS = {
|
|
83
|
+
a: ['href'],
|
|
84
|
+
EuiHeaderLogo: ['href']
|
|
85
|
+
};
|
|
50
86
|
const HTML_TEXT_ELEMENTS = exports.HTML_TEXT_ELEMENTS = new Set(['p', 'span', 'strong', 'em', 'b', 'i', 'small', 'code']);
|
|
51
87
|
const EUI_TEXT_COMPONENTS = exports.EUI_TEXT_COMPONENTS = new Set(['EuiText', 'EuiTextColor', 'EuiTextAlign', 'EuiCode', 'EuiMark', 'EuiHighlight']);
|
|
52
88
|
const HTML_ACTION_ELEMENTS = exports.HTML_ACTION_ELEMENTS = new Set(['button', 'a']);
|
|
53
89
|
|
|
90
|
+
/** Native HTML elements that are focusable and respond to user input. */
|
|
91
|
+
const INTERACTIVE_HTML_ELEMENTS = exports.INTERACTIVE_HTML_ELEMENTS = ['a', 'button', 'input', 'select', 'textarea'];
|
|
92
|
+
|
|
54
93
|
/**
|
|
55
94
|
* Transparent layout wrappers inside `EuiCallOut` children that the rule should
|
|
56
95
|
* traverse rather than treat as opaque custom components.
|
|
@@ -62,4 +101,18 @@ const CALLOUT_LAYOUT_CONTAINERS = exports.CALLOUT_LAYOUT_CONTAINERS = new Set(['
|
|
|
62
101
|
* (e.g. `FormattedMessage` from react-intl used extensively in Kibana).
|
|
63
102
|
* Rules treat these the same as `HTML_TEXT_ELEMENTS` / `EUI_TEXT_COMPONENTS`.
|
|
64
103
|
*/
|
|
65
|
-
const I18N_TEXT_COMPONENTS = exports.I18N_TEXT_COMPONENTS = new Set(['FormattedMessage']);
|
|
104
|
+
const I18N_TEXT_COMPONENTS = exports.I18N_TEXT_COMPONENTS = new Set(['FormattedMessage']);
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* EUI components that render their content **inside** a single focusable
|
|
108
|
+
* `<button>` or `<a>`.
|
|
109
|
+
*
|
|
110
|
+
* Nesting another interactive element in their content produces invalid HTML
|
|
111
|
+
* (e.g. `<button>` inside `<button>`) and leaves the inner control unreachable
|
|
112
|
+
* or ambiguous for keyboard and screen-reader users.
|
|
113
|
+
*
|
|
114
|
+
* This is deliberately narrower than `INTERACTIVE_EUI_COMPONENTS`, which also
|
|
115
|
+
* contains composite components (`EuiBasicTable`, `EuiSelectable`, `EuiSideNav`,
|
|
116
|
+
* …) whose entire purpose is to host controls.
|
|
117
|
+
*/
|
|
118
|
+
const LEAF_INTERACTIVE_EUI_COMPONENTS = exports.LEAF_INTERACTIVE_EUI_COMPONENTS = ['EuiButton', 'EuiButtonEmpty', 'EuiButtonIcon', 'EuiContextMenuItem', 'EuiFacetButton', 'EuiFilterButton', 'EuiHeaderLink', 'EuiHeaderSectionItemButton', 'EuiKeyPadMenuItem', 'EuiLink', 'EuiListGroupItem', 'EuiStepHorizontal', 'EuiTab'];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
/**
|
|
3
|
+
* Whether any of `propNames` is present on the element with a statically
|
|
4
|
+
* non-empty value. Dynamic values (`prop={maybe}`) count as present, since
|
|
5
|
+
* their runtime value is unknown.
|
|
6
|
+
*/
|
|
7
|
+
export declare function hasAnyProp(openingElement: TSESTree.JSXOpeningElement, propNames: string[], presenceOnlyProps?: Set<string>): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Whether an element has a prop that makes it respond to user input
|
|
10
|
+
* (`onClick` or `href`).
|
|
11
|
+
*/
|
|
12
|
+
export declare function hasInteractivityProp(openingElement: TSESTree.JSXOpeningElement): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Whether a JSX element renders a focusable control.
|
|
15
|
+
*
|
|
16
|
+
* Native interactive HTML elements and unconditionally interactive EUI
|
|
17
|
+
* components always qualify. Everything in
|
|
18
|
+
* `CONDITIONALLY_INTERACTIVE_EUI_COMPONENTS` and
|
|
19
|
+
* `CONDITIONALLY_INTERACTIVE_HTML_ELEMENTS` — `EuiBadge`, `EuiListGroupItem`,
|
|
20
|
+
* a bare `<a>`, … — qualifies only when given a prop that makes it focusable.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isInteractiveElement(element: TSESTree.JSXElement): boolean;
|
|
23
|
+
//# sourceMappingURL=is_interactive_element.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"is_interactive_element.d.ts","sourceRoot":"","sources":["../../../src/utils/is_interactive_element.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AA2BzD;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,cAAc,EAAE,QAAQ,CAAC,iBAAiB,EAC1C,SAAS,EAAE,MAAM,EAAE,EACnB,iBAAiB,cAAoB,GACpC,OAAO,CAmBT;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,cAAc,EAAE,QAAQ,CAAC,iBAAiB,GACzC,OAAO,CAET;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,QAAQ,CAAC,UAAU,GAAG,OAAO,CAgB1E"}
|