Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
78 changes: 21 additions & 57 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,16 +108,6 @@ const restrictedSyntax = [
},
];

/** `no-restricted-syntax` rules for components. */
const restrictedSyntaxComponents = [
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

restrictedSyntaxComponents is effectively replaced by the new @wordpress/components-no-unsafe-button-disabled rule

{
selector:
'JSXOpeningElement[name.name="Button"]:not(:has(JSXAttribute[name.name="accessibleWhenDisabled"])) JSXAttribute[name.name="disabled"]',
message:
'`disabled` used without the `accessibleWhenDisabled` prop. Disabling a control without maintaining focusability can cause accessibility issues, by hiding their presence from screen reader users, or preventing focus from returning to a trigger element. (Ignore this error if you truly mean to disable.)',
},
];

module.exports = {
root: true,
extends: [
Expand Down Expand Up @@ -257,11 +247,8 @@ module.exports = {
],
excludedFiles: [ '**/*.native.js' ],
rules: {
'no-restricted-syntax': [
'error',
...restrictedSyntax,
...restrictedSyntaxComponents,
],
'no-restricted-syntax': [ 'error', ...restrictedSyntax ],
'@wordpress/components-no-unsafe-button-disabled': 'error',
},
},
{
Expand All @@ -271,47 +258,9 @@ module.exports = {
'**/*.@(native|ios|android).js',
],
rules: {
'no-restricted-syntax': [
'error',
...restrictedSyntax,
...restrictedSyntaxComponents,
// Temporary rules until we're ready to officially default to the new size.
...[
'BorderBoxControl',
'BorderControl',
'BoxControl',
'Button',
'ComboboxControl',
'CustomSelectControl',

'FontAppearanceControl',
'FontFamilyControl',
'FontSizePicker',
'FormTokenField',
'InputControl',
'LetterSpacingControl',
'LineHeightControl',
'NumberControl',
'RangeControl',
'SelectControl',
'TextControl',
'ToggleGroupControl',
'UnitControl',
].map( ( componentName ) => ( {
// Falsy `__next40pxDefaultSize` without a non-default `size` prop.
selector: `JSXOpeningElement[name.name="${ componentName }"]:not(:has(JSXAttribute[name.name="__next40pxDefaultSize"][value.expression.value!=false])):not(:has(JSXAttribute[name.name="size"][value.value!="default"]))`,
message:
componentName +
' should have the `__next40pxDefaultSize` prop when using the default size.',
} ) ),
{
// Falsy `__next40pxDefaultSize` without a `render` prop.
selector:
'JSXOpeningElement[name.name="FormFileUpload"]:not(:has(JSXAttribute[name.name="__next40pxDefaultSize"][value.expression.value!=false])):not(:has(JSXAttribute[name.name="render"]))',
message:
'FormFileUpload should have the `__next40pxDefaultSize` prop to opt-in to the new default size.',
},
],
Comment on lines -278 to -314
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This rule is now replaced by the new @wordpress/components-no-missing-40px-size-prop rule

'no-restricted-syntax': [ 'error', ...restrictedSyntax ],
'@wordpress/components-no-unsafe-button-disabled': 'error',
'@wordpress/components-no-missing-40px-size-prop': 'error',
},
},
{
Expand Down Expand Up @@ -443,7 +392,6 @@ module.exports = {
'no-restricted-syntax': [
'error',
...restrictedSyntax,
...restrictedSyntaxComponents,
{
selector:
':matches(Literal[value=/--wp-admin-theme-/],TemplateElement[value.cooked=/--wp-admin-theme-/])',
Expand All @@ -460,6 +408,22 @@ module.exports = {
],
},
},
{
// Override the @wordpress/components-* rules by adding the
// `checkLocalImports` flag, which adds the linting also to relative
// imports.
files: [ 'packages/components/src/**' ],
rules: {
'@wordpress/components-no-unsafe-button-disabled': [
'error',
{ checkLocalImports: true },
],
// '@wordpress/components-no-missing-40px-size-prop': [
// 'error',
// { checkLocalImports: true },
// ],
Comment on lines +421 to +424
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the current ESLint configuration, we were not checking for the __next40pxDefaultSize prop within the @wordpress/components package.

I'll work on a follow-up PR where I enable this rule and fix all errors.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up: #74622

Comment thread
ciampo marked this conversation as resolved.
Comment thread
ciampo marked this conversation as resolved.
},
},
{
files: [ 'packages/components/src/**' ],
excludedFiles: [ 'packages/components/src/**/@(test|stories)/**' ],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ function ButtonBlockAppender(
);

return (
// Disable reason: There shouldn't be a case where this button is disabled but not visually hidden.
// eslint-disable-next-line @wordpress/components-no-unsafe-button-disabled
Comment on lines +57 to +58
Copy link
Copy Markdown
Contributor Author

@ciampo ciampo Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new rule errors at the JSX tag level instead of the prop level. Another improvement over the old rule, is that disabling the rule would also disable all other no-restricted-syntax errors. Having a custom rule allows us to be more precise when disabling it.

<Button
__next40pxDefaultSize
ref={ ref }
Expand All @@ -66,8 +68,6 @@ function ButtonBlockAppender(
onClick={ onToggle }
aria-haspopup={ isToggleButton ? 'true' : undefined }
aria-expanded={ isToggleButton ? isOpen : undefined }
// Disable reason: There shouldn't be a case where this button is disabled but not visually hidden.
// eslint-disable-next-line no-restricted-syntax
disabled={ disabled }
label={ label }
showTooltip
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ function FontSizePicker( props ) {
{ ...props }
fontSizes={ fontSizes }
disableCustomFontSizes={ ! customFontSize }
__next40pxDefaultSize
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new rules are able to track renamed imports 🚀

/>
);
}
Expand Down
5 changes: 0 additions & 5 deletions packages/components/src/button/test/index.tsx
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes in this file are a direct consequence of https://github.com/WordPress/gutenberg/pull/74611/changes#r2690292410.

In a follow-up, I will either add the eslint-disable-* comments back, or fix the underlying component usage directly.

Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,6 @@ describe( 'Button', () => {
} );

it( 'should add a disabled prop to the button', () => {
// eslint-disable-next-line no-restricted-syntax
render( <Button disabled /> );

expect( screen.getByRole( 'button' ) ).toBeDisabled();
Expand Down Expand Up @@ -522,7 +521,6 @@ describe( 'Button', () => {

it( 'should become a button again when disabled is supplied', () => {
// @ts-expect-error - a button should not have `href`
// eslint-disable-next-line no-restricted-syntax
render( <Button href="https://wordpress.org/" disabled /> );

expect( screen.getByRole( 'button' ) ).toBeVisible();
Expand Down Expand Up @@ -618,7 +616,6 @@ describe( 'Button', () => {
} );

it( 'should not break when the legacy __experimentalIsFocusable prop is passed', () => {
// eslint-disable-next-line no-restricted-syntax
render( <Button disabled __experimentalIsFocusable /> );
const button = screen.getByRole( 'button' );

Expand All @@ -633,10 +630,8 @@ describe( 'Button', () => {
{ /* @ts-expect-error - `target` requires `href` */ }
<Button target="foo" />

{ /* eslint-disable no-restricted-syntax */ }
{ /* @ts-expect-error - `disabled` is only for buttons */ }
<Button href="foo" disabled />
{ /* eslint-enable no-restricted-syntax */ }

<Button href="foo" type="image/png" />
{ /* @ts-expect-error - if button, type must be submit/reset/button */ }
Expand Down
4 changes: 2 additions & 2 deletions packages/components/src/form-token-field/token.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,13 @@ export default function Token( {
<span aria-hidden="true">{ transformedValue }</span>
</span>

{ /* Disable reason: Even when FormTokenField itself is accessibly disabled, token reset buttons shouldn't be in the tab sequence. */ }
{ /* eslint-disable-next-line @wordpress/components-no-unsafe-button-disabled */ }
<Button
className="components-form-token-field__remove-token"
size="small"
icon={ closeSmall }
onClick={ ! disabled ? onClick : undefined }
// Disable reason: Even when FormTokenField itself is accessibly disabled, token reset buttons shouldn't be in the tab sequence.
// eslint-disable-next-line no-restricted-syntax
disabled={ disabled }
label={ messages.remove }
aria-describedby={ `components-form-token-field__token-text-${ instanceId }` }
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

- Added [`no-setting-ds-tokens`](https://github.com/WordPress/gutenberg/blob/HEAD/packages/eslint-plugin/docs/rules/no-setting-ds-tokens.md) rule to disallow setting Design System token CSS custom properties (`--wpds-*`). ([#74325](https://github.com/WordPress/gutenberg/pull/74325))
- Added [`no-unknown-ds-tokens`](https://github.com/WordPress/gutenberg/blob/HEAD/packages/eslint-plugin/docs/rules/no-unknown-ds-tokens.md) rule to disallow unknown Design System tokens. ([#74325](https://github.com/WordPress/gutenberg/pull/74325))
- Added [`components-no-missing-40px-size-prop`](https://github.com/WordPress/gutenberg/blob/HEAD/packages/eslint-plugin/docs/rules/components-no-missing-40px-size-prop.md) rule to opt-in to the new 40px default size for components from the `@wordpress/components` package. ([#74611](https://github.com/WordPress/gutenberg/pull/74611))
- Added [`components-no-unsafe-button-disabled`](https://github.com/WordPress/gutenberg/blob/HEAD/packages/eslint-plugin/docs/rules/components-no-unsafe-button-disabled.md) rule to ensure that buttons from the `@wordpress/components` package are accessible when disabled. ([#74611](https://github.com/WordPress/gutenberg/pull/74611))

### Enhancements

Expand Down
4 changes: 3 additions & 1 deletion packages/eslint-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,10 @@ The granular rulesets will not define any environment globals. As such, if they
| [i18n-text-domain](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/i18n-text-domain.md) | Enforce passing valid text domains. | ✓ |
| [i18n-translator-comments](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/i18n-translator-comments.md) | Enforce adding translator comments. | ✓ |
| [no-base-control-with-label-without-id](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/no-base-control-with-label-without-id.md) | Disallow the usage of BaseControl component with a label prop set but omitting the id property. | ✓ |
| [components-no-missing-40px-size-prop](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/components-no-missing-40px-size-prop.md) | Disallow missing `__next40pxDefaultSize` prop on `@wordpress/components` components. | ✓ |
| [components-no-unsafe-button-disabled](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/components-no-unsafe-button-disabled.md) | Disallow using `disabled` on Button without `accessibleWhenDisabled`. | ✓ |
| [no-unguarded-get-range-at](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/no-unguarded-get-range-at.md) | Disallow the usage of unguarded `getRangeAt` calls. | ✓ |
| [no-unsafe-wp-apis](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/no-unsafe-wp-apis.md) | Disallow the usage of unsafe APIs from `@wordpress/*` packagesl | ✓ |
| [no-unsafe-wp-apis](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/no-unsafe-wp-apis.md) | Disallow the usage of unsafe APIs from `@wordpress/*` packages | ✓ |
| [no-unused-vars-before-return](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/no-unused-vars-before-return.md) | Disallow assigning variable values if unused before a return. | ✓ |
| [no-wp-process-env](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/no-wp-process-env.md) | Disallow legacy usage of WordPress variables via `process.env` like `process.env.SCRIPT_DEBUG`. | ✓ |
| [react-no-unsafe-timeout](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/react-no-unsafe-timeout.md) | Disallow unsafe `setTimeout` in component. | |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Disallow missing `__next40pxDefaultSize` prop (components-no-missing-40px-size-prop)

Enforces that specific components from `@wordpress/components` include the `__next40pxDefaultSize` prop to opt-in to the new 40px default size.

This is a temporary rule to help migrate components to the new default size. Once the grace period is over and all components default to the new 40px size, this rule can be removed.

## Rule details

The following components are checked by this rule:

- BorderBoxControl
- BorderControl
- BoxControl
- Button
- ComboboxControl
- CustomSelectControl
- FontAppearanceControl
- FontFamilyControl
- FontSizePicker
- FormFileUpload (special case - see below)
- FormTokenField
- InputControl
- LetterSpacingControl
- LineHeightControl
- NumberControl
- RangeControl
- SelectControl
- TextControl
- ToggleGroupControl
- UnitControl

Examples of **incorrect** code for this rule:

```jsx
import { Button, InputControl } from '@wordpress/components';

<Button>Click me</Button>
<InputControl value={value} onChange={onChange} />
<Button __next40pxDefaultSize={false}>Click me</Button>
<Button size="default">Click me</Button>
```

Examples of **correct** code for this rule:

```jsx
import { Button, InputControl } from '@wordpress/components';

<Button __next40pxDefaultSize>Click me</Button>
<Button __next40pxDefaultSize={true}>Click me</Button>
<InputControl __next40pxDefaultSize value={value} onChange={onChange} />
<Button size="small">Click me</Button>
<Button size="compact">Click me</Button>
```

## FormFileUpload special case

`FormFileUpload` can use either the `__next40pxDefaultSize` prop or the `render` prop to be considered valid:

```jsx
import { FormFileUpload } from '@wordpress/components';

// Both are valid:
<FormFileUpload __next40pxDefaultSize />
<FormFileUpload render={({ open }) => <button onClick={open}>Upload</button>} />
```

## Options

### checkLocalImports

When set to `true`, the rule also checks components imported from relative paths. This is useful inside the `@wordpress/components` package itself, where components are imported via relative paths instead of `@wordpress/components`.

```json
{
"@wordpress/components-no-missing-40px-size-prop": [
"error",
{ "checkLocalImports": true }
]
}
```

## Important notes

- By default, this rule only applies to components imported from `@wordpress/components`.
- Components from other packages (like `@wordpress/ui`) or locally defined components with the same name are not affected.
- Aliased imports (e.g., `import { Button as WPButton }`) are correctly tracked.
- Components with a non-default `size` prop (e.g., `size="small"`, `size="compact"`) are exempt from this rule.
Comment thread
mirka marked this conversation as resolved.
- Use the `checkLocalImports` option when linting inside the `@wordpress/components` package.
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Disallow using disabled on Button without accessibleWhenDisabled (components-no-unsafe-button-disabled)

Disabling a `Button` component without maintaining focusability can cause accessibility issues by hiding its presence from screen reader users, or preventing focus from returning to a trigger element.

This rule enforces that `Button` components from `@wordpress/components` include the `accessibleWhenDisabled` prop when the `disabled` prop is set.

## Rule details

Examples of **incorrect** code for this rule:

```jsx
import { Button } from '@wordpress/components';

<Button disabled>Click me</Button>
<Button disabled={true}>Click me</Button>
<Button disabled={someVar}>Click me</Button>
```

Examples of **correct** code for this rule:

```jsx
import { Button } from '@wordpress/components';

<Button disabled accessibleWhenDisabled>Click me</Button>
<Button disabled accessibleWhenDisabled={true}>Click me</Button>
<Button disabled accessibleWhenDisabled={false}>Click me</Button>
<Button disabled accessibleWhenDisabled={someVar}>Click me</Button>
<Button disabled={false}>Click me</Button>
<Button onClick={handleClick}>Click me</Button>
```

## Options

### checkLocalImports

When set to `true`, the rule also checks `Button` components imported from relative paths. This is useful inside the `@wordpress/components` package itself, where components are imported via relative paths instead of `@wordpress/components`.

```json
{
"@wordpress/components-no-unsafe-button-disabled": [
"error",
{ "checkLocalImports": true }
]
}
```

## Important notes

- By default, this rule only applies to `Button` components imported from `@wordpress/components`.
- `Button` components from other packages (like `@wordpress/ui`) or locally defined components with the same name are not affected.
- Aliased imports (e.g., `import { Button as WPButton }`) are correctly tracked.
- Use the `checkLocalImports` option when linting inside the `@wordpress/components` package.
Loading
Loading