@elastic/eslint-plugin-eui 2.16.0 → 3.0.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 (35) hide show
  1. package/README.md +99 -3
  2. package/lib/cjs/index.js +4 -1
  3. package/lib/cjs/rules/button_group_no_invalid_children.d.ts +1 -1
  4. package/lib/cjs/rules/button_group_no_invalid_children.d.ts.map +1 -1
  5. package/lib/cjs/rules/button_group_no_invalid_children.js +76 -48
  6. package/lib/cjs/rules/button_group_selection_require_id.d.ts +3 -0
  7. package/lib/cjs/rules/button_group_selection_require_id.d.ts.map +1 -0
  8. package/lib/cjs/rules/button_group_selection_require_id.js +114 -0
  9. package/lib/cjs/rules/no_deprecated_icon_aliases.d.ts +54 -5
  10. package/lib/cjs/rules/no_deprecated_icon_aliases.d.ts.map +1 -1
  11. package/lib/cjs/rules/no_deprecated_icon_aliases.js +70 -8
  12. package/lib/cjs/utils/button_group_constants.d.ts +6 -0
  13. package/lib/cjs/utils/button_group_constants.d.ts.map +1 -0
  14. package/lib/cjs/utils/button_group_constants.js +19 -0
  15. package/lib/cjs/utils/collect_jsx_children.d.ts +3 -0
  16. package/lib/cjs/utils/collect_jsx_children.d.ts.map +1 -0
  17. package/lib/cjs/utils/collect_jsx_children.js +26 -0
  18. package/lib/esm/index.js +3 -0
  19. package/lib/esm/index.js.map +1 -1
  20. package/lib/esm/rules/button_group_no_invalid_children.d.ts +1 -1
  21. package/lib/esm/rules/button_group_no_invalid_children.js +81 -48
  22. package/lib/esm/rules/button_group_no_invalid_children.js.map +1 -1
  23. package/lib/esm/rules/button_group_selection_require_id.d.ts +2 -0
  24. package/lib/esm/rules/button_group_selection_require_id.js +118 -0
  25. package/lib/esm/rules/button_group_selection_require_id.js.map +1 -0
  26. package/lib/esm/rules/no_deprecated_icon_aliases.d.ts +54 -5
  27. package/lib/esm/rules/no_deprecated_icon_aliases.js +75 -6
  28. package/lib/esm/rules/no_deprecated_icon_aliases.js.map +1 -1
  29. package/lib/esm/utils/button_group_constants.d.ts +5 -0
  30. package/lib/esm/utils/button_group_constants.js +20 -0
  31. package/lib/esm/utils/button_group_constants.js.map +1 -0
  32. package/lib/esm/utils/collect_jsx_children.d.ts +2 -0
  33. package/lib/esm/utils/collect_jsx_children.js +21 -0
  34. package/lib/esm/utils/collect_jsx_children.js.map +1 -0
  35. package/package.json +1 -1
package/README.md CHANGED
@@ -377,10 +377,14 @@ When the render-prop child is not an `EuiToolTip`, `beforeMessage` simply config
377
377
 
378
378
  Enforce that `EuiButtonGroup` children (when using the Children API) are valid button components.
379
379
 
380
- Valid direct children are:
381
- - `variant="default"`: `EuiButton`, `EuiButtonEmpty`, and `EuiButtonIcon`
380
+ Valid direct children depend on the variant:
381
+ - `variant="default"` (or no variant): `EuiButton`, `EuiButtonEmpty`, and `EuiButtonIcon`
382
+ - `variant="segmented"`: `EuiButton` and `EuiButtonIcon` only (`EuiButtonEmpty` is not allowed)
383
+ - `variant="selection"`: `EuiButton` and `EuiButtonIcon` only (`EuiButtonEmpty` is not allowed)
382
384
 
383
- Besides those button components, these three wrapper components are also allowed: `EuiPopover`, `EuiToolTip` and `EuiCopy`.
385
+ In addition, these wrapper components are allowed for all variants: `EuiPopover`, `EuiToolTip`, and `EuiCopy`.
386
+
387
+ For `variant="segmented"` and `variant="selection"`, all children must also use the **same button type** — either all `EuiButton` or all `EuiButtonIcon`. Mixing both types is reported as an error, including when buttons appear inside `EuiToolTip`, as an `EuiPopover` trigger, or in an `EuiCopy` render prop.
384
388
 
385
389
  #### Examples
386
390
 
@@ -391,6 +395,29 @@ Besides those button components, these three wrapper components are also allowed
391
395
  <EuiFlexGroup>...</EuiFlexGroup>
392
396
  </EuiButtonGroup>
393
397
 
398
+ // ✗ Bad - variant="segmented" with EuiButtonEmpty (not allowed)
399
+ <EuiButtonGroup legend="Actions" variant="segmented">
400
+ <EuiButton>Save</EuiButton>
401
+ <EuiButtonEmpty color="text">Cancel</EuiButtonEmpty>
402
+ </EuiButtonGroup>
403
+
404
+ // ✗ Bad - variant="segmented" mixing EuiButton and EuiButtonIcon
405
+ <EuiButtonGroup legend="Actions" variant="segmented">
406
+ <EuiButton>Save</EuiButton>
407
+ <EuiButtonIcon iconType="trash" aria-label="Delete" />
408
+ </EuiButtonGroup>
409
+
410
+ // ✗ Bad - variant="selection" with EuiButtonEmpty (not allowed)
411
+ <EuiButtonGroup legend="Format" variant="selection">
412
+ <EuiButtonEmpty color="text">Bold</EuiButtonEmpty>
413
+ </EuiButtonGroup>
414
+
415
+ // ✗ Bad - variant="selection" mixing EuiButton and EuiButtonIcon
416
+ <EuiButtonGroup legend="Format" variant="selection">
417
+ <EuiButton id="bold">Bold</EuiButton>
418
+ <EuiButtonIcon id="italic" iconType="italic" aria-label="Italic" />
419
+ </EuiButtonGroup>
420
+
394
421
  // ✓ Good - direct buttons
395
422
  <EuiButtonGroup legend="Actions">
396
423
  <EuiButton>Save</EuiButton>
@@ -442,6 +469,30 @@ Besides those button components, these three wrapper components are also allowed
442
469
  Panel content
443
470
  </EuiPopover>
444
471
  </EuiButtonGroup>
472
+
473
+ // ✓ Good - variant="segmented" with all EuiButton
474
+ <EuiButtonGroup legend="Actions" variant="segmented">
475
+ <EuiButton>Save</EuiButton>
476
+ <EuiButton color="danger">Delete</EuiButton>
477
+ </EuiButtonGroup>
478
+
479
+ // ✓ Good - variant="segmented" with all EuiButtonIcon
480
+ <EuiButtonGroup legend="Actions" variant="segmented">
481
+ <EuiButtonIcon iconType="pencil" aria-label="Edit" />
482
+ <EuiButtonIcon iconType="trash" aria-label="Delete" />
483
+ </EuiButtonGroup>
484
+
485
+ // ✓ Good - variant="selection" with all EuiButton
486
+ <EuiButtonGroup legend="Format" variant="selection">
487
+ <EuiButton id="bold">Bold</EuiButton>
488
+ <EuiButton id="italic">Italic</EuiButton>
489
+ </EuiButtonGroup>
490
+
491
+ // ✓ Good - variant="selection" with all EuiButtonIcon
492
+ <EuiButtonGroup legend="Format" variant="selection">
493
+ <EuiButtonIcon id="bold" iconType="bold" aria-label="Bold" />
494
+ <EuiButtonIcon id="italic" iconType="italic" aria-label="Italic" />
495
+ </EuiButtonGroup>
445
496
  ```
446
497
 
447
498
  #### Custom button wrapper components
@@ -453,6 +504,51 @@ If a project-specific button component (e.g. `<SaveButton />`) is used as a chil
453
504
  <SaveButton />
454
505
  ```
455
506
 
507
+ ### `@elastic/eui/button-group-selection-require-id`
508
+
509
+ Enforce that every `EuiButton` and `EuiButtonIcon` child of an `EuiButtonGroup` with `variant="selection"` has an `id` prop. The selection variant uses the `id` of each button to track which buttons are selected, so a missing `id` will cause incorrect or broken selection state at runtime.
510
+
511
+ The rule only fires when `variant="selection"` is set as a static string. Dynamic variants (`variant={myVar}`) are skipped conservatively. It traverses the same nesting depth as `button-group-no-invalid-children`: fragments, conditionals, variable resolution, `.map()`, and the three supported wrappers (`EuiToolTip`, `EuiPopover` trigger, `EuiCopy` render prop). Children with spread props (`{...props}`) are skipped — the `id` may be provided via the spread.
512
+
513
+ #### Examples
514
+
515
+ ```tsx
516
+ // ✗ Bad - missing id on direct child
517
+ <EuiButtonGroup legend="Format" variant="selection">
518
+ <EuiButton>Bold</EuiButton>
519
+ </EuiButtonGroup>
520
+
521
+ // ✗ Bad - missing id on button inside EuiToolTip
522
+ <EuiButtonGroup legend="Format" variant="selection">
523
+ <EuiToolTip content="Italic">
524
+ <EuiButton>Italic</EuiButton>
525
+ </EuiToolTip>
526
+ </EuiButtonGroup>
527
+
528
+ // ✗ Bad - missing id on EuiPopover trigger
529
+ <EuiButtonGroup legend="Format" variant="selection">
530
+ <EuiPopover button={<EuiButton>More</EuiButton>} isOpen={false} closePopover={() => {}}>
531
+ Panel content
532
+ </EuiPopover>
533
+ </EuiButtonGroup>
534
+
535
+ // ✓ Good - all children have id
536
+ <EuiButtonGroup legend="Format" variant="selection">
537
+ <EuiButton id="bold">Bold</EuiButton>
538
+ <EuiToolTip content="Italic">
539
+ <EuiButton id="italic">Italic</EuiButton>
540
+ </EuiToolTip>
541
+ <EuiPopover button={<EuiButton id="more">More</EuiButton>} isOpen={false} closePopover={() => {}}>
542
+ Panel content
543
+ </EuiPopover>
544
+ </EuiButtonGroup>
545
+
546
+ // ✓ Good - spread props are skipped (id may come from the spread)
547
+ <EuiButtonGroup legend="Format" variant="selection">
548
+ <EuiButton {...buttonProps} />
549
+ </EuiButtonGroup>
550
+ ```
551
+
456
552
  ## Testing
457
553
 
458
554
  ### Running unit tests
package/lib/cjs/index.js CHANGED
@@ -24,6 +24,7 @@ var _tooltip_no_interactive_content = require("./rules/a11y/tooltip_no_interacti
24
24
  var _tooltip_button_icon_wrap = require("./rules/a11y/tooltip_button_icon_wrap");
25
25
  var _no_deprecated_icon_aliases = require("./rules/no_deprecated_icon_aliases");
26
26
  var _button_group_no_invalid_children = require("./rules/button_group_no_invalid_children");
27
+ var _button_group_selection_require_id = require("./rules/button_group_selection_require_id");
27
28
  /*
28
29
  * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
29
30
  * or more contributor license agreements. Licensed under the Elastic License
@@ -57,7 +58,8 @@ const config = {
57
58
  'require-href-for-link': _require_href_for_link.RequireHrefForLink,
58
59
  'tooltip-no-interactive-content': _tooltip_no_interactive_content.TooltipNoInteractiveContent,
59
60
  'tooltip-button-icon-wrap': _tooltip_button_icon_wrap.TooltipButtonIconWrap,
60
- 'button-group-no-invalid-children': _button_group_no_invalid_children.ButtonGroupNoInvalidChildren
61
+ 'button-group-no-invalid-children': _button_group_no_invalid_children.ButtonGroupNoInvalidChildren,
62
+ 'button-group-selection-require-id': _button_group_selection_require_id.ButtonGroupSelectionRequireId
61
63
  },
62
64
  configs: {
63
65
  recommended: {
@@ -65,6 +67,7 @@ const config = {
65
67
  rules: {
66
68
  '@elastic/eui/accessible-interactive-element': 'warn',
67
69
  '@elastic/eui/button-group-no-invalid-children': 'error',
70
+ '@elastic/eui/button-group-selection-require-id': 'error',
68
71
  '@elastic/eui/callout-announce-on-mount': 'warn',
69
72
  '@elastic/eui/callout-prefer-props-for-content': 'warn',
70
73
  '@elastic/eui/consistent-is-invalid-props': 'warn',
@@ -1,3 +1,3 @@
1
1
  import { type TSESLint } from '@typescript-eslint/utils';
2
- export declare const ButtonGroupNoInvalidChildren: TSESLint.RuleModule<"invalidUnresolvableWrapperChild" | "invalidWrapperChild" | "invalidChild" | "invalidUnresolvableChild" | "invalidPopoverButton" | "invalidUnresolvablePopoverButton", [], unknown, TSESLint.RuleListener>;
2
+ export declare const ButtonGroupNoInvalidChildren: TSESLint.RuleModule<"invalidUnresolvableWrapperChild" | "invalidWrapperChild" | "invalidChild" | "invalidUnresolvableChild" | "invalidPopoverButton" | "invalidUnresolvablePopoverButton" | "invalidMixedTypes", [], unknown, TSESLint.RuleListener>;
3
3
  //# sourceMappingURL=button_group_no_invalid_children.d.ts.map
@@ -1 +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"}
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,sPAsPxC,CAAC"}
@@ -8,8 +8,9 @@ var _utils = require("@typescript-eslint/utils");
8
8
  var _has_spread = require("../utils/has_spread");
9
9
  var _flat_map = require("../utils/flat_map");
10
10
  var _get_element_name = require("../utils/get_element_name");
11
- var _walk_jsx_children = require("../utils/walk_jsx_children");
12
11
  var _get_attr_value = require("../utils/get_attr_value");
12
+ var _collect_jsx_children = require("../utils/collect_jsx_children");
13
+ var _button_group_constants = require("../utils/button_group_constants");
13
14
  /*
14
15
  * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
15
16
  * or more contributor license agreements. Licensed under the Elastic License
@@ -18,37 +19,27 @@ var _get_attr_value = require("../utils/get_attr_value");
18
19
  * Side Public License, v 1.
19
20
  */
20
21
 
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(', ');
22
+ const VALID_WRAPPERS_LIST = Array.from(_button_group_constants.VALID_WRAPPERS).join(', ');
26
23
  function isCustomComponent(name) {
27
24
  return name[0] === name[0].toUpperCase();
28
25
  }
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));
26
+ function reportInvalidWrapperChildren(wrapper, wrapperName, context, validButtons, allowed, seenButtonTypes) {
27
+ const children = (0, _flat_map.flatMap)(wrapper.children, c => (0, _collect_jsx_children.collectJsxChildren)(c, context.sourceCode));
42
28
  for (const wrapperChild of children) {
43
29
  if ((0, _has_spread.hasSpread)(wrapperChild.openingElement.attributes)) continue;
44
30
  const wrapperChildName = (0, _get_element_name.getElementName)(wrapperChild.openingElement);
45
- if (wrapperChildName === null || VALID_BUTTONS.has(wrapperChildName)) continue;
31
+ if (wrapperChildName === null) continue;
32
+ if (validButtons.has(wrapperChildName)) {
33
+ seenButtonTypes?.add(wrapperChildName);
34
+ continue;
35
+ }
46
36
  context.report({
47
37
  node: wrapperChild.openingElement,
48
- messageId: isCustomComponent(wrapperChildName) ? 'invalidUnresolvableWrapperChild' : 'invalidWrapperChild',
38
+ messageId: isCustomComponent(wrapperChildName) && !_button_group_constants.VALID_BUTTONS.has(wrapperChildName) ? 'invalidUnresolvableWrapperChild' : 'invalidWrapperChild',
49
39
  data: {
50
40
  name: wrapperChildName,
51
- wrapper: wrapperName
41
+ wrapper: wrapperName,
42
+ allowed
52
43
  }
53
44
  });
54
45
  }
@@ -60,25 +51,36 @@ const ButtonGroupNoInvalidChildren = exports.ButtonGroupNoInvalidChildren = _uti
60
51
  const {
61
52
  openingElement
62
53
  } = node;
63
- if (openingElement.name.type !== 'JSXIdentifier' || openingElement.name.name !== BUTTON_GROUP) {
54
+ if (openingElement.name.type !== 'JSXIdentifier' || openingElement.name.name !== _button_group_constants.BUTTON_GROUP) {
64
55
  return;
65
56
  }
66
57
 
67
- // Only validate when the variant is "default" (or absent the default).
68
- // Rules for "segmented" and "selection" are added in later chunks.
58
+ // Validate "default", "segmented", and "selection" variants.
69
59
  // Dynamic variant values (variables) are conservatively skipped.
70
60
  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));
61
+ if (variant !== undefined && variant !== 'default' && variant !== 'segmented' && variant !== 'selection') return;
62
+ const isSegmented = variant === 'segmented';
63
+ const isSelection = variant === 'selection';
64
+ const validButtons = isSegmented ? _button_group_constants.SEGMENTED_VALID_BUTTONS : isSelection ? _button_group_constants.SELECTION_VALID_BUTTONS : _button_group_constants.VALID_BUTTONS;
65
+ const allowed = Array.from(validButtons).join(', ');
66
+ const children = (0, _flat_map.flatMap)(node.children, c => (0, _collect_jsx_children.collectJsxChildren)(c, context.sourceCode));
73
67
  if (children.length === 0) return;
68
+
69
+ // Collect seen button types for the segmented/selection mixed-type check.
70
+ // Populated during the main loop to avoid a second traversal.
71
+ const seenButtonTypes = isSegmented || isSelection ? new Set() : null;
74
72
  for (const child of children) {
75
73
  const name = (0, _get_element_name.getElementName)(child.openingElement);
76
74
  if (name === null) continue;
77
- if (VALID_BUTTONS.has(name)) continue;
78
- if (VALID_WRAPPERS.has(name)) {
75
+ if (validButtons.has(name)) {
76
+ seenButtonTypes?.add(name);
77
+ continue;
78
+ }
79
+ if (_button_group_constants.VALID_WRAPPERS.has(name)) {
79
80
  if (name === 'EuiToolTip') {
80
81
  // Validate JSX children (expanding fragments/conditionals).
81
- reportInvalidWrapperChildren(child, name, context);
82
+ // Also collects button types for the segmented mixed-type check.
83
+ reportInvalidWrapperChildren(child, name, context, validButtons, allowed, seenButtonTypes ?? undefined);
82
84
  } else if (name === 'EuiPopover') {
83
85
  // The trigger is the `button` prop, not JSX children (panel
84
86
  // content). The prop value may be a JSXExpressionContainer or
@@ -86,23 +88,32 @@ const ButtonGroupNoInvalidChildren = exports.ButtonGroupNoInvalidChildren = _uti
86
88
  // EuiToolTip wrapping the trigger button is also supported.
87
89
  const buttonProp = child.openingElement.attributes.find(attr => attr.type === 'JSXAttribute' && attr.name.type === 'JSXIdentifier' && attr.name.name === 'button');
88
90
  if (buttonProp?.value != null) {
89
- const triggerElements = collectJsxChildren(buttonProp.value, context.sourceCode);
91
+ const triggerElements = (0, _collect_jsx_children.collectJsxChildren)(buttonProp.value, context.sourceCode);
90
92
  for (const triggerElement of triggerElements) {
91
93
  if ((0, _has_spread.hasSpread)(triggerElement.openingElement.attributes)) continue;
92
94
  const triggerElementName = (0, _get_element_name.getElementName)(triggerElement.openingElement);
93
- if (triggerElementName === null || VALID_BUTTONS.has(triggerElementName)) continue;
95
+ if (triggerElementName === null) continue;
96
+ if (validButtons.has(triggerElementName)) {
97
+ seenButtonTypes?.add(triggerElementName);
98
+ continue;
99
+ }
94
100
  if (triggerElementName === 'EuiToolTip') {
95
101
  // EuiToolTip wrapping the trigger — validate its children.
96
- const tooltipChildren = (0, _flat_map.flatMap)(triggerElement.children, c => collectJsxChildren(c, context.sourceCode));
102
+ const tooltipChildren = (0, _flat_map.flatMap)(triggerElement.children, c => (0, _collect_jsx_children.collectJsxChildren)(c, context.sourceCode));
97
103
  for (const tooltipChild of tooltipChildren) {
98
104
  if ((0, _has_spread.hasSpread)(tooltipChild.openingElement.attributes)) continue;
99
105
  const tooltipChildName = (0, _get_element_name.getElementName)(tooltipChild.openingElement);
100
- if (tooltipChildName === null || VALID_BUTTONS.has(tooltipChildName)) continue;
106
+ if (tooltipChildName === null) continue;
107
+ if (validButtons.has(tooltipChildName)) {
108
+ seenButtonTypes?.add(tooltipChildName);
109
+ continue;
110
+ }
101
111
  context.report({
102
112
  node: tooltipChild.openingElement,
103
- messageId: isCustomComponent(tooltipChildName) ? 'invalidUnresolvablePopoverButton' : 'invalidPopoverButton',
113
+ messageId: isCustomComponent(tooltipChildName) && !_button_group_constants.VALID_BUTTONS.has(tooltipChildName) ? 'invalidUnresolvablePopoverButton' : 'invalidPopoverButton',
104
114
  data: {
105
- name: tooltipChildName
115
+ name: tooltipChildName,
116
+ allowed
106
117
  }
107
118
  });
108
119
  }
@@ -110,9 +121,10 @@ const ButtonGroupNoInvalidChildren = exports.ButtonGroupNoInvalidChildren = _uti
110
121
  }
111
122
  context.report({
112
123
  node: triggerElement.openingElement,
113
- messageId: isCustomComponent(triggerElementName) ? 'invalidUnresolvablePopoverButton' : 'invalidPopoverButton',
124
+ messageId: isCustomComponent(triggerElementName) && !_button_group_constants.VALID_BUTTONS.has(triggerElementName) ? 'invalidUnresolvablePopoverButton' : 'invalidPopoverButton',
114
125
  data: {
115
- name: triggerElementName
126
+ name: triggerElementName,
127
+ allowed
116
128
  }
117
129
  });
118
130
  }
@@ -121,7 +133,8 @@ const ButtonGroupNoInvalidChildren = exports.ButtonGroupNoInvalidChildren = _uti
121
133
  // Children is a render prop: {(copy) => <EuiButton />}
122
134
  // ArrowFunctionExpression with expression body is expanded by
123
135
  // collectJsxChildren, so validation works the same as EuiToolTip.
124
- reportInvalidWrapperChildren(child, name, context);
136
+ // Also collects button types for the segmented mixed-type check.
137
+ reportInvalidWrapperChildren(child, name, context, validButtons, allowed, seenButtonTypes ?? undefined);
125
138
  }
126
139
  continue;
127
140
  }
@@ -131,9 +144,23 @@ const ButtonGroupNoInvalidChildren = exports.ButtonGroupNoInvalidChildren = _uti
131
144
  if ((0, _has_spread.hasSpread)(child.openingElement.attributes)) continue;
132
145
  context.report({
133
146
  node: child.openingElement,
134
- messageId: isCustomComponent(name) ? 'invalidUnresolvableChild' : 'invalidChild',
147
+ messageId: isCustomComponent(name) && !_button_group_constants.VALID_BUTTONS.has(name) ? 'invalidUnresolvableChild' : 'invalidChild',
148
+ data: {
149
+ name,
150
+ allowed
151
+ }
152
+ });
153
+ }
154
+
155
+ // For segmented/selection, all children must be the same button type —
156
+ // either all EuiButton or all EuiButtonIcon. Types are collected during
157
+ // the main loop above, including from EuiToolTip, EuiPopover, and EuiCopy.
158
+ if (seenButtonTypes?.has('EuiButton') && seenButtonTypes.has('EuiButtonIcon')) {
159
+ context.report({
160
+ node: openingElement,
161
+ messageId: 'invalidMixedTypes',
135
162
  data: {
136
- name
163
+ variant: isSelection ? 'selection' : 'segmented'
137
164
  }
138
165
  });
139
166
  }
@@ -143,16 +170,17 @@ const ButtonGroupNoInvalidChildren = exports.ButtonGroupNoInvalidChildren = _uti
143
170
  meta: {
144
171
  type: 'problem',
145
172
  docs: {
146
- description: `Enforce that EuiButtonGroup children are ${VALID_BUTTONS_LIST}, or a supported wrapper (${VALID_WRAPPERS_LIST})`
173
+ description: `Enforce that EuiButtonGroup children are valid button components, or a supported wrapper (${VALID_WRAPPERS_LIST})`
147
174
  },
148
175
  schema: [],
149
176
  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(' ')
177
+ invalidChild: [`{{ name }} is not a valid child of EuiButtonGroup.`, `Allowed children: {{ allowed }}.`, `Allowed wrappers: ${VALID_WRAPPERS_LIST}.`].join(' '),
178
+ invalidUnresolvableChild: [`{{ name }} cannot be verified as a valid child of EuiButtonGroup.`, `Allowed children: {{ allowed }}.`, `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(' '),
179
+ invalidPopoverButton: [`{{ name }} is not a valid trigger button for EuiPopover in EuiButtonGroup.`, `The \`button\` prop must be {{ allowed }}.`].join(' '),
180
+ invalidUnresolvablePopoverButton: [`{{ name }} cannot be verified as a valid trigger button for EuiPopover in EuiButtonGroup.`, `The \`button\` prop must be {{ allowed }}.`, `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(' '),
181
+ invalidWrapperChild: [`{{ name }} inside {{ wrapper }} is not a valid button for EuiButtonGroup.`, `The wrapper must contain {{ allowed }}.`].join(' '),
182
+ invalidUnresolvableWrapperChild: [`{{ name }} inside {{ wrapper }} cannot be verified as a valid button for EuiButtonGroup.`, `The wrapper must contain {{ allowed }}.`, `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(' '),
183
+ invalidMixedTypes: [`EuiButtonGroup with variant="{{ variant }}" must use a single button type throughout.`, `Use either all EuiButton or all EuiButtonIcon children — not both.`].join(' ')
156
184
  }
157
185
  },
158
186
  defaultOptions: []
@@ -0,0 +1,3 @@
1
+ import { type TSESLint } from '@typescript-eslint/utils';
2
+ export declare const ButtonGroupSelectionRequireId: TSESLint.RuleModule<"missingId", [], unknown, TSESLint.RuleListener>;
3
+ //# sourceMappingURL=button_group_selection_require_id.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"button_group_selection_require_id.d.ts","sourceRoot":"","sources":["../../../src/rules/button_group_selection_require_id.ts"],"names":[],"mappings":"AAQA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;AA8BlC,eAAO,MAAM,6BAA6B,sEAiHtC,CAAC"}
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.ButtonGroupSelectionRequireId = 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 _get_attr_value = require("../utils/get_attr_value");
12
+ var _has_meaningful_attr = require("../utils/has_meaningful_attr");
13
+ var _collect_jsx_children = require("../utils/collect_jsx_children");
14
+ var _button_group_constants = require("../utils/button_group_constants");
15
+ /*
16
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
17
+ * or more contributor license agreements. Licensed under the Elastic License
18
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
19
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
20
+ * Side Public License, v 1.
21
+ */
22
+
23
+ function checkButtonForId(element, context) {
24
+ const name = (0, _get_element_name.getElementName)(element.openingElement);
25
+ if (name === null || !_button_group_constants.SELECTION_VALID_BUTTONS.has(name)) return;
26
+ const {
27
+ attributes
28
+ } = element.openingElement;
29
+ if ((0, _has_meaningful_attr.hasMeaningfulAttr)(element.openingElement, 'id')) return;
30
+ if ((0, _has_spread.hasSpread)(attributes)) return;
31
+ context.report({
32
+ node: element.openingElement,
33
+ messageId: 'missingId',
34
+ data: {
35
+ name
36
+ }
37
+ });
38
+ }
39
+ const ButtonGroupSelectionRequireId = exports.ButtonGroupSelectionRequireId = _utils.ESLintUtils.RuleCreator.withoutDocs({
40
+ create(context) {
41
+ return {
42
+ JSXElement(node) {
43
+ const {
44
+ openingElement
45
+ } = node;
46
+ if (openingElement.name.type !== 'JSXIdentifier' || openingElement.name.name !== _button_group_constants.BUTTON_GROUP) {
47
+ return;
48
+ }
49
+
50
+ // Only validate variant="selection". Dynamic or other variants are skipped.
51
+ const variant = (0, _get_attr_value.findAttrValue)(context, openingElement.attributes, 'variant');
52
+ if (variant !== 'selection') return;
53
+ const children = (0, _flat_map.flatMap)(node.children, c => (0, _collect_jsx_children.collectJsxChildren)(c, context.sourceCode));
54
+ if (children.length === 0) return;
55
+ for (const child of children) {
56
+ const name = (0, _get_element_name.getElementName)(child.openingElement);
57
+ if (name === null) continue;
58
+ if (_button_group_constants.SELECTION_VALID_BUTTONS.has(name)) {
59
+ checkButtonForId(child, context);
60
+ continue;
61
+ }
62
+ if (_button_group_constants.VALID_WRAPPERS.has(name)) {
63
+ if (name === 'EuiToolTip') {
64
+ const wrapperChildren = (0, _flat_map.flatMap)(child.children, c => (0, _collect_jsx_children.collectJsxChildren)(c, context.sourceCode));
65
+ for (const wrapperChild of wrapperChildren) {
66
+ checkButtonForId(wrapperChild, context);
67
+ }
68
+ } else if (name === 'EuiPopover') {
69
+ const buttonProp = child.openingElement.attributes.find(attr => attr.type === 'JSXAttribute' && attr.name.type === 'JSXIdentifier' && attr.name.name === 'button');
70
+ if (buttonProp?.value != null) {
71
+ const triggerElements = (0, _collect_jsx_children.collectJsxChildren)(buttonProp.value, context.sourceCode);
72
+ for (const triggerElement of triggerElements) {
73
+ const triggerName = (0, _get_element_name.getElementName)(triggerElement.openingElement);
74
+ if (triggerName === null) continue;
75
+ if (_button_group_constants.SELECTION_VALID_BUTTONS.has(triggerName)) {
76
+ checkButtonForId(triggerElement, context);
77
+ continue;
78
+ }
79
+ if (triggerName === 'EuiToolTip') {
80
+ // EuiToolTip wrapping the popover trigger — check its children.
81
+ const tooltipChildren = (0, _flat_map.flatMap)(triggerElement.children, c => (0, _collect_jsx_children.collectJsxChildren)(c, context.sourceCode));
82
+ for (const tooltipChild of tooltipChildren) {
83
+ checkButtonForId(tooltipChild, context);
84
+ }
85
+ }
86
+ }
87
+ }
88
+ } else if (name === 'EuiCopy') {
89
+ // Children is a render prop; walkJsxChildren expands it.
90
+ const wrapperChildren = (0, _flat_map.flatMap)(child.children, c => (0, _collect_jsx_children.collectJsxChildren)(c, context.sourceCode));
91
+ for (const wrapperChild of wrapperChildren) {
92
+ checkButtonForId(wrapperChild, context);
93
+ }
94
+ }
95
+ continue;
96
+ }
97
+
98
+ // Non-button, non-wrapper children are skipped — the other rule handles those.
99
+ }
100
+ }
101
+ };
102
+ },
103
+ meta: {
104
+ type: 'problem',
105
+ docs: {
106
+ description: 'Enforce that EuiButtonGroup children have an id prop when variant="selection"'
107
+ },
108
+ schema: [],
109
+ messages: {
110
+ missingId: [`{{ name }} inside EuiButtonGroup with variant="selection" is missing a required id prop.`, `Each button must have a unique id so the group can track selection state.`].join(' ')
111
+ }
112
+ },
113
+ defaultOptions: []
114
+ });