diff --git a/packages/js/product-editor/changelog/add-core-combobox b/packages/js/product-editor/changelog/add-core-combobox
new file mode 100644
index 00000000000..5b40961683c
--- /dev/null
+++ b/packages/js/product-editor/changelog/add-core-combobox
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add a ComboboxControl component
diff --git a/packages/js/product-editor/src/components/combobox-control/combobox-control.tsx b/packages/js/product-editor/src/components/combobox-control/combobox-control.tsx
new file mode 100644
index 00000000000..51c657f885f
--- /dev/null
+++ b/packages/js/product-editor/src/components/combobox-control/combobox-control.tsx
@@ -0,0 +1,127 @@
+/**
+ * External dependencies
+ */
+import type { ForwardedRef } from 'react';
+import { ComboboxControl as Combobox } from '@wordpress/components';
+import { useInstanceId } from '@wordpress/compose';
+import {
+ createElement,
+ forwardRef,
+ useEffect,
+ useLayoutEffect,
+ useRef,
+} from '@wordpress/element';
+import classNames from 'classnames';
+
+/**
+ * Internal dependencies
+ */
+import type { ComboboxControlProps } from './types';
+
+/**
+ * This is a wrapper + a work around the Combobox to
+ * expose important properties and events from the
+ * internal input element that are required when
+ * validating the field in the context of a form
+ */
+export const ComboboxControl = forwardRef( function ForwardedComboboxControl(
+ {
+ id,
+ name,
+ allowReset,
+ className,
+ help,
+ hideLabelFromVision,
+ label,
+ messages,
+ value,
+ options,
+ onFilterValueChange,
+ onChange,
+ onBlur,
+ }: ComboboxControlProps,
+ ref: ForwardedRef< HTMLInputElement >
+) {
+ const inputElementRef = useRef< HTMLInputElement >();
+ const generatedId = useInstanceId(
+ ComboboxControl,
+ 'woocommerce-combobox-control'
+ ) as string;
+ const currentId = id ?? generatedId;
+
+ useLayoutEffect(
+ /**
+ * The Combobox component does not expose the ref to the
+ * internal native input element removing the ability to
+ * focus the element when validating it in the context
+ * of a form
+ */
+ function initializeRefs() {
+ inputElementRef.current = document.querySelector(
+ `.${ currentId } [role="combobox"]`
+ ) as HTMLInputElement;
+
+ if ( name ) {
+ inputElementRef.current?.setAttribute( 'name', name );
+ }
+
+ if ( ref ) {
+ if ( typeof ref === 'function' ) {
+ ref( inputElementRef.current );
+ } else {
+ ref.current = inputElementRef.current;
+ }
+ }
+ },
+ [ currentId, name, ref ]
+ );
+
+ useEffect(
+ function overrideBlur() {
+ /**
+ * The Combobox component clear the value of its internal
+ * input control when losing the focus, even when the
+ * selected value is set, affecting the validation behavior
+ * on bluring
+ */
+ function handleBlur( event: FocusEvent ) {
+ onBlur?.( {
+ ...event,
+ target: {
+ ...event.target,
+ value,
+ },
+ } as never );
+ }
+
+ inputElementRef.current?.addEventListener( 'blur', handleBlur );
+
+ return () => {
+ inputElementRef.current?.removeEventListener(
+ 'blur',
+ handleBlur
+ );
+ };
+ },
+ [ value, onBlur ]
+ );
+
+ return (
+
+ );
+} );
diff --git a/packages/js/product-editor/src/components/combobox-control/index.ts b/packages/js/product-editor/src/components/combobox-control/index.ts
new file mode 100644
index 00000000000..0c5d351e22e
--- /dev/null
+++ b/packages/js/product-editor/src/components/combobox-control/index.ts
@@ -0,0 +1,2 @@
+export * from './combobox-control';
+export * from './types';
diff --git a/packages/js/product-editor/src/components/custom-fields/custom-field-name-control/style.scss b/packages/js/product-editor/src/components/combobox-control/style.scss
similarity index 96%
rename from packages/js/product-editor/src/components/custom-fields/custom-field-name-control/style.scss
rename to packages/js/product-editor/src/components/combobox-control/style.scss
index b0263f5d19a..030b9bfda3b 100644
--- a/packages/js/product-editor/src/components/custom-fields/custom-field-name-control/style.scss
+++ b/packages/js/product-editor/src/components/combobox-control/style.scss
@@ -1,4 +1,4 @@
-.woocommerce-custom-field-name-control {
+.woocommerce-combobox-control {
background-color: #fff;
&.has-error {
diff --git a/packages/js/product-editor/src/components/combobox-control/types.ts b/packages/js/product-editor/src/components/combobox-control/types.ts
new file mode 100644
index 00000000000..67e78f9fe79
--- /dev/null
+++ b/packages/js/product-editor/src/components/combobox-control/types.ts
@@ -0,0 +1,13 @@
+/**
+ * External dependencies
+ */
+import { ComboboxControl as Combobox } from '@wordpress/components';
+
+export type ComboboxControlProps = Combobox.Props &
+ Pick<
+ React.DetailedHTMLProps<
+ React.InputHTMLAttributes< HTMLInputElement >,
+ HTMLInputElement
+ >,
+ 'id' | 'name' | 'onBlur'
+ >;
diff --git a/packages/js/product-editor/src/components/custom-fields/custom-field-name-control/custom-field-name-control.tsx b/packages/js/product-editor/src/components/custom-fields/custom-field-name-control/custom-field-name-control.tsx
index 8f17a78672f..44eb7fd03c2 100644
--- a/packages/js/product-editor/src/components/custom-fields/custom-field-name-control/custom-field-name-control.tsx
+++ b/packages/js/product-editor/src/components/custom-fields/custom-field-name-control/custom-field-name-control.tsx
@@ -3,24 +3,20 @@
*/
import type { ForwardedRef } from 'react';
import apiFetch from '@wordpress/api-fetch';
-import { ComboboxControl } from '@wordpress/components';
-import { useDebounce, useInstanceId } from '@wordpress/compose';
+import { useDebounce } from '@wordpress/compose';
import {
createElement,
forwardRef,
useCallback,
- useEffect,
- useLayoutEffect,
useMemo,
- useRef,
useState,
} from '@wordpress/element';
import { addQueryArgs } from '@wordpress/url';
-import classNames from 'classnames';
/**
* Internal dependencies
*/
+import { ComboboxControl, ComboboxControlProps } from '../../combobox-control';
import type { ComboboxControlOption } from '../../attribute-combobox-field/types';
import type { CustomFieldNameControlProps } from './types';
@@ -56,35 +52,13 @@ async function searchCustomFieldNames( search?: string ) {
} );
}
-/**
- * This is a wrapper + a work around the Combobox to
- * expose important properties and events from the
- * internal input element that are required when
- * validating the field in the context of a form
- */
export const CustomFieldNameControl = forwardRef(
function ForwardedCustomFieldNameControl(
- {
- allowReset,
- className,
- help,
- hideLabelFromVision,
- label,
- messages,
- value,
- onChange,
- onBlur,
- }: CustomFieldNameControlProps,
+ { value, onBlur, ...props }: CustomFieldNameControlProps,
ref: ForwardedRef< HTMLInputElement >
) {
- const inputElementRef = useRef< HTMLInputElement >();
- const id = useInstanceId(
- CustomFieldNameControl,
- 'woocommerce-custom-field-name'
- );
-
const [ customFieldNames, setCustomFieldNames ] = useState<
- ComboboxControl.Props[ 'options' ]
+ ComboboxControlProps[ 'options' ]
>( [] );
const options = useMemo(
@@ -109,29 +83,6 @@ export const CustomFieldNameControl = forwardRef(
[ customFieldNames, value ]
);
- useLayoutEffect(
- /**
- * The Combobox component does not expose the ref to the
- * internal native input element removing the ability to
- * focus the element when validating it in the context
- * of a form
- */
- function initializeRefs() {
- inputElementRef.current = document.querySelector(
- `.${ id } [role="combobox"]`
- ) as HTMLInputElement;
-
- if ( ref ) {
- if ( typeof ref === 'function' ) {
- ref( inputElementRef.current );
- } else {
- ref.current = inputElementRef.current;
- }
- }
- },
- [ id, ref ]
- );
-
const handleFilterValueChange = useDebounce(
useCallback(
function onFilterValueChange( search: string ) {
@@ -144,50 +95,19 @@ export const CustomFieldNameControl = forwardRef(
250
);
- useEffect(
- function overrideBlur() {
- /**
- * The Combobox component clear the value of its internal
- * input control when losing the focus, even when the
- * selected value is set, afecting the validation behavior
- * on bluring
- */
- function handleBlur( event: FocusEvent ) {
- setCustomFieldNames( [] );
- if ( inputElementRef.current ) {
- inputElementRef.current.value = value;
- }
- onBlur?.( event as never );
- }
-
- inputElementRef.current?.addEventListener( 'blur', handleBlur );
-
- return () => {
- inputElementRef.current?.removeEventListener(
- 'blur',
- handleBlur
- );
- };
- },
- [ value, onBlur ]
- );
+ function handleBlur( event: React.FocusEvent< HTMLInputElement > ) {
+ setCustomFieldNames( [] );
+ onBlur?.( event );
+ }
return (
);
}
diff --git a/packages/js/product-editor/src/components/custom-fields/style.scss b/packages/js/product-editor/src/components/custom-fields/style.scss
index 8560c2aadeb..6a4137cc9c8 100644
--- a/packages/js/product-editor/src/components/custom-fields/style.scss
+++ b/packages/js/product-editor/src/components/custom-fields/style.scss
@@ -1,6 +1,5 @@
@import "./create-modal/style.scss";
@import "./edit-modal/style.scss";
-@import "./custom-field-name-control/style.scss";
.woocommerce-product-custom-fields {
&__table {
diff --git a/packages/js/product-editor/src/components/index.ts b/packages/js/product-editor/src/components/index.ts
index c58638dcd16..074645ed104 100644
--- a/packages/js/product-editor/src/components/index.ts
+++ b/packages/js/product-editor/src/components/index.ts
@@ -104,3 +104,8 @@ export {
export { PluginSidebar as __experimentalModalBlockEditorPluginSidebar } from './iframe-editor';
export { PluginMoreMenuItem as __experimentalModalBlockEditorPluginMoreMenuItem } from './iframe-editor';
+
+export {
+ ComboboxControl as __experimentalComboboxControl,
+ type ComboboxControlProps,
+} from './combobox-control';
diff --git a/packages/js/product-editor/src/style.scss b/packages/js/product-editor/src/style.scss
index e4de003e389..ce54a3a1c3a 100644
--- a/packages/js/product-editor/src/style.scss
+++ b/packages/js/product-editor/src/style.scss
@@ -52,6 +52,7 @@
@import "components/attribute-combobox-field/styles.scss";
@import "components/number-control/style.scss";
@import "components/empty-state/style.scss";
+@import "components/combobox-control/style.scss";
/* Field Blocks */