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
|
* 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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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