@elastic/eslint-plugin-eui 2.7.0 → 2.8.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 CHANGED
@@ -167,6 +167,21 @@ Ensure interactive EUI components (like e.g. `EuiLink`, `EuiButton`, `EuiRadio`)
167
167
 
168
168
  Ensure `EuiInMemoryTable`, `EuiBasicTable` have a `tableCaption` property for accessibility.
169
169
 
170
+ ### `@elastic/eui/badge-accessibility-rules`
171
+
172
+ Ensure the `EuiBadge` includes appropriate accessibility attributes.
173
+
174
+ - `iconOnClick` and `onClick` must not reference the same callback. The rule autofixes by removing `iconOnClick`.
175
+ - `iconOnClickAriaLabel` is only valid when `iconOnClick` is present. The rule autofixes by removing `iconOnClickAriaLabel`.
176
+ - `onClickAriaLabel` is only valid when `onClick` is present. The rule autofixes by removing `onClickAriaLabel`.
177
+
178
+ ### `@elastic/eui/icon-accessibility-rules`
179
+
180
+ Ensure the `EuiIcon` includes appropriate accessibility attributes.
181
+
182
+ - `EuiIcon` has an accessible name via `title`, `aria-label`, or `aria-labelledby`; otherwise mark it decorative with `aria-hidden={true}`
183
+ - Do not combine `tabIndex` with `aria-hidden`
184
+
170
185
  ## Testing
171
186
 
172
187
  ### Running unit tests
package/lib/cjs/index.js CHANGED
@@ -14,6 +14,8 @@ var _require_aria_label_for_modals = require("./rules/a11y/require_aria_label_fo
14
14
  var _require_table_caption = require("./rules/a11y/require_table_caption");
15
15
  var _sr_output_disabled_tooltip = require("./rules/a11y/sr_output_disabled_tooltip");
16
16
  var _tooltip_focusable_anchor = require("./rules/a11y/tooltip_focusable_anchor");
17
+ var _badge_accessibility_rules = require("./rules/a11y/badge_accessibility_rules");
18
+ var _icon_accessibility_rules = require("./rules/a11y/icon_accessibility_rules");
17
19
  /*
18
20
  * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
19
21
  * or more contributor license agreements. Licensed under the Elastic License
@@ -37,7 +39,9 @@ const config = {
37
39
  'require-aria-label-for-modals': _require_aria_label_for_modals.RequireAriaLabelForModals,
38
40
  'require-table-caption': _require_table_caption.RequireTableCaption,
39
41
  'sr-output-disabled-tooltip': _sr_output_disabled_tooltip.ScreenReaderOutputDisabledTooltip,
40
- 'tooltip-focusable-anchor': _tooltip_focusable_anchor.TooltipFocusableAnchor
42
+ 'tooltip-focusable-anchor': _tooltip_focusable_anchor.TooltipFocusableAnchor,
43
+ 'badge-accessibility-rules': _badge_accessibility_rules.EuiBadgeAccessibilityRules,
44
+ 'icon-accessibility-rules': _icon_accessibility_rules.EuiIconAccessibilityRules
41
45
  },
42
46
  configs: {
43
47
  recommended: {
@@ -56,7 +60,9 @@ const config = {
56
60
  '@elastic/eui/require-aria-label-for-modals': 'warn',
57
61
  '@elastic/eui/require-table-caption': 'warn',
58
62
  '@elastic/eui/sr-output-disabled-tooltip': 'warn',
59
- '@elastic/eui/tooltip-focusable-anchor': 'warn'
63
+ '@elastic/eui/tooltip-focusable-anchor': 'warn',
64
+ '@elastic/eui/badge-accessibility-rules': 'warn',
65
+ '@elastic/eui/icon-accessibility-rules': 'warn'
60
66
  }
61
67
  }
62
68
  }
@@ -0,0 +1,3 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const EuiBadgeAccessibilityRules: ESLintUtils.RuleModule<"sameCallback" | "iconOnClickAriaLabelWithoutIconOnClick" | "onClickAriaLabelWithoutOnClick", [], unknown, ESLintUtils.RuleListener>;
3
+ //# sourceMappingURL=badge_accessibility_rules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"badge_accessibility_rules.d.ts","sourceRoot":"","sources":["../../../../src/rules/a11y/badge_accessibility_rules.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAIvD,eAAO,MAAM,0BAA0B,6JAyFrC,CAAC"}
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.EuiBadgeAccessibilityRules = void 0;
7
+ var _utils = require("@typescript-eslint/utils");
8
+ const BADGE_COMPONENT = 'EuiBadge';
9
+ const EuiBadgeAccessibilityRules = exports.EuiBadgeAccessibilityRules = _utils.ESLintUtils.RuleCreator.withoutDocs({
10
+ create(context) {
11
+ return {
12
+ JSXElement(node) {
13
+ const {
14
+ openingElement
15
+ } = node;
16
+ if (openingElement.name.type !== 'JSXIdentifier' || openingElement.name.name !== BADGE_COMPONENT) {
17
+ return;
18
+ }
19
+ let iconOnClick, onClick, iconOnClickAriaLabel, onClickAriaLabel;
20
+ for (const attr of openingElement.attributes) {
21
+ if (attr.type === 'JSXAttribute' && attr.name.type === 'JSXIdentifier') {
22
+ switch (attr.name.name) {
23
+ case 'iconOnClick':
24
+ iconOnClick = attr;
25
+ break;
26
+ case 'onClick':
27
+ onClick = attr;
28
+ break;
29
+ case 'iconOnClickAriaLabel':
30
+ iconOnClickAriaLabel = attr;
31
+ break;
32
+ case 'onClickAriaLabel':
33
+ onClickAriaLabel = attr;
34
+ break;
35
+ }
36
+ }
37
+ }
38
+
39
+ // 1. iconOnClick and onClick cannot refer to the same callback
40
+ if (iconOnClick && onClick && iconOnClick.value && onClick.value && iconOnClick.value.type === 'JSXExpressionContainer' && onClick.value.type === 'JSXExpressionContainer' && iconOnClick.value.expression.type === 'Identifier' && onClick.value.expression.type === 'Identifier' && iconOnClick.value.expression.name === onClick.value.expression.name) {
41
+ context.report({
42
+ node: iconOnClick,
43
+ messageId: 'sameCallback',
44
+ fix: fixer => fixer.removeRange([iconOnClick.range[0], iconOnClick.range[1]])
45
+ });
46
+ }
47
+
48
+ // 2. iconOnClickAriaLabel should be set only if iconOnClick is set
49
+ if (iconOnClickAriaLabel && !iconOnClick) {
50
+ context.report({
51
+ node: iconOnClickAriaLabel,
52
+ messageId: 'iconOnClickAriaLabelWithoutIconOnClick',
53
+ fix: fixer => fixer.removeRange([iconOnClickAriaLabel.range[0], iconOnClickAriaLabel.range[1]])
54
+ });
55
+ }
56
+
57
+ // 3. onClickAriaLabel should be set only if onClick is set
58
+ if (onClickAriaLabel && !onClick) {
59
+ context.report({
60
+ node: onClickAriaLabel,
61
+ messageId: 'onClickAriaLabelWithoutOnClick',
62
+ fix: fixer => fixer.removeRange([onClickAriaLabel.range[0], onClickAriaLabel.range[1]])
63
+ });
64
+ }
65
+ }
66
+ };
67
+ },
68
+ meta: {
69
+ type: 'problem',
70
+ docs: {
71
+ description: `Ensure the ${BADGE_COMPONENT} includes appropriate accessibility attributes`
72
+ },
73
+ fixable: 'code',
74
+ schema: [],
75
+ messages: {
76
+ sameCallback: '`iconOnClick` and `onClick` should not refer to the same callback. Remove `iconOnClick` and keep only `onClick`.',
77
+ iconOnClickAriaLabelWithoutIconOnClick: '`iconOnClickAriaLabel` should only be set if `iconOnClick` is present. Remove this prop.',
78
+ onClickAriaLabelWithoutOnClick: '`onClickAriaLabel` should only be set if `onClick` is present. Remove this prop.'
79
+ }
80
+ },
81
+ defaultOptions: []
82
+ });
@@ -0,0 +1,3 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const EuiIconAccessibilityRules: ESLintUtils.RuleModule<"missingTitleOrAriaHidden" | "tabIndexWithAriaHidden", [], unknown, ESLintUtils.RuleListener>;
3
+ //# sourceMappingURL=icon_accessibility_rules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icon_accessibility_rules.d.ts","sourceRoot":"","sources":["../../../../src/rules/a11y/icon_accessibility_rules.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAY,MAAM,0BAA0B,CAAC;AAKjE,eAAO,MAAM,yBAAyB,sHAyFpC,CAAC"}
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.EuiIconAccessibilityRules = void 0;
7
+ var _utils = require("@typescript-eslint/utils");
8
+ var _remove_attr = require("../../utils/remove_attr");
9
+ const COMPONENT = 'EuiIcon';
10
+ const EuiIconAccessibilityRules = exports.EuiIconAccessibilityRules = _utils.ESLintUtils.RuleCreator.withoutDocs({
11
+ create(context) {
12
+ return {
13
+ JSXElement(node) {
14
+ const {
15
+ openingElement
16
+ } = node;
17
+ if (openingElement.name.type !== 'JSXIdentifier' || openingElement.name.name !== COMPONENT) {
18
+ return;
19
+ }
20
+ let ariaHiddenAttr;
21
+ let tabIndexAttr;
22
+ let isIconNamed = false;
23
+ for (const attr of openingElement.attributes) {
24
+ if (attr.type !== 'JSXAttribute' || attr.name.type !== 'JSXIdentifier') continue;
25
+ const name = attr.name.name;
26
+ if (name === 'aria-hidden') ariaHiddenAttr = attr;
27
+ if (name === 'tabIndex') tabIndexAttr = attr;
28
+ if (['title', 'aria-labelledby', 'aria-label'].includes(name)) {
29
+ isIconNamed = true;
30
+ }
31
+ }
32
+ const hasAriaHiddenTrue = !!ariaHiddenAttr && ariaHiddenAttr.value && (
33
+ // aria-hidden={true}
34
+ ariaHiddenAttr.value.type === 'JSXExpressionContainer' && ariaHiddenAttr.value.expression.type === 'Literal' && ariaHiddenAttr.value.expression.value === true ||
35
+ // aria-hidden='true'
36
+ ariaHiddenAttr.value.type === 'Literal' && ariaHiddenAttr.value.value === 'true');
37
+
38
+ // Case: `tabIndex` and `aria-hidden` cannot be used together
39
+ if (tabIndexAttr && hasAriaHiddenTrue) {
40
+ context.report({
41
+ node: openingElement,
42
+ messageId: 'tabIndexWithAriaHidden',
43
+ fix: fixer => {
44
+ if (!ariaHiddenAttr?.range) return null;
45
+ const [start, end] = (0, _remove_attr.removeAttribute)(context, ariaHiddenAttr);
46
+ return [fixer.removeRange([start, end])];
47
+ }
48
+ });
49
+ return;
50
+ }
51
+
52
+ // Require accessible name or `aria-hidden={true}`;
53
+ if (!isIconNamed && !hasAriaHiddenTrue) {
54
+ context.report({
55
+ node: openingElement,
56
+ messageId: 'missingTitleOrAriaHidden',
57
+ fix: fixer => {
58
+ if (tabIndexAttr) return null;
59
+ const end = openingElement.range[1];
60
+ const insertPos = openingElement.selfClosing ? end - 2 : end - 1; // before '/>' or '>'
61
+ const insertRange = [insertPos, insertPos];
62
+ return [fixer.insertTextBeforeRange(insertRange, ' aria-hidden={true}')];
63
+ }
64
+ });
65
+ }
66
+ }
67
+ };
68
+ },
69
+ meta: {
70
+ type: 'suggestion',
71
+ docs: {
72
+ description: `Ensure the EuiIcon includes appropriate accessibility attributes`
73
+ },
74
+ fixable: 'code',
75
+ schema: [],
76
+ messages: {
77
+ missingTitleOrAriaHidden: 'Add a `title`, `aria-label`, or `aria-labelledby` to EuiIcon, or set `aria-hidden={true}` if it is decorative.',
78
+ tabIndexWithAriaHidden: 'Do not use `tabIndex` together with `aria-hidden`. Remove `aria-hidden` or provide an accessible name.'
79
+ }
80
+ },
81
+ defaultOptions: []
82
+ });
@@ -0,0 +1,26 @@
1
+ import { type TSESTree, type TSESLint } from '@typescript-eslint/utils';
2
+ /**
3
+ * Computes the removal range for a JSX attribute, including a preceding space
4
+ * when present, to keep formatting intact after autofix.
5
+ *
6
+ * This helper is useful in ESLint rule fixers when calling `fixer.removeRange(...)`,
7
+ * ensuring that the attribute and its leading space are removed cleanly.
8
+ *
9
+ * @typeParam TContext - An ESLint rule context type extending `TSESLint.RuleContext`.
10
+ * @param context - The current ESLint rule context providing access to `SourceCode`.
11
+ * @param attr - The JSX attribute node to remove.
12
+ * @returns A readonly tuple `[start, end]` representing the inclusive start and exclusive end indexes for removal.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * context.report({
17
+ * node: openingElement,
18
+ * messageId: 'removeAttr',
19
+ * fix: fixer => {
20
+ * const [start, end] = removeAttribute(context, ariaHiddenAttr);
21
+ * return fixer.removeRange([start, end]);
22
+ * },
23
+ * });
24
+ **/
25
+ export declare function removeAttribute<TContext extends TSESLint.RuleContext<string, unknown[]>>(context: TContext, attr: TSESTree.JSXAttribute): readonly [number, number];
26
+ //# sourceMappingURL=remove_attr.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remove_attr.d.ts","sourceRoot":"","sources":["../../../src/utils/remove_attr.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,QAAQ,EAAC,MAAM,0BAA0B,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;IAsBI;AAEJ,wBAAgB,eAAe,CAC7B,QAAQ,SAAS,QAAQ,CAAC,WAAW,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,EAExD,OAAO,EAAE,QAAQ,EACjB,IAAI,EAAE,QAAQ,CAAC,YAAY,6BAO5B"}
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.removeAttribute = removeAttribute;
7
+ /**
8
+ * Computes the removal range for a JSX attribute, including a preceding space
9
+ * when present, to keep formatting intact after autofix.
10
+ *
11
+ * This helper is useful in ESLint rule fixers when calling `fixer.removeRange(...)`,
12
+ * ensuring that the attribute and its leading space are removed cleanly.
13
+ *
14
+ * @typeParam TContext - An ESLint rule context type extending `TSESLint.RuleContext`.
15
+ * @param context - The current ESLint rule context providing access to `SourceCode`.
16
+ * @param attr - The JSX attribute node to remove.
17
+ * @returns A readonly tuple `[start, end]` representing the inclusive start and exclusive end indexes for removal.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * context.report({
22
+ * node: openingElement,
23
+ * messageId: 'removeAttr',
24
+ * fix: fixer => {
25
+ * const [start, end] = removeAttribute(context, ariaHiddenAttr);
26
+ * return fixer.removeRange([start, end]);
27
+ * },
28
+ * });
29
+ **/
30
+
31
+ function removeAttribute(context, attr) {
32
+ const {
33
+ sourceCode
34
+ } = context;
35
+ const start = attr.range[0];
36
+ const before = sourceCode.text[start - 1];
37
+ const rangeStart = before === ' ' ? start - 1 : start;
38
+ return [rangeStart, attr.range[1]];
39
+ }
package/lib/esm/index.js CHANGED
@@ -21,6 +21,8 @@ const require_aria_label_for_modals_1 = require("./rules/a11y/require_aria_label
21
21
  const require_table_caption_1 = require("./rules/a11y/require_table_caption");
22
22
  const sr_output_disabled_tooltip_1 = require("./rules/a11y/sr_output_disabled_tooltip");
23
23
  const tooltip_focusable_anchor_1 = require("./rules/a11y/tooltip_focusable_anchor");
24
+ const badge_accessibility_rules_1 = require("./rules/a11y/badge_accessibility_rules");
25
+ const icon_accessibility_rules_1 = require("./rules/a11y/icon_accessibility_rules");
24
26
  const config = {
25
27
  rules: {
26
28
  'accessible-interactive-element': accessible_interactive_element_1.AccessibleInteractiveElements,
@@ -37,6 +39,8 @@ const config = {
37
39
  'require-table-caption': require_table_caption_1.RequireTableCaption,
38
40
  'sr-output-disabled-tooltip': sr_output_disabled_tooltip_1.ScreenReaderOutputDisabledTooltip,
39
41
  'tooltip-focusable-anchor': tooltip_focusable_anchor_1.TooltipFocusableAnchor,
42
+ 'badge-accessibility-rules': badge_accessibility_rules_1.EuiBadgeAccessibilityRules,
43
+ 'icon-accessibility-rules': icon_accessibility_rules_1.EuiIconAccessibilityRules
40
44
  },
41
45
  configs: {
42
46
  recommended: {
@@ -56,6 +60,8 @@ const config = {
56
60
  '@elastic/eui/require-table-caption': 'warn',
57
61
  '@elastic/eui/sr-output-disabled-tooltip': 'warn',
58
62
  '@elastic/eui/tooltip-focusable-anchor': 'warn',
63
+ '@elastic/eui/badge-accessibility-rules': 'warn',
64
+ '@elastic/eui/icon-accessibility-rules': 'warn',
59
65
  },
60
66
  },
61
67
  },
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;AAGH,gGAA4F;AAC5F,sFAAgF;AAChF,0FAAoF;AACpF,+DAAuD;AACvD,uDAAkD;AAClD,iFAA2E;AAC3E,iEAA2D;AAC3D,gGAA0F;AAC1F,gFAA0E;AAC1E,0EAAoE;AACpE,8FAAuF;AACvF,8EAAyE;AACzE,wFAA4F;AAC5F,oFAA+E;AAE/E,MAAM,MAAM,GAAG;IACb,KAAK,EAAE;QACL,gCAAgC,EAAE,8DAA6B;QAC/D,2BAA2B,EAAE,kDAAsB;QACnD,6BAA6B,EAAE,sDAAwB;QACvD,kBAAkB,EAAE,8BAAW;QAC/B,cAAc,EAAE,yBAAU;QAC1B,2BAA2B,EAAE,kDAAsB;QACnD,mBAAmB,EAAE,kCAAc;QACnC,gCAAgC,EAAE,4DAA2B;QAC7D,wBAAwB,EAAG,4CAAmB;QAC9C,qBAAqB,EAAE,sCAAgB;QACvC,+BAA+B,EAAE,yDAAyB;QAC1D,uBAAuB,EAAE,2CAAmB;QAC5C,4BAA4B,EAAE,8DAAiC;QAC/D,0BAA0B,EAAE,iDAAsB;KACnD;IACD,OAAO,EAAE;QACP,WAAW,EAAE;YACX,OAAO,EAAE,CAAC,4BAA4B,CAAC;YACvC,KAAK,EAAE;gBACL,6CAA6C,EAAE,MAAM;gBACrD,wCAAwC,EAAE,MAAM;gBAChD,0CAA0C,EAAE,MAAM;gBAClD,+BAA+B,EAAE,MAAM;gBACvC,2BAA2B,EAAE,MAAM;gBACnC,wCAAwC,EAAE,MAAM;gBAChD,gCAAgC,EAAE,MAAM;gBACxC,6CAA6C,EAAE,MAAM;gBACrD,qCAAqC,EAAE,MAAM;gBAC7C,kCAAkC,EAAE,MAAM;gBAC1C,4CAA4C,EAAE,MAAM;gBACpD,oCAAoC,EAAE,MAAM;gBAC5C,yCAAyC,EAAE,MAAM;gBACjD,uCAAuC,EAAE,MAAM;aAChD;SACF;KACF;CACF,CAAC;AAEF,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;AAGH,gGAA4F;AAC5F,sFAAgF;AAChF,0FAAoF;AACpF,+DAAuD;AACvD,uDAAkD;AAClD,iFAA2E;AAC3E,iEAA2D;AAC3D,gGAA0F;AAC1F,gFAA0E;AAC1E,0EAAoE;AACpE,8FAAuF;AACvF,8EAAyE;AACzE,wFAA4F;AAC5F,oFAA+E;AAC/E,sFAAoF;AACpF,oFAAkF;AAElF,MAAM,MAAM,GAAG;IACb,KAAK,EAAE;QACL,gCAAgC,EAAE,8DAA6B;QAC/D,2BAA2B,EAAE,kDAAsB;QACnD,6BAA6B,EAAE,sDAAwB;QACvD,kBAAkB,EAAE,8BAAW;QAC/B,cAAc,EAAE,yBAAU;QAC1B,2BAA2B,EAAE,kDAAsB;QACnD,mBAAmB,EAAE,kCAAc;QACnC,gCAAgC,EAAE,4DAA2B;QAC7D,wBAAwB,EAAG,4CAAmB;QAC9C,qBAAqB,EAAE,sCAAgB;QACvC,+BAA+B,EAAE,yDAAyB;QAC1D,uBAAuB,EAAE,2CAAmB;QAC5C,4BAA4B,EAAE,8DAAiC;QAC/D,0BAA0B,EAAE,iDAAsB;QAClD,2BAA2B,EAAE,sDAA0B;QACvD,0BAA0B,EAAE,oDAAyB;KACtD;IACD,OAAO,EAAE;QACP,WAAW,EAAE;YACX,OAAO,EAAE,CAAC,4BAA4B,CAAC;YACvC,KAAK,EAAE;gBACL,6CAA6C,EAAE,MAAM;gBACrD,wCAAwC,EAAE,MAAM;gBAChD,0CAA0C,EAAE,MAAM;gBAClD,+BAA+B,EAAE,MAAM;gBACvC,2BAA2B,EAAE,MAAM;gBACnC,wCAAwC,EAAE,MAAM;gBAChD,gCAAgC,EAAE,MAAM;gBACxC,6CAA6C,EAAE,MAAM;gBACrD,qCAAqC,EAAE,MAAM;gBAC7C,kCAAkC,EAAE,MAAM;gBAC1C,4CAA4C,EAAE,MAAM;gBACpD,oCAAoC,EAAE,MAAM;gBAC5C,yCAAyC,EAAE,MAAM;gBACjD,uCAAuC,EAAE,MAAM;gBAC/C,wCAAwC,EAAE,MAAM;gBAChD,uCAAuC,EAAE,MAAM;aAChD;SACF;KACF;CACF,CAAC;AAEF,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const EuiBadgeAccessibilityRules: ESLintUtils.RuleModule<"sameCallback" | "iconOnClickAriaLabelWithoutIconOnClick" | "onClickAriaLabelWithoutOnClick", [], unknown, ESLintUtils.RuleListener>;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EuiBadgeAccessibilityRules = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const BADGE_COMPONENT = 'EuiBadge';
6
+ exports.EuiBadgeAccessibilityRules = utils_1.ESLintUtils.RuleCreator.withoutDocs({
7
+ create(context) {
8
+ return {
9
+ JSXElement(node) {
10
+ const { openingElement } = node;
11
+ if (openingElement.name.type !== 'JSXIdentifier' ||
12
+ openingElement.name.name !== BADGE_COMPONENT) {
13
+ return;
14
+ }
15
+ let iconOnClick, onClick, iconOnClickAriaLabel, onClickAriaLabel;
16
+ for (const attr of openingElement.attributes) {
17
+ if (attr.type === 'JSXAttribute' && attr.name.type === 'JSXIdentifier') {
18
+ switch (attr.name.name) {
19
+ case 'iconOnClick':
20
+ iconOnClick = attr;
21
+ break;
22
+ case 'onClick':
23
+ onClick = attr;
24
+ break;
25
+ case 'iconOnClickAriaLabel':
26
+ iconOnClickAriaLabel = attr;
27
+ break;
28
+ case 'onClickAriaLabel':
29
+ onClickAriaLabel = attr;
30
+ break;
31
+ }
32
+ }
33
+ }
34
+ // 1. iconOnClick and onClick cannot refer to the same callback
35
+ if (iconOnClick &&
36
+ onClick &&
37
+ iconOnClick.value &&
38
+ onClick.value &&
39
+ iconOnClick.value.type === 'JSXExpressionContainer' &&
40
+ onClick.value.type === 'JSXExpressionContainer' &&
41
+ iconOnClick.value.expression.type === 'Identifier' &&
42
+ onClick.value.expression.type === 'Identifier' &&
43
+ iconOnClick.value.expression.name === onClick.value.expression.name) {
44
+ context.report({
45
+ node: iconOnClick,
46
+ messageId: 'sameCallback',
47
+ fix: fixer => fixer.removeRange([iconOnClick.range[0], iconOnClick.range[1]]),
48
+ });
49
+ }
50
+ // 2. iconOnClickAriaLabel should be set only if iconOnClick is set
51
+ if (iconOnClickAriaLabel && !iconOnClick) {
52
+ context.report({
53
+ node: iconOnClickAriaLabel,
54
+ messageId: 'iconOnClickAriaLabelWithoutIconOnClick',
55
+ fix: fixer => fixer.removeRange([iconOnClickAriaLabel.range[0], iconOnClickAriaLabel.range[1]]),
56
+ });
57
+ }
58
+ // 3. onClickAriaLabel should be set only if onClick is set
59
+ if (onClickAriaLabel && !onClick) {
60
+ context.report({
61
+ node: onClickAriaLabel,
62
+ messageId: 'onClickAriaLabelWithoutOnClick',
63
+ fix: fixer => fixer.removeRange([onClickAriaLabel.range[0], onClickAriaLabel.range[1]]),
64
+ });
65
+ }
66
+ },
67
+ };
68
+ },
69
+ meta: {
70
+ type: 'problem',
71
+ docs: {
72
+ description: `Ensure the ${BADGE_COMPONENT} includes appropriate accessibility attributes`,
73
+ },
74
+ fixable: 'code',
75
+ schema: [],
76
+ messages: {
77
+ sameCallback: '`iconOnClick` and `onClick` should not refer to the same callback. Remove `iconOnClick` and keep only `onClick`.',
78
+ iconOnClickAriaLabelWithoutIconOnClick: '`iconOnClickAriaLabel` should only be set if `iconOnClick` is present. Remove this prop.',
79
+ onClickAriaLabelWithoutOnClick: '`onClickAriaLabel` should only be set if `onClick` is present. Remove this prop.',
80
+ },
81
+ },
82
+ defaultOptions: [],
83
+ });
84
+ //# sourceMappingURL=badge_accessibility_rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"badge_accessibility_rules.js","sourceRoot":"","sources":["../../../../src/rules/a11y/badge_accessibility_rules.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const EuiIconAccessibilityRules: ESLintUtils.RuleModule<"missingTitleOrAriaHidden" | "tabIndexWithAriaHidden", [], unknown, ESLintUtils.RuleListener>;
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EuiIconAccessibilityRules = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const remove_attr_1 = require("../../utils/remove_attr");
6
+ const COMPONENT = 'EuiIcon';
7
+ exports.EuiIconAccessibilityRules = utils_1.ESLintUtils.RuleCreator.withoutDocs({
8
+ create(context) {
9
+ return {
10
+ JSXElement(node) {
11
+ const { openingElement } = node;
12
+ if (openingElement.name.type !== 'JSXIdentifier' ||
13
+ openingElement.name.name !== COMPONENT) {
14
+ return;
15
+ }
16
+ let ariaHiddenAttr;
17
+ let tabIndexAttr;
18
+ let isIconNamed = false;
19
+ for (const attr of openingElement.attributes) {
20
+ if (attr.type !== 'JSXAttribute' || attr.name.type !== 'JSXIdentifier')
21
+ continue;
22
+ const name = attr.name.name;
23
+ if (name === 'aria-hidden')
24
+ ariaHiddenAttr = attr;
25
+ if (name === 'tabIndex')
26
+ tabIndexAttr = attr;
27
+ if (['title', 'aria-labelledby', 'aria-label'].includes(name)) {
28
+ isIconNamed = true;
29
+ }
30
+ }
31
+ const hasAriaHiddenTrue = !!ariaHiddenAttr &&
32
+ ariaHiddenAttr.value &&
33
+ (
34
+ // aria-hidden={true}
35
+ (ariaHiddenAttr.value.type === 'JSXExpressionContainer' &&
36
+ ariaHiddenAttr.value.expression.type === 'Literal' &&
37
+ ariaHiddenAttr.value.expression.value === true) ||
38
+ // aria-hidden='true'
39
+ (ariaHiddenAttr.value.type === 'Literal' &&
40
+ ariaHiddenAttr.value.value === 'true'));
41
+ // Case: `tabIndex` and `aria-hidden` cannot be used together
42
+ if (tabIndexAttr && hasAriaHiddenTrue) {
43
+ context.report({
44
+ node: openingElement,
45
+ messageId: 'tabIndexWithAriaHidden',
46
+ fix: fixer => {
47
+ if (!ariaHiddenAttr?.range)
48
+ return null;
49
+ const [start, end] = (0, remove_attr_1.removeAttribute)(context, ariaHiddenAttr);
50
+ return [fixer.removeRange([start, end])];
51
+ }
52
+ });
53
+ return;
54
+ }
55
+ // Require accessible name or `aria-hidden={true}`;
56
+ if (!isIconNamed && !hasAriaHiddenTrue) {
57
+ context.report({
58
+ node: openingElement,
59
+ messageId: 'missingTitleOrAriaHidden',
60
+ fix: fixer => {
61
+ if (tabIndexAttr)
62
+ return null;
63
+ const end = openingElement.range[1];
64
+ const insertPos = openingElement.selfClosing ? end - 2 : end - 1; // before '/>' or '>'
65
+ const insertRange = [insertPos, insertPos];
66
+ return [fixer.insertTextBeforeRange(insertRange, ' aria-hidden={true}')];
67
+ }
68
+ });
69
+ }
70
+ }
71
+ };
72
+ },
73
+ meta: {
74
+ type: 'suggestion',
75
+ docs: {
76
+ description: `Ensure the EuiIcon includes appropriate accessibility attributes`
77
+ },
78
+ fixable: 'code',
79
+ schema: [],
80
+ messages: {
81
+ missingTitleOrAriaHidden: 'Add a `title`, `aria-label`, or `aria-labelledby` to EuiIcon, or set `aria-hidden={true}` if it is decorative.',
82
+ tabIndexWithAriaHidden: 'Do not use `tabIndex` together with `aria-hidden`. Remove `aria-hidden` or provide an accessible name.'
83
+ }
84
+ },
85
+ defaultOptions: []
86
+ });
87
+ //# sourceMappingURL=icon_accessibility_rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icon_accessibility_rules.js","sourceRoot":"","sources":["../../../../src/rules/a11y/icon_accessibility_rules.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import { type TSESTree, type TSESLint } from '@typescript-eslint/utils';
2
+ /**
3
+ * Computes the removal range for a JSX attribute, including a preceding space
4
+ * when present, to keep formatting intact after autofix.
5
+ *
6
+ * This helper is useful in ESLint rule fixers when calling `fixer.removeRange(...)`,
7
+ * ensuring that the attribute and its leading space are removed cleanly.
8
+ *
9
+ * @typeParam TContext - An ESLint rule context type extending `TSESLint.RuleContext`.
10
+ * @param context - The current ESLint rule context providing access to `SourceCode`.
11
+ * @param attr - The JSX attribute node to remove.
12
+ * @returns A readonly tuple `[start, end]` representing the inclusive start and exclusive end indexes for removal.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * context.report({
17
+ * node: openingElement,
18
+ * messageId: 'removeAttr',
19
+ * fix: fixer => {
20
+ * const [start, end] = removeAttribute(context, ariaHiddenAttr);
21
+ * return fixer.removeRange([start, end]);
22
+ * },
23
+ * });
24
+ **/
25
+ export declare function removeAttribute<TContext extends TSESLint.RuleContext<string, unknown[]>>(context: TContext, attr: TSESTree.JSXAttribute): readonly [number, number];
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.removeAttribute = removeAttribute;
4
+ /**
5
+ * Computes the removal range for a JSX attribute, including a preceding space
6
+ * when present, to keep formatting intact after autofix.
7
+ *
8
+ * This helper is useful in ESLint rule fixers when calling `fixer.removeRange(...)`,
9
+ * ensuring that the attribute and its leading space are removed cleanly.
10
+ *
11
+ * @typeParam TContext - An ESLint rule context type extending `TSESLint.RuleContext`.
12
+ * @param context - The current ESLint rule context providing access to `SourceCode`.
13
+ * @param attr - The JSX attribute node to remove.
14
+ * @returns A readonly tuple `[start, end]` representing the inclusive start and exclusive end indexes for removal.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * context.report({
19
+ * node: openingElement,
20
+ * messageId: 'removeAttr',
21
+ * fix: fixer => {
22
+ * const [start, end] = removeAttribute(context, ariaHiddenAttr);
23
+ * return fixer.removeRange([start, end]);
24
+ * },
25
+ * });
26
+ **/
27
+ function removeAttribute(context, attr) {
28
+ const { sourceCode } = context;
29
+ const start = attr.range[0];
30
+ const before = sourceCode.text[start - 1];
31
+ const rangeStart = before === ' ' ? start - 1 : start;
32
+ return [rangeStart, attr.range[1]];
33
+ }
34
+ //# sourceMappingURL=remove_attr.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remove_attr.js","sourceRoot":"","sources":["../../../src/utils/remove_attr.ts"],"names":[],"mappings":";;AA0BA,0CAWC;AAnCD;;;;;;;;;;;;;;;;;;;;;;IAsBI;AAEJ,SAAgB,eAAe,CAG7B,OAAiB,EACjB,IAA2B;IAC3B,MAAM,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC;IAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC5B,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;IAC1C,MAAM,UAAU,GAAG,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IAEtD,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAU,CAAC;AAC9C,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elastic/eslint-plugin-eui",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",