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 */