grafana/packages/grafana-eslint-rules
Jack Westbrook 8c41137bcf
Frontend: Update to Eslint 9 (#94823)
* chore(eslint): bump all eslint related packages to latest

* chore(eslint): update linting scripts work with v9

* chore(eslint): introduce flat config

* chore(eslint): delete legacy configs

* feat(grafana-eslint-rules): update rules to use eslint 9 APIs

* chore(eslint): migrate all nested eslintrc files over to root config

* chore(packages): bump eslint dependencies

* feat(betterer): make it work with eslint 9

* style(grafana-data): remove non-existant ban-types rule from disable declarations

* chore(wip): [wip] link eslint-config-grafana

* chore(packages): add @eslint/compat

* chore(eslint): add compat to testing library and fix alerting rules

* chore(eslint): bump grafana eslint-config to v8

* chore(explore): delete legacy eslint config

* chore: clean codeowners file, remove grafana/eslint-config from e2e plugins

* test(eslint-rules): fix no-border-radius-literal and no-aria-label-e2e-selectors rule tests

* Add .js to prettier checks so new eslint.config.js file isn't missed

* chore(eslint): move emotion/syntax-preference to grafana/defaults

* test(eslint): use core-js structured-clone

* revert(services): undo merge backend-format githook changes

* test(eslint-rules): remove structured-clone polyfill from tests

* chore(eslint): add back public/lib/monaco to ignore list, sort alphabetically

* chore(e2e-plugins): remove eslint config 7 from plugins package.json

---------

Co-authored-by: Tom Ratcliffe <tom.ratcliffe@grafana.com>
2024-11-07 16:31:06 +01:00
..
rules Frontend: Update to Eslint 9 (#94823) 2024-11-07 16:31:06 +01:00
tests Frontend: Update to Eslint 9 (#94823) 2024-11-07 16:31:06 +01:00
index.cjs Chore: Fix typo in lint rule file name (#88662) 2024-06-04 12:08:37 +03:00
LICENSE_APACHE2 Chore: eslint rule for preventing e2e selectors in aria-label (#59731) 2023-01-18 15:02:35 +00:00
package.json Frontend: Update to Eslint 9 (#94823) 2024-11-07 16:31:06 +01:00
project.json Build: Nx improvements (#88341) 2024-10-15 14:25:45 +02:00
README.md Internationalisation: Update docs with nested variable examples (#89484) 2024-06-21 09:48:40 +01:00
tsconfig.json Chore: Fix custom eslint rule typechecking (#85886) 2024-04-10 17:12:31 +01:00

Grafana ESLint Rules

This package contains custom eslint rules for use within the Grafana codebase only. They're extremely specific to our codebase, and are of little use to anyone else. They're not published to NPM, and are consumed through the Yarn workspace.

Rules

no-aria-label-selectors

Require aria-label JSX properties to not include selectors from the @grafana/e2e-selectors package.

Previously we hijacked the aria-label property to use as E2E selectors as an attempt to "improve accessibility" while making this easier for testing. However, this lead to many elements having poor, verbose, and unnecessary labels.

Now, we prefer using data-testid for E2E selectors.

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.

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 media query.

There is a handleMotion utility function exposed on the theme that can help with this.

Examples

// Bad ❌
const getStyles = (theme: GrafanaTheme2) => ({
  loading: css({
    animationName: rotate,
    animationDuration: '2s',
    animationIterationCount: 'infinite',
  }),
});

// Good ✅
const getStyles = (theme: GrafanaTheme2) => ({
  loading: css({
    [theme.transitions.handleMotion('no-preference')]: {
      animationName: rotate,
      animationDuration: '2s',
      animationIterationCount: 'infinite',
    },
    [theme.transitions.handleMotion('reduce')]: {
      animationName: pulse,
      animationDuration: '2s',
      animationIterationCount: 'infinite',
    },
  }),
});

// Good ✅
const getStyles = (theme: GrafanaTheme2) => ({
  loading: css({
    '@media (prefers-reduced-motion: no-preference)': {
      animationName: rotate,
      animationDuration: '2s',
      animationIterationCount: 'infinite',
    },
    '@media (prefers-reduced-motion: reduce)': {
      animationName: pulse,
      animationDuration: '2s',
      animationIterationCount: 'infinite',
    },
  }),
});

Note we've switched the potentially sensitive rotating animation to a less intense pulse animation when prefers-reduced-motion is set.

Animations that involve only non-moving properties, like opacity, color, and blurs, are unlikely to be problematic. In those cases, you still need to wrap the animation in a prefers-reduced-motion media query, but you can use the same animation for both cases:

// Bad ❌
const getStyles = (theme: GrafanaTheme2) => ({
  card: css({
    transition: theme.transitions.create(['background-color'], {
      duration: theme.transitions.duration.short,
    }),
  }),
});

// Good ✅
const getStyles = (theme: GrafanaTheme2) => ({
  card: css({
    [theme.transitions.handleMotion('no-preference', 'reduce')]: {
      transition: theme.transitions.create(['background-color'], {
        duration: theme.transitions.duration.short,
      }),
    },
  }),
});

// Good ✅
const getStyles = (theme: GrafanaTheme2) => ({
  card: css({
    '@media (prefers-reduced-motion: no-preference), @media (prefers-reduced-motion: reduce)': {
      transition: theme.transitions.create(['background-color'], {
        duration: theme.transitions.duration.short,
      }),
    },
  }),
});

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!

no-untranslated-strings

Check if strings are marked for translation.

// Bad ❌
<InlineToast placement="top" referenceElement={buttonRef.current}>
  Copied
</InlineToast>

// Good ✅
<InlineToast placement="top" referenceElement={buttonRef.current}>
  <Trans i18nKey="clipboard-button.inline-toast.success">Copied</Trans>
</InlineToast>

Passing variables to translations

// Bad ❌
const SearchTitle = ({ term }) => <div>Results for {term}</div>;

// Good ✅
const SearchTitle = ({ term }) => <Trans i18nKey="search-page.results-title">Results for {{ term }}</Trans>;

// Good ✅ (if you need to interpolate variables inside nested components)
const SearchTerm = ({ term }) => <Text color="success">{term}</Text>;
const SearchTitle = ({ term }) => (
  <Trans i18nKey="search-page.results-title">
    Results for <SearchTerm term={term} />
  </Trans>
);

// Good ✅ (if you need to interpolate variables and additional translated strings inside nested components)
const SearchTitle = ({ term }) => (
  <Trans i18nKey="search-page.results-title" values={{ myVariable: term }}>
    Results for <Text color="success">{'{{ myVariable }}'} and this translated text is also in green</Text>
  </Trans>
);

How to translate props or attributes

Right now, we only check if a string is wrapped up by the Trans tag. We currently do not apply this rule to props, attributes or similar, but we also ask for them to be translated with the t() function.

// Bad ❌
<input type="value" placeholder={'Username'} />;

// Good ✅
const placeholder = t('form.username-placeholder', 'Username');
return <input type="value" placeholder={placeholder} />;

Check more info about how translations work in Grafana in Internationalization.md