Eslint: Allow 'unset' in no-border-radius-literal lint rule (#106619)

* allow border radius of 0

* Prefer unset or initial over 0

* readme

* add an autofix for 0 -> unset

* replace 0 with unset

* fix fixes tests

* fix snapshot

* Fix lint in SecretFormField

* fix unused cx
This commit is contained in:
Josh Hunt
2025-07-04 15:43:48 +01:00
committed by GitHub
parent efbcf9d8f7
commit 443ea5924c
10 changed files with 135 additions and 64 deletions
+2
View File
@@ -18,6 +18,8 @@ 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`.
Instead of using `0` to remove a previously set border-radius, use `unset`.
### `no-unreduced-motion`
Avoid direct use of `animation*` or `transition*` properties.
@@ -4,6 +4,16 @@ const createRule = ESLintUtils.RuleCreator(
(name) => `https://github.com/grafana/grafana/blob/main/packages/grafana-eslint-rules/README.md#${name}`
);
const BORDER_RADIUS_PROPERTIES = [
'borderRadius',
'borderTopLeftRadius',
'borderTopRightRadius',
'borderBottomLeftRadius',
'borderBottomRightRadius',
];
const RE_ZERO_VALUE = /^0([a-zA-Z%]*)$/;
const borderRadiusRule = createRule({
create(context) {
return {
@@ -11,13 +21,30 @@ const borderRadiusRule = createRule({
if (
node.type === AST_NODE_TYPES.Property &&
node.key.type === AST_NODE_TYPES.Identifier &&
node.key.name === 'borderRadius' &&
BORDER_RADIUS_PROPERTIES.includes(node.key.name) &&
node.value.type === AST_NODE_TYPES.Literal
) {
context.report({
node,
messageId: 'borderRadiusId',
});
const value = node.value.value;
if (value === 'unset' || value === 'initial') {
// Allow 'unset' or 'initial' to remove border radius
return;
} else if (value === 0 || RE_ZERO_VALUE.test(value)) {
// Require 'unset' or 'initial' to remove border radius instead of `0` or `0px`
context.report({
node,
messageId: 'borderRadiusNoZeroValue',
fix(fixer) {
return fixer.replaceText(node.value, "'unset'");
},
});
} else {
// Otherwise, require theme tokens are used
context.report({
node,
messageId: 'borderRadiusUseTokens',
});
}
}
},
};
@@ -25,11 +52,13 @@ const borderRadiusRule = createRule({
name: 'no-border-radius-literal',
meta: {
type: 'problem',
fixable: 'code',
docs: {
description: 'Check if border-radius theme tokens are used',
},
messages: {
borderRadiusId: 'Prefer using theme.shape.radius tokens instead of literal values.',
borderRadiusUseTokens: 'Prefer using theme.shape.radius tokens instead of literal values.',
borderRadiusNoZeroValue: 'Use unset or initial to remove a border radius.',
},
schema: [],
},
@@ -14,8 +14,12 @@ RuleTester.setDefaultConfig({
},
});
const expectedError = {
messageId: 'borderRadiusId',
const useTokenError = {
messageId: 'borderRadiusUseTokens',
};
const noZeroValueError = {
messageId: 'borderRadiusNoZeroValue',
};
const ruleTester = new RuleTester();
@@ -31,20 +35,44 @@ ruleTester.run('eslint no-border-radius-literal', noBorderRadiusLiteral, {
{
code: `css({ borderRadius: theme.shape.radius.pill })`,
},
{
code: `css({ borderTopLeftRadius: theme.shape.radius.pill })`,
},
{
code: `css({ borderTopRightRadius: theme.shape.radius.pill })`,
},
{
code: `css({ borderBottomLeftRadius: theme.shape.radius.pill })`,
},
{
code: `css({ borderBottomRightRadius: theme.shape.radius.pill })`,
},
// allow values to remove border radius
{
code: `css({ borderRadius: 'initial' })`,
},
{
code: `css({ borderRadius: 'unset' })`,
},
],
invalid: [
{
code: `css({ borderRadius: '2px' })`,
errors: [expectedError],
errors: [useTokenError],
},
{
code: `css({ borderRadius: 2 })`, // should error on px shorthand
errors: [useTokenError],
},
{
code: `css({ lineHeight: 1 }, { borderRadius: '2px' })`,
errors: [expectedError],
errors: [useTokenError],
},
{
code: `css([{ lineHeight: 1 }, { borderRadius: '2px' }])`,
errors: [expectedError],
errors: [useTokenError],
},
{
name: 'nested classes',
@@ -56,7 +84,40 @@ css({
},
},
})`,
errors: [expectedError],
errors: [useTokenError],
},
{
code: `css({ borderTopLeftRadius: 1 })`,
errors: [useTokenError],
},
{
code: `css({ borderTopRightRadius: "2px" })`,
errors: [useTokenError],
},
{
code: `css({ borderBottomLeftRadius: 3 })`,
errors: [useTokenError],
},
{
code: `css({ borderBottomRightRadius: "4px" })`,
errors: [useTokenError],
},
// should use unset or initial to remove border radius
{
code: `css({ borderRadius: 0 })`,
output: `css({ borderRadius: 'unset' })`,
errors: [noZeroValueError],
},
{
code: `css({ borderRadius: '0px' })`,
output: `css({ borderRadius: 'unset' })`,
errors: [noZeroValueError],
},
{
code: `css({ borderRadius: "0%" })`,
output: `css({ borderRadius: 'unset' })`,
errors: [noZeroValueError],
},
],
});
@@ -27,14 +27,14 @@ const getStyles = (theme: GrafanaTheme2) => ({
borderRadius: theme.shape.radius.default,
'> .button-group:not(:first-child) > button, > button:not(:first-child)': {
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderTopLeftRadius: 'unset',
borderBottomLeftRadius: 'unset',
borderLeft: `1px solid rgba(255, 255, 255, 0.12)`,
},
'> .button-group:not(:last-child) > button, > button:not(:last-child)': {
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
borderTopRightRadius: 'unset',
borderBottomRightRadius: 'unset',
borderRight: `1px solid rgba(0, 0, 0, 0.12)`,
},
}),
@@ -168,8 +168,8 @@ export const getInputStyles = stylesFactory(({ theme, invalid = false, width }:
'&:not(:first-child):last-child': {
'> input': {
borderLeft: 'none',
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderTopLeftRadius: 'unset',
borderBottomLeftRadius: 'unset',
},
},
@@ -177,8 +177,8 @@ export const getInputStyles = stylesFactory(({ theme, invalid = false, width }:
'&:first-child:not(:last-child)': {
'> input': {
borderRight: 'none',
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
borderTopRightRadius: 'unset',
borderBottomRightRadius: 'unset',
},
},
@@ -186,10 +186,10 @@ export const getInputStyles = stylesFactory(({ theme, invalid = false, width }:
'&:not(:first-child):not(:last-child)': {
'> input': {
borderRight: 'none',
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderTopRightRadius: 'unset',
borderBottomRightRadius: 'unset',
borderTopLeftRadius: 'unset',
borderBottomLeftRadius: 'unset',
},
},
@@ -238,20 +238,20 @@ export const getInputStyles = stylesFactory(({ theme, invalid = false, width }:
position: 'relative',
'&:first-child': {
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
borderTopRightRadius: 'unset',
borderBottomRightRadius: 'unset',
'> :last-child': {
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
borderTopRightRadius: 'unset',
borderBottomRightRadius: 'unset',
},
},
'&:last-child': {
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderTopLeftRadius: 'unset',
borderBottomLeftRadius: 'unset',
'> :first-child': {
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderTopLeftRadius: 'unset',
borderBottomLeftRadius: 'unset',
},
},
'> *:focus': {
@@ -266,8 +266,8 @@ export const getInputStyles = stylesFactory(({ theme, invalid = false, width }:
paddingLeft: theme.spacing(1),
paddingRight: theme.spacing(0.5),
borderRight: 'none',
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
borderTopRightRadius: 'unset',
borderBottomRightRadius: 'unset',
})
),
suffix: cx(
@@ -277,8 +277,8 @@ export const getInputStyles = stylesFactory(({ theme, invalid = false, width }:
paddingLeft: theme.spacing(1),
paddingRight: theme.spacing(1),
borderLeft: 'none',
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderTopLeftRadius: 'unset',
borderBottomLeftRadius: 'unset',
right: 0,
})
),
@@ -118,8 +118,8 @@ export class RefreshPicker extends PureComponent<Props> {
{!noIntervalPicker && (
<ButtonSelect
className={css({
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderTopLeftRadius: 'unset',
borderBottomLeftRadius: 'unset',
})}
value={selectedValue}
options={options}
@@ -1,4 +1,3 @@
import { css, cx } from '@emotion/css';
import { omit } from 'lodash';
import { InputHTMLAttributes } from 'react';
import * as React from 'react';
@@ -26,19 +25,6 @@ export interface Props extends Omit<InputHTMLAttributes<HTMLInputElement>, 'onRe
interactive?: boolean;
}
const getSecretFormFieldStyles = () => {
return {
noRadiusInput: css({
borderBottomRightRadius: '0 !important',
borderTopRightRadius: '0 !important',
}),
noRadiusButton: css({
borderBottomLeftRadius: '0 !important',
borderTopLeftRadius: '0 !important',
}),
};
};
/**
* Form field that has 2 states configured and not configured. If configured it will not show its contents and adds
* a reset button that will clear the input and makes it accessible. In non configured state it behaves like normal
@@ -58,7 +44,6 @@ export const SecretFormField = ({
interactive,
...inputProps
}: Props) => {
const styles = getSecretFormFieldStyles();
return (
<FormField
label={label!}
@@ -70,7 +55,7 @@ export const SecretFormField = ({
<>
<input
type="text"
className={cx(`gf-form-input width-${inputWidth}`, styles.noRadiusInput)}
className={`gf-form-input width-${inputWidth}`}
disabled={true}
value="configured"
{...omit(inputProps, 'value')}
+1 -7
View File
@@ -6,6 +6,7 @@ package extensions
import (
_ "cloud.google.com/go/kms/apiv1"
_ "cloud.google.com/go/kms/apiv1/kmspb"
_ "cloud.google.com/go/spanner"
_ "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
_ "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys"
_ "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
@@ -29,13 +30,6 @@ import (
_ "github.com/spf13/cobra" // used by the standalone apiserver cli
_ "github.com/stretchr/testify/require"
_ "golang.org/x/time/rate"
_ "k8s.io/api"
_ "k8s.io/apimachinery/pkg/util/httpstream/spdy"
_ "k8s.io/apimachinery/pkg/util/proxy"
_ "k8s.io/kube-aggregator/pkg/apiserver/scheme"
_ "k8s.io/kube-aggregator/pkg/generated/openapi"
_ "k8s.io/kube-aggregator/pkg/registry/apiservice/rest"
_ "sigs.k8s.io/randfill"
_ "xorm.io/builder"
_ "github.com/grafana/authlib/authn"
@@ -99,8 +99,8 @@ const getStyles = (theme: GrafanaTheme2) => ({
display: 'flex',
// No border for second element (inputs) as label and input border is shared
'> :nth-child(2)': css({
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderTopLeftRadius: 'unset',
borderBottomLeftRadius: 'unset',
}),
}),
labelWrapper: css({
@@ -67,7 +67,7 @@ exports[`VariableQueryEditor renders correctly 1`] = `
</div>
</div>
<div
class="css-1h17wob-input-suffix"
class="css-1ms3s8l-input-suffix"
>
<svg
aria-hidden="true"