@elastic/eslint-plugin-eui 2.12.0 → 2.13.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.
Files changed (32) hide show
  1. package/README.md +77 -0
  2. package/lib/cjs/index.js +11 -2
  3. package/lib/cjs/rules/a11y/tooltip_button_icon_wrap.d.ts +3 -0
  4. package/lib/cjs/rules/a11y/tooltip_button_icon_wrap.d.ts.map +1 -0
  5. package/lib/cjs/rules/a11y/tooltip_button_icon_wrap.js +104 -0
  6. package/lib/cjs/rules/a11y/tooltip_no_interactive_content.d.ts +3 -0
  7. package/lib/cjs/rules/a11y/tooltip_no_interactive_content.d.ts.map +1 -0
  8. package/lib/cjs/rules/a11y/tooltip_no_interactive_content.js +113 -0
  9. package/lib/cjs/rules/href_or_on_click.js +1 -1
  10. package/lib/cjs/rules/require_href_for_link.d.ts +3 -0
  11. package/lib/cjs/rules/require_href_for_link.d.ts.map +1 -0
  12. package/lib/cjs/rules/require_href_for_link.js +59 -0
  13. package/lib/cjs/utils/constants.d.ts +7 -0
  14. package/lib/cjs/utils/constants.d.ts.map +1 -1
  15. package/lib/cjs/utils/constants.js +10 -2
  16. package/lib/esm/index.js +9 -0
  17. package/lib/esm/index.js.map +1 -1
  18. package/lib/esm/rules/a11y/tooltip_button_icon_wrap.d.ts +2 -0
  19. package/lib/esm/rules/a11y/tooltip_button_icon_wrap.js +123 -0
  20. package/lib/esm/rules/a11y/tooltip_button_icon_wrap.js.map +1 -0
  21. package/lib/esm/rules/a11y/tooltip_no_interactive_content.d.ts +2 -0
  22. package/lib/esm/rules/a11y/tooltip_no_interactive_content.js +115 -0
  23. package/lib/esm/rules/a11y/tooltip_no_interactive_content.js.map +1 -0
  24. package/lib/esm/rules/href_or_on_click.js +1 -1
  25. package/lib/esm/rules/href_or_on_click.js.map +1 -1
  26. package/lib/esm/rules/require_href_for_link.d.ts +2 -0
  27. package/lib/esm/rules/require_href_for_link.js +54 -0
  28. package/lib/esm/rules/require_href_for_link.js.map +1 -0
  29. package/lib/esm/utils/constants.d.ts +7 -0
  30. package/lib/esm/utils/constants.js +45 -29
  31. package/lib/esm/utils/constants.js.map +1 -1
  32. package/package.json +1 -1
package/README.md CHANGED
@@ -13,8 +13,14 @@ This package contains an eslint plugin that enforces some default rules for usin
13
13
 
14
14
  `<EuiButton />` should either be a button or a link, for a11y purposes. When given an `href` the button behaves as a link, otherwise an `onClick` handler is expected and it will behave as a button.
15
15
 
16
+ ### `@elastic/eui/href-or-on-click`
17
+
18
+ `<EuiButton />` should either be a button or a link, for a11y purposes. When given an `href` the button behaves as a link, otherwise an `onClick` handler is expected and it will behave as a button.
19
+
16
20
  In some cases it makes sense to disable this rule locally, such as when <kbd>cmd</kbd> + click should open the link in a new tab, but a standard click should use the `history.pushState()` API to change the URL without triggering a full page load.
17
21
 
22
+ **Exception**: `EuiLink` has to be provided with both `onClick` and `href` so that it renders as an anchor and support Ctrl/Cmd+Click to open in a new tab, and other standard link interactions. See `@elastic/eui/require-href-for-link`.
23
+
18
24
  ### `@elastic/eui/no-restricted-eui-imports`
19
25
 
20
26
  At times, we deprecate features that may need more highlighting and/or that are not possible to annotate with JSDoc `@deprecated`, e.g. JSON token imports: `@elastic/eui/dist/eui_theme_*.json` (for context: https://github.com/elastic/kibana/issues/199715#json-tokens).
@@ -175,6 +181,10 @@ Ensure the `EuiBadge` includes appropriate accessibility attributes.
175
181
  - `iconOnClickAriaLabel` is only valid when `iconOnClick` is present. The rule autofixes by removing `iconOnClickAriaLabel`.
176
182
  - `onClickAriaLabel` is only valid when `onClick` is present. The rule autofixes by removing `onClickAriaLabel`.
177
183
 
184
+ ### `@elastic/eui/require-href-for-link`
185
+
186
+ Ensure `EuiLink` components that have an `onClick` handler also include an `href` prop. Without `href`, the component does not render as a true link, which means users cannot Ctrl/Cmd+Click to open in a new tab or use other standard link interactions. The rule bails out when spread attributes are present, since `href` may be provided via the spread.
187
+
178
188
  ### `@elastic/eui/icon-accessibility-rules`
179
189
 
180
190
  Ensure the `EuiIcon` includes appropriate accessibility attributes.
@@ -182,6 +192,73 @@ Ensure the `EuiIcon` includes appropriate accessibility attributes.
182
192
  - `EuiIcon` has an accessible name via `title`, `aria-label`, or `aria-labelledby`; otherwise mark it decorative with `aria-hidden={true}`
183
193
  - Do not combine `tabIndex` with `aria-hidden`
184
194
 
195
+ ### `@elastic/eui/tooltip-button-icon-wrap`
196
+
197
+ Ensure `EuiButtonIcon` is wrapped with `EuiToolTip` for sighted users.
198
+
199
+ Browser-native tooltips (the `title` prop) are unstyled, have no delay control, and are not keyboard-accessible. Every icon button should have a visible tooltip so that sighted users who do not rely on screen readers can understand its purpose.
200
+
201
+ The rule reports two situations:
202
+
203
+ - **`title` prop is present** - remove it and use `<EuiToolTip content={…}>` instead. The rule auto-fixes by removing `title` and wrapping the button with `EuiToolTip` (or only removing `title` when the button is already wrapped).
204
+ - **No `EuiToolTip` wrapper** - the button icon has no visible tooltip. The rule auto-fixes by wrapping the button with `<EuiToolTip content={ariaLabel}>` when `aria-label` is a static string or expression.
205
+
206
+ Buttons with spread props (`{...props}`) are intentionally skipped when no `title` prop is explicitly present because their final prop set cannot be statically determined.
207
+
208
+ #### Examples
209
+
210
+ ```tsx
211
+ // ✗ Bad - browser tooltip, not keyboard-accessible
212
+ <EuiButtonIcon title="Edit item" aria-label="Edit item" iconType="pencil" />
213
+
214
+ // ✓ Fixed automatically
215
+ <EuiToolTip content="Edit item">
216
+ <EuiButtonIcon aria-label="Edit item" iconType="pencil" />
217
+ </EuiToolTip>
218
+ ```
219
+
220
+ ```tsx
221
+ // ✗ Bad - no tooltip for sighted users
222
+ <EuiButtonIcon aria-label="Delete" iconType="trash" />
223
+
224
+ // ✓ Fixed automatically
225
+ <EuiToolTip content="Delete">
226
+ <EuiButtonIcon aria-label="Delete" iconType="trash" />
227
+ </EuiToolTip>
228
+ ```
229
+
230
+ ### `@elastic/eui/tooltip-no-interactive-content`
231
+
232
+ Disallow interactive elements inside `EuiToolTip` and `EuiIconTip` `content` and `title` props.
233
+
234
+ Tooltip content is rendered in a portal with `role="tooltip"`. It is shown only on hover or focus of the trigger element and is not itself reachable by keyboard. Placing interactive elements (links, buttons, inputs, etc.) inside tooltip content makes them inaccessible to keyboard and screen-reader users. Use `EuiPopover` for content that requires interaction.
235
+
236
+ The rule checks both the `content` and `title` props of `EuiToolTip` and `EuiIconTip` for the following elements: `a`, `button`, `input`, `select`, `textarea`, `EuiLink`, `EuiButton`, `EuiButtonEmpty`, `EuiButtonIcon`, and other interactive EUI components.
237
+
238
+ Variable content (e.g. `content={tooltipContent}`) cannot be statically analyzed and is intentionally skipped.
239
+
240
+ #### Examples
241
+
242
+ ```tsx
243
+ // ✗ Bad - link inside tooltip is not keyboard-reachable
244
+ <EuiToolTip content={<EuiLink href="/docs">Learn more</EuiLink>}>
245
+ <EuiButton>Hover me</EuiButton>
246
+ </EuiToolTip>
247
+
248
+ // ✓ Use `EuiPopover` for interactive content
249
+ <EuiPopover button={<EuiButton>Click me</EuiButton>} ...>
250
+ <EuiLink href="/docs">Learn more</EuiLink>
251
+ </EuiPopover>
252
+ ```
253
+
254
+ ```tsx
255
+ // ✗ Bad - button inside `EuiIconTip` content
256
+ <EuiIconTip content={<EuiButton>Click</EuiButton>} type="info" />
257
+
258
+ // ✓ Use plain text or non-interactive JSX
259
+ <EuiIconTip content="Informational text" type="info" />
260
+ ```
261
+
185
262
  ### `@elastic/eui/prefer-tooltip-trigger-focus-test-utility`
186
263
 
187
264
  Flags `fireEvent.focus()` inside `it`/`test` blocks that also query for a tooltip element (`getByRole('tooltip')`, `queryByRole('tooltip')`, `findByRole('tooltip')` or any selector containing `euiToolTip`). Plain `fireEvent.focus` does not simulate `:focus-visible` in jsdom and will not trigger `EuiToolTip`, so tooltip focus tests will silently pass without actually showing the tooltip.
package/lib/cjs/index.js CHANGED
@@ -4,6 +4,7 @@ var _accessible_interactive_element = require("./rules/a11y/accessible_interacti
4
4
  var _callout_announce_on_mount = require("./rules/a11y/callout_announce_on_mount");
5
5
  var _consistent_is_invalid_props = require("./rules/a11y/consistent_is_invalid_props");
6
6
  var _href_or_on_click = require("./rules/href_or_on_click");
7
+ var _require_href_for_link = require("./rules/require_href_for_link");
7
8
  var _no_css_color = require("./rules/no_css_color");
8
9
  var _no_restricted_eui_imports = require("./rules/no_restricted_eui_imports");
9
10
  var _no_static_z_index = require("./rules/no_static_z_index");
@@ -17,6 +18,8 @@ var _tooltip_focusable_anchor = require("./rules/a11y/tooltip_focusable_anchor")
17
18
  var _prefer_tooltip_trigger_focus_test_utility = require("./rules/prefer_tooltip_trigger_focus_test_utility");
18
19
  var _badge_accessibility_rules = require("./rules/a11y/badge_accessibility_rules");
19
20
  var _icon_accessibility_rules = require("./rules/a11y/icon_accessibility_rules");
21
+ var _tooltip_no_interactive_content = require("./rules/a11y/tooltip_no_interactive_content");
22
+ var _tooltip_button_icon_wrap = require("./rules/a11y/tooltip_button_icon_wrap");
20
23
  /*
21
24
  * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
22
25
  * or more contributor license agreements. Licensed under the Elastic License
@@ -43,7 +46,10 @@ const config = {
43
46
  'tooltip-focusable-anchor': _tooltip_focusable_anchor.TooltipFocusableAnchor,
44
47
  'prefer-tooltip-trigger-focus-test-utility': _prefer_tooltip_trigger_focus_test_utility.PreferTooltipTriggerFocusTestUtility,
45
48
  'badge-accessibility-rules': _badge_accessibility_rules.EuiBadgeAccessibilityRules,
46
- 'icon-accessibility-rules': _icon_accessibility_rules.EuiIconAccessibilityRules
49
+ 'icon-accessibility-rules': _icon_accessibility_rules.EuiIconAccessibilityRules,
50
+ 'require-href-for-link': _require_href_for_link.RequireHrefForLink,
51
+ 'tooltip-no-interactive-content': _tooltip_no_interactive_content.TooltipNoInteractiveContent,
52
+ 'tooltip-button-icon-wrap': _tooltip_button_icon_wrap.TooltipButtonIconWrap
47
53
  },
48
54
  configs: {
49
55
  recommended: {
@@ -65,7 +71,10 @@ const config = {
65
71
  '@elastic/eui/tooltip-focusable-anchor': 'warn',
66
72
  '@elastic/eui/prefer-tooltip-trigger-focus-test-utility': 'warn',
67
73
  '@elastic/eui/badge-accessibility-rules': 'warn',
68
- '@elastic/eui/icon-accessibility-rules': 'warn'
74
+ '@elastic/eui/icon-accessibility-rules': 'warn',
75
+ '@elastic/eui/require-href-for-link': 'warn',
76
+ '@elastic/eui/tooltip-no-interactive-content': 'warn',
77
+ '@elastic/eui/tooltip-button-icon-wrap': 'warn'
69
78
  }
70
79
  }
71
80
  }
@@ -0,0 +1,3 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const TooltipButtonIconWrap: ESLintUtils.RuleModule<"useEuiToolTipInsteadOfTitle" | "wrapWithEuiToolTip", [], unknown, ESLintUtils.RuleListener>;
3
+ //# sourceMappingURL=tooltip_button_icon_wrap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tooltip_button_icon_wrap.d.ts","sourceRoot":"","sources":["../../../../src/rules/a11y/tooltip_button_icon_wrap.ts"],"names":[],"mappings":"AAQA,OAAO,EAAiB,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAqCtE,eAAO,MAAM,qBAAqB,qHAwGhC,CAAC"}
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.TooltipButtonIconWrap = void 0;
7
+ var _utils = require("@typescript-eslint/utils");
8
+ var _remove_attr = require("../../utils/remove_attr");
9
+ var _has_spread = require("../../utils/has_spread");
10
+ /*
11
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
12
+ * or more contributor license agreements. Licensed under the Elastic License
13
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
14
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
15
+ * Side Public License, v 1.
16
+ */
17
+
18
+ const BUTTON_ICON = 'EuiButtonIcon';
19
+ const TOOLTIP = 'EuiToolTip';
20
+ function isWrappedByTooltip(node) {
21
+ let current = node.parent;
22
+ while (current) {
23
+ switch (current.type) {
24
+ case 'JSXElement':
25
+ {
26
+ const el = current;
27
+ if (el.openingElement.name.type === 'JSXIdentifier' && el.openingElement.name.name === TOOLTIP) {
28
+ return true;
29
+ }
30
+ current = current.parent;
31
+ break;
32
+ }
33
+ case 'JSXFragment':
34
+ case 'JSXExpressionContainer':
35
+ case 'LogicalExpression':
36
+ case 'ConditionalExpression':
37
+ current = current.parent;
38
+ break;
39
+ default:
40
+ return false;
41
+ }
42
+ }
43
+ return false;
44
+ }
45
+ const TooltipButtonIconWrap = exports.TooltipButtonIconWrap = _utils.ESLintUtils.RuleCreator.withoutDocs({
46
+ create(context) {
47
+ return {
48
+ JSXElement(node) {
49
+ const {
50
+ openingElement
51
+ } = node;
52
+ if (openingElement.name.type !== 'JSXIdentifier' || openingElement.name.name !== BUTTON_ICON) {
53
+ return;
54
+ }
55
+ let titleAttr;
56
+ let ariaLabelAttr;
57
+ for (const attr of openingElement.attributes) {
58
+ if (attr.type !== 'JSXAttribute' || attr.name.type !== 'JSXIdentifier') continue;
59
+ if (attr.name.name === 'title') titleAttr = attr;
60
+ if (attr.name.name === 'aria-label') ariaLabelAttr = attr;
61
+ }
62
+ if (titleAttr) {
63
+ const alreadyWrapped = isWrappedByTooltip(node);
64
+ context.report({
65
+ node: openingElement,
66
+ messageId: 'useEuiToolTipInsteadOfTitle',
67
+ fix: titleAttr.value ? fixer => {
68
+ const titleValue = context.sourceCode.getText(titleAttr.value);
69
+ const [removeStart, removeEnd] = (0, _remove_attr.removeAttribute)(context, titleAttr);
70
+ if (alreadyWrapped) {
71
+ return fixer.removeRange([removeStart, removeEnd]);
72
+ }
73
+ return [fixer.removeRange([removeStart, removeEnd]), fixer.insertTextBefore(node, `<${TOOLTIP} content=${titleValue}>\n `), fixer.insertTextAfter(node, `\n</${TOOLTIP}>`)];
74
+ } : undefined
75
+ });
76
+ return;
77
+ }
78
+ if (!isWrappedByTooltip(node) && !(0, _has_spread.hasSpread)(openingElement.attributes)) {
79
+ context.report({
80
+ node: openingElement,
81
+ messageId: 'wrapWithEuiToolTip',
82
+ fix: ariaLabelAttr?.value ? fixer => {
83
+ const ariaLabelValue = context.sourceCode.getText(ariaLabelAttr.value);
84
+ return [fixer.insertTextBefore(node, `<${TOOLTIP} content=${ariaLabelValue}>\n `), fixer.insertTextAfter(node, `\n</${TOOLTIP}>`)];
85
+ } : undefined
86
+ });
87
+ }
88
+ }
89
+ };
90
+ },
91
+ meta: {
92
+ type: 'suggestion',
93
+ docs: {
94
+ description: `Ensure ${BUTTON_ICON} is wrapped with ${TOOLTIP} for sighted users`
95
+ },
96
+ fixable: 'code',
97
+ schema: [],
98
+ messages: {
99
+ useEuiToolTipInsteadOfTitle: [`Remove the \`title\` prop from ${BUTTON_ICON} and use ${TOOLTIP} instead.`, 'Browser-native tooltips are unstyled, have no delay control and are not keyboard-accessible.', `Wrap with <${TOOLTIP} content={label}> and use \`aria-label\` for screen readers.`].join(' '),
100
+ wrapWithEuiToolTip: [`${BUTTON_ICON} has no visible tooltip for sighted users.`, `Wrap with <${TOOLTIP} content={ariaLabel}> using the button's \`aria-label\` as content.`].join(' ')
101
+ }
102
+ },
103
+ defaultOptions: []
104
+ });
@@ -0,0 +1,3 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const TooltipNoInteractiveContent: ESLintUtils.RuleModule<"noInteractiveContent", [], unknown, ESLintUtils.RuleListener>;
3
+ //# sourceMappingURL=tooltip_no_interactive_content.d.ts.map
@@ -0,0 +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;AA+DtE,eAAO,MAAM,2BAA2B,uFA8DtC,CAAC"}
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.TooltipNoInteractiveContent = void 0;
7
+ var _utils = require("@typescript-eslint/utils");
8
+ var _constants = require("../../utils/constants");
9
+ /*
10
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
11
+ * or more contributor license agreements. Licensed under the Elastic License
12
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
13
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
14
+ * Side Public License, v 1.
15
+ */
16
+
17
+ const TOOLTIP_COMPONENTS = ['EuiToolTip', 'EuiIconTip'];
18
+ const TOOLTIP_CONTENT_PROPS = ['content', 'title'];
19
+ const INTERACTIVE_HTML_ELEMENTS = ['a', 'button', 'input', 'select', 'textarea'];
20
+ const CONDITIONALLY_INTERACTIVE_SET = new Set(_constants.CONDITIONALLY_INTERACTIVE_EUI_COMPONENTS);
21
+ const INTERACTIVE_ELEMENTS = new Set([...INTERACTIVE_HTML_ELEMENTS, ..._constants.INTERACTIVE_EUI_COMPONENTS.filter(c => !CONDITIONALLY_INTERACTIVE_SET.has(c))]);
22
+ function findInteractiveElement(node) {
23
+ if (node.type === 'JSXElement') {
24
+ const el = node;
25
+ const {
26
+ name
27
+ } = el.openingElement;
28
+ if (name.type === 'JSXIdentifier' && INTERACTIVE_ELEMENTS.has(name.name)) {
29
+ return el.openingElement;
30
+ }
31
+ for (const child of el.children) {
32
+ const found = findInteractiveElement(child);
33
+ if (found) return found;
34
+ }
35
+ } else if (node.type === 'JSXFragment') {
36
+ for (const child of node.children) {
37
+ const found = findInteractiveElement(child);
38
+ if (found) return found;
39
+ }
40
+ } else if (node.type === 'JSXExpressionContainer') {
41
+ const {
42
+ expression
43
+ } = node;
44
+ if (expression.type !== 'JSXEmptyExpression') {
45
+ return findInteractiveElement(expression);
46
+ }
47
+ } else if (node.type === 'LogicalExpression') {
48
+ const {
49
+ left,
50
+ right
51
+ } = node;
52
+ return findInteractiveElement(left) ?? findInteractiveElement(right);
53
+ } else if (node.type === 'ConditionalExpression') {
54
+ const {
55
+ consequent,
56
+ alternate
57
+ } = node;
58
+ return findInteractiveElement(consequent) ?? findInteractiveElement(alternate);
59
+ }
60
+ return null;
61
+ }
62
+ const TooltipNoInteractiveContent = exports.TooltipNoInteractiveContent = _utils.ESLintUtils.RuleCreator.withoutDocs({
63
+ create(context) {
64
+ return {
65
+ JSXElement(node) {
66
+ const {
67
+ openingElement
68
+ } = node;
69
+ if (openingElement.name.type !== 'JSXIdentifier' || !TOOLTIP_COMPONENTS.includes(openingElement.name.name)) {
70
+ return;
71
+ }
72
+ const componentName = openingElement.name.name;
73
+ for (const attr of openingElement.attributes) {
74
+ if (attr.type !== 'JSXAttribute' || attr.name.type !== 'JSXIdentifier' || !TOOLTIP_CONTENT_PROPS.includes(attr.name.name)) {
75
+ continue;
76
+ }
77
+ if (!attr.value || attr.value.type !== 'JSXExpressionContainer') {
78
+ continue;
79
+ }
80
+ const {
81
+ expression
82
+ } = attr.value;
83
+ if (expression.type === 'JSXEmptyExpression') {
84
+ continue;
85
+ }
86
+ const interactiveEl = findInteractiveElement(expression);
87
+ if (interactiveEl) {
88
+ context.report({
89
+ node: interactiveEl,
90
+ messageId: 'noInteractiveContent',
91
+ data: {
92
+ propName: attr.name.name,
93
+ componentName,
94
+ elementName: interactiveEl.name.name
95
+ }
96
+ });
97
+ }
98
+ }
99
+ }
100
+ };
101
+ },
102
+ meta: {
103
+ type: 'problem',
104
+ docs: {
105
+ description: 'Disallow interactive elements in `EuiToolTip` and `EuiIconTip` content'
106
+ },
107
+ schema: [],
108
+ messages: {
109
+ noInteractiveContent: '{{ elementName }} inside {{ componentName }} {{ propName }} is not keyboard-reachable. Tooltip content renders in a portal with `role="tooltip"`. Use `EuiPopover` for interactive content instead.'
110
+ }
111
+ },
112
+ defaultOptions: []
113
+ });
@@ -13,7 +13,7 @@ var _utils = require("@typescript-eslint/utils");
13
13
  * Side Public License, v 1.
14
14
  */
15
15
 
16
- const componentNames = ['EuiButton', 'EuiButtonEmpty', 'EuiLink', 'EuiBadge'];
16
+ const componentNames = ['EuiButton', 'EuiButtonEmpty', 'EuiBadge'];
17
17
  const HrefOnClick = exports.HrefOnClick = _utils.ESLintUtils.RuleCreator.withoutDocs({
18
18
  create(context) {
19
19
  return {
@@ -0,0 +1,3 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const RequireHrefForLink: ESLintUtils.RuleModule<"requireHrefForLink", [], unknown, ESLintUtils.RuleListener>;
3
+ //# sourceMappingURL=require_href_for_link.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require_href_for_link.d.ts","sourceRoot":"","sources":["../../../src/rules/require_href_for_link.ts"],"names":[],"mappings":"AAQA,OAAO,EAAY,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAIjE,eAAO,MAAM,kBAAkB,qFAmD7B,CAAC"}
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.RequireHrefForLink = void 0;
7
+ var _utils = require("@typescript-eslint/utils");
8
+ var _has_spread = require("../utils/has_spread");
9
+ /*
10
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
11
+ * or more contributor license agreements. Licensed under the Elastic License
12
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
13
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
14
+ * Side Public License, v 1.
15
+ */
16
+
17
+ const RequireHrefForLink = exports.RequireHrefForLink = _utils.ESLintUtils.RuleCreator.withoutDocs({
18
+ create(context) {
19
+ return {
20
+ JSXOpeningElement(node) {
21
+ if (node.name.type !== 'JSXIdentifier' || node.name.name !== 'EuiLink') {
22
+ return;
23
+ }
24
+
25
+ // Bail out if props are spread — we can't statically determine
26
+ // whether `href` is provided via the spread
27
+ if ((0, _has_spread.hasSpread)(node.attributes)) {
28
+ return;
29
+ }
30
+
31
+ // Check if the node has `href` and `onClick` attributes
32
+ const hasHref = node.attributes.some(attr => attr.type === 'JSXAttribute' && attr.name.name === 'href');
33
+ const hasOnClick = node.attributes.some(attr => attr.type === 'JSXAttribute' && attr.name.name === 'onClick');
34
+
35
+ // Report an issue if `onClick` is present without `href`
36
+ if (hasOnClick && !hasHref) {
37
+ context.report({
38
+ node,
39
+ messageId: 'requireHrefForLink',
40
+ data: {
41
+ name: node.name.name
42
+ }
43
+ });
44
+ }
45
+ }
46
+ };
47
+ },
48
+ meta: {
49
+ type: 'suggestion',
50
+ docs: {
51
+ description: 'Recommend including `href` alongside `onClick` on EuiLink so that Ctrl/Cmd+Click (open link in new tab) works.'
52
+ },
53
+ schema: [],
54
+ messages: {
55
+ requireHrefForLink: '<{{name}}> has `onClick` but no `href`. Consider adding `href` so that the component renders as a link and supports Ctrl/Cmd+Click / "Open link in new tab".'
56
+ }
57
+ },
58
+ defaultOptions: []
59
+ });
@@ -24,4 +24,11 @@ export declare const NON_INTERACTIVE_HTML_TAGS: string[];
24
24
  * This list should be kept up to date with EUI's interactive component offerings.
25
25
  */
26
26
  export declare const INTERACTIVE_EUI_COMPONENTS: string[];
27
+ /**
28
+ * EUI components that are only interactive when `onClick` or `href` is provided.
29
+ * Without those props they render as a plain non-focusable element (span/div),
30
+ * so rules that need to distinguish unconditionally-interactive components should
31
+ * exclude these.
32
+ */
33
+ export declare const CONDITIONALLY_INTERACTIVE_EUI_COMPONENTS: string[];
27
34
  //# 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,UAyCtC,CAAC"}
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;;;;;GAKG;AACH,eAAO,MAAM,wCAAwC,UAIpD,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 = void 0;
6
+ exports.NON_INTERACTIVE_HTML_TAGS = exports.INTERACTIVE_EUI_COMPONENTS = exports.CONDITIONALLY_INTERACTIVE_EUI_COMPONENTS = 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
@@ -38,4 +38,12 @@ const NON_INTERACTIVE_HTML_TAGS = exports.NON_INTERACTIVE_HTML_TAGS = ['div', 's
38
38
  *
39
39
  * This list should be kept up to date with EUI's interactive component offerings.
40
40
  */
41
- const INTERACTIVE_EUI_COMPONENTS = exports.INTERACTIVE_EUI_COMPONENTS = ['EuiLink', 'EuiButton', 'EuiButtonEmpty', 'EuiButtonIcon', 'EuiFacetButton', 'EuiHeaderLink', 'EuiHeaderSectionItemButton', 'EuiHeaderLogo', 'EuiListGroupItem', 'EuiPinnableListGroup', 'EuiSideNav', 'EuiBreadcrumbs', 'EuiTab', 'EuiContextMenuItem', 'EuiKeyPadMenuItem', 'EuiPagination', 'EuiTreeView', 'EuiStepHorizontal', 'EuiCard', 'EuiCheckableCard', 'EuiBasicTable', 'EuiInMemoryTable', 'EuiFilterButton', 'EuiFilterSelectItem', 'EuiFilterSelectable', 'EuiBadge', 'EuiBetaBadge', 'EuiSelectable', 'EuiComboBox', 'EuiSuperSelect', 'EuiSelect', 'EuiCheckbox', 'EuiRadio', 'EuiSwitch', 'EuiButtonGroup', 'EuiRange', 'EuiDualRange', 'EuiColorPicker', 'EuiDatePicker', 'EuiSuperDatePicker'];
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
+
43
+ /**
44
+ * EUI components that are only interactive when `onClick` or `href` is provided.
45
+ * Without those props they render as a plain non-focusable element (span/div),
46
+ * so rules that need to distinguish unconditionally-interactive components should
47
+ * exclude these.
48
+ */
49
+ const CONDITIONALLY_INTERACTIVE_EUI_COMPONENTS = exports.CONDITIONALLY_INTERACTIVE_EUI_COMPONENTS = ['EuiBadge', 'EuiBetaBadge', 'EuiCard'];
package/lib/esm/index.js CHANGED
@@ -11,6 +11,7 @@ const accessible_interactive_element_1 = require("./rules/a11y/accessible_intera
11
11
  const callout_announce_on_mount_1 = require("./rules/a11y/callout_announce_on_mount");
12
12
  const consistent_is_invalid_props_1 = require("./rules/a11y/consistent_is_invalid_props");
13
13
  const href_or_on_click_1 = require("./rules/href_or_on_click");
14
+ const require_href_for_link_1 = require("./rules/require_href_for_link");
14
15
  const no_css_color_1 = require("./rules/no_css_color");
15
16
  const no_restricted_eui_imports_1 = require("./rules/no_restricted_eui_imports");
16
17
  const no_static_z_index_1 = require("./rules/no_static_z_index");
@@ -24,6 +25,8 @@ const tooltip_focusable_anchor_1 = require("./rules/a11y/tooltip_focusable_ancho
24
25
  const prefer_tooltip_trigger_focus_test_utility_1 = require("./rules/prefer_tooltip_trigger_focus_test_utility");
25
26
  const badge_accessibility_rules_1 = require("./rules/a11y/badge_accessibility_rules");
26
27
  const icon_accessibility_rules_1 = require("./rules/a11y/icon_accessibility_rules");
28
+ const tooltip_no_interactive_content_1 = require("./rules/a11y/tooltip_no_interactive_content");
29
+ const tooltip_button_icon_wrap_1 = require("./rules/a11y/tooltip_button_icon_wrap");
27
30
  const config = {
28
31
  rules: {
29
32
  'accessible-interactive-element': accessible_interactive_element_1.AccessibleInteractiveElements,
@@ -43,6 +46,9 @@ const config = {
43
46
  'prefer-tooltip-trigger-focus-test-utility': prefer_tooltip_trigger_focus_test_utility_1.PreferTooltipTriggerFocusTestUtility,
44
47
  'badge-accessibility-rules': badge_accessibility_rules_1.EuiBadgeAccessibilityRules,
45
48
  'icon-accessibility-rules': icon_accessibility_rules_1.EuiIconAccessibilityRules,
49
+ 'require-href-for-link': require_href_for_link_1.RequireHrefForLink,
50
+ 'tooltip-no-interactive-content': tooltip_no_interactive_content_1.TooltipNoInteractiveContent,
51
+ 'tooltip-button-icon-wrap': tooltip_button_icon_wrap_1.TooltipButtonIconWrap,
46
52
  },
47
53
  configs: {
48
54
  recommended: {
@@ -65,6 +71,9 @@ const config = {
65
71
  '@elastic/eui/prefer-tooltip-trigger-focus-test-utility': 'warn',
66
72
  '@elastic/eui/badge-accessibility-rules': 'warn',
67
73
  '@elastic/eui/icon-accessibility-rules': 'warn',
74
+ '@elastic/eui/require-href-for-link': 'warn',
75
+ '@elastic/eui/tooltip-no-interactive-content': 'warn',
76
+ '@elastic/eui/tooltip-button-icon-wrap': 'warn',
68
77
  },
69
78
  },
70
79
  },
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;AAEH,gGAA4F;AAC5F,sFAAgF;AAChF,0FAAoF;AACpF,+DAAuD;AACvD,uDAAkD;AAClD,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;AAElF,MAAM,MAAM,GAAG;IACb,KAAK,EAAE;QACL,gCAAgC,EAAE,8DAA6B;QAC/D,2BAA2B,EAAE,kDAAsB;QACnD,6BAA6B,EAAE,sDAAwB;QACvD,kBAAkB,EAAE,8BAAW;QAC/B,cAAc,EAAE,yBAAU;QAC1B,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;KACtD;IACD,OAAO,EAAE;QACP,WAAW,EAAE;YACX,OAAO,EAAE,CAAC,4BAA4B,CAAC;YACvC,KAAK,EAAE;gBACL,6CAA6C,EAAE,MAAM;gBACrD,wCAAwC,EAAE,MAAM;gBAChD,0CAA0C,EAAE,MAAM;gBAClD,+BAA+B,EAAE,MAAM;gBACvC,2BAA2B,EAAE,MAAM;gBACnC,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;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,0FAAoF;AACpF,+DAAuD;AACvD,yEAAmE;AACnE,uDAAkD;AAClD,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;AAE9E,MAAM,MAAM,GAAG;IACb,KAAK,EAAE;QACL,gCAAgC,EAAE,8DAA6B;QAC/D,2BAA2B,EAAE,kDAAsB;QACnD,6BAA6B,EAAE,sDAAwB;QACvD,kBAAkB,EAAE,8BAAW;QAC/B,cAAc,EAAE,yBAAU;QAC1B,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;KAClD;IACD,OAAO,EAAE;QACP,WAAW,EAAE;YACX,OAAO,EAAE,CAAC,4BAA4B,CAAC;YACvC,KAAK,EAAE;gBACL,6CAA6C,EAAE,MAAM;gBACrD,wCAAwC,EAAE,MAAM;gBAChD,0CAA0C,EAAE,MAAM;gBAClD,+BAA+B,EAAE,MAAM;gBACvC,2BAA2B,EAAE,MAAM;gBACnC,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,2 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const TooltipButtonIconWrap: ESLintUtils.RuleModule<"useEuiToolTipInsteadOfTitle" | "wrapWithEuiToolTip", [], unknown, ESLintUtils.RuleListener>;
@@ -0,0 +1,123 @@
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.TooltipButtonIconWrap = void 0;
11
+ const utils_1 = require("@typescript-eslint/utils");
12
+ const remove_attr_1 = require("../../utils/remove_attr");
13
+ const has_spread_1 = require("../../utils/has_spread");
14
+ const BUTTON_ICON = 'EuiButtonIcon';
15
+ const TOOLTIP = 'EuiToolTip';
16
+ function isWrappedByTooltip(node) {
17
+ let current = node.parent;
18
+ while (current) {
19
+ switch (current.type) {
20
+ case 'JSXElement': {
21
+ const el = current;
22
+ if (el.openingElement.name.type === 'JSXIdentifier' &&
23
+ el.openingElement.name.name === TOOLTIP) {
24
+ return true;
25
+ }
26
+ current = current.parent;
27
+ break;
28
+ }
29
+ case 'JSXFragment':
30
+ case 'JSXExpressionContainer':
31
+ case 'LogicalExpression':
32
+ case 'ConditionalExpression':
33
+ current = current.parent;
34
+ break;
35
+ default:
36
+ return false;
37
+ }
38
+ }
39
+ return false;
40
+ }
41
+ exports.TooltipButtonIconWrap = utils_1.ESLintUtils.RuleCreator.withoutDocs({
42
+ create(context) {
43
+ return {
44
+ JSXElement(node) {
45
+ const { openingElement } = node;
46
+ if (openingElement.name.type !== 'JSXIdentifier' ||
47
+ openingElement.name.name !== BUTTON_ICON) {
48
+ return;
49
+ }
50
+ let titleAttr;
51
+ let ariaLabelAttr;
52
+ for (const attr of openingElement.attributes) {
53
+ if (attr.type !== 'JSXAttribute' ||
54
+ attr.name.type !== 'JSXIdentifier')
55
+ continue;
56
+ if (attr.name.name === 'title')
57
+ titleAttr = attr;
58
+ if (attr.name.name === 'aria-label')
59
+ ariaLabelAttr = attr;
60
+ }
61
+ if (titleAttr) {
62
+ const alreadyWrapped = isWrappedByTooltip(node);
63
+ context.report({
64
+ node: openingElement,
65
+ messageId: 'useEuiToolTipInsteadOfTitle',
66
+ fix: titleAttr.value
67
+ ? (fixer) => {
68
+ const titleValue = context.sourceCode.getText(titleAttr.value);
69
+ const [removeStart, removeEnd] = (0, remove_attr_1.removeAttribute)(context, titleAttr);
70
+ if (alreadyWrapped) {
71
+ return fixer.removeRange([removeStart, removeEnd]);
72
+ }
73
+ return [
74
+ fixer.removeRange([removeStart, removeEnd]),
75
+ fixer.insertTextBefore(node, `<${TOOLTIP} content=${titleValue}>\n `),
76
+ fixer.insertTextAfter(node, `\n</${TOOLTIP}>`),
77
+ ];
78
+ }
79
+ : undefined,
80
+ });
81
+ return;
82
+ }
83
+ if (!isWrappedByTooltip(node) &&
84
+ !(0, has_spread_1.hasSpread)(openingElement.attributes)) {
85
+ context.report({
86
+ node: openingElement,
87
+ messageId: 'wrapWithEuiToolTip',
88
+ fix: ariaLabelAttr?.value
89
+ ? (fixer) => {
90
+ const ariaLabelValue = context.sourceCode.getText(ariaLabelAttr.value);
91
+ return [
92
+ fixer.insertTextBefore(node, `<${TOOLTIP} content=${ariaLabelValue}>\n `),
93
+ fixer.insertTextAfter(node, `\n</${TOOLTIP}>`),
94
+ ];
95
+ }
96
+ : undefined,
97
+ });
98
+ }
99
+ },
100
+ };
101
+ },
102
+ meta: {
103
+ type: 'suggestion',
104
+ docs: {
105
+ description: `Ensure ${BUTTON_ICON} is wrapped with ${TOOLTIP} for sighted users`,
106
+ },
107
+ fixable: 'code',
108
+ schema: [],
109
+ messages: {
110
+ useEuiToolTipInsteadOfTitle: [
111
+ `Remove the \`title\` prop from ${BUTTON_ICON} and use ${TOOLTIP} instead.`,
112
+ 'Browser-native tooltips are unstyled, have no delay control and are not keyboard-accessible.',
113
+ `Wrap with <${TOOLTIP} content={label}> and use \`aria-label\` for screen readers.`,
114
+ ].join(' '),
115
+ wrapWithEuiToolTip: [
116
+ `${BUTTON_ICON} has no visible tooltip for sighted users.`,
117
+ `Wrap with <${TOOLTIP} content={ariaLabel}> using the button's \`aria-label\` as content.`,
118
+ ].join(' '),
119
+ },
120
+ },
121
+ defaultOptions: [],
122
+ });
123
+ //# sourceMappingURL=tooltip_button_icon_wrap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tooltip_button_icon_wrap.js","sourceRoot":"","sources":["../../../../src/rules/a11y/tooltip_button_icon_wrap.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const TooltipNoInteractiveContent: ESLintUtils.RuleModule<"noInteractiveContent", [], unknown, ESLintUtils.RuleListener>;
@@ -0,0 +1,115 @@
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.TooltipNoInteractiveContent = void 0;
11
+ const utils_1 = require("@typescript-eslint/utils");
12
+ const constants_1 = require("../../utils/constants");
13
+ const TOOLTIP_COMPONENTS = ['EuiToolTip', 'EuiIconTip'];
14
+ const TOOLTIP_CONTENT_PROPS = ['content', 'title'];
15
+ const INTERACTIVE_HTML_ELEMENTS = [
16
+ 'a',
17
+ 'button',
18
+ 'input',
19
+ 'select',
20
+ 'textarea',
21
+ ];
22
+ const CONDITIONALLY_INTERACTIVE_SET = new Set(constants_1.CONDITIONALLY_INTERACTIVE_EUI_COMPONENTS);
23
+ const INTERACTIVE_ELEMENTS = new Set([
24
+ ...INTERACTIVE_HTML_ELEMENTS,
25
+ ...constants_1.INTERACTIVE_EUI_COMPONENTS.filter((c) => !CONDITIONALLY_INTERACTIVE_SET.has(c)),
26
+ ]);
27
+ function findInteractiveElement(node) {
28
+ if (node.type === 'JSXElement') {
29
+ const el = node;
30
+ const { name } = el.openingElement;
31
+ if (name.type === 'JSXIdentifier' && INTERACTIVE_ELEMENTS.has(name.name)) {
32
+ return el.openingElement;
33
+ }
34
+ for (const child of el.children) {
35
+ const found = findInteractiveElement(child);
36
+ if (found)
37
+ return found;
38
+ }
39
+ }
40
+ else if (node.type === 'JSXFragment') {
41
+ for (const child of node.children) {
42
+ const found = findInteractiveElement(child);
43
+ if (found)
44
+ return found;
45
+ }
46
+ }
47
+ else if (node.type === 'JSXExpressionContainer') {
48
+ const { expression } = node;
49
+ if (expression.type !== 'JSXEmptyExpression') {
50
+ return findInteractiveElement(expression);
51
+ }
52
+ }
53
+ else if (node.type === 'LogicalExpression') {
54
+ const { left, right } = node;
55
+ return findInteractiveElement(left) ?? findInteractiveElement(right);
56
+ }
57
+ else if (node.type === 'ConditionalExpression') {
58
+ const { consequent, alternate } = node;
59
+ return (findInteractiveElement(consequent) ?? findInteractiveElement(alternate));
60
+ }
61
+ return null;
62
+ }
63
+ exports.TooltipNoInteractiveContent = utils_1.ESLintUtils.RuleCreator.withoutDocs({
64
+ create(context) {
65
+ return {
66
+ JSXElement(node) {
67
+ const { openingElement } = node;
68
+ if (openingElement.name.type !== 'JSXIdentifier' ||
69
+ !TOOLTIP_COMPONENTS.includes(openingElement.name.name)) {
70
+ return;
71
+ }
72
+ const componentName = openingElement.name.name;
73
+ for (const attr of openingElement.attributes) {
74
+ if (attr.type !== 'JSXAttribute' ||
75
+ attr.name.type !== 'JSXIdentifier' ||
76
+ !TOOLTIP_CONTENT_PROPS.includes(attr.name.name)) {
77
+ continue;
78
+ }
79
+ if (!attr.value || attr.value.type !== 'JSXExpressionContainer') {
80
+ continue;
81
+ }
82
+ const { expression } = attr.value;
83
+ if (expression.type === 'JSXEmptyExpression') {
84
+ continue;
85
+ }
86
+ const interactiveEl = findInteractiveElement(expression);
87
+ if (interactiveEl) {
88
+ context.report({
89
+ node: interactiveEl,
90
+ messageId: 'noInteractiveContent',
91
+ data: {
92
+ propName: attr.name.name,
93
+ componentName,
94
+ elementName: interactiveEl.name
95
+ .name,
96
+ },
97
+ });
98
+ }
99
+ }
100
+ },
101
+ };
102
+ },
103
+ meta: {
104
+ type: 'problem',
105
+ docs: {
106
+ description: 'Disallow interactive elements in `EuiToolTip` and `EuiIconTip` content',
107
+ },
108
+ schema: [],
109
+ messages: {
110
+ noInteractiveContent: '{{ elementName }} inside {{ componentName }} {{ propName }} is not keyboard-reachable. Tooltip content renders in a portal with `role="tooltip"`. Use `EuiPopover` for interactive content instead.',
111
+ },
112
+ },
113
+ defaultOptions: [],
114
+ });
115
+ //# sourceMappingURL=tooltip_no_interactive_content.js.map
@@ -0,0 +1 @@
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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"}
@@ -9,7 +9,7 @@
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.HrefOnClick = void 0;
11
11
  const utils_1 = require("@typescript-eslint/utils");
12
- const componentNames = ['EuiButton', 'EuiButtonEmpty', 'EuiLink', 'EuiBadge'];
12
+ const componentNames = ['EuiButton', 'EuiButtonEmpty', 'EuiBadge'];
13
13
  exports.HrefOnClick = utils_1.ESLintUtils.RuleCreator.withoutDocs({
14
14
  create(context) {
15
15
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"href_or_on_click.js","sourceRoot":"","sources":["../../../src/rules/href_or_on_click.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;AAEH,oDAAiE;AAEjE,MAAM,cAAc,GAAG,CAAC,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;AAEjE,QAAA,WAAW,GAAG,mBAAW,CAAC,WAAW,CAAC,WAAW,CAAC;IAC7D,MAAM,CAAC,OAAO;QACZ,OAAO;YACL,iBAAiB,CAAC,IAAgC;gBAChD,uDAAuD;gBACvD,IACE,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,eAAe;oBAClC,CAAC,cAAc,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EACxC,CAAC;oBACD,OAAO;gBACT,CAAC;gBAED,6DAA6D;gBAC7D,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAClC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,cAAc,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,CACpE,CAAC;gBACF,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CACrC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,cAAc,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,CACvE,CAAC;gBAEF,iDAAiD;gBACjD,IAAI,OAAO,IAAI,UAAU,EAAE,CAAC;oBAC1B,OAAO,CAAC,MAAM,CAAC;wBACb,IAAI;wBACJ,SAAS,EAAE,eAAe;wBAC1B,IAAI,EAAE;4BACJ,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI;yBACrB;qBACF,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;IACD,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACJ,WAAW,EACT,2EAA2E;SAC9E;QACD,MAAM,EAAE,EAAE;QACV,QAAQ,EAAE;YACR,aAAa,EACX,6MAA6M;SAChN;KACF;IACD,cAAc,EAAE,EAAE;CACnB,CAAC,CAAC"}
1
+ {"version":3,"file":"href_or_on_click.js","sourceRoot":"","sources":["../../../src/rules/href_or_on_click.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;AAEH,oDAAiE;AAEjE,MAAM,cAAc,GAAG,CAAC,WAAW,EAAE,gBAAgB,EAAE,UAAU,CAAC,CAAC;AAEtD,QAAA,WAAW,GAAG,mBAAW,CAAC,WAAW,CAAC,WAAW,CAAC;IAC7D,MAAM,CAAC,OAAO;QACZ,OAAO;YACL,iBAAiB,CAAC,IAAgC;gBAChD,uDAAuD;gBACvD,IACE,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,eAAe;oBAClC,CAAC,cAAc,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EACxC,CAAC;oBACD,OAAO;gBACT,CAAC;gBAED,6DAA6D;gBAC7D,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAClC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,cAAc,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,CACpE,CAAC;gBACF,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CACrC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,cAAc,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,CACvE,CAAC;gBAEF,iDAAiD;gBACjD,IAAI,OAAO,IAAI,UAAU,EAAE,CAAC;oBAC1B,OAAO,CAAC,MAAM,CAAC;wBACb,IAAI;wBACJ,SAAS,EAAE,eAAe;wBAC1B,IAAI,EAAE;4BACJ,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI;yBACrB;qBACF,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;IACD,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACJ,WAAW,EACT,2EAA2E;SAC9E;QACD,MAAM,EAAE,EAAE;QACV,QAAQ,EAAE;YACR,aAAa,EACX,6MAA6M;SAChN;KACF;IACD,cAAc,EAAE,EAAE;CACnB,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const RequireHrefForLink: ESLintUtils.RuleModule<"requireHrefForLink", [], unknown, ESLintUtils.RuleListener>;
@@ -0,0 +1,54 @@
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.RequireHrefForLink = void 0;
11
+ const utils_1 = require("@typescript-eslint/utils");
12
+ const has_spread_1 = require("../utils/has_spread");
13
+ exports.RequireHrefForLink = utils_1.ESLintUtils.RuleCreator.withoutDocs({
14
+ create(context) {
15
+ return {
16
+ JSXOpeningElement(node) {
17
+ if (node.name.type !== 'JSXIdentifier' ||
18
+ node.name.name !== 'EuiLink') {
19
+ return;
20
+ }
21
+ // Bail out if props are spread — we can't statically determine
22
+ // whether `href` is provided via the spread
23
+ if ((0, has_spread_1.hasSpread)(node.attributes)) {
24
+ return;
25
+ }
26
+ // Check if the node has `href` and `onClick` attributes
27
+ const hasHref = node.attributes.some((attr) => attr.type === 'JSXAttribute' && attr.name.name === 'href');
28
+ const hasOnClick = node.attributes.some((attr) => attr.type === 'JSXAttribute' && attr.name.name === 'onClick');
29
+ // Report an issue if `onClick` is present without `href`
30
+ if (hasOnClick && !hasHref) {
31
+ context.report({
32
+ node,
33
+ messageId: 'requireHrefForLink',
34
+ data: {
35
+ name: node.name.name,
36
+ },
37
+ });
38
+ }
39
+ },
40
+ };
41
+ },
42
+ meta: {
43
+ type: 'suggestion',
44
+ docs: {
45
+ description: 'Recommend including `href` alongside `onClick` on EuiLink so that Ctrl/Cmd+Click (open link in new tab) works.',
46
+ },
47
+ schema: [],
48
+ messages: {
49
+ requireHrefForLink: '<{{name}}> has `onClick` but no `href`. Consider adding `href` so that the component renders as a link and supports Ctrl/Cmd+Click / "Open link in new tab".',
50
+ },
51
+ },
52
+ defaultOptions: [],
53
+ });
54
+ //# sourceMappingURL=require_href_for_link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require_href_for_link.js","sourceRoot":"","sources":["../../../src/rules/require_href_for_link.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;AAEH,oDAAiE;AAEjE,oDAAgD;AAEnC,QAAA,kBAAkB,GAAG,mBAAW,CAAC,WAAW,CAAC,WAAW,CAAC;IACpE,MAAM,CAAC,OAAO;QACZ,OAAO;YACL,iBAAiB,CAAC,IAAgC;gBAChD,IACE,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,eAAe;oBAClC,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,EAC5B,CAAC;oBACD,OAAO;gBACT,CAAC;gBAED,+DAA+D;gBAC/D,4CAA4C;gBAC5C,IAAI,IAAA,sBAAS,EAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;oBAC/B,OAAO;gBACT,CAAC;gBAED,wDAAwD;gBACxD,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAClC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,cAAc,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,CACpE,CAAC;gBACF,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CACrC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,cAAc,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,CACvE,CAAC;gBAEF,yDAAyD;gBACzD,IAAI,UAAU,IAAI,CAAC,OAAO,EAAE,CAAC;oBAC3B,OAAO,CAAC,MAAM,CAAC;wBACb,IAAI;wBACJ,SAAS,EAAE,oBAAoB;wBAC/B,IAAI,EAAE;4BACJ,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI;yBACrB;qBACF,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;IACD,IAAI,EAAE;QACJ,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE;YACJ,WAAW,EACT,gHAAgH;SACnH;QACD,MAAM,EAAE,EAAE;QACV,QAAQ,EAAE;YACR,kBAAkB,EAChB,8JAA8J;SACjK;KACF;IACD,cAAc,EAAE,EAAE;CACnB,CAAC,CAAC"}
@@ -24,3 +24,10 @@ export declare const NON_INTERACTIVE_HTML_TAGS: string[];
24
24
  * This list should be kept up to date with EUI's interactive component offerings.
25
25
  */
26
26
  export declare const INTERACTIVE_EUI_COMPONENTS: string[];
27
+ /**
28
+ * EUI components that are only interactive when `onClick` or `href` is provided.
29
+ * Without those props they render as a plain non-focusable element (span/div),
30
+ * so rules that need to distinguish unconditionally-interactive components should
31
+ * exclude these.
32
+ */
33
+ export declare const CONDITIONALLY_INTERACTIVE_EUI_COMPONENTS: string[];
@@ -7,7 +7,7 @@
7
7
  * Side Public License, v 1.
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.INTERACTIVE_EUI_COMPONENTS = exports.NON_INTERACTIVE_HTML_TAGS = void 0;
10
+ exports.CONDITIONALLY_INTERACTIVE_EUI_COMPONENTS = exports.INTERACTIVE_EUI_COMPONENTS = exports.NON_INTERACTIVE_HTML_TAGS = void 0;
11
11
  /**
12
12
  * A list of standard HTML tags that are considered **non-interactive** elements.
13
13
  *
@@ -75,45 +75,61 @@ exports.NON_INTERACTIVE_HTML_TAGS = [
75
75
  * This list should be kept up to date with EUI's interactive component offerings.
76
76
  */
77
77
  exports.INTERACTIVE_EUI_COMPONENTS = [
78
- 'EuiLink',
78
+ 'EuiBadge',
79
+ 'EuiBasicTable',
80
+ 'EuiBetaBadge',
81
+ 'EuiBreadcrumbs',
79
82
  'EuiButton',
80
83
  'EuiButtonEmpty',
84
+ 'EuiButtonGroup',
81
85
  'EuiButtonIcon',
86
+ 'EuiCard',
87
+ 'EuiCheckableCard',
88
+ 'EuiCheckbox',
89
+ 'EuiColorPicker',
90
+ 'EuiComboBox',
91
+ 'EuiContextMenuItem',
92
+ 'EuiDatePicker',
93
+ 'EuiDualRange',
82
94
  'EuiFacetButton',
95
+ 'EuiFieldNumber',
96
+ 'EuiFieldPassword',
97
+ 'EuiFieldSearch',
98
+ 'EuiFieldText',
99
+ 'EuiFilterButton',
100
+ 'EuiFilterSelectItem',
101
+ 'EuiFilterSelectable',
83
102
  'EuiHeaderLink',
84
- 'EuiHeaderSectionItemButton',
85
103
  'EuiHeaderLogo',
104
+ 'EuiHeaderSectionItemButton',
105
+ 'EuiInMemoryTable',
106
+ 'EuiKeyPadMenuItem',
107
+ 'EuiLink',
86
108
  'EuiListGroupItem',
109
+ 'EuiPagination',
87
110
  'EuiPinnableListGroup',
111
+ 'EuiRadio',
112
+ 'EuiRange',
113
+ 'EuiSelect',
114
+ 'EuiSelectable',
88
115
  'EuiSideNav',
89
- 'EuiBreadcrumbs',
90
- 'EuiTab',
91
- 'EuiContextMenuItem',
92
- 'EuiKeyPadMenuItem',
93
- 'EuiPagination',
94
- 'EuiTreeView',
95
116
  'EuiStepHorizontal',
96
- 'EuiCard',
97
- 'EuiCheckableCard',
98
- 'EuiBasicTable',
99
- 'EuiInMemoryTable',
100
- 'EuiFilterButton',
101
- 'EuiFilterSelectItem',
102
- 'EuiFilterSelectable',
103
- 'EuiBadge',
104
- 'EuiBetaBadge',
105
- 'EuiSelectable',
106
- 'EuiComboBox',
117
+ 'EuiSuperDatePicker',
107
118
  'EuiSuperSelect',
108
- 'EuiSelect',
109
- 'EuiCheckbox',
110
- 'EuiRadio',
111
119
  'EuiSwitch',
112
- 'EuiButtonGroup',
113
- 'EuiRange',
114
- 'EuiDualRange',
115
- 'EuiColorPicker',
116
- 'EuiDatePicker',
117
- 'EuiSuperDatePicker'
120
+ 'EuiTab',
121
+ 'EuiTextArea',
122
+ 'EuiTreeView'
123
+ ];
124
+ /**
125
+ * EUI components that are only interactive when `onClick` or `href` is provided.
126
+ * Without those props they render as a plain non-focusable element (span/div),
127
+ * so rules that need to distinguish unconditionally-interactive components should
128
+ * exclude these.
129
+ */
130
+ exports.CONDITIONALLY_INTERACTIVE_EUI_COMPONENTS = [
131
+ 'EuiBadge',
132
+ 'EuiBetaBadge',
133
+ 'EuiCard',
118
134
  ];
119
135
  //# sourceMappingURL=constants.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../src/utils/constants.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;AAEH;;;;;;;;;;;;GAYG;AACU,QAAA,yBAAyB,GAAG;IACvC,KAAK;IACL,MAAM;IACN,GAAG;IACH,SAAS;IACT,OAAO;IACP,YAAY;IACZ,IAAI;IACJ,SAAS;IACT,MAAM;IACN,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,YAAY;IACZ,QAAQ;IACR,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,KAAK;IACL,IAAI;IACJ,MAAM;IACN,KAAK;IACL,IAAI;IACJ,KAAK;IACL,SAAS;IACT,OAAO;IACP,QAAQ;IACR,KAAK;IACL,KAAK;IACL,OAAO;IACP,OAAO;IACP,IAAI;IACJ,OAAO;IACP,IAAI;IACJ,OAAO;IACP,IAAI;IACJ,IAAI;CACL,CAAC;AAEF;;;;;;;;;;GAUG;AACU,QAAA,0BAA0B,GAAG;IACxC,SAAS;IACT,WAAW;IACX,gBAAgB;IAChB,eAAe;IACf,gBAAgB;IAChB,eAAe;IACf,4BAA4B;IAC5B,eAAe;IACf,kBAAkB;IAClB,sBAAsB;IACtB,YAAY;IACZ,gBAAgB;IAChB,QAAQ;IACR,oBAAoB;IACpB,mBAAmB;IACnB,eAAe;IACf,aAAa;IACb,mBAAmB;IACnB,SAAS;IACT,kBAAkB;IAClB,eAAe;IACf,kBAAkB;IAClB,iBAAiB;IACjB,qBAAqB;IACrB,qBAAqB;IACrB,UAAU;IACV,cAAc;IACd,eAAe;IACf,aAAa;IACb,gBAAgB;IAChB,WAAW;IACX,aAAa;IACb,UAAU;IACV,WAAW;IACX,gBAAgB;IAChB,UAAU;IACV,cAAc;IACd,gBAAgB;IAChB,eAAe;IACf,oBAAoB;CACrB,CAAC"}
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../src/utils/constants.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;AAEH;;;;;;;;;;;;GAYG;AACU,QAAA,yBAAyB,GAAG;IACvC,KAAK;IACL,MAAM;IACN,GAAG;IACH,SAAS;IACT,OAAO;IACP,YAAY;IACZ,IAAI;IACJ,SAAS;IACT,MAAM;IACN,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,YAAY;IACZ,QAAQ;IACR,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,KAAK;IACL,IAAI;IACJ,MAAM;IACN,KAAK;IACL,IAAI;IACJ,KAAK;IACL,SAAS;IACT,OAAO;IACP,QAAQ;IACR,KAAK;IACL,KAAK;IACL,OAAO;IACP,OAAO;IACP,IAAI;IACJ,OAAO;IACP,IAAI;IACJ,OAAO;IACP,IAAI;IACJ,IAAI;CACL,CAAC;AAEF;;;;;;;;;;GAUG;AACU,QAAA,0BAA0B,GAAG;IACxC,UAAU;IACV,eAAe;IACf,cAAc;IACd,gBAAgB;IAChB,WAAW;IACX,gBAAgB;IAChB,gBAAgB;IAChB,eAAe;IACf,SAAS;IACT,kBAAkB;IAClB,aAAa;IACb,gBAAgB;IAChB,aAAa;IACb,oBAAoB;IACpB,eAAe;IACf,cAAc;IACd,gBAAgB;IAChB,gBAAgB;IAChB,kBAAkB;IAClB,gBAAgB;IAChB,cAAc;IACd,iBAAiB;IACjB,qBAAqB;IACrB,qBAAqB;IACrB,eAAe;IACf,eAAe;IACf,4BAA4B;IAC5B,kBAAkB;IAClB,mBAAmB;IACnB,SAAS;IACT,kBAAkB;IAClB,eAAe;IACf,sBAAsB;IACtB,UAAU;IACV,UAAU;IACV,WAAW;IACX,eAAe;IACf,YAAY;IACZ,mBAAmB;IACnB,oBAAoB;IACpB,gBAAgB;IAChB,WAAW;IACX,QAAQ;IACR,aAAa;IACb,aAAa;CACd,CAAC;AAEF;;;;;GAKG;AACU,QAAA,wCAAwC,GAAG;IACtD,UAAU;IACV,cAAc;IACd,SAAS;CACV,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elastic/eslint-plugin-eui",
3
- "version": "2.12.0",
3
+ "version": "2.13.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",