Create combobox-control (#49691)

* Create combobox-control

* Exporting the combobox control

* Integrate the combobox control into the custom field names component

* Remove some non needed styles

* Add changelog file

* Fix comment text typo

Co-authored-by: louwie17 <lourensschep@gmail.com>

* Fix linter errors

---------

Co-authored-by: louwie17 <lourensschep@gmail.com>
This commit is contained in:
Maikel Perez 2024-07-24 11:13:24 -04:00 committed by GitHub
parent ac5b051fc2
commit 4689a09ec3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 164 additions and 93 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add a ComboboxControl component

View File

@ -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 (
<Combobox
allowReset={ allowReset }
help={ help }
hideLabelFromVision={ hideLabelFromVision }
label={ label }
messages={ messages }
value={ value }
options={ options }
onChange={ onChange }
onFilterValueChange={ onFilterValueChange }
className={ classNames(
'woocommerce-combobox-control',
currentId,
className
) }
/>
);
} );

View File

@ -0,0 +1,2 @@
export * from './combobox-control';
export * from './types';

View File

@ -1,4 +1,4 @@
.woocommerce-custom-field-name-control { .woocommerce-combobox-control {
background-color: #fff; background-color: #fff;
&.has-error { &.has-error {

View File

@ -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'
>;

View File

@ -3,24 +3,20 @@
*/ */
import type { ForwardedRef } from 'react'; import type { ForwardedRef } from 'react';
import apiFetch from '@wordpress/api-fetch'; import apiFetch from '@wordpress/api-fetch';
import { ComboboxControl } from '@wordpress/components'; import { useDebounce } from '@wordpress/compose';
import { useDebounce, useInstanceId } from '@wordpress/compose';
import { import {
createElement, createElement,
forwardRef, forwardRef,
useCallback, useCallback,
useEffect,
useLayoutEffect,
useMemo, useMemo,
useRef,
useState, useState,
} from '@wordpress/element'; } from '@wordpress/element';
import { addQueryArgs } from '@wordpress/url'; import { addQueryArgs } from '@wordpress/url';
import classNames from 'classnames';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { ComboboxControl, ComboboxControlProps } from '../../combobox-control';
import type { ComboboxControlOption } from '../../attribute-combobox-field/types'; import type { ComboboxControlOption } from '../../attribute-combobox-field/types';
import type { CustomFieldNameControlProps } from './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( export const CustomFieldNameControl = forwardRef(
function ForwardedCustomFieldNameControl( function ForwardedCustomFieldNameControl(
{ { value, onBlur, ...props }: CustomFieldNameControlProps,
allowReset,
className,
help,
hideLabelFromVision,
label,
messages,
value,
onChange,
onBlur,
}: CustomFieldNameControlProps,
ref: ForwardedRef< HTMLInputElement > ref: ForwardedRef< HTMLInputElement >
) { ) {
const inputElementRef = useRef< HTMLInputElement >();
const id = useInstanceId(
CustomFieldNameControl,
'woocommerce-custom-field-name'
);
const [ customFieldNames, setCustomFieldNames ] = useState< const [ customFieldNames, setCustomFieldNames ] = useState<
ComboboxControl.Props[ 'options' ] ComboboxControlProps[ 'options' ]
>( [] ); >( [] );
const options = useMemo( const options = useMemo(
@ -109,29 +83,6 @@ export const CustomFieldNameControl = forwardRef(
[ customFieldNames, value ] [ 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( const handleFilterValueChange = useDebounce(
useCallback( useCallback(
function onFilterValueChange( search: string ) { function onFilterValueChange( search: string ) {
@ -144,50 +95,19 @@ export const CustomFieldNameControl = forwardRef(
250 250
); );
useEffect( function handleBlur( event: React.FocusEvent< HTMLInputElement > ) {
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( [] ); setCustomFieldNames( [] );
if ( inputElementRef.current ) { onBlur?.( event );
inputElementRef.current.value = value;
} }
onBlur?.( event as never );
}
inputElementRef.current?.addEventListener( 'blur', handleBlur );
return () => {
inputElementRef.current?.removeEventListener(
'blur',
handleBlur
);
};
},
[ value, onBlur ]
);
return ( return (
<ComboboxControl <ComboboxControl
allowReset={ allowReset } { ...props }
help={ help } ref={ ref }
hideLabelFromVision={ hideLabelFromVision }
label={ label }
messages={ messages }
value={ value } value={ value }
options={ options } options={ options }
onChange={ onChange }
onFilterValueChange={ handleFilterValueChange } onFilterValueChange={ handleFilterValueChange }
className={ classNames( onBlur={ handleBlur }
id,
'woocommerce-custom-field-name-control',
className
) }
/> />
); );
} }

View File

@ -1,6 +1,5 @@
@import "./create-modal/style.scss"; @import "./create-modal/style.scss";
@import "./edit-modal/style.scss"; @import "./edit-modal/style.scss";
@import "./custom-field-name-control/style.scss";
.woocommerce-product-custom-fields { .woocommerce-product-custom-fields {
&__table { &__table {

View File

@ -104,3 +104,8 @@ export {
export { PluginSidebar as __experimentalModalBlockEditorPluginSidebar } from './iframe-editor'; export { PluginSidebar as __experimentalModalBlockEditorPluginSidebar } from './iframe-editor';
export { PluginMoreMenuItem as __experimentalModalBlockEditorPluginMoreMenuItem } from './iframe-editor'; export { PluginMoreMenuItem as __experimentalModalBlockEditorPluginMoreMenuItem } from './iframe-editor';
export {
ComboboxControl as __experimentalComboboxControl,
type ComboboxControlProps,
} from './combobox-control';

View File

@ -52,6 +52,7 @@
@import "components/attribute-combobox-field/styles.scss"; @import "components/attribute-combobox-field/styles.scss";
@import "components/number-control/style.scss"; @import "components/number-control/style.scss";
@import "components/empty-state/style.scss"; @import "components/empty-state/style.scss";
@import "components/combobox-control/style.scss";
/* Field Blocks */ /* Field Blocks */