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:
parent
0eb4383918
commit
ae20724210
|
@ -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',
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { addFilter } from '@wordpress/hooks';
|
||||
import { store as blockEditorStore, Warning } from '@wordpress/block-editor';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
@ -9,6 +10,7 @@ import { Icon, search } from '@wordpress/icons';
|
|||
import { getSettingWithCoercion } from '@woocommerce/settings';
|
||||
import { isBoolean } from '@woocommerce/types';
|
||||
import { Button } from '@wordpress/components';
|
||||
import type { Block as BlockType } from '@wordpress/blocks';
|
||||
import {
|
||||
// @ts-ignore waiting for @types/wordpress__blocks update
|
||||
registerBlockVariation,
|
||||
|
@ -21,8 +23,10 @@ import {
|
|||
*/
|
||||
import './style.scss';
|
||||
import './editor.scss';
|
||||
import { withProductSearchControls } from './inspector-controls';
|
||||
import Block from './block';
|
||||
import Edit from './edit';
|
||||
import { SEARCH_BLOCK_NAME, SEARCH_VARIATION_NAME } from './constants';
|
||||
|
||||
const isBlockVariationAvailable = getSettingWithCoercion(
|
||||
'isBlockVariationAvailable',
|
||||
|
@ -71,6 +75,7 @@ const PRODUCT_SEARCH_ATTRIBUTES = {
|
|||
query: {
|
||||
post_type: 'product',
|
||||
},
|
||||
namespace: SEARCH_VARIATION_NAME,
|
||||
};
|
||||
|
||||
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' ),
|
||||
apiVersion: 3,
|
||||
icon: {
|
||||
|
@ -146,7 +151,7 @@ registerBlockType( 'woocommerce/product-search', {
|
|||
isMatch: ( { idBase, instance } ) =>
|
||||
idBase === 'woocommerce_product_search' && !! instance?.raw,
|
||||
transform: ( { instance } ) =>
|
||||
createBlock( 'woocommerce/product-search', {
|
||||
createBlock( SEARCH_VARIATION_NAME, {
|
||||
label:
|
||||
instance.raw.title ||
|
||||
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 ) {
|
||||
registerBlockVariation( 'core/search', {
|
||||
name: 'woocommerce/product-search',
|
||||
name: SEARCH_VARIATION_NAME,
|
||||
title: __( 'Product Search', 'woocommerce' ),
|
||||
icon: {
|
||||
src: (
|
||||
|
@ -199,4 +226,9 @@ if ( isBlockVariationAvailable ) {
|
|||
),
|
||||
attributes: PRODUCT_SEARCH_ATTRIBUTES,
|
||||
} );
|
||||
addFilter(
|
||||
'editor.BlockEdit',
|
||||
SEARCH_BLOCK_NAME,
|
||||
withProductSearchControls
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 } />
|
||||
);
|
||||
};
|
|
@ -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 >;
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add inspector controls to Product Search block #51247
|
Loading…
Reference in New Issue