@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.
- package/README.md +99 -3
- package/lib/cjs/index.js +4 -1
- package/lib/cjs/rules/button_group_no_invalid_children.d.ts +1 -1
- package/lib/cjs/rules/button_group_no_invalid_children.d.ts.map +1 -1
- package/lib/cjs/rules/button_group_no_invalid_children.js +76 -48
- package/lib/cjs/rules/button_group_selection_require_id.d.ts +3 -0
- package/lib/cjs/rules/button_group_selection_require_id.d.ts.map +1 -0
- package/lib/cjs/rules/button_group_selection_require_id.js +114 -0
- package/lib/cjs/rules/no_deprecated_icon_aliases.d.ts +54 -5
- package/lib/cjs/rules/no_deprecated_icon_aliases.d.ts.map +1 -1
- package/lib/cjs/rules/no_deprecated_icon_aliases.js +70 -8
- package/lib/cjs/utils/button_group_constants.d.ts +6 -0
- package/lib/cjs/utils/button_group_constants.d.ts.map +1 -0
- package/lib/cjs/utils/button_group_constants.js +19 -0
- package/lib/cjs/utils/collect_jsx_children.d.ts +3 -0
- package/lib/cjs/utils/collect_jsx_children.d.ts.map +1 -0
- package/lib/cjs/utils/collect_jsx_children.js +26 -0
- package/lib/esm/index.js +3 -0
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/rules/button_group_no_invalid_children.d.ts +1 -1
- package/lib/esm/rules/button_group_no_invalid_children.js +81 -48
- package/lib/esm/rules/button_group_no_invalid_children.js.map +1 -1
- package/lib/esm/rules/button_group_selection_require_id.d.ts +2 -0
- package/lib/esm/rules/button_group_selection_require_id.js +118 -0
- package/lib/esm/rules/button_group_selection_require_id.js.map +1 -0
- package/lib/esm/rules/no_deprecated_icon_aliases.d.ts +54 -5
- package/lib/esm/rules/no_deprecated_icon_aliases.js +75 -6
- package/lib/esm/rules/no_deprecated_icon_aliases.js.map +1 -1
- package/lib/esm/utils/button_group_constants.d.ts +5 -0
- package/lib/esm/utils/button_group_constants.js +20 -0
- package/lib/esm/utils/button_group_constants.js.map +1 -0
- package/lib/esm/utils/collect_jsx_children.d.ts +2 -0
- package/lib/esm/utils/collect_jsx_children.js +21 -0
- package/lib/esm/utils/collect_jsx_children.js.map +1 -0
- 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
|
|
381
|
-
- `variant="default"
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
30
|
-
const
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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 (
|
|
78
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
151
|
-
invalidUnresolvableChild: [`{{ name }} cannot be verified as a valid child of EuiButtonGroup.`, `Allowed children:
|
|
152
|
-
invalidPopoverButton: [`{{ name }} is not a valid trigger button for EuiPopover in EuiButtonGroup.`, `The \`button\` prop must be
|
|
153
|
-
invalidUnresolvablePopoverButton: [`{{ name }} cannot be verified as a valid trigger button for EuiPopover in EuiButtonGroup.`, `The \`button\` prop must be
|
|
154
|
-
invalidWrapperChild: [`{{ name }} inside {{ wrapper }} is not a valid button for EuiButtonGroup.`, `The wrapper must contain
|
|
155
|
-
invalidUnresolvableWrapperChild: [`{{ name }} inside {{ wrapper }} cannot be verified as a valid button for EuiButtonGroup.`, `The wrapper must contain
|
|
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 @@
|
|
|
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
|
+
});
|