diff --git a/packages/eslint-plugin/changelogs/upcoming/9236.md b/packages/eslint-plugin/changelogs/upcoming/9236.md
new file mode 100644
index 00000000000..888a6d948e0
--- /dev/null
+++ b/packages/eslint-plugin/changelogs/upcoming/9236.md
@@ -0,0 +1 @@
+- Added `no-static-z-index` rule
\ No newline at end of file
diff --git a/packages/eslint-plugin/src/index.ts b/packages/eslint-plugin/src/index.ts
index 168704d760d..be612c85e24 100644
--- a/packages/eslint-plugin/src/index.ts
+++ b/packages/eslint-plugin/src/index.ts
@@ -7,54 +7,56 @@
*/
+import { AccessibleInteractiveElements } from './rules/a11y/accessible_interactive_element';
+import { CallOutAnnounceOnMount } from './rules/a11y/callout_announce_on_mount';
+import { ConsistentIsInvalidProps } from './rules/a11y/consistent_is_invalid_props';
import { HrefOnClick } from './rules/href_or_on_click';
-import { NoRestrictedEuiImports } from './rules/no_restricted_eui_imports';
import { NoCssColor } from './rules/no_css_color';
-
+import { NoRestrictedEuiImports } from './rules/no_restricted_eui_imports';
+import { NoStaticZIndex } from './rules/no_static_z_index';
+import { NoUnnamedInteractiveElement } from './rules/a11y/no_unnamed_interactive_element';
+import { NoUnnamedRadioGroup } from './rules/a11y/no_unnamed_radio_group';
+import { PreferEuiIconTip } from './rules/a11y/prefer_eui_icon_tip';
import { RequireAriaLabelForModals } from './rules/a11y/require_aria_label_for_modals';
-import { ConsistentIsInvalidProps } from './rules/a11y/consistent_is_invalid_props';
+import { RequireTableCaption } from './rules/a11y/require_table_caption';
import { ScreenReaderOutputDisabledTooltip } from './rules/a11y/sr_output_disabled_tooltip';
-import { PreferEuiIconTip } from './rules/a11y/prefer_eui_icon_tip';
-import { NoUnnamedRadioGroup } from './rules/a11y/no_unnamed_radio_group';
-import { NoUnnamedInteractiveElement } from './rules/a11y/no_unnamed_interactive_element';
import { TooltipFocusableAnchor } from './rules/a11y/tooltip_focusable_anchor';
-import { CallOutAnnounceOnMount } from './rules/a11y/callout_announce_on_mount';
-import { AccessibleInteractiveElements } from './rules/a11y/accessible_interactive_element';
-import { RequireTableCaption } from './rules/a11y/require_table_caption';
const config = {
rules: {
+ 'accessible-interactive-element': AccessibleInteractiveElements,
+ 'callout-announce-on-mount': CallOutAnnounceOnMount,
+ 'consistent-is-invalid-props': ConsistentIsInvalidProps,
'href-or-on-click': HrefOnClick,
- 'no-restricted-eui-imports': NoRestrictedEuiImports,
'no-css-color': NoCssColor,
+ 'no-restricted-eui-imports': NoRestrictedEuiImports,
+ 'no-static-z-index': NoStaticZIndex,
+ 'no-unnamed-interactive-element': NoUnnamedInteractiveElement,
+ 'no-unnamed-radio-group' : NoUnnamedRadioGroup,
+ 'prefer-eui-icon-tip': PreferEuiIconTip,
'require-aria-label-for-modals': RequireAriaLabelForModals,
- 'consistent-is-invalid-props': ConsistentIsInvalidProps,
+ 'require-table-caption': RequireTableCaption,
'sr-output-disabled-tooltip': ScreenReaderOutputDisabledTooltip,
- 'prefer-eui-icon-tip': PreferEuiIconTip,
- 'no-unnamed-radio-group' : NoUnnamedRadioGroup,
- 'callout-announce-on-mount': CallOutAnnounceOnMount,
- 'no-unnamed-interactive-element': NoUnnamedInteractiveElement,
'tooltip-focusable-anchor': TooltipFocusableAnchor,
- 'accessible-interactive-element': AccessibleInteractiveElements,
- 'require-table-caption': RequireTableCaption,
},
configs: {
recommended: {
plugins: ['@elastic/eslint-plugin-eui'],
rules: {
+ '@elastic/eui/accessible-interactive-element': 'warn',
+ '@elastic/eui/callout-announce-on-mount': 'warn',
+ '@elastic/eui/consistent-is-invalid-props': 'warn',
'@elastic/eui/href-or-on-click': 'warn',
- '@elastic/eui/no-restricted-eui-imports': 'warn',
'@elastic/eui/no-css-color': 'warn',
+ '@elastic/eui/no-restricted-eui-imports': 'warn',
+ '@elastic/eui/no-static-z-index': 'warn',
+ '@elastic/eui/no-unnamed-interactive-element': 'warn',
+ '@elastic/eui/no-unnamed-radio-group': 'warn',
+ '@elastic/eui/prefer-eui-icon-tip': 'warn',
'@elastic/eui/require-aria-label-for-modals': 'warn',
- '@elastic/eui/consistent-is-invalid-props': 'warn',
+ '@elastic/eui/require-table-caption': 'warn',
'@elastic/eui/sr-output-disabled-tooltip': 'warn',
- '@elastic/eui/prefer-eui-icon-tip': 'warn',
- '@elastic/eui/no-unnamed-radio-group': 'warn',
- '@elastic/eui/callout-announce-on-mount': 'warn',
- '@elastic/eui/no-unnamed-interactive-element': 'warn',
'@elastic/eui/tooltip-focusable-anchor': 'warn',
- '@elastic/eui/accessible-interactive-element': 'warn',
- '@elastic/eui/require-table-caption': 'warn',
},
},
},
diff --git a/packages/eslint-plugin/src/rules/no_css_color.ts b/packages/eslint-plugin/src/rules/no_css_color.ts
index bd657b98c7f..6be2f515381 100644
--- a/packages/eslint-plugin/src/rules/no_css_color.ts
+++ b/packages/eslint-plugin/src/rules/no_css_color.ts
@@ -10,6 +10,7 @@ import { CSSStyleDeclaration } from 'cssstyle';
import { TSESTree, ESLintUtils } from '@typescript-eslint/utils';
import { resolveMemberExpressionRoot } from '../utils/resolve_member_expression_root';
+import { getPropertyName } from '../utils/get_property_name';
import {
ReportDescriptor,
RuleContext,
@@ -64,7 +65,7 @@ const checkPropertySpecifiesInvalidCSSColor = ([property, value]: string[]) => {
'initial',
'unset',
'revert',
- 'revert-layer'
+ 'revert-layer',
];
const normalizedColorValue = colorValue.toLowerCase().trim();
@@ -85,18 +86,17 @@ const raiseReportIfPropertyHasInvalidCssColor = (
) => {
let didReport = false;
- if (
- propertyNode.key.type === 'Identifier' &&
- !htmlElementColorDeclarationRegex.test(propertyNode.key.name)
- ) {
+ const propertyName = getPropertyName(propertyNode);
+
+ if (!propertyName || !htmlElementColorDeclarationRegex.test(propertyName)) {
return didReport;
}
if (propertyNode.value.type === 'Literal') {
if (
(didReport = checkPropertySpecifiesInvalidCSSColor([
- // @ts-expect-error the key name is present in this scenario
- propertyNode.key.name,
+ propertyName,
+ // @ts-expect-error the value is present in this scenario
propertyNode.value.value,
]))
) {
@@ -201,7 +201,10 @@ const handleObjectProperties = (
).name;
const spreadElementDeclaration = context.sourceCode
- .getScope((propertyParentNode!.value as TSESTree.JSXExpressionContainer).expression!)
+ .getScope(
+ (propertyParentNode!.value as TSESTree.JSXExpressionContainer)
+ .expression!
+ )
.references.find(
(ref: { identifier: { name: string } }) =>
ref.identifier.name === spreadElementIdentifierName
@@ -461,7 +464,8 @@ export const NoCssColor = ESLintUtils.RuleCreator.withoutDocs({
let declarationPropertiesNode: TSESTree.Property[] = [];
if (node.value.expression.body.type === 'ObjectExpression') {
- declarationPropertiesNode = node.value.expression.body.properties as TSESTree.Property[];
+ declarationPropertiesNode = node.value.expression.body
+ .properties as TSESTree.Property[];
}
if (node.value.expression.body.type === 'BlockStatement') {
diff --git a/packages/eslint-plugin/src/rules/no_static_z_index.test.ts b/packages/eslint-plugin/src/rules/no_static_z_index.test.ts
new file mode 100644
index 00000000000..74e856b3f56
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/no_static_z_index.test.ts
@@ -0,0 +1,331 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import dedent from 'dedent';
+import { RuleTester } from '@typescript-eslint/rule-tester';
+
+import { NoStaticZIndex } from './no_static_z_index';
+
+const languageOptions = {
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+};
+
+const ruleTester = new RuleTester();
+
+ruleTester.run('no-static-z-index', NoStaticZIndex, {
+ valid: [
+ {
+ // Valid: Using euiTheme variable in inline style
+ filename: 'test.tsx',
+ code: dedent`
+ import React from 'react';
+ import { EuiCode } from '@elastic/eui';
+
+ function TestComponent() {
+ const euiTheme = { levels: { mask: 1000 } };
+ return (
+ test
+ )
+ }`,
+ languageOptions,
+ },
+ {
+ // Valid: CSS keyword 'auto'
+ filename: 'test.tsx',
+ code: dedent`
+ import React from 'react';
+
+ function TestComponent() {
+ return (
+
test
+ )
+ }`,
+ languageOptions,
+ },
+ {
+ // Valid: Emotion css with euiTheme variable
+ filename: 'test.tsx',
+ code: dedent`
+ import React from 'react';
+ import { css } from '@emotion/react';
+
+ function TestComponent() {
+ const theme = { zIndex: { modal: 2000 } };
+ return (
+ test
+ )
+ }`,
+ languageOptions,
+ },
+ {
+ // Valid: Object style with variable
+ filename: 'test.tsx',
+ code: dedent`
+ import React from 'react';
+
+ function TestComponent() {
+ const zIndexValue = someDynamicValue;
+ const style = { zIndex: zIndexValue };
+ return test
+ }`,
+ languageOptions,
+ },
+ {
+ // Valid: Commented out z-index in css template literal
+ filename: 'test.tsx',
+ code: dedent`
+ import React from 'react';
+ import { css } from '@emotion/react';
+
+ function TestComponent() {
+ return (
+ test
+ )
+ }`,
+ languageOptions,
+ },
+ ],
+
+ invalid: [
+ {
+ // Invalid: Inline style with static number
+ filename: 'test.tsx',
+ code: dedent`
+ import React from 'react';
+
+ function TestComponent() {
+ return (
+ test
+ )
+ }`,
+ languageOptions,
+ errors: [{ messageId: 'noStaticZIndexSpecific' }],
+ },
+ {
+ // Invalid: Inline style with static string number
+ filename: 'test.tsx',
+ code: dedent`
+ import React from 'react';
+
+ function TestComponent() {
+ return (
+ test
+ )
+ }`,
+ languageOptions,
+ errors: [{ messageId: 'noStaticZIndexSpecific' }],
+ },
+ {
+ // Invalid: Emotion css prop with static value
+ filename: 'test.tsx',
+ code: dedent`
+ import React from 'react';
+ import { css } from '@emotion/react';
+
+ function TestComponent() {
+ return (
+ test
+ )
+ }`,
+ languageOptions,
+ errors: [{ messageId: 'noStaticZIndex' }],
+ },
+ {
+ // Invalid: Variable with static value used in style
+ filename: 'test.tsx',
+ code: dedent`
+ import React from 'react';
+
+ function TestComponent() {
+ const myStyle = { zIndex: 10 };
+ return test
+ }`,
+ languageOptions,
+ errors: [{ messageId: 'noStaticZIndexSpecificDeclaredVariable' }],
+ },
+ {
+ // Invalid: Variable with static value used in css prop (object style)
+ filename: 'test.tsx',
+ code: dedent`
+ import React from 'react';
+ import { css } from '@emotion/react';
+
+ const myCss = css({ zIndex: 100 });
+
+ function TestComponent() {
+ return test
+ }`,
+ languageOptions,
+ errors: [{ messageId: 'noStaticZIndexSpecificDeclaredVariable' }],
+ },
+ {
+ // Invalid: css template literal with static z-index
+ filename: 'test.tsx',
+ code: dedent`
+ import { css } from '@emotion/css';
+
+ const codeCss = css\` z-index: 10; \`
+ `,
+ languageOptions,
+ errors: [{ messageId: 'noStaticZIndex' }],
+ },
+ {
+ // Invalid: css template literal with nested static z-index
+ filename: 'test.tsx',
+ code: dedent`
+ import { css } from '@emotion/react';
+
+ const codeCss = css\`
+ &:hover {
+ z-index: 10;
+ }
+ \`
+ `,
+ languageOptions,
+ errors: [{ messageId: 'noStaticZIndex' }],
+ },
+ {
+ // Invalid: css object with static z-index
+ filename: 'test.tsx',
+ code: dedent`
+ import { css } from '@emotion/react';
+
+ function TestComponent() {
+ return test
+ }
+ `,
+ languageOptions,
+ errors: [{ messageId: 'noStaticZIndexSpecific' }],
+ },
+ {
+ // Invalid: arrow function returning object with static z-index
+ filename: 'test.tsx',
+ code: dedent`
+ import { css } from '@emotion/react';
+
+ function TestComponent() {
+ return ({ zIndex: 5 })}>test
+ }
+ `,
+ languageOptions,
+ errors: [{ messageId: 'noStaticZIndexSpecific' }],
+ },
+ {
+ // Invalid: css with multiple arguments, one with static z-index
+ filename: 'test.tsx',
+ code: dedent`
+ import { css } from '@emotion/react';
+
+ function TestComponent() {
+ return test
+ }
+ `,
+ languageOptions,
+ errors: [{ messageId: 'noStaticZIndexSpecific' }],
+ },
+ {
+ // Invalid: Variable with static value used in css prop (object style, nested)
+ filename: 'test.tsx',
+ code: dedent`
+ import React from 'react';
+ import { css } from '@emotion/react';
+
+ const myCss = css({ container: { zIndex: 100 } });
+
+ function TestComponent() {
+ return test
+ }`,
+ languageOptions,
+ errors: [{ messageId: 'noStaticZIndexSpecificDeclaredVariable' }],
+ },
+ {
+ // Invalid: css array, one with static z-index
+ filename: 'test.tsx',
+ code: dedent`
+ import { css } from '@emotion/react';
+
+ function TestComponent() {
+ return test
+ }
+ `,
+ languageOptions,
+ errors: [{ messageId: 'noStaticZIndexSpecific' }],
+ },
+ {
+ // Invalid: Conditional expression with static z-index
+ filename: 'test.tsx',
+ code: dedent`
+ import React from 'react';
+
+ function TestComponent() {
+ const isTrue = true;
+ return (
+ test
+ )
+ }`,
+ languageOptions,
+ errors: [
+ { messageId: 'noStaticZIndexSpecific' },
+ { messageId: 'noStaticZIndexSpecific' },
+ ],
+ },
+ {
+ // Invalid: Logical expression with static z-index
+ filename: 'test.tsx',
+ code: dedent`
+ import React from 'react';
+
+ function TestComponent() {
+ const isTrue = true;
+ return (
+ test
+ )
+ }`,
+ languageOptions,
+ errors: [{ messageId: 'noStaticZIndexSpecific' }],
+ },
+ {
+ // Invalid: TSAsExpression with static z-index
+ filename: 'test.tsx',
+ code: dedent`
+ import React from 'react';
+
+ function TestComponent() {
+ return (
+ test
+ )
+ }`,
+ languageOptions,
+ errors: [{ messageId: 'noStaticZIndexSpecific' }],
+ },
+ {
+ // Invalid: UnaryExpression with static z-index
+ filename: 'test.tsx',
+ code: dedent`
+ import React from 'react';
+
+ function TestComponent() {
+ return (
+ test
+ )
+ }`,
+ languageOptions,
+ errors: [{ messageId: 'noStaticZIndexSpecific' }],
+ },
+ ],
+});
diff --git a/packages/eslint-plugin/src/rules/no_static_z_index.ts b/packages/eslint-plugin/src/rules/no_static_z_index.ts
new file mode 100644
index 00000000000..7269134ea24
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/no_static_z_index.ts
@@ -0,0 +1,515 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { TSESTree, ESLintUtils } from '@typescript-eslint/utils';
+import {
+ ReportDescriptor,
+ RuleContext,
+} from '@typescript-eslint/utils/ts-eslint';
+import { getPropertyName } from '../utils/get_property_name';
+
+type MessageIds =
+ | 'noStaticZIndex'
+ | 'noStaticZIndexSpecific'
+ | 'noStaticZIndexSpecificDeclaredVariable';
+
+const propertiesCheckingZIndex = ['zIndex', 'z-index'];
+
+const zIndexDeclarationRegex = RegExp(
+ String.raw`(${propertiesCheckingZIndex.join('|')})`
+);
+
+// Regex to find z-index declarations in CSS strings
+// Matches:
+// 1. z-index property name (case insensitive)
+// 2. colon
+// 3. value (captured group 1) until semicolon, closing brace, or !important
+const cssZIndexRegex = /z-index\s*:\s*([^;!}]+)/gi;
+
+const checkPropertySpecifiesInvalidZIndex = (
+ property: string,
+ value: unknown
+) => {
+ if (!property || value === undefined || value === null) return false;
+
+ const normalizedProperty = property.trim();
+ const isZIndex = propertiesCheckingZIndex.includes(normalizedProperty);
+
+ if (!isZIndex) return false;
+
+ return isInvalidZIndexValue(value);
+};
+
+const isInvalidZIndexValue = (value: unknown): boolean => {
+ const allowedCssKeywords = [
+ 'auto',
+ 'inherit',
+ 'initial',
+ 'unset',
+ 'revert',
+ 'revert-layer',
+ ];
+
+ const stringValue = String(value).trim().toLowerCase();
+
+ if (allowedCssKeywords.includes(stringValue)) {
+ return false;
+ }
+
+ // Check if it's a number (positive, negative, or zero)
+ // This regex allows integers. z-index only accepts integers.
+ if (/^-?\d+$/.test(stringValue)) {
+ return true;
+ }
+
+ return false;
+};
+
+const raiseReportIfPropertyHasInvalidZIndex = (
+ context: RuleContext,
+ propertyNode: TSESTree.Property,
+ messageToReport: ReportDescriptor
+) => {
+ let didReport = false;
+ const propertyName = getPropertyName(propertyNode);
+
+ if (!propertyName || !zIndexDeclarationRegex.test(propertyName)) {
+ return didReport;
+ }
+
+ const visitNode = (node: TSESTree.Node) => {
+ // Handle Literal values: zIndex: 10, 'z-index': '10'
+ if (node.type === 'Literal') {
+ if (checkPropertySpecifiesInvalidZIndex(propertyName, node.value)) {
+ didReport = true;
+ context.report({
+ ...messageToReport,
+ loc: node.loc,
+ });
+ }
+ }
+ // Handle Identifier values: zIndex: someVar
+ else if (node.type === 'Identifier') {
+ const identifierName = node.name;
+ const identifierDeclaration = context.sourceCode
+ .getScope(node)
+ .variables.find((variable) => variable.name === identifierName);
+
+ const identifierDeclarationInit =
+ identifierDeclaration?.defs[0]?.node.type === 'VariableDeclarator'
+ ? identifierDeclaration.defs[0].node.init
+ : undefined;
+
+ if (
+ identifierDeclarationInit?.type === 'Literal' &&
+ checkPropertySpecifiesInvalidZIndex(
+ propertyName,
+ identifierDeclarationInit.value
+ )
+ ) {
+ didReport = true;
+ context.report({
+ loc: node.loc,
+ messageId: 'noStaticZIndexSpecificDeclaredVariable',
+ data: {
+ property: propertyName,
+ line: String(node.loc.start.line),
+ variableName: node.name,
+ },
+ });
+ }
+ } else if (node.type === 'ConditionalExpression') {
+ visitNode(node.consequent);
+ visitNode(node.alternate);
+ } else if (node.type === 'LogicalExpression') {
+ visitNode(node.left);
+ visitNode(node.right);
+ } else if (node.type === 'TSAsExpression') {
+ visitNode(node.expression);
+ } else if (node.type === 'UnaryExpression') {
+ if (node.operator === '-') {
+ if (
+ node.argument.type === 'Literal' &&
+ typeof node.argument.value === 'number'
+ ) {
+ if (
+ checkPropertySpecifiesInvalidZIndex(
+ propertyName,
+ -node.argument.value
+ )
+ ) {
+ didReport = true;
+ context.report({
+ ...messageToReport,
+ loc: node.loc,
+ });
+ }
+ } else {
+ visitNode(node.argument);
+ }
+ }
+ }
+ };
+
+ if (propertyNode.value) {
+ visitNode(propertyNode.value);
+ }
+
+ return didReport;
+};
+
+const handleObjectProperties = (
+ context: RuleContext,
+ propertyParentNode: TSESTree.JSXAttribute | TSESTree.Node,
+ property: TSESTree.ObjectLiteralElement,
+ reportMessage: ReportDescriptor
+) => {
+ if (property.type === 'Property') {
+ if (property.value.type === 'ObjectExpression') {
+ property.value.properties.forEach((nestedProperty) => {
+ const nestedReportMessage = {
+ ...reportMessage,
+ loc: nestedProperty.loc,
+ };
+
+ if (nestedReportMessage.data) {
+ const newData: Record = {
+ ...nestedReportMessage.data,
+ property: getPropertyName(nestedProperty) || 'unknown',
+ };
+
+ if ('line' in newData) {
+ newData.line = String(nestedProperty.loc.start.line);
+ }
+
+ nestedReportMessage.data = newData;
+ }
+
+ handleObjectProperties(
+ context,
+ propertyParentNode,
+ nestedProperty,
+ nestedReportMessage
+ );
+ });
+ } else {
+ raiseReportIfPropertyHasInvalidZIndex(context, property, reportMessage);
+ }
+ } else if (property.type === 'SpreadElement') {
+ if (property.argument.type !== 'Identifier') {
+ return;
+ }
+ const spreadElementIdentifierName = property.argument.name;
+
+ let scopeNode: TSESTree.Node = propertyParentNode;
+ if (
+ propertyParentNode.type === 'JSXAttribute' &&
+ propertyParentNode.value?.type === 'JSXExpressionContainer'
+ ) {
+ scopeNode = propertyParentNode.value.expression;
+ }
+
+ const spreadElementDeclaration = context.sourceCode
+ .getScope(scopeNode)
+ .references.find(
+ (ref) => ref.identifier.name === spreadElementIdentifierName
+ )?.resolved;
+
+ if (!spreadElementDeclaration) {
+ return;
+ }
+
+ const propertyName = getPropertyName(property) || 'spread';
+
+ reportMessage = {
+ loc: propertyParentNode.loc,
+ messageId: 'noStaticZIndexSpecificDeclaredVariable',
+ data: {
+ property: propertyName,
+ variableName: spreadElementIdentifierName,
+ line: String(property.loc.start.line),
+ },
+ };
+
+ const spreadElementDeclarationNode =
+ spreadElementDeclaration.defs[0]?.node.type === 'VariableDeclarator'
+ ? spreadElementDeclaration.defs[0].node.init
+ : undefined;
+
+ if (spreadElementDeclarationNode?.type === 'ObjectExpression') {
+ spreadElementDeclarationNode.properties.forEach((spreadProperty) => {
+ handleObjectProperties(
+ context,
+ propertyParentNode,
+ spreadProperty,
+ reportMessage
+ );
+ });
+ }
+ }
+};
+
+const checkTemplateLiteralForZIndex = (
+ context: RuleContext,
+ node: TSESTree.TemplateLiteral
+) => {
+ for (let i = 0; i < node.quasis.length; i++) {
+ const declarationTemplateNode = node.quasis[i];
+ const rawValue = declarationTemplateNode.value.raw;
+ // Strip comments
+ const valueWithoutComments = rawValue.replace(/\/\*[\s\S]*?\*\//g, '');
+
+ let match;
+ // reset regex state
+ cssZIndexRegex.lastIndex = 0;
+
+ while ((match = cssZIndexRegex.exec(valueWithoutComments)) !== null) {
+ const value = match[1].trim();
+ if (isInvalidZIndexValue(value)) {
+ context.report({
+ node: declarationTemplateNode,
+ messageId: 'noStaticZIndex',
+ });
+ }
+ }
+ }
+};
+
+export const NoStaticZIndex = ESLintUtils.RuleCreator.withoutDocs({
+ create(context) {
+ return {
+ TaggedTemplateExpression(node) {
+ if (
+ node.tag.type !== 'Identifier' ||
+ (node.tag.type === 'Identifier' && node.tag.name !== 'css')
+ ) {
+ return;
+ }
+
+ checkTemplateLiteralForZIndex(context, node.quasi);
+ },
+ JSXAttribute(node: TSESTree.JSXAttribute) {
+ if (!(node.name.name === 'style' || node.name.name === 'css')) {
+ return;
+ }
+
+ if (!node.value || node.value.type !== 'JSXExpressionContainer') {
+ return;
+ }
+
+ const expression = node.value.expression;
+
+ // Handle identifier expression: style={someStyle}
+ if (expression.type === 'Identifier') {
+ const styleVariableName = expression.name;
+ const nodeScope = context.sourceCode.getScope(expression);
+
+ const variableDeclarationMatches = nodeScope.references.find(
+ (ref) => ref.identifier.name === styleVariableName
+ )?.resolved;
+
+ let variableInitializationNode;
+
+ if (
+ variableDeclarationMatches?.defs[0]?.node.type ===
+ 'VariableDeclarator' &&
+ variableDeclarationMatches.defs[0].node.init
+ ) {
+ variableInitializationNode =
+ variableDeclarationMatches.defs[0].node.init;
+
+ if (variableInitializationNode.type === 'ObjectExpression') {
+ variableInitializationNode.properties.forEach((property) => {
+ handleObjectProperties(context, node, property, {
+ loc: property.loc,
+ messageId: 'noStaticZIndexSpecificDeclaredVariable',
+ data: {
+ property: getPropertyName(property) || 'unknown',
+ variableName: styleVariableName,
+ line: String(property.loc.start.line),
+ },
+ });
+ });
+ } else if (
+ variableInitializationNode.type === 'CallExpression' &&
+ variableInitializationNode.callee.type === 'Identifier' &&
+ variableInitializationNode.callee.name === 'css'
+ ) {
+ variableInitializationNode.arguments.forEach((argument) => {
+ if (argument.type === 'ObjectExpression') {
+ argument.properties.forEach((property) => {
+ handleObjectProperties(context, node, property, {
+ loc: property.loc,
+ messageId: 'noStaticZIndexSpecificDeclaredVariable',
+ data: {
+ property: getPropertyName(property) || 'unknown',
+ variableName: styleVariableName,
+ line: String(property.loc.start.line),
+ },
+ });
+ });
+ }
+ });
+ }
+ }
+ return;
+ }
+
+ // Handle inline object: style={{ zIndex: 10 }}
+ if (expression.type === 'ObjectExpression') {
+ const declarationPropertiesNode = expression.properties;
+
+ declarationPropertiesNode?.forEach((property) => {
+ handleObjectProperties(context, node, property, {
+ loc: property.loc,
+ messageId: 'noStaticZIndexSpecific',
+ data: {
+ property: getPropertyName(property) || 'unknown',
+ },
+ });
+ });
+
+ return;
+ }
+
+ // Handle inline CallExpression: css={css({ zIndex: 10 })}
+ if (
+ expression.type === 'CallExpression' &&
+ expression.callee.type === 'Identifier' &&
+ expression.callee.name === 'css'
+ ) {
+ expression.arguments.forEach((argument) => {
+ if (argument.type === 'ObjectExpression') {
+ argument.properties.forEach((property) => {
+ handleObjectProperties(context, node, property, {
+ loc: node.loc,
+ messageId: 'noStaticZIndexSpecific',
+ data: {
+ property: getPropertyName(property) || 'unknown',
+ },
+ });
+ });
+ }
+ });
+ return;
+ }
+
+ // Handle inline ArrayExpression: css={[...]}
+ if (expression.type === 'ArrayExpression') {
+ expression.elements.forEach((element) => {
+ if (!element) return;
+
+ if (element.type === 'ObjectExpression') {
+ element.properties.forEach((property) => {
+ handleObjectProperties(context, node, property, {
+ loc: property.loc,
+ messageId: 'noStaticZIndexSpecific',
+ data: {
+ property: getPropertyName(property) || 'unknown',
+ },
+ });
+ });
+ } else if (
+ element.type === 'CallExpression' &&
+ element.callee.type === 'Identifier' &&
+ element.callee.name === 'css'
+ ) {
+ element.arguments.forEach((argument) => {
+ if (argument.type === 'ObjectExpression') {
+ argument.properties.forEach((property) => {
+ handleObjectProperties(context, node, property, {
+ loc: property.loc,
+ messageId: 'noStaticZIndexSpecific',
+ data: {
+ property: getPropertyName(property) || 'unknown',
+ },
+ });
+ });
+ }
+ });
+ }
+ });
+ }
+
+ // Handle css prop with template literal or function
+ if (node.name.name === 'css') {
+ // css={`...`}
+ if (expression.type === 'TemplateLiteral') {
+ checkTemplateLiteralForZIndex(context, expression);
+ return;
+ }
+
+ // css={() => ({ ... })} or css={function() { return { ... } }}
+ if (
+ expression.type === 'FunctionExpression' ||
+ expression.type === 'ArrowFunctionExpression'
+ ) {
+ let declarationPropertiesNode: TSESTree.ObjectLiteralElement[] = [];
+
+ if (expression.body.type === 'ObjectExpression') {
+ declarationPropertiesNode = expression.body.properties;
+ }
+
+ if (expression.body.type === 'BlockStatement') {
+ const functionReturnStatementNode = expression.body.body?.find(
+ (_node) => {
+ return _node.type === 'ReturnStatement';
+ }
+ );
+
+ if (
+ functionReturnStatementNode?.type === 'ReturnStatement' &&
+ functionReturnStatementNode.argument?.type ===
+ 'ObjectExpression'
+ ) {
+ declarationPropertiesNode =
+ functionReturnStatementNode.argument.properties.filter(
+ (property): property is TSESTree.Property =>
+ property.type === 'Property'
+ );
+ }
+ }
+
+ if (!declarationPropertiesNode.length) {
+ return;
+ }
+
+ declarationPropertiesNode.forEach((property) => {
+ handleObjectProperties(context, node, property, {
+ loc: property.loc,
+ messageId: 'noStaticZIndexSpecific',
+ data: {
+ property: getPropertyName(property) || 'unknown',
+ },
+ });
+ });
+
+ return;
+ }
+ }
+ },
+ };
+ },
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description:
+ 'Use `z-index` definitions from `euiTheme` as opposed to static values',
+ },
+ messages: {
+ noStaticZIndexSpecificDeclaredVariable:
+ 'Avoid using a literal z-index value for "{{property}}", use an EUI theme z-index level instead in declared variable {{variableName}} on line {{line}}',
+ noStaticZIndexSpecific:
+ 'Avoid using a literal z-index value for "{{property}}", use an EUI theme z-index level instead',
+ noStaticZIndex:
+ 'Avoid using a literal z-index value, use an EUI theme z-index level instead',
+ },
+ schema: [],
+ },
+ defaultOptions: [],
+});
diff --git a/packages/eslint-plugin/src/utils/get_property_name.ts b/packages/eslint-plugin/src/utils/get_property_name.ts
new file mode 100644
index 00000000000..f143abd8a50
--- /dev/null
+++ b/packages/eslint-plugin/src/utils/get_property_name.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { TSESTree } from '@typescript-eslint/utils';
+
+export const getPropertyName = (
+ propertyNode: TSESTree.Property | TSESTree.SpreadElement
+): string | null => {
+ if (propertyNode.type === 'Property') {
+ if (propertyNode.key.type === 'Identifier') {
+ return propertyNode.key.name;
+ }
+ if (propertyNode.key.type === 'Literal') {
+ return String(propertyNode.key.value);
+ }
+ } else if (
+ propertyNode.type === 'SpreadElement' &&
+ propertyNode.argument.type === 'Identifier'
+ ) {
+ return propertyNode.argument.name;
+ }
+ return null;
+};