From 5e74b6962b1e4248924d576379e80e5912c19951 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Mon, 15 Apr 2024 09:18:56 +0100 Subject: [PATCH] Chore: Add lint rule for `no-unreduced-motion` (#85862) * add lint rule for no-unreduced-motion * update to satisfy types --- packages/grafana-eslint-rules/README.md | 14 +++- packages/grafana-eslint-rules/index.cjs | 2 + .../rules/no-aria-label-e2e-selectors.cjs | 6 +- .../rules/no-border-radius-literal.cjs | 2 +- .../rules/no-unreduced-motion.cjs | 65 +++++++++++++++++++ .../rules/theme-token-usage.cjs | 2 +- 6 files changed, 81 insertions(+), 10 deletions(-) create mode 100644 packages/grafana-eslint-rules/rules/no-unreduced-motion.cjs diff --git a/packages/grafana-eslint-rules/README.md b/packages/grafana-eslint-rules/README.md index 1bd800c979e..187e1d2deb9 100644 --- a/packages/grafana-eslint-rules/README.md +++ b/packages/grafana-eslint-rules/README.md @@ -4,7 +4,7 @@ This package contains custom eslint rules for use within the Grafana codebase on ## Rules -### `@grafana/no-aria-label-selectors` +### `no-aria-label-selectors` Require aria-label JSX properties to not include selectors from the `@grafana/e2e-selectors` package. @@ -12,12 +12,20 @@ Previously we hijacked the aria-label property to use as E2E selectors as an att Now, we prefer using data-testid for E2E selectors. -### `@grafana/no-border-radius-literal` +### `no-border-radius-literal` Check if border-radius theme tokens are used. To improve the consistency across Grafana we encourage devs to use tokens instead of custom values. In this case, we want the `borderRadius` to use the appropriate token such as `theme.shape.radius.default`, `theme.shape.radius.pill` or `theme.shape.radius.circle`. -### `@grafana/theme-token-usage` +### `no-unreduced-motion` + +Avoid direct use of `animation*` or `transition*` properties. + +To account for users with motion sensitivities, these should always be wrapped in a [`prefers-reduced-motion`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion) media query. + +`@grafana/ui` exposes a `handledReducedMotion` utility function that can be used to handle this. + +### `theme-token-usage` Used to find all instances of `theme` tokens being used in the codebase and emit the counts as metrics. Should **not** be used as an actual lint rule! diff --git a/packages/grafana-eslint-rules/index.cjs b/packages/grafana-eslint-rules/index.cjs index 581ca0b2e87..d7df5009b06 100644 --- a/packages/grafana-eslint-rules/index.cjs +++ b/packages/grafana-eslint-rules/index.cjs @@ -1,9 +1,11 @@ const noAriaLabelSelectors = require('./rules/no-aria-label-e2e-selectors.cjs'); const noBorderRadiusLiteral = require('./rules/no-border-radius-literal.cjs'); +const noUnreducedMotion = require('./rules/no-unreduced-motion.cjs'); const themeTokenUsage = require('./rules/theme-token-usage.cjs'); module.exports = { rules: { + 'no-unreduced-motion': noUnreducedMotion, 'no-aria-label-selectors': noAriaLabelSelectors, 'no-border-radius-literal': noBorderRadiusLiteral, 'theme-token-usage': themeTokenUsage, diff --git a/packages/grafana-eslint-rules/rules/no-aria-label-e2e-selectors.cjs b/packages/grafana-eslint-rules/rules/no-aria-label-e2e-selectors.cjs index 2f6ab8b8cd0..3662c0abfea 100644 --- a/packages/grafana-eslint-rules/rules/no-aria-label-e2e-selectors.cjs +++ b/packages/grafana-eslint-rules/rules/no-aria-label-e2e-selectors.cjs @@ -15,10 +15,7 @@ const { ESLintUtils } = require('@typescript-eslint/utils'); const GRAFANA_E2E_PACKAGE_NAME = '@grafana/e2e-selectors'; -const createRule = ESLintUtils.RuleCreator( - // TODO: find a proper url? - (name) => `https://github.com/grafana/grafana#${name}` -); +const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/grafana/grafana/blob/main/packages/grafana-eslint-rules/README.md#${name}`); // A relative simple lint rule that will look of the `selectors` export from @grafana/e2e-selectors // is used in an aria-label @@ -69,7 +66,6 @@ const rule = createRule({ meta: { docs: { description: 'aria-label should not contain e2e selectors', - // recommended: 'error', }, messages: { useDataTestId: 'Use data-testid for E2E selectors instead of aria-label', diff --git a/packages/grafana-eslint-rules/rules/no-border-radius-literal.cjs b/packages/grafana-eslint-rules/rules/no-border-radius-literal.cjs index 7c37b228892..6386ced4437 100644 --- a/packages/grafana-eslint-rules/rules/no-border-radius-literal.cjs +++ b/packages/grafana-eslint-rules/rules/no-border-radius-literal.cjs @@ -1,7 +1,7 @@ // @ts-check const { ESLintUtils, AST_NODE_TYPES } = require('@typescript-eslint/utils'); -const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/grafana/grafana#${name}`); +const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/grafana/grafana/blob/main/packages/grafana-eslint-rules/README.md#${name}`); const borderRadiusRule = createRule({ create(context) { diff --git a/packages/grafana-eslint-rules/rules/no-unreduced-motion.cjs b/packages/grafana-eslint-rules/rules/no-unreduced-motion.cjs new file mode 100644 index 00000000000..a8f673a4be2 --- /dev/null +++ b/packages/grafana-eslint-rules/rules/no-unreduced-motion.cjs @@ -0,0 +1,65 @@ +// @ts-check +const { ESLintUtils, AST_NODE_TYPES } = require('@typescript-eslint/utils'); + +const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/grafana/grafana/blob/main/packages/grafana-eslint-rules/README.md#${name}`); + +const restrictedProperties = ['animation', 'transition']; + +const isRestrictedProperty = (/** @type string */ propertyName) => { + return restrictedProperties.some((prop) => propertyName.startsWith(prop)); +}; + +const rule = createRule({ + create(context) { + return { + CallExpression(node) { + if ( + node.callee.type === AST_NODE_TYPES.Identifier && + node.callee.name === 'css' + ) { + const cssObjects = node.arguments.flatMap((node) => { + switch (node.type) { + case AST_NODE_TYPES.ObjectExpression: + return [node]; + case AST_NODE_TYPES.ArrayExpression: + return node.elements.filter(v => v?.type === AST_NODE_TYPES.ObjectExpression); + default: + return []; + } + }); + + for (const cssObject of cssObjects) { + if (cssObject?.type === AST_NODE_TYPES.ObjectExpression) { + for (const property of cssObject.properties) { + if ( + property.type === AST_NODE_TYPES.Property && + property.key.type === AST_NODE_TYPES.Identifier && + isRestrictedProperty(property.key.name) + ) { + context.report({ + node: property, + messageId: 'noUnreducedMotion', + }); + } + } + } + } + } + }, + }; + }, + name: 'no-unreduced-motion', + meta: { + type: 'problem', + docs: { + description: 'Check if animation or transition properties are used directly.', + }, + messages: { + noUnreducedMotion: 'Avoid direct use of `animation*` or `transition*` properties. Use the `handleReducedMotion` utility function or wrap in a `prefers-reduced-motion` media query.', + }, + schema: [], + }, + defaultOptions: [], +}); + +module.exports = rule; diff --git a/packages/grafana-eslint-rules/rules/theme-token-usage.cjs b/packages/grafana-eslint-rules/rules/theme-token-usage.cjs index 3d98fe2cad2..abb56ea6b49 100644 --- a/packages/grafana-eslint-rules/rules/theme-token-usage.cjs +++ b/packages/grafana-eslint-rules/rules/theme-token-usage.cjs @@ -1,7 +1,7 @@ // @ts-check const { ESLintUtils, AST_NODE_TYPES } = require('@typescript-eslint/utils'); -const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/grafana/grafana#${name}`); +const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/grafana/grafana/blob/main/packages/grafana-eslint-rules/README.md#${name}`); const themeTokenUsage = createRule({ create(context) {