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
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
### Internal

- Remove `ValidatedCheckboxControl` from the private APIs; it now lives internally in `@wordpress/dataviews`, its only consumer ([#81435](https://github.com/WordPress/gutenberg/pull/81435)).
- Remove `ValidatedComboboxControl` from the private APIs; it now lives internally in `@wordpress/dataviews`, its only consumer ([#81449](https://github.com/WordPress/gutenberg/pull/81449)).
- Remove `ValidatedNumberControl` from the private APIs; it now lives internally in `@wordpress/dataviews`, its only consumer ([#81433](https://github.com/WordPress/gutenberg/pull/81433)).
- Remove `ValidatedRadioControl` from the private APIs; it now lives internally in `@wordpress/dataviews`, its only consumer ([#81434](https://github.com/WordPress/gutenberg/pull/81434)).
- Remove `ValidatedSelectControl` from the private APIs; it now lives internally in `@wordpress/dataviews`, its only consumer ([#81391](https://github.com/WordPress/gutenberg/pull/81391)).
Expand Down
2 changes: 0 additions & 2 deletions packages/components/src/private-apis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { Tabs } from './tabs';
import { lock } from './lock-unlock';
import Badge from './badge';
import {
ValidatedComboboxControl,
ValidatedInputControl,
ValidatedContentEditableControl,
ValidatedTextareaControl,
Expand All @@ -26,7 +25,6 @@ lock( privateApis, {
Badge,
useDrag,
ValidatedInputControl,
ValidatedComboboxControl,
ValidatedContentEditableControl,
ValidatedTextareaControl,
ValidatedToggleControl,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from './combobox-control';
export * from './custom-select-control';
export * from './form-token-field';
export * from './input-control';
Expand Down

This file was deleted.

7 changes: 0 additions & 7 deletions packages/components/src/validated-form-controls/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,6 @@
--wp-components-color-accent: #{$alert-red};
border-color: $alert-red;
}

// For ComboboxControl

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I pushed a commit to remove this style as well.

.components-combobox-control__suggestions-container:has(
input:invalid[data-validity-visible]
):not(:has([aria-expanded="true"])) {
--wp-admin-theme-color: #{$alert-red};
}
}

.components-validated-control__wrapper-with-error-delegate {
Expand Down
1 change: 1 addition & 0 deletions packages/dataviews/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- DataViews: Replace the inlined `kebabCase` utility with the new `@wordpress/kebab-case` package. [#81294](https://github.com/WordPress/gutenberg/pull/81294)
- DataForm: Internalize `ValidatedSelectControl` and its `ControlWithError` foundation instead of unlocking them from the `@wordpress/components` private APIs. The foundation is a temporary copy slated to be replaced by the upcoming `@wordpress/ui` implementation. [#81391](https://github.com/WordPress/gutenberg/pull/81391)
- DataForm: Internalize `ValidatedCheckboxControl` instead of unlocking it from the `@wordpress/components` private APIs. [#81435](https://github.com/WordPress/gutenberg/pull/81435)
- DataForm: Internalize `ValidatedComboboxControl` instead of unlocking it from the `@wordpress/components` private APIs. [#81449](https://github.com/WordPress/gutenberg/pull/81449)
- DataForm: Internalize `ValidatedNumberControl` instead of unlocking it from the `@wordpress/components` private APIs. [#81433](https://github.com/WordPress/gutenberg/pull/81433)
- DataForm: Internalize `ValidatedRadioControl` instead of unlocking it from the `@wordpress/components` private APIs. [#81434](https://github.com/WordPress/gutenberg/pull/81434)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { privateApis, Spinner } from '@wordpress/components';
import { Spinner } from '@wordpress/components';
import { useCallback } from '@wordpress/element';
import type { DataFormControlProps } from '../../types';
import useElements from '../../hooks/use-elements';
import { unlock } from '../../lock-unlock';
import { ValidatedComboboxControl } from '../validated-form-controls';
import getCustomValidity from './utils/get-custom-validity';

const { ValidatedComboboxControl } = unlock( privateApis );

export default function Combobox< Item >( {
data,
field,
Expand All @@ -19,7 +17,7 @@ export default function Combobox< Item >( {
const value = getValue( { item: data } ) ?? '';

const onChangeControl = useCallback(
( newValue: string | null ) =>
( newValue: string | null | undefined ) =>

@oandregal oandregal Aug 12, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This happened in some of my PRs too: by doing const { component } = unlock<T = any>( ... ): T the type of component becomes any, and so the existing types for the validated component are lost. I didn't realize it until now, it's unfortunate that we lose the types we have when using private APIs.

TypeScript playground example:

(code)
// Minimal reproduction of the ValidatedComboboxControl typing issue
// (Gutenberg PR #81449, packages/dataviews combobox.tsx)

// ─── 1. A trimmed-down ComboboxControlProps ────────────────────────────
// The real one is in packages/components/src/combobox-control/types.ts
type ComboboxControlProps = {
	// `value` is an OPTIONAL prop. Optionality adds `undefined` to its type.
	value?: string | null;

	// `onChange`'s parameter is declared via an indexed access on `value`:
	onChange?: ( value: ComboboxControlProps[ 'value' ] ) => void;
};

// Hover this: it's `string | null | undefined`, NOT `string | null`.
type OnChangeParam = ComboboxControlProps[ 'value' ];

declare function ValidatedComboboxControl(
	props: ComboboxControlProps
): void;

// ─── 2. Before the PR: the component came from unlock(), which is `any` ─
// Real signature, from @wordpress/private-apis:
//   declare function unlock<T = any>(object: unknown): T;
declare function unlock< T = any >( object: unknown ): T;

const UntypedValidatedComboboxControl = unlock( {} ); // type: any

// No type checking at all — the too-narrow handler compiled fine:
UntypedValidatedComboboxControl( {
	onChange: ( newValue: string | null ) => {},
} );
// ...and so would something totally wrong:
UntypedValidatedComboboxControl( {
	onChange: ( newValue: number ) => {},
} );

// ─── 3. After the PR: the vendored component is properly typed ─────────

// ❌ Error: the component may call onChange(undefined), but this handler
// only accepts string | null. Under strictFunctionTypes a callback must
// accept EVERYTHING the callee might pass (contravariance).
ValidatedComboboxControl( {
	onChange: ( newValue: string | null ) => {},
} );

// ✅ This is the fix shipped in the PR:
ValidatedComboboxControl( {
	onChange: ( newValue: string | null | undefined ) => {},
} );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Wouldn't it be better to "hoist" the value type, so that we get a narrower and more reliable type in onChangeControl?

+type ComboboxControlPropsValue = string | null;
 
 type ComboboxControlProps = {
-	value?: ComboboxControlProps['value'];
-	onChange?: ( value: ComboboxControlProps['value'] ) => void;
+	value?: ComboboxControlPropsValue;
+	onChange?: ( value: ComboboxControlPropsValue ) => void;
 };
 
 declare function ValidatedComboboxControl(
 	props: ComboboxControlProps
 ): void;
 
 ValidatedComboboxControl( {
-	onChange: ( newValue: string | null | undefined ) => {},
+	onChange: ( newValue: string | null ) => {},
 } );

@oandregal oandregal Aug 13, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That's great feedback, thanks. I've prepared #81568 to address it.

Here's a "fun with types" example:

  1. newValue valid values are string | null | undefined:
type CaseOne = {
	value?: string | null;
	onChange?: ( newValue: CaseOne[ 'value' ] ) => void;
};
  1. newValue valid values are string | null:
type CaseTwo = {
	value?: string | null;
	onChange?( newValue: CaseTwo[ 'value' ] ): void;
};

onChange( setValue( { item: data, value: newValue ?? '' } ) ),
[ data, onChange, setValue ]
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { useMergeRefs } from '@wordpress/compose';
import { forwardRef, useEffect, useRef } from '@wordpress/element';
import { ControlWithError } from '../control-with-error';
import { useMergeRefs } from '@wordpress/compose';
import { ComboboxControl } from '@wordpress/components';
import { ControlWithError } from './control-with-error';
import type { ValidatedControlProps } from './types';
import ComboboxControl from '../../combobox-control';

type ComboboxControlProps = React.ComponentProps< typeof ComboboxControl >;

type ValidatedComboboxControlProps = Omit<
ComboboxControlProps,
'__next40pxDefaultSize'
> &
ValidatedControlProps;

const UnforwardedValidatedComboboxControl = (
{
required,
customValidity,
markWhenOptional,
...restProps
}: Omit<
React.ComponentProps< typeof ComboboxControl >,
'__next40pxDefaultSize'
> &
ValidatedControlProps,
}: ValidatedComboboxControlProps,
forwardedRef: React.ForwardedRef< HTMLInputElement >
) => {
const validityTargetRef = useRef< HTMLInputElement >( null );
Expand Down Expand Up @@ -50,7 +54,8 @@ const UnforwardedValidatedComboboxControl = (
);
};

export const ValidatedComboboxControl = forwardRef(
UnforwardedValidatedComboboxControl
);
export const ValidatedComboboxControl: React.ForwardRefExoticComponent<
React.PropsWithoutRef< ValidatedComboboxControlProps > &
React.RefAttributes< HTMLInputElement >
> = forwardRef( UnforwardedValidatedComboboxControl );
ValidatedComboboxControl.displayName = 'ValidatedComboboxControl';
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { ControlWithError } from './control-with-error';
export { ValidatedCheckboxControl } from './checkbox-control';
export { ValidatedComboboxControl } from './combobox-control';
export { ValidatedNumberControl } from './number-control';
export { ValidatedRadioControl } from './radio-control';
export { ValidatedSelectControl } from './select-control';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
--wp-components-color-accent: #{$alert-red};
border-color: $alert-red;
}

// For ComboboxControl
.components-combobox-control__suggestions-container:has(
input:invalid[data-validity-visible]
):not(:has([aria-expanded="true"])) {
--wp-admin-theme-color: #{$alert-red};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Image

It was a bit tricky to get, but this requires:

  1. the component being in invalid state
  2. closed
  3. focused

}
}

.dataviews-validated-control__indicator {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ValidatedComboboxControl } from '../components';
import { ValidatedComboboxControl } from '../combobox-control';

// The `help` prop is rendered visually by BaseControl but is not
// programmatically associated with the combobox input via aria-describedby.
Expand Down
Loading