diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-search/constants.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-search/constants.ts
new file mode 100644
index 00000000000..ae42399cf3d
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-search/constants.ts
@@ -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',
+}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-search/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-search/index.tsx
index 9691e2cdf9a..89bd2ac4dac 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-search/index.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-search/index.tsx
@@ -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
+ );
}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-search/inspector-controls.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-search/inspector-controls.tsx
new file mode 100644
index 00000000000..3212dd9e76e
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-search/inspector-controls.tsx
@@ -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 (
+
+
+ &
+ 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 && (
+ {
+ setAttributes( {
+ buttonPosition: value,
+ } );
+ } }
+ value={ getInputAndButtonOption(
+ buttonPosition
+ ) }
+ >
+
+
+
+ ) }
+ {
+ setAttributes( {
+ buttonUseIcon: value,
+ } );
+ } }
+ value={ buttonUseIcon }
+ >
+
+
+
+ >
+ ) }
+
+ setAttributes( {
+ showLabel: showInputLabel,
+ } )
+ }
+ />
+
+
+ );
+};
+
+export const withProductSearchControls =
+ < T extends EditorBlock< T > >( BlockEdit: ElementType ) =>
+ ( props: ProductSearchBlockProps ) => {
+ return isWooSearchBlockVariation( props ) ? (
+ <>
+
+
+ >
+ ) : (
+
+ );
+ };
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-search/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-search/types.ts
new file mode 100644
index 00000000000..290523ae727
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-search/types.ts
@@ -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 >;
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-search/utils.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-search/utils.ts
new file mode 100644
index 00000000000..5b70f82a27a
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-search/utils.ts
@@ -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;
+}
diff --git a/plugins/woocommerce/changelog/add-47890_inspector_control_to_product_search b/plugins/woocommerce/changelog/add-47890_inspector_control_to_product_search
new file mode 100644
index 00000000000..351aa293cfd
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-47890_inspector_control_to_product_search
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add inspector controls to Product Search block #51247