Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/eslint-plugin/changelogs/upcoming/9236.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add a `no-static-z-index` rule
52 changes: 27 additions & 25 deletions packages/eslint-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
},
},
Expand Down
22 changes: 13 additions & 9 deletions packages/eslint-plugin/src/rules/no_css_color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -64,7 +65,7 @@ const checkPropertySpecifiesInvalidCSSColor = ([property, value]: string[]) => {
'initial',
'unset',
'revert',
'revert-layer'
'revert-layer',
];

const normalizedColorValue = colorValue.toLowerCase().trim();
Expand All @@ -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,
]))
) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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') {
Expand Down
242 changes: 242 additions & 0 deletions packages/eslint-plugin/src/rules/no_static_z_index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
/*
* 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 (
<EuiCode style={{ zIndex: euiTheme.levels.mask }}>test</EuiCode>
)
}`,
languageOptions,
},
{
// Valid: CSS keyword 'auto'
filename: 'test.tsx',
code: dedent`
import React from 'react';

function TestComponent() {
return (
<div style={{ zIndex: 'auto' }}>test</div>
)
}`,
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 (
<div css={css\`
z-index: \${theme.zIndex.modal};
\`}>test</div>
)
}`,
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 <div style={style}>test</div>
}`,
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 (
<div css={css\`
/* z-index: 50; */
\`}>test</div>
)
}`,
languageOptions,
},
],

invalid: [
{
// Invalid: Inline style with static number
filename: 'test.tsx',
code: dedent`
import React from 'react';

function TestComponent() {
return (
<div style={{ zIndex: 100 }}>test</div>
)
}`,
languageOptions,
errors: [{ messageId: 'noStaticZIndexSpecific' }],
},
{
// Invalid: Inline style with static string number
filename: 'test.tsx',
code: dedent`
import React from 'react';

function TestComponent() {
return (
<div style={{ zIndex: '999' }}>test</div>
)
}`,
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 (
<div css={css\`
z-index: 50;
\`}>test</div>
)
}`,
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 <div style={myStyle}>test</div>
}`,
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 <div css={myCss}>test</div>
}`,
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 <div css={css({ zIndex: 5 })}>test</div>
}
`,
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 <div css={() => ({ zIndex: 5 })}>test</div>
}
`,
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 <div css={css(someStyle, { zIndex: 5 })}>test</div>
}
`,
languageOptions,
errors: [{ messageId: 'noStaticZIndexSpecific' }],
},
],
});
Loading