Add inspector controls to Product Search block (#51247)

* Add inspector controls to product search

* Add changelog

* Move inspector panel to Styles

* Remove not used Fragment

* improve code

* Rename enum

* Rename type ProductSearchBlockProps

* Use PositionOptions constant

* Change hook namespace
This commit is contained in:
Fernando Marichal 2024-09-13 18:52:12 -03:00 committed by GitHub
parent 0eb4383918
commit ae20724210
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 303 additions and 3 deletions

View File

@ -0,0 +1,10 @@
export const SEARCH_BLOCK_NAME = 'core/search';
export const SEARCH_VARIATION_NAME = 'woocommerce/product-search';
export enum PositionOptions {
OUTSIDE = 'button-outside',
INSIDE = 'button-inside',
NO_BUTTON = 'no-button',
BUTTON_ONLY = 'button-only',
INPUT_AND_BUTTON = 'input-and-button',
}

View File

@ -2,6 +2,7 @@
/** /**
* External dependencies * External dependencies
*/ */
import { addFilter } from '@wordpress/hooks';
import { store as blockEditorStore, Warning } from '@wordpress/block-editor'; import { store as blockEditorStore, Warning } from '@wordpress/block-editor';
import { useDispatch, useSelect } from '@wordpress/data'; import { useDispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
@ -9,6 +10,7 @@ import { Icon, search } from '@wordpress/icons';
import { getSettingWithCoercion } from '@woocommerce/settings'; import { getSettingWithCoercion } from '@woocommerce/settings';
import { isBoolean } from '@woocommerce/types'; import { isBoolean } from '@woocommerce/types';
import { Button } from '@wordpress/components'; import { Button } from '@wordpress/components';
import type { Block as BlockType } from '@wordpress/blocks';
import { import {
// @ts-ignore waiting for @types/wordpress__blocks update // @ts-ignore waiting for @types/wordpress__blocks update
registerBlockVariation, registerBlockVariation,
@ -21,8 +23,10 @@ import {
*/ */
import './style.scss'; import './style.scss';
import './editor.scss'; import './editor.scss';
import { withProductSearchControls } from './inspector-controls';
import Block from './block'; import Block from './block';
import Edit from './edit'; import Edit from './edit';
import { SEARCH_BLOCK_NAME, SEARCH_VARIATION_NAME } from './constants';
const isBlockVariationAvailable = getSettingWithCoercion( const isBlockVariationAvailable = getSettingWithCoercion(
'isBlockVariationAvailable', 'isBlockVariationAvailable',
@ -71,6 +75,7 @@ const PRODUCT_SEARCH_ATTRIBUTES = {
query: { query: {
post_type: 'product', post_type: 'product',
}, },
namespace: SEARCH_VARIATION_NAME,
}; };
const DeprecatedBlockEdit = ( { clientId }: { clientId: string } ) => { const DeprecatedBlockEdit = ( { clientId }: { clientId: string } ) => {
@ -115,7 +120,7 @@ const DeprecatedBlockEdit = ( { clientId }: { clientId: string } ) => {
); );
}; };
registerBlockType( 'woocommerce/product-search', { registerBlockType( SEARCH_VARIATION_NAME, {
title: __( 'Product Search', 'woocommerce' ), title: __( 'Product Search', 'woocommerce' ),
apiVersion: 3, apiVersion: 3,
icon: { icon: {
@ -146,7 +151,7 @@ registerBlockType( 'woocommerce/product-search', {
isMatch: ( { idBase, instance } ) => isMatch: ( { idBase, instance } ) =>
idBase === 'woocommerce_product_search' && !! instance?.raw, idBase === 'woocommerce_product_search' && !! instance?.raw,
transform: ( { instance } ) => transform: ( { instance } ) =>
createBlock( 'woocommerce/product-search', { createBlock( SEARCH_VARIATION_NAME, {
label: label:
instance.raw.title || instance.raw.title ||
PRODUCT_SEARCH_ATTRIBUTES.label, PRODUCT_SEARCH_ATTRIBUTES.label,
@ -172,9 +177,31 @@ registerBlockType( 'woocommerce/product-search', {
}, },
} ); } );
function registerProductSearchNamespace( props: BlockType, blockName: string ) {
if ( blockName === 'core/search' ) {
// Gracefully handle if settings.attributes is undefined.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore -- We need this because `attributes` is marked as `readonly`
props.attributes = {
...props.attributes,
namespace: {
type: 'string',
},
};
}
return props;
}
addFilter(
'blocks.registerBlockType',
SEARCH_VARIATION_NAME,
registerProductSearchNamespace
);
if ( isBlockVariationAvailable ) { if ( isBlockVariationAvailable ) {
registerBlockVariation( 'core/search', { registerBlockVariation( 'core/search', {
name: 'woocommerce/product-search', name: SEARCH_VARIATION_NAME,
title: __( 'Product Search', 'woocommerce' ), title: __( 'Product Search', 'woocommerce' ),
icon: { icon: {
src: ( src: (
@ -199,4 +226,9 @@ if ( isBlockVariationAvailable ) {
), ),
attributes: PRODUCT_SEARCH_ATTRIBUTES, attributes: PRODUCT_SEARCH_ATTRIBUTES,
} ); } );
addFilter(
'editor.BlockEdit',
SEARCH_BLOCK_NAME,
withProductSearchControls
);
} }

View File

@ -0,0 +1,156 @@
/**
* External dependencies
*/
import { type ElementType, useEffect, useState } from '@wordpress/element';
import { EditorBlock } from '@woocommerce/types';
import { __ } from '@wordpress/i18n';
import { InspectorControls } from '@wordpress/block-editor';
import {
PanelBody,
RadioControl,
ToggleControl,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - Ignoring because `__experimentalToggleGroupControl` is not yet in the type definitions.
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
__experimentalToggleGroupControl as ToggleGroupControl,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - Ignoring because `__experimentalToggleGroupControlOption` is not yet in the type definitions.
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
__experimentalToggleGroupControlOption as ToggleGroupControlOption,
} from '@wordpress/components';
/**
* Internal dependencies
*/
import {
getInputAndButtonOption,
getSelectedRadioControlOption,
isInputAndButtonOption,
isWooSearchBlockVariation,
} from './utils';
import { ButtonPositionProps, ProductSearchBlockProps } from './types';
import { PositionOptions } from './constants';
const ProductSearchControls = ( props: ProductSearchBlockProps ) => {
const { attributes, setAttributes } = props;
const { buttonPosition, buttonUseIcon, showLabel } = attributes;
const [ initialPosition, setInitialPosition ] =
useState< ButtonPositionProps >( buttonPosition );
useEffect( () => {
if (
isInputAndButtonOption( buttonPosition ) &&
initialPosition !== buttonPosition
) {
setInitialPosition( buttonPosition );
}
}, [ buttonPosition ] );
return (
<InspectorControls group="styles">
<PanelBody title={ __( 'Styles', 'woocommerce' ) }>
<RadioControl
selected={ getSelectedRadioControlOption( buttonPosition ) }
options={ [
{
label: __( 'Input and button', 'woocommerce' ),
value: PositionOptions.INPUT_AND_BUTTON,
},
{
label: __( 'Input only', 'woocommerce' ),
value: PositionOptions.NO_BUTTON,
},
{
label: __( 'Button only', 'woocommerce' ),
value: PositionOptions.BUTTON_ONLY,
},
] }
onChange={ (
selected: Partial< ButtonPositionProps > &
PositionOptions.INPUT_AND_BUTTON
) => {
if ( selected !== PositionOptions.INPUT_AND_BUTTON ) {
setAttributes( {
buttonPosition: selected,
} );
} else {
const newButtonPosition =
getInputAndButtonOption( initialPosition );
setAttributes( {
buttonPosition: newButtonPosition,
} );
}
} }
/>
{ buttonPosition !== PositionOptions.NO_BUTTON && (
<>
{ buttonPosition !== PositionOptions.BUTTON_ONLY && (
<ToggleGroupControl
label={ __( 'BUTTON POSITION', 'woocommerce' ) }
isBlock
onChange={ ( value: ButtonPositionProps ) => {
setAttributes( {
buttonPosition: value,
} );
} }
value={ getInputAndButtonOption(
buttonPosition
) }
>
<ToggleGroupControlOption
value={ PositionOptions.INSIDE }
label={ __( 'Inside', 'woocommerce' ) }
/>
<ToggleGroupControlOption
value={ PositionOptions.OUTSIDE }
label={ __( 'Outside', 'woocommerce' ) }
/>
</ToggleGroupControl>
) }
<ToggleGroupControl
label={ __( 'BUTTON APPEARANCE', 'woocommerce' ) }
isBlock
onChange={ ( value: boolean ) => {
setAttributes( {
buttonUseIcon: value,
} );
} }
value={ buttonUseIcon }
>
<ToggleGroupControlOption
value={ false }
label={ __( 'Text', 'woocommerce' ) }
/>
<ToggleGroupControlOption
value={ true }
label={ __( 'Icon', 'woocommerce' ) }
/>
</ToggleGroupControl>
</>
) }
<ToggleControl
label={ __( 'Show input label', 'woocommerce' ) }
checked={ showLabel }
onChange={ ( showInputLabel: boolean ) =>
setAttributes( {
showLabel: showInputLabel,
} )
}
/>
</PanelBody>
</InspectorControls>
);
};
export const withProductSearchControls =
< T extends EditorBlock< T > >( BlockEdit: ElementType ) =>
( props: ProductSearchBlockProps ) => {
return isWooSearchBlockVariation( props ) ? (
<>
<ProductSearchControls { ...props } />
<BlockEdit { ...props } />
</>
) : (
<BlockEdit { ...props } />
);
};

View File

@ -0,0 +1,23 @@
/**
* External dependencies
*/
import type { EditorBlock } from '@woocommerce/types';
export type ButtonPositionProps =
| 'button-outside'
| 'button-inside'
| 'no-button'
| 'button-only';
export interface SearchBlockAttributes {
buttonPosition: ButtonPositionProps;
buttonText?: string;
buttonUseIcon: boolean;
isSearchFieldHidden: boolean;
label?: string;
namespace?: string;
placeholder?: string;
showLabel: boolean;
}
export type ProductSearchBlockProps = EditorBlock< SearchBlockAttributes >;

View File

@ -0,0 +1,75 @@
/**
* Internal dependencies
*/
import {
PositionOptions,
SEARCH_BLOCK_NAME,
SEARCH_VARIATION_NAME,
} from './constants';
import { ButtonPositionProps, ProductSearchBlockProps } from './types';
/**
* Identifies if a block is a Search block variation from our conventions
*
* We are extending Gutenberg's core Search block with our variations, and
* also adding extra namespaced attributes. If those namespaced attributes
* are present, we can be fairly sure it is our own registered variation.
*
* @param {ProductSearchBlockProps} block - A WooCommerce block.
*/
export function isWooSearchBlockVariation( block: ProductSearchBlockProps ) {
return (
block.name === SEARCH_BLOCK_NAME &&
block.attributes?.namespace === SEARCH_VARIATION_NAME
);
}
/**
* Checks if the given button position is a valid option for input and button placement.
*
* The function verifies if the provided `buttonPosition` matches one of the predefined
* values for placing a button either inside or outside an input field.
*
* @param {string} buttonPosition - The position of the button to check.
*/
export function isInputAndButtonOption( buttonPosition: string ): boolean {
return (
buttonPosition === 'button-outside' ||
buttonPosition === 'button-inside'
);
}
/**
* Returns the option for the selected button position
*
* Based on the provided `buttonPosition`, the function returns a predefined option
* if the position is valid for input and button placement. If the position is not
* one of the predefined options, it returns the original `buttonPosition`.
*
* @param {string} buttonPosition - The position of the button to evaluate.
*/
export function getSelectedRadioControlOption(
buttonPosition: string
): string {
if ( isInputAndButtonOption( buttonPosition ) ) {
return PositionOptions.INPUT_AND_BUTTON;
}
return buttonPosition;
}
/**
* Returns the appropriate option for input and button placement based on the given value
*
* This function checks if the provided `value` is a valid option for placing a button either
* inside or outside an input field. If the `value` is valid, it is returned as is. If the `value`
* is not valid, the function returns a default option.
*
* @param {ButtonPositionProps} value - The position of the button to evaluate.
*/
export function getInputAndButtonOption( value: ButtonPositionProps ) {
if ( isInputAndButtonOption( value ) ) {
return value;
}
// The default value is 'inside' for input and button.
return PositionOptions.OUTSIDE;
}

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add inspector controls to Product Search block #51247