@elastic/eslint-plugin-eui 2.15.0 → 2.16.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 (59) hide show
  1. package/README.md +125 -0
  2. package/lib/cjs/index.js +10 -1
  3. package/lib/cjs/rules/a11y/no_nested_copy_tooltip.d.ts +3 -0
  4. package/lib/cjs/rules/a11y/no_nested_copy_tooltip.d.ts.map +1 -0
  5. package/lib/cjs/rules/a11y/no_nested_copy_tooltip.js +84 -0
  6. package/lib/cjs/rules/a11y/tooltip_button_icon_wrap.d.ts.map +1 -1
  7. package/lib/cjs/rules/a11y/tooltip_button_icon_wrap.js +29 -1
  8. package/lib/cjs/rules/a11y/tooltip_no_interactive_content.d.ts.map +1 -1
  9. package/lib/cjs/rules/a11y/tooltip_no_interactive_content.js +17 -45
  10. package/lib/cjs/rules/button_group_no_invalid_children.d.ts +3 -0
  11. package/lib/cjs/rules/button_group_no_invalid_children.d.ts.map +1 -0
  12. package/lib/cjs/rules/button_group_no_invalid_children.js +159 -0
  13. package/lib/cjs/rules/callout_prefer_props_for_content.d.ts.map +1 -1
  14. package/lib/cjs/rules/callout_prefer_props_for_content.js +93 -156
  15. package/lib/cjs/rules/no_deprecated_icon_aliases.d.ts +174 -0
  16. package/lib/cjs/rules/no_deprecated_icon_aliases.d.ts.map +1 -0
  17. package/lib/cjs/rules/no_deprecated_icon_aliases.js +302 -0
  18. package/lib/cjs/utils/flat_map.d.ts +2 -0
  19. package/lib/cjs/utils/flat_map.d.ts.map +1 -0
  20. package/lib/cjs/utils/flat_map.js +24 -0
  21. package/lib/cjs/utils/has_meaningful_attr.d.ts +20 -0
  22. package/lib/cjs/utils/has_meaningful_attr.d.ts.map +1 -0
  23. package/lib/cjs/utils/has_meaningful_attr.js +61 -0
  24. package/lib/cjs/utils/is_fragment.d.ts +8 -0
  25. package/lib/cjs/utils/is_fragment.d.ts.map +1 -0
  26. package/lib/cjs/utils/is_fragment.js +26 -0
  27. package/lib/cjs/utils/walk_jsx_children.d.ts +30 -0
  28. package/lib/cjs/utils/walk_jsx_children.d.ts.map +1 -0
  29. package/lib/cjs/utils/walk_jsx_children.js +210 -0
  30. package/lib/esm/index.js +9 -0
  31. package/lib/esm/index.js.map +1 -1
  32. package/lib/esm/rules/a11y/no_nested_copy_tooltip.d.ts +2 -0
  33. package/lib/esm/rules/a11y/no_nested_copy_tooltip.js +84 -0
  34. package/lib/esm/rules/a11y/no_nested_copy_tooltip.js.map +1 -0
  35. package/lib/esm/rules/a11y/tooltip_button_icon_wrap.js +31 -0
  36. package/lib/esm/rules/a11y/tooltip_button_icon_wrap.js.map +1 -1
  37. package/lib/esm/rules/a11y/tooltip_no_interactive_content.js +19 -42
  38. package/lib/esm/rules/a11y/tooltip_no_interactive_content.js.map +1 -1
  39. package/lib/esm/rules/button_group_no_invalid_children.d.ts +2 -0
  40. package/lib/esm/rules/button_group_no_invalid_children.js +198 -0
  41. package/lib/esm/rules/button_group_no_invalid_children.js.map +1 -0
  42. package/lib/esm/rules/callout_prefer_props_for_content.js +75 -112
  43. package/lib/esm/rules/callout_prefer_props_for_content.js.map +1 -1
  44. package/lib/esm/rules/no_deprecated_icon_aliases.d.ts +173 -0
  45. package/lib/esm/rules/no_deprecated_icon_aliases.js +307 -0
  46. package/lib/esm/rules/no_deprecated_icon_aliases.js.map +1 -0
  47. package/lib/esm/utils/flat_map.d.ts +1 -0
  48. package/lib/esm/utils/flat_map.js +22 -0
  49. package/lib/esm/utils/flat_map.js.map +1 -0
  50. package/lib/esm/utils/has_meaningful_attr.d.ts +19 -0
  51. package/lib/esm/utils/has_meaningful_attr.js +55 -0
  52. package/lib/esm/utils/has_meaningful_attr.js.map +1 -0
  53. package/lib/esm/utils/is_fragment.d.ts +7 -0
  54. package/lib/esm/utils/is_fragment.js +26 -0
  55. package/lib/esm/utils/is_fragment.js.map +1 -0
  56. package/lib/esm/utils/walk_jsx_children.d.ts +29 -0
  57. package/lib/esm/utils/walk_jsx_children.js +213 -0
  58. package/lib/esm/utils/walk_jsx_children.js.map +1 -0
  59. package/package.json +1 -1
package/README.md CHANGED
@@ -29,6 +29,16 @@ We don't use `no-restricted-imports` because ESLint doesn't allow multiple error
29
29
 
30
30
  All deprecations still must follow our [deprecation process](../../wiki/eui-team-processes/deprecations.md).
31
31
 
32
+ ### `@elastic/eui/no-deprecated-icon-aliases`
33
+
34
+ Disallows deprecated EUI icon aliases in static JSX props on components imported
35
+ from `@elastic/eui`. The rule checks props ending in `IconType`, the `type` prop
36
+ on `EuiIcon` and `EuiIconTip`, and component-specific icon props such as
37
+ `timelineAvatar`, `icon`, `logo`, `iconLeft`, and `iconRight`. It automatically
38
+ replaces deprecated aliases with their supported icon handles.
39
+
40
+ See the full table of icon replacements here: https://github.com/elastic/eui/issues/9561
41
+
32
42
  ### `@elastic/eui/no-css-color`
33
43
 
34
44
  This rule warns engineers to not use literal css color in the codebase, particularly for CSS properties that apply color to either the html element or text nodes, but rather urge users to defer to using the color tokens provided by EUI.
@@ -256,6 +266,8 @@ The rule reports two situations:
256
266
 
257
267
  Buttons with spread props (`{...props}`) are intentionally skipped when no `title` prop is explicitly present because their final prop set cannot be statically determined.
258
268
 
269
+ **Exception**: an `EuiButtonIcon` rendered as the render-prop child of an `EuiCopy` that sets a non-empty `beforeMessage` is not flagged for a missing wrapper. `EuiCopy` wraps its child in an `EuiToolTip` using `beforeMessage` as the content, so adding another `EuiToolTip` would create nested, conflicting tooltips — see `@elastic/eui/no-nested-copy-tooltip`. This exception is conditional: because `EuiToolTip` suppresses itself when its content is empty, an `EuiButtonIcon` inside an `EuiCopy` with a missing or statically empty/falsy `beforeMessage` (e.g. `""`, `{null}`, `{undefined}`, `{false}`) has no visible tooltip and is still reported. A button passed through the `beforeMessage` prop itself (rather than the render-prop child) is also still reported, since it is not the element `EuiCopy`'s tooltip wraps. A `title` prop is always reported in this case, since browser-native tooltips remain discouraged.
270
+
259
271
  #### Examples
260
272
 
261
273
  ```tsx
@@ -266,6 +278,11 @@ Buttons with spread props (`{...props}`) are intentionally skipped when no `titl
266
278
  <EuiToolTip content="Edit item">
267
279
  <EuiButtonIcon aria-label="Edit item" iconType="pencil" />
268
280
  </EuiToolTip>
281
+
282
+ // ✓ Good - EuiCopy provides the tooltip via `beforeMessage`, no wrapper needed
283
+ <EuiCopy textToCopy="some text" beforeMessage="Copy me">
284
+ {(copy) => <EuiButtonIcon onClick={copy} aria-label="Copy" iconType="copy" />}
285
+ </EuiCopy>
269
286
  ```
270
287
 
271
288
  ```tsx
@@ -328,6 +345,114 @@ it('shows tooltip on focus', async () => {
328
345
  ```
329
346
 
330
347
 
348
+ ### `@elastic/eui/no-nested-copy-tooltip`
349
+
350
+ Disallow wrapping the `EuiCopy` render-prop child in an `EuiToolTip` when the `beforeMessage` prop is set.
351
+
352
+ `EuiCopy` already wraps its child in an `EuiToolTip` internally and uses `beforeMessage` as that tooltip's content. If the render-prop returns its own `EuiToolTip` as the first child, the result is a nested, conflicting tooltip. Remove the `EuiToolTip` wrapper and move its message into the `beforeMessage` prop so `EuiCopy`'s built-in tooltip is the single source of truth.
353
+
354
+ This rule reports the pattern but does not autofix it, because the tooltip's `content` and the existing `beforeMessage` value cannot always be merged safely (e.g. when either is a variable or JSX). Apply the fix manually.
355
+
356
+ #### Examples
357
+
358
+ ```tsx
359
+ // ✗ Bad - nested tooltips, the child EuiToolTip conflicts with `beforeMessage`
360
+ <EuiCopy textToCopy="some text" beforeMessage="Click to copy">
361
+ {(copy) => (
362
+ <EuiToolTip content="Copy me">
363
+ <EuiButton onClick={copy}>Copy</EuiButton>
364
+ </EuiToolTip>
365
+ )}
366
+ </EuiCopy>
367
+
368
+ // ✓ Good - drop the EuiToolTip wrapper and let `beforeMessage` drive the tooltip
369
+ <EuiCopy textToCopy="some text" beforeMessage="Copy me">
370
+ {(copy) => <EuiButton onClick={copy}>Copy</EuiButton>}
371
+ </EuiCopy>
372
+ ```
373
+
374
+ When the render-prop child is not an `EuiToolTip`, `beforeMessage` simply configures `EuiCopy`'s own tooltip and the pattern is left untouched.
375
+
376
+ ### `@elastic/eui/button-group-no-invalid-children`
377
+
378
+ Enforce that `EuiButtonGroup` children (when using the Children API) are valid button components.
379
+
380
+ Valid direct children are:
381
+ - `variant="default"`: `EuiButton`, `EuiButtonEmpty`, and `EuiButtonIcon`
382
+
383
+ Besides those button components, these three wrapper components are also allowed: `EuiPopover`, `EuiToolTip` and `EuiCopy`.
384
+
385
+ #### Examples
386
+
387
+ ```tsx
388
+ // ✗ Bad - non-button elements
389
+ <EuiButtonGroup legend="Actions">
390
+ <div>Not a button</div>
391
+ <EuiFlexGroup>...</EuiFlexGroup>
392
+ </EuiButtonGroup>
393
+
394
+ // ✓ Good - direct buttons
395
+ <EuiButtonGroup legend="Actions">
396
+ <EuiButton>Save</EuiButton>
397
+ <EuiButtonEmpty color="text">Cancel</EuiButtonEmpty>
398
+ </EuiButtonGroup>
399
+
400
+ // ✓ Good - icon button with tooltip
401
+ <EuiButtonGroup legend="Actions">
402
+ <EuiButton>Save</EuiButton>
403
+ <EuiToolTip content="Delete">
404
+ <EuiButtonIcon iconType="trash" aria-label="Delete" />
405
+ </EuiToolTip>
406
+ </EuiButtonGroup>
407
+
408
+ // ✓ Good - EuiCopy with render prop (expression or block body)
409
+ <EuiButtonGroup legend="Actions">
410
+ <EuiCopy textToCopy="text">
411
+ {(copy) => <EuiButton onClick={copy}>Copy</EuiButton>}
412
+ </EuiCopy>
413
+ </EuiButtonGroup>
414
+
415
+ // ✓ Good - .map() with expression or block body
416
+ <EuiButtonGroup legend="Actions">
417
+ {buttons.map((b) => <EuiButton key={b.id} onClick={b.onClick}>{b.label}</EuiButton>)}
418
+ </EuiButtonGroup>
419
+
420
+ // ✓ Good - EuiPopover with a button trigger
421
+ <EuiButtonGroup legend="Actions">
422
+ <EuiPopover
423
+ button={<EuiButton onClick={togglePopover}>More</EuiButton>}
424
+ isOpen={isOpen}
425
+ closePopover={closePopover}
426
+ >
427
+ Panel content
428
+ </EuiPopover>
429
+ </EuiButtonGroup>
430
+
431
+ // ✓ Good - EuiPopover with an EuiToolTip-wrapped icon trigger
432
+ <EuiButtonGroup legend="Actions">
433
+ <EuiPopover
434
+ button={
435
+ <EuiToolTip content="More options">
436
+ <EuiButtonIcon iconType="boxesVertical" aria-label="More options" />
437
+ </EuiToolTip>
438
+ }
439
+ isOpen={isOpen}
440
+ closePopover={closePopover}
441
+ >
442
+ Panel content
443
+ </EuiPopover>
444
+ </EuiButtonGroup>
445
+ ```
446
+
447
+ #### Custom button wrapper components
448
+
449
+ If a project-specific button component (e.g. `<SaveButton />`) is used as a child and the rule cannot resolve it statically, it reports `invalidUnresolvableChild` which suggests suppressing the rule inline with a comment:
450
+
451
+ ```tsx
452
+ // eslint-disable-next-line @elastic/eui/button-group-no-invalid-children -- SaveButton returns EuiButton
453
+ <SaveButton />
454
+ ```
455
+
331
456
  ## Testing
332
457
 
333
458
  ### Running unit tests
package/lib/cjs/index.js CHANGED
@@ -7,6 +7,7 @@ var _consistent_is_invalid_props = require("./rules/a11y/consistent_is_invalid_p
7
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
+ var _no_nested_copy_tooltip = require("./rules/a11y/no_nested_copy_tooltip");
10
11
  var _no_restricted_eui_imports = require("./rules/no_restricted_eui_imports");
11
12
  var _no_static_z_index = require("./rules/no_static_z_index");
12
13
  var _no_unnamed_interactive_element = require("./rules/a11y/no_unnamed_interactive_element");
@@ -21,6 +22,8 @@ var _badge_accessibility_rules = require("./rules/a11y/badge_accessibility_rules
21
22
  var _icon_accessibility_rules = require("./rules/a11y/icon_accessibility_rules");
22
23
  var _tooltip_no_interactive_content = require("./rules/a11y/tooltip_no_interactive_content");
23
24
  var _tooltip_button_icon_wrap = require("./rules/a11y/tooltip_button_icon_wrap");
25
+ var _no_deprecated_icon_aliases = require("./rules/no_deprecated_icon_aliases");
26
+ var _button_group_no_invalid_children = require("./rules/button_group_no_invalid_children");
24
27
  /*
25
28
  * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
26
29
  * or more contributor license agreements. Licensed under the Elastic License
@@ -37,6 +40,8 @@ const config = {
37
40
  'consistent-is-invalid-props': _consistent_is_invalid_props.ConsistentIsInvalidProps,
38
41
  'href-or-on-click': _href_or_on_click.HrefOnClick,
39
42
  'no-css-color': _no_css_color.NoCssColor,
43
+ 'no-deprecated-icon-aliases': _no_deprecated_icon_aliases.NoDeprecatedIconAliases,
44
+ 'no-nested-copy-tooltip': _no_nested_copy_tooltip.NoNestedCopyTooltip,
40
45
  'no-restricted-eui-imports': _no_restricted_eui_imports.NoRestrictedEuiImports,
41
46
  'no-static-z-index': _no_static_z_index.NoStaticZIndex,
42
47
  'no-unnamed-interactive-element': _no_unnamed_interactive_element.NoUnnamedInteractiveElement,
@@ -51,18 +56,22 @@ const config = {
51
56
  'icon-accessibility-rules': _icon_accessibility_rules.EuiIconAccessibilityRules,
52
57
  'require-href-for-link': _require_href_for_link.RequireHrefForLink,
53
58
  'tooltip-no-interactive-content': _tooltip_no_interactive_content.TooltipNoInteractiveContent,
54
- 'tooltip-button-icon-wrap': _tooltip_button_icon_wrap.TooltipButtonIconWrap
59
+ 'tooltip-button-icon-wrap': _tooltip_button_icon_wrap.TooltipButtonIconWrap,
60
+ 'button-group-no-invalid-children': _button_group_no_invalid_children.ButtonGroupNoInvalidChildren
55
61
  },
56
62
  configs: {
57
63
  recommended: {
58
64
  plugins: ['@elastic/eslint-plugin-eui'],
59
65
  rules: {
60
66
  '@elastic/eui/accessible-interactive-element': 'warn',
67
+ '@elastic/eui/button-group-no-invalid-children': 'error',
61
68
  '@elastic/eui/callout-announce-on-mount': 'warn',
62
69
  '@elastic/eui/callout-prefer-props-for-content': 'warn',
63
70
  '@elastic/eui/consistent-is-invalid-props': 'warn',
64
71
  '@elastic/eui/href-or-on-click': 'warn',
65
72
  '@elastic/eui/no-css-color': 'warn',
73
+ '@elastic/eui/no-deprecated-icon-aliases': 'warn',
74
+ '@elastic/eui/no-nested-copy-tooltip': 'warn',
66
75
  '@elastic/eui/no-restricted-eui-imports': 'warn',
67
76
  '@elastic/eui/no-static-z-index': 'warn',
68
77
  '@elastic/eui/no-unnamed-interactive-element': 'warn',
@@ -0,0 +1,3 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const NoNestedCopyTooltip: ESLintUtils.RuleModule<"redundantTooltipWrapper", [], unknown, ESLintUtils.RuleListener>;
3
+ //# sourceMappingURL=no_nested_copy_tooltip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no_nested_copy_tooltip.d.ts","sourceRoot":"","sources":["../../../../src/rules/a11y/no_nested_copy_tooltip.ts"],"names":[],"mappings":"AAQA,OAAO,EAAiB,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAsDtE,eAAO,MAAM,mBAAmB,0FA6C/B,CAAC"}
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.NoNestedCopyTooltip = void 0;
7
+ var _utils = require("@typescript-eslint/utils");
8
+ var _has_meaningful_attr = require("../../utils/has_meaningful_attr");
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 COPY = 'EuiCopy';
18
+ const TOOLTIP = 'EuiToolTip';
19
+ const BEFORE_MESSAGE = 'beforeMessage';
20
+
21
+ /**
22
+ * Resolves the JSX element returned by the `EuiCopy` render-prop child.
23
+ *
24
+ * `EuiCopy` renders its children through a function (`children(copy)`), so the
25
+ * "first child" is the root JSX element that function returns. This handles
26
+ * both implicit arrow returns (`(copy) => <El />`) and block bodies with an
27
+ * explicit `return` statement. Non-render-prop children (e.g. a leading JSX
28
+ * comment) are skipped so the render-prop function is still found.
29
+ */
30
+ function getRenderPropRootElement(node) {
31
+ const renderProp = node.children.find(child => child.type === 'JSXExpressionContainer' && (child.expression.type === 'ArrowFunctionExpression' || child.expression.type === 'FunctionExpression'));
32
+ if (!renderProp) return null;
33
+ const expression = renderProp.expression;
34
+ let {
35
+ body
36
+ } = expression;
37
+ if (body.type === 'BlockStatement') {
38
+ const returnStatement = body.body.find(statement => statement.type === 'ReturnStatement');
39
+ if (!returnStatement?.argument) return null;
40
+ body = returnStatement.argument;
41
+ }
42
+ return body.type === 'JSXElement' ? body : null;
43
+ }
44
+ function isToolTipElement(element) {
45
+ const {
46
+ name
47
+ } = element.openingElement;
48
+ return name.type === 'JSXIdentifier' && name.name === TOOLTIP;
49
+ }
50
+ const NoNestedCopyTooltip = exports.NoNestedCopyTooltip = _utils.ESLintUtils.RuleCreator.withoutDocs({
51
+ create(context) {
52
+ return {
53
+ JSXElement(node) {
54
+ const {
55
+ openingElement
56
+ } = node;
57
+ if (openingElement.name.type !== 'JSXIdentifier' || openingElement.name.name !== COPY) {
58
+ return;
59
+ }
60
+
61
+ // A statically empty/falsy `beforeMessage` renders no `EuiCopy`
62
+ // tooltip, so a child `EuiToolTip` is not redundant — skip those.
63
+ if (!(0, _has_meaningful_attr.hasMeaningfulAttr)(openingElement, BEFORE_MESSAGE)) return;
64
+ const rootElement = getRenderPropRootElement(node);
65
+ if (!rootElement || !isToolTipElement(rootElement)) return;
66
+ context.report({
67
+ node: rootElement,
68
+ messageId: 'redundantTooltipWrapper'
69
+ });
70
+ }
71
+ };
72
+ },
73
+ meta: {
74
+ type: 'problem',
75
+ docs: {
76
+ description: `Disallow wrapping the ${COPY} render-prop child in an ${TOOLTIP} when \`${BEFORE_MESSAGE}\` is set`
77
+ },
78
+ schema: [],
79
+ messages: {
80
+ redundantTooltipWrapper: [`${COPY} already wraps its child in an ${TOOLTIP} and uses \`${BEFORE_MESSAGE}\` as that tooltip's content.`, `Wrapping the child in another ${TOOLTIP} creates a nested, conflicting tooltip.`, `Remove the ${TOOLTIP} wrapper and move its message into the \`${BEFORE_MESSAGE}\` prop instead.`].join(' ')
81
+ }
82
+ },
83
+ defaultOptions: []
84
+ });
@@ -1 +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;AAoCtE,eAAO,MAAM,qBAAqB,qHAiEhC,CAAC"}
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;AA0EtE,eAAO,MAAM,qBAAqB,qHAkEhC,CAAC"}
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.TooltipButtonIconWrap = void 0;
7
7
  var _utils = require("@typescript-eslint/utils");
8
8
  var _has_spread = require("../../utils/has_spread");
9
+ var _has_meaningful_attr = require("../../utils/has_meaningful_attr");
9
10
  /*
10
11
  * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
11
12
  * or more contributor license agreements. Licensed under the Elastic License
@@ -16,6 +17,8 @@ var _has_spread = require("../../utils/has_spread");
16
17
 
17
18
  const BUTTON_ICON = 'EuiButtonIcon';
18
19
  const TOOLTIP = 'EuiToolTip';
20
+ const COPY = 'EuiCopy';
21
+ const BEFORE_MESSAGE = 'beforeMessage';
19
22
  function isWrappedByTooltip(node) {
20
23
  let current = node.parent;
21
24
  while (current) {
@@ -41,6 +44,31 @@ function isWrappedByTooltip(node) {
41
44
  }
42
45
  return false;
43
46
  }
47
+
48
+ /**
49
+ * `EuiCopy` renders its render-prop child inside its own `EuiToolTip`, but only
50
+ * when `beforeMessage` is set — that prop is the tooltip's `content`, and
51
+ * `EuiToolTip` suppresses itself when the content is empty. So an
52
+ * `EuiButtonIcon` rendered within an `<EuiCopy beforeMessage="…">` already has a
53
+ * tooltip for sighted users and wrapping it in another `EuiToolTip` would create
54
+ * nested, conflicting tooltips — see the `no-nested-copy-tooltip` rule. Without
55
+ * a meaningful `beforeMessage` there is no such tooltip, so the button must
56
+ * still be wrapped.
57
+ *
58
+ * The exemption only applies to buttons in the render-prop output. If the walk
59
+ * crosses a `JSXAttribute` before reaching the `EuiCopy`, the button lives in a
60
+ * prop value (e.g. `beforeMessage={<EuiButtonIcon />}`) rather than the child
61
+ * `EuiCopy` renders, so it is not covered by the built-in tooltip.
62
+ */
63
+ function isInsideEuiCopyWithBeforeMessage(node) {
64
+ for (let current = node.parent; current; current = current.parent) {
65
+ if (current.type === 'JSXAttribute') return false;
66
+ if (current.type === 'JSXElement' && current.openingElement.name.type === 'JSXIdentifier' && current.openingElement.name.name === COPY) {
67
+ return (0, _has_meaningful_attr.hasMeaningfulAttr)(current.openingElement, BEFORE_MESSAGE);
68
+ }
69
+ }
70
+ return false;
71
+ }
44
72
  const TooltipButtonIconWrap = exports.TooltipButtonIconWrap = _utils.ESLintUtils.RuleCreator.withoutDocs({
45
73
  create(context) {
46
74
  return {
@@ -63,7 +91,7 @@ const TooltipButtonIconWrap = exports.TooltipButtonIconWrap = _utils.ESLintUtils
63
91
  });
64
92
  return;
65
93
  }
66
- if (!isWrappedByTooltip(node) && !(0, _has_spread.hasSpread)(openingElement.attributes)) {
94
+ if (!isWrappedByTooltip(node) && !isInsideEuiCopyWithBeforeMessage(node) && !(0, _has_spread.hasSpread)(openingElement.attributes)) {
67
95
  context.report({
68
96
  node: openingElement,
69
97
  messageId: 'wrapWithEuiToolTip'
@@ -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;AA+DtE,eAAO,MAAM,2BAA2B,uFA8DtC,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;AA4BtE,eAAO,MAAM,2BAA2B,uFAgFtC,CAAC"}
@@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.TooltipNoInteractiveContent = void 0;
7
7
  var _utils = require("@typescript-eslint/utils");
8
8
  var _constants = require("../../utils/constants");
9
+ var _walk_jsx_children = require("../../utils/walk_jsx_children");
10
+ var _get_element_name = require("../../utils/get_element_name");
9
11
  /*
10
12
  * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
11
13
  * or more contributor license agreements. Licensed under the Elastic License
@@ -19,46 +21,6 @@ const TOOLTIP_CONTENT_PROPS = ['content', 'title'];
19
21
  const INTERACTIVE_HTML_ELEMENTS = ['a', 'button', 'input', 'select', 'textarea'];
20
22
  const CONDITIONALLY_INTERACTIVE_SET = new Set(_constants.CONDITIONALLY_INTERACTIVE_EUI_COMPONENTS);
21
23
  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
24
  const TooltipNoInteractiveContent = exports.TooltipNoInteractiveContent = _utils.ESLintUtils.RuleCreator.withoutDocs({
63
25
  create(context) {
64
26
  return {
@@ -83,18 +45,28 @@ const TooltipNoInteractiveContent = exports.TooltipNoInteractiveContent = _utils
83
45
  if (expression.type === 'JSXEmptyExpression') {
84
46
  continue;
85
47
  }
86
- const interactiveEl = findInteractiveElement(expression);
87
- if (interactiveEl) {
48
+ let found = false;
49
+ (0, _walk_jsx_children.walkJsxChildren)(expression, leaf => {
50
+ if (found || leaf.type !== 'JSXElement') return;
51
+ const el = leaf;
52
+ const name = (0, _get_element_name.getElementName)(el.openingElement);
53
+ if (!name || !INTERACTIVE_ELEMENTS.has(name)) return;
88
54
  context.report({
89
- node: interactiveEl,
55
+ node: el.openingElement,
90
56
  messageId: 'noInteractiveContent',
91
57
  data: {
92
58
  propName: attr.name.name,
93
59
  componentName,
94
- elementName: interactiveEl.name.name
60
+ elementName: name
95
61
  }
96
62
  });
97
- }
63
+ found = true;
64
+ }, {
65
+ shouldSkip: el => {
66
+ const name = (0, _get_element_name.getElementName)(el.openingElement);
67
+ return !name || !INTERACTIVE_ELEMENTS.has(name);
68
+ }
69
+ });
98
70
  }
99
71
  }
100
72
  };
@@ -0,0 +1,3 @@
1
+ import { type TSESLint } from '@typescript-eslint/utils';
2
+ export declare const ButtonGroupNoInvalidChildren: TSESLint.RuleModule<"invalidUnresolvableWrapperChild" | "invalidWrapperChild" | "invalidChild" | "invalidUnresolvableChild" | "invalidPopoverButton" | "invalidUnresolvablePopoverButton", [], unknown, TSESLint.RuleListener>;
3
+ //# sourceMappingURL=button_group_no_invalid_children.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"button_group_no_invalid_children.d.ts","sourceRoot":"","sources":["../../../src/rules/button_group_no_invalid_children.ts"],"names":[],"mappings":"AAQA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;AAwDlC,eAAO,MAAM,4BAA4B,gOAuLxC,CAAC"}
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.ButtonGroupNoInvalidChildren = void 0;
7
+ var _utils = require("@typescript-eslint/utils");
8
+ var _has_spread = require("../utils/has_spread");
9
+ var _flat_map = require("../utils/flat_map");
10
+ var _get_element_name = require("../utils/get_element_name");
11
+ var _walk_jsx_children = require("../utils/walk_jsx_children");
12
+ var _get_attr_value = require("../utils/get_attr_value");
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
+ const BUTTON_GROUP = 'EuiButtonGroup';
22
+ const VALID_BUTTONS = new Set(['EuiButton', 'EuiButtonEmpty', 'EuiButtonIcon']);
23
+ const VALID_WRAPPERS = new Set(['EuiToolTip', 'EuiPopover', 'EuiCopy']);
24
+ const VALID_BUTTONS_LIST = Array.from(VALID_BUTTONS).join(', ');
25
+ const VALID_WRAPPERS_LIST = Array.from(VALID_WRAPPERS).join(', ');
26
+ function isCustomComponent(name) {
27
+ return name[0] === name[0].toUpperCase();
28
+ }
29
+ function collectJsxChildren(node, sourceCode) {
30
+ const results = [];
31
+ (0, _walk_jsx_children.walkJsxChildren)(node, leaf => {
32
+ if (leaf.type === 'JSXElement') {
33
+ results.push(leaf);
34
+ }
35
+ }, {
36
+ sourceCode
37
+ });
38
+ return results;
39
+ }
40
+ function reportInvalidWrapperChildren(wrapper, wrapperName, context) {
41
+ const children = (0, _flat_map.flatMap)(wrapper.children, c => collectJsxChildren(c, context.sourceCode));
42
+ for (const wrapperChild of children) {
43
+ if ((0, _has_spread.hasSpread)(wrapperChild.openingElement.attributes)) continue;
44
+ const wrapperChildName = (0, _get_element_name.getElementName)(wrapperChild.openingElement);
45
+ if (wrapperChildName === null || VALID_BUTTONS.has(wrapperChildName)) continue;
46
+ context.report({
47
+ node: wrapperChild.openingElement,
48
+ messageId: isCustomComponent(wrapperChildName) ? 'invalidUnresolvableWrapperChild' : 'invalidWrapperChild',
49
+ data: {
50
+ name: wrapperChildName,
51
+ wrapper: wrapperName
52
+ }
53
+ });
54
+ }
55
+ }
56
+ const ButtonGroupNoInvalidChildren = exports.ButtonGroupNoInvalidChildren = _utils.ESLintUtils.RuleCreator.withoutDocs({
57
+ create(context) {
58
+ return {
59
+ JSXElement(node) {
60
+ const {
61
+ openingElement
62
+ } = node;
63
+ if (openingElement.name.type !== 'JSXIdentifier' || openingElement.name.name !== BUTTON_GROUP) {
64
+ return;
65
+ }
66
+
67
+ // Only validate when the variant is "default" (or absent — the default).
68
+ // Rules for "segmented" and "selection" are added in later chunks.
69
+ // Dynamic variant values (variables) are conservatively skipped.
70
+ const variant = (0, _get_attr_value.findAttrValue)(context, openingElement.attributes, 'variant');
71
+ if (variant !== undefined && variant !== 'default') return;
72
+ const children = (0, _flat_map.flatMap)(node.children, c => collectJsxChildren(c, context.sourceCode));
73
+ if (children.length === 0) return;
74
+ for (const child of children) {
75
+ const name = (0, _get_element_name.getElementName)(child.openingElement);
76
+ if (name === null) continue;
77
+ if (VALID_BUTTONS.has(name)) continue;
78
+ if (VALID_WRAPPERS.has(name)) {
79
+ if (name === 'EuiToolTip') {
80
+ // Validate JSX children (expanding fragments/conditionals).
81
+ reportInvalidWrapperChildren(child, name, context);
82
+ } else if (name === 'EuiPopover') {
83
+ // The trigger is the `button` prop, not JSX children (panel
84
+ // content). The prop value may be a JSXExpressionContainer or
85
+ // a bare JSX element; collectJsxChildren handles both.
86
+ // EuiToolTip wrapping the trigger button is also supported.
87
+ const buttonProp = child.openingElement.attributes.find(attr => attr.type === 'JSXAttribute' && attr.name.type === 'JSXIdentifier' && attr.name.name === 'button');
88
+ if (buttonProp?.value != null) {
89
+ const triggerElements = collectJsxChildren(buttonProp.value, context.sourceCode);
90
+ for (const triggerElement of triggerElements) {
91
+ if ((0, _has_spread.hasSpread)(triggerElement.openingElement.attributes)) continue;
92
+ const triggerElementName = (0, _get_element_name.getElementName)(triggerElement.openingElement);
93
+ if (triggerElementName === null || VALID_BUTTONS.has(triggerElementName)) continue;
94
+ if (triggerElementName === 'EuiToolTip') {
95
+ // EuiToolTip wrapping the trigger — validate its children.
96
+ const tooltipChildren = (0, _flat_map.flatMap)(triggerElement.children, c => collectJsxChildren(c, context.sourceCode));
97
+ for (const tooltipChild of tooltipChildren) {
98
+ if ((0, _has_spread.hasSpread)(tooltipChild.openingElement.attributes)) continue;
99
+ const tooltipChildName = (0, _get_element_name.getElementName)(tooltipChild.openingElement);
100
+ if (tooltipChildName === null || VALID_BUTTONS.has(tooltipChildName)) continue;
101
+ context.report({
102
+ node: tooltipChild.openingElement,
103
+ messageId: isCustomComponent(tooltipChildName) ? 'invalidUnresolvablePopoverButton' : 'invalidPopoverButton',
104
+ data: {
105
+ name: tooltipChildName
106
+ }
107
+ });
108
+ }
109
+ continue;
110
+ }
111
+ context.report({
112
+ node: triggerElement.openingElement,
113
+ messageId: isCustomComponent(triggerElementName) ? 'invalidUnresolvablePopoverButton' : 'invalidPopoverButton',
114
+ data: {
115
+ name: triggerElementName
116
+ }
117
+ });
118
+ }
119
+ }
120
+ } else if (name === 'EuiCopy') {
121
+ // Children is a render prop: {(copy) => <EuiButton />}
122
+ // ArrowFunctionExpression with expression body is expanded by
123
+ // collectJsxChildren, so validation works the same as EuiToolTip.
124
+ reportInvalidWrapperChildren(child, name, context);
125
+ }
126
+ continue;
127
+ }
128
+
129
+ // For elements that aren't known valid buttons or wrappers, a spread
130
+ // prevents static classification; skip conservatively.
131
+ if ((0, _has_spread.hasSpread)(child.openingElement.attributes)) continue;
132
+ context.report({
133
+ node: child.openingElement,
134
+ messageId: isCustomComponent(name) ? 'invalidUnresolvableChild' : 'invalidChild',
135
+ data: {
136
+ name
137
+ }
138
+ });
139
+ }
140
+ }
141
+ };
142
+ },
143
+ meta: {
144
+ type: 'problem',
145
+ docs: {
146
+ description: `Enforce that EuiButtonGroup children are ${VALID_BUTTONS_LIST}, or a supported wrapper (${VALID_WRAPPERS_LIST})`
147
+ },
148
+ schema: [],
149
+ messages: {
150
+ invalidChild: [`{{ name }} is not a valid child of EuiButtonGroup.`, `Allowed children: ${VALID_BUTTONS_LIST}.`, `Allowed wrappers: ${VALID_WRAPPERS_LIST}.`].join(' '),
151
+ invalidUnresolvableChild: [`{{ name }} cannot be verified as a valid child of EuiButtonGroup.`, `Allowed children: ${VALID_BUTTONS_LIST}.`, `Allowed wrappers: ${VALID_WRAPPERS_LIST}.`, `If {{ name }} is a shared button wrapper component only containing`, `valid button children, suppress this rule inline with a comment`, `explaining why it's valid.`, `// eslint-disable-next-line @elastic/eui/button-group-no-invalid-children -- SaveButton only wraps EuiButton`].join(' '),
152
+ invalidPopoverButton: [`{{ name }} is not a valid trigger button for EuiPopover in EuiButtonGroup.`, `The \`button\` prop must be ${VALID_BUTTONS_LIST}.`].join(' '),
153
+ invalidUnresolvablePopoverButton: [`{{ name }} cannot be verified as a valid trigger button for EuiPopover in EuiButtonGroup.`, `The \`button\` prop must be ${VALID_BUTTONS_LIST}.`, `If {{ name }} is a shared button wrapper component only containing`, `valid button children, suppress this rule inline with a comment`, `explaining why it's valid.`, `// eslint-disable-next-line @elastic/eui/button-group-no-invalid-children -- SaveButton only wraps EuiButton`].join(' '),
154
+ invalidWrapperChild: [`{{ name }} inside {{ wrapper }} is not a valid button for EuiButtonGroup.`, `The wrapper must contain ${VALID_BUTTONS_LIST}.`].join(' '),
155
+ invalidUnresolvableWrapperChild: [`{{ name }} inside {{ wrapper }} cannot be verified as a valid button for EuiButtonGroup.`, `The wrapper must contain ${VALID_BUTTONS_LIST}.`, `If {{ name }} is a shared button wrapper component, suppress this rule inline`, `with a comment explaining why it renders valid button children:`, `// eslint-disable-next-line @elastic/eui/button-group-no-invalid-children -- SaveButton wraps EuiButton`].join(' ')
156
+ }
157
+ },
158
+ defaultOptions: []
159
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"callout_prefer_props_for_content.d.ts","sourceRoot":"","sources":["../../../src/rules/callout_prefer_props_for_content.ts"],"names":[],"mappings":"AAQA,OAAO,EAAiB,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAqBtE,KAAK,UAAU,GACX,uBAAuB,GACvB,kBAAkB,GAClB,qBAAqB,CAAC;AAC1B,KAAK,OAAO,GAAG,CAAC;IAAE,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAAC;AAsI3C,eAAO,MAAM,4BAA4B,gFAyExC,CAAC"}
1
+ {"version":3,"file":"callout_prefer_props_for_content.d.ts","sourceRoot":"","sources":["../../../src/rules/callout_prefer_props_for_content.ts"],"names":[],"mappings":"AAQA,OAAO,EAAiB,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAoBtE,KAAK,UAAU,GACX,uBAAuB,GACvB,kBAAkB,GAClB,qBAAqB,CAAC;AAC1B,KAAK,OAAO,GAAG,CAAC;IAAE,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAAC;AAE3C,eAAO,MAAM,4BAA4B,gFA2JvC,CAAC"}