Commit 21862e62f7a for woocommerce

commit 21862e62f7ab4e6ba3567f9dc7604763e88a47ab
Author: Luigi Teschio <gigitux@gmail.com>
Date:   Sat May 16 14:31:11 2026 +0200

    Add searchable chip select to products app (#64909)

    * Add tree select control to products app

    * Replace tree select control with searchable chip select

    * fix build

    * lint code

diff --git a/packages/js/experimental-products-app/changelog/add-tree-select-control b/packages/js/experimental-products-app/changelog/add-tree-select-control
new file mode 100644
index 00000000000..2f3dcd7f0ff
--- /dev/null
+++ b/packages/js/experimental-products-app/changelog/add-tree-select-control
@@ -0,0 +1,4 @@
+Significance: patch
+Type: add
+
+Add a tree select control to the experimental products app.
diff --git a/packages/js/experimental-products-app/package.json b/packages/js/experimental-products-app/package.json
index ec3a564b7cc..ae9da6e0565 100644
--- a/packages/js/experimental-products-app/package.json
+++ b/packages/js/experimental-products-app/package.json
@@ -59,6 +59,7 @@
 		"@wordpress/api-fetch": "7.44.0",
 		"@wordpress/url": "catalog:wp-min",
 		"@wordpress/base-styles": "6.20.0",
+		"@base-ui/react": "1.4.1",
 		"clsx": "2.1.x",
 		"react": "18.3.x",
 		"react-dom": "18.3.x"
diff --git a/packages/js/experimental-products-app/src/fields/components/searchable-chip-select/chip-with-remove.tsx b/packages/js/experimental-products-app/src/fields/components/searchable-chip-select/chip-with-remove.tsx
new file mode 100644
index 00000000000..f11471a8e08
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/components/searchable-chip-select/chip-with-remove.tsx
@@ -0,0 +1,46 @@
+/**
+ * External dependencies
+ */
+import { Combobox as BaseCombobox } from '@base-ui/react/combobox';
+import clsx from 'clsx';
+import { forwardRef } from 'react';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import type { SearchableChipSelectChipWithRemoveProps } from './types';
+
+export const ChipWithRemove = forwardRef<
+	HTMLDivElement,
+	SearchableChipSelectChipWithRemoveProps
+>( function ChipWithRemove(
+	{ className, children, prefix, ...restProps },
+	ref
+) {
+	return (
+		<BaseCombobox.Chip
+			ref={ ref }
+			className={ clsx(
+				'woocommerce-searchable-chip-select__chip',
+				className
+			) }
+			{ ...restProps }
+		>
+			{ prefix && (
+				<span className="woocommerce-searchable-chip-select__chip-prefix">
+					{ prefix }
+				</span>
+			) }
+			<span className="woocommerce-searchable-chip-select__chip-content">
+				{ children }
+			</span>
+			<BaseCombobox.ChipRemove
+				className="woocommerce-searchable-chip-select__chip-remove"
+				aria-label={ __( 'Remove', 'woocommerce' ) }
+			>
+				<span aria-hidden="true">×</span>
+			</BaseCombobox.ChipRemove>
+		</BaseCombobox.Chip>
+	);
+} );
diff --git a/packages/js/experimental-products-app/src/fields/components/searchable-chip-select/index.ts b/packages/js/experimental-products-app/src/fields/components/searchable-chip-select/index.ts
new file mode 100644
index 00000000000..0e5ececc8d3
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/components/searchable-chip-select/index.ts
@@ -0,0 +1,39 @@
+/*
+ * These components are copy-pasted from a private repository while we wait for
+ * @wordpress/ui to support an equivalent component. Remove this implementation
+ * once @wordpress/ui provides one.
+ */
+
+/**
+ * Internal dependencies
+ */
+import { ChipWithRemove } from './chip-with-remove';
+import { Item } from './item';
+import { SearchableChipSelect as _SearchableChipSelect } from './searchable-chip-select';
+import { SearchableChipSelectControl as _SearchableChipSelectControl } from './searchable-chip-select-control';
+
+Item.displayName = 'SearchableChipSelect.Item';
+ChipWithRemove.displayName = 'SearchableChipSelect.ChipWithRemove';
+
+export const SearchableChipSelect = Object.assign( _SearchableChipSelect, {
+	Item,
+	ChipWithRemove,
+} );
+
+export const Combobox = SearchableChipSelect;
+
+export const SearchableChipSelectControl = Object.assign(
+	_SearchableChipSelectControl,
+	{
+		Item,
+		ChipWithRemove,
+	}
+);
+
+export type {
+	Item as SearchableChipSelectItem,
+	SearchableChipSelectChipWithRemoveProps,
+	SearchableChipSelectControlProps,
+	SearchableChipSelectItemProps,
+	SearchableChipSelectProps,
+} from './types';
diff --git a/packages/js/experimental-products-app/src/fields/components/searchable-chip-select/item.tsx b/packages/js/experimental-products-app/src/fields/components/searchable-chip-select/item.tsx
new file mode 100644
index 00000000000..8c33a0649d9
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/components/searchable-chip-select/item.tsx
@@ -0,0 +1,26 @@
+/**
+ * External dependencies
+ */
+import { Combobox as BaseCombobox } from '@base-ui/react/combobox';
+import clsx from 'clsx';
+import { forwardRef } from 'react';
+
+/**
+ * Internal dependencies
+ */
+import type { SearchableChipSelectItemProps } from './types';
+
+export const Item = forwardRef< HTMLDivElement, SearchableChipSelectItemProps >(
+	function Item( { className, ...restProps }, ref ) {
+		return (
+			<BaseCombobox.Item
+				ref={ ref }
+				className={ clsx(
+					'woocommerce-searchable-chip-select__item',
+					className
+				) }
+				{ ...restProps }
+			/>
+		);
+	}
+);
diff --git a/packages/js/experimental-products-app/src/fields/components/searchable-chip-select/searchable-chip-select-control.tsx b/packages/js/experimental-products-app/src/fields/components/searchable-chip-select/searchable-chip-select-control.tsx
new file mode 100644
index 00000000000..6017f385cdb
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/components/searchable-chip-select/searchable-chip-select-control.tsx
@@ -0,0 +1,61 @@
+/**
+ * External dependencies
+ */
+import { forwardRef, useId } from 'react';
+
+/**
+ * Internal dependencies
+ */
+import { SearchableChipSelect } from './searchable-chip-select';
+import type { SearchableChipSelectControlProps } from './types';
+
+function mergeIds( ...ids: Array< string | undefined > ) {
+	return ids.filter( Boolean ).join( ' ' ) || undefined;
+}
+
+/**
+ * A searchable multi-selection field with label and description wiring.
+ */
+export const SearchableChipSelectControl = forwardRef<
+	HTMLDivElement,
+	SearchableChipSelectControlProps
+>( function SearchableChipSelectControl(
+	{
+		className,
+		label,
+		description,
+		'aria-labelledby': ariaLabelledby,
+		'aria-describedby': ariaDescribedby,
+		...restProps
+	},
+	ref
+) {
+	const id = useId();
+	const labelId = `${ id }-label`;
+	const descriptionId = description ? `${ id }-description` : undefined;
+
+	return (
+		<div className={ className }>
+			<div
+				id={ labelId }
+				className="woocommerce-searchable-chip-select__label"
+			>
+				{ label }
+			</div>
+			<SearchableChipSelect
+				ref={ ref }
+				aria-labelledby={ mergeIds( ariaLabelledby, labelId ) }
+				aria-describedby={ mergeIds( ariaDescribedby, descriptionId ) }
+				{ ...restProps }
+			/>
+			{ description && (
+				<div
+					id={ descriptionId }
+					className="woocommerce-searchable-chip-select__description"
+				>
+					{ description }
+				</div>
+			) }
+		</div>
+	);
+} );
diff --git a/packages/js/experimental-products-app/src/fields/components/searchable-chip-select/searchable-chip-select.tsx b/packages/js/experimental-products-app/src/fields/components/searchable-chip-select/searchable-chip-select.tsx
new file mode 100644
index 00000000000..181622df584
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/components/searchable-chip-select/searchable-chip-select.tsx
@@ -0,0 +1,176 @@
+/**
+ * External dependencies
+ */
+import { Combobox as BaseCombobox } from '@base-ui/react/combobox';
+import clsx from 'clsx';
+import { forwardRef } from 'react';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import type { Item, SearchableChipSelectProps } from './types';
+
+const DEFAULT_ITEMS: Item[] = [];
+
+function itemToStringLabel( item: Item ) {
+	return item.label;
+}
+
+function itemToStringValue( item: Item ) {
+	return item.value;
+}
+
+function isItemEqualToValue( item: Item, value: Item ) {
+	return item.value === value.value;
+}
+
+/**
+ * A searchable multi-selection component with chips, with support for
+ * a footer item to create new items.
+ */
+export const SearchableChipSelect = forwardRef<
+	HTMLDivElement,
+	SearchableChipSelectProps
+>( function SearchableChipSelect(
+	{
+		children,
+		creatableItem,
+		disabled,
+		emptyContent = __( 'No results found.', 'woocommerce' ),
+		items = DEFAULT_ITEMS,
+		chipsContent,
+		searchPlaceholder = __( 'Search', 'woocommerce' ),
+		showClearButton = true,
+		clearButtonLabel = __( 'Clear all', 'woocommerce' ),
+		'aria-label': ariaLabel,
+		'aria-labelledby': ariaLabelledby,
+		'aria-describedby': ariaDescribedby,
+		itemToStringLabel: customItemToStringLabel = itemToStringLabel,
+		itemToStringValue: customItemToStringValue = itemToStringValue,
+		isItemEqualToValue: customIsItemEqualToValue = isItemEqualToValue,
+		...restProps
+	},
+	ref
+) {
+	const rootItems = creatableItem ? [ ...items, creatableItem ] : items;
+
+	return (
+		<BaseCombobox.Root< Item, true >
+			items={ rootItems }
+			multiple
+			disabled={ disabled }
+			itemToStringLabel={ customItemToStringLabel }
+			itemToStringValue={ customItemToStringValue }
+			isItemEqualToValue={ customIsItemEqualToValue }
+			{ ...restProps }
+		>
+			<BaseCombobox.Chips
+				ref={ ref }
+				render={
+					<div
+						className={ clsx(
+							'woocommerce-searchable-chip-select',
+							disabled &&
+								'woocommerce-searchable-chip-select--is-disabled'
+						) }
+					/>
+				}
+			>
+				<BaseCombobox.Value>
+					{ ( value: Item[] ) =>
+						value.length > 0 ? (
+							<div className="woocommerce-searchable-chip-select__chips-edit-area">
+								<div className="woocommerce-searchable-chip-select__chips-list">
+									{ chipsContent
+										? chipsContent( value )
+										: value.map( ( item ) => (
+												<BaseCombobox.Chip
+													key={ item.value }
+													className="woocommerce-searchable-chip-select__chip"
+												>
+													<span className="woocommerce-searchable-chip-select__chip-content">
+														{ item.label }
+													</span>
+													<BaseCombobox.ChipRemove
+														className="woocommerce-searchable-chip-select__chip-remove"
+														aria-label={ __(
+															'Remove',
+															'woocommerce'
+														) }
+													>
+														<span aria-hidden="true">
+															×
+														</span>
+													</BaseCombobox.ChipRemove>
+												</BaseCombobox.Chip>
+										  ) ) }
+								</div>
+								{ showClearButton && (
+									<BaseCombobox.Clear
+										className="woocommerce-searchable-chip-select__clear"
+										aria-label={ clearButtonLabel }
+									>
+										<span aria-hidden="true">×</span>
+									</BaseCombobox.Clear>
+								) }
+							</div>
+						) : null
+					}
+				</BaseCombobox.Value>
+
+				<BaseCombobox.Input
+					className="woocommerce-searchable-chip-select__input"
+					placeholder={ searchPlaceholder }
+					aria-label={ ariaLabel }
+					aria-labelledby={ ariaLabelledby }
+					aria-describedby={ ariaDescribedby }
+				/>
+			</BaseCombobox.Chips>
+
+			<BaseCombobox.Portal>
+				<BaseCombobox.Positioner className="woocommerce-searchable-chip-select__positioner">
+					<BaseCombobox.Popup className="woocommerce-searchable-chip-select__popup">
+						<BaseCombobox.Empty className="woocommerce-searchable-chip-select__empty">
+							{ emptyContent }
+						</BaseCombobox.Empty>
+						<BaseCombobox.List className="woocommerce-searchable-chip-select__list">
+							<BaseCombobox.Collection>
+								{ ( item: Item, index: number ) => {
+									if ( item.value === creatableItem?.value ) {
+										return null;
+									}
+									if ( children ) {
+										return children( item, index );
+									}
+									return (
+										<BaseCombobox.Item
+											key={ item.value }
+											value={ item }
+											disabled={ item.disabled }
+											className="woocommerce-searchable-chip-select__item"
+										>
+											{ item.label }
+										</BaseCombobox.Item>
+									);
+								} }
+							</BaseCombobox.Collection>
+							{ creatableItem && (
+								<BaseCombobox.Item
+									value={ creatableItem }
+									disabled={ creatableItem.disabled }
+									className={ clsx(
+										'woocommerce-searchable-chip-select__item',
+										'woocommerce-searchable-chip-select__item--is-creatable'
+									) }
+								>
+									{ creatableItem.label }
+								</BaseCombobox.Item>
+							) }
+						</BaseCombobox.List>
+					</BaseCombobox.Popup>
+				</BaseCombobox.Positioner>
+			</BaseCombobox.Portal>
+		</BaseCombobox.Root>
+	);
+} );
diff --git a/packages/js/experimental-products-app/src/fields/components/searchable-chip-select/style.scss b/packages/js/experimental-products-app/src/fields/components/searchable-chip-select/style.scss
new file mode 100644
index 00000000000..7b8be7b7b6e
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/components/searchable-chip-select/style.scss
@@ -0,0 +1,163 @@
+.woocommerce-searchable-chip-select {
+	box-sizing: border-box;
+	display: flex;
+	flex-direction: column;
+	width: 100%;
+	min-height: 40px;
+	border: 1px solid #949494;
+	border-radius: 2px;
+	background: #fff;
+	color: #1e1e1e;
+	font-size: 13px;
+	line-height: 1.4;
+
+	&:focus-within {
+		border-color: var( --wp-admin-theme-color, #3858e9 );
+		box-shadow: 0 0 0 1px var( --wp-admin-theme-color, #3858e9 );
+		outline: 2px solid transparent;
+	}
+
+	&--is-disabled {
+		background: #f6f7f7;
+		color: #757575;
+	}
+
+	&__chips-edit-area {
+		display: flex;
+		gap: 8px;
+		align-items: flex-start;
+		padding: 8px 8px 0;
+	}
+
+	&__chips-list {
+		display: flex;
+		flex: 1 1 auto;
+		flex-wrap: wrap;
+		gap: 4px;
+	}
+
+	&__chip {
+		box-sizing: border-box;
+		display: inline-flex;
+		align-items: center;
+		overflow: hidden;
+		min-height: 24px;
+		border: 1px solid #c3c4c7;
+		border-radius: 12px;
+		background: #f6f7f7;
+		color: #1e1e1e;
+	}
+
+	&__chip-prefix {
+		display: inline-flex;
+		align-items: center;
+		justify-content: center;
+		width: 20px;
+		height: 20px;
+		margin-inline-start: 1px;
+		overflow: hidden;
+		border-radius: 50%;
+	}
+
+	&__chip-content {
+		padding: 2px 4px 2px 8px;
+	}
+
+	&__chip-remove,
+	&__clear {
+		display: inline-flex;
+		align-items: center;
+		justify-content: center;
+		width: 24px;
+		height: 24px;
+		border: 0;
+		border-radius: 2px;
+		background: transparent;
+		color: inherit;
+		cursor: pointer;
+
+		&:hover {
+			background: #e0e0e0;
+		}
+	}
+
+	&__input {
+		box-sizing: border-box;
+		width: 100%;
+		min-height: 38px;
+		padding: 8px;
+		border: 0;
+		background: transparent;
+		color: inherit;
+		font: inherit;
+		outline: none;
+
+		&:focus {
+			border: 0;
+			box-shadow: none;
+			outline: none;
+		}
+	}
+
+	&__positioner {
+		z-index: 100000;
+	}
+
+	&__popup {
+		box-sizing: border-box;
+		width: var( --anchor-width );
+		max-height: min( var( --available-height ), 320px );
+		overflow: auto;
+		border: 1px solid #c3c4c7;
+		border-radius: 2px;
+		background: #fff;
+		box-shadow: 0 2px 6px rgb( 0 0 0 / 20% );
+	}
+
+	&__list {
+		margin: 0;
+		padding: 4px 0;
+	}
+
+	&__item,
+	&__empty {
+		box-sizing: border-box;
+		padding: 8px 12px;
+	}
+
+	&__item {
+		cursor: default;
+
+		&[data-highlighted] {
+			background: var( --wp-admin-theme-color, #3858e9 );
+			color: #fff;
+		}
+
+		&[data-selected] {
+			font-weight: 600;
+		}
+
+		&[data-disabled] {
+			color: #757575;
+		}
+
+		&--is-creatable {
+			border-top: 1px solid #e0e0e0;
+		}
+	}
+
+	&__empty {
+		color: #757575;
+	}
+
+	&__label {
+		margin-bottom: 4px;
+		font-weight: 500;
+	}
+
+	&__description {
+		margin-top: 4px;
+		color: #757575;
+		font-size: 12px;
+	}
+}
diff --git a/packages/js/experimental-products-app/src/fields/components/searchable-chip-select/test/index.test.tsx b/packages/js/experimental-products-app/src/fields/components/searchable-chip-select/test/index.test.tsx
new file mode 100644
index 00000000000..b070e3f4dbf
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/components/searchable-chip-select/test/index.test.tsx
@@ -0,0 +1,146 @@
+/**
+ * External dependencies
+ */
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { createRef } from 'react';
+
+/**
+ * Internal dependencies
+ */
+import { SearchableChipSelect, SearchableChipSelectControl } from '../index';
+
+describe( 'SearchableChipSelect', () => {
+	const mockItems = [
+		{ value: 'apple', label: 'Apple' },
+		{ value: 'banana', label: 'Banana' },
+		{ value: 'cherry', label: 'Cherry' },
+	];
+
+	it( 'forwards ref', () => {
+		const ref = createRef< HTMLDivElement >();
+
+		render( <SearchableChipSelect ref={ ref } /> );
+
+		expect( ref.current ).toBeInstanceOf( HTMLDivElement );
+	} );
+
+	it( 'passes aria-label and aria-describedby props to the input', () => {
+		render(
+			<>
+				<SearchableChipSelect
+					aria-label="My label"
+					aria-describedby="searchable-chip-select-description"
+				/>
+				<p id="searchable-chip-select-description">My description</p>
+			</>
+		);
+
+		expect(
+			screen.getByRole( 'combobox', {
+				name: 'My label',
+				description: 'My description',
+			} )
+		).toBeInTheDocument();
+	} );
+
+	it( 'passes aria-labelledby prop to the input', () => {
+		render(
+			<>
+				<p id="searchable-chip-select-label">My label</p>
+				<SearchableChipSelect aria-labelledby="searchable-chip-select-label" />
+			</>
+		);
+
+		expect(
+			screen.getByRole( 'combobox', {
+				name: 'My label',
+			} )
+		).toBeInTheDocument();
+	} );
+
+	it( 'renders accessible control label and description', () => {
+		render(
+			<SearchableChipSelectControl
+				label="Fruits"
+				description="Choose your favorite fruits"
+				items={ mockItems }
+			/>
+		);
+
+		expect(
+			screen.getByRole( 'combobox', {
+				name: 'Fruits',
+				description: 'Choose your favorite fruits',
+			} )
+		).toBeVisible();
+	} );
+
+	it( 'renders custom empty content', async () => {
+		render(
+			<SearchableChipSelectControl
+				label="Fruits"
+				items={ [] }
+				emptyContent="No fruit found."
+			/>
+		);
+
+		await userEvent.click(
+			screen.getByRole( 'combobox', {
+				name: 'Fruits',
+			} )
+		);
+
+		expect( screen.getByRole( 'status' ) ).toHaveTextContent(
+			'No fruit found.'
+		);
+	} );
+
+	it( 'supports custom chip and item renderers', async () => {
+		const ref = createRef< HTMLDivElement >();
+		const chipRef = createRef< HTMLDivElement >();
+		const itemRef = createRef< HTMLDivElement >();
+
+		render(
+			<SearchableChipSelectControl
+				ref={ ref }
+				label="Fruits"
+				items={ mockItems }
+				defaultValue={ [ mockItems[ 0 ] ] }
+				chipsContent={ ( value ) =>
+					value.map( ( item ) => (
+						<SearchableChipSelectControl.ChipWithRemove
+							key={ item.value }
+							ref={ chipRef }
+						>
+							{ item.label }
+						</SearchableChipSelectControl.ChipWithRemove>
+					) )
+				}
+			>
+				{ ( item ) => (
+					<SearchableChipSelectControl.Item
+						key={ item.value }
+						ref={ item.value === 'banana' ? itemRef : undefined }
+						value={ item }
+					>
+						{ item.label }
+					</SearchableChipSelectControl.Item>
+				) }
+			</SearchableChipSelectControl>
+		);
+
+		expect( ref.current ).toBeInstanceOf( HTMLDivElement );
+		expect( chipRef.current ).toBeInstanceOf( HTMLDivElement );
+
+		await userEvent.click(
+			screen.getByRole( 'combobox', {
+				name: 'Fruits',
+			} )
+		);
+
+		await waitFor( () => {
+			expect( itemRef.current ).toBeInstanceOf( HTMLDivElement );
+		} );
+	} );
+} );
diff --git a/packages/js/experimental-products-app/src/fields/components/searchable-chip-select/types.ts b/packages/js/experimental-products-app/src/fields/components/searchable-chip-select/types.ts
new file mode 100644
index 00000000000..461c4637402
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/components/searchable-chip-select/types.ts
@@ -0,0 +1,87 @@
+/**
+ * External dependencies
+ */
+import type {
+	ComboboxChipProps,
+	ComboboxCollectionProps,
+	ComboboxEmptyProps,
+	ComboboxInputProps,
+	ComboboxItemProps,
+	ComboboxRootProps,
+} from '@base-ui/react/combobox';
+import type { ReactNode } from 'react';
+
+export type SearchableChipSelectItem = {
+	label: string;
+	value: string;
+	disabled?: boolean;
+};
+
+export type Item = SearchableChipSelectItem;
+
+export type SearchableChipSelectProps = Omit<
+	ComboboxRootProps< SearchableChipSelectItem, true >,
+	'children' | 'items' | 'multiple'
+> &
+	Partial<
+		Pick<
+			ComboboxInputProps,
+			'aria-label' | 'aria-labelledby' | 'aria-describedby'
+		>
+	> & {
+		/**
+		 * The array of option items.
+		 */
+		items?: SearchableChipSelectItem[];
+		/**
+		 * A render function for custom rendering the list of matching items.
+		 */
+		children?: ComboboxCollectionProps[ 'children' ];
+		/**
+		 * The item that triggers the creation of a new item.
+		 */
+		creatableItem?: SearchableChipSelectItem;
+		/**
+		 * A render function for custom rendering the selected chips.
+		 */
+		chipsContent?: ( value: SearchableChipSelectItem[] ) => ReactNode;
+		/**
+		 * The custom content to use instead of the default empty state.
+		 */
+		emptyContent?: ComboboxEmptyProps[ 'children' ];
+		/**
+		 * The placeholder text to use for the search input.
+		 */
+		searchPlaceholder?: ComboboxInputProps[ 'placeholder' ];
+		/**
+		 * Whether to show the clear button to remove all selected items.
+		 *
+		 * @default true
+		 */
+		showClearButton?: boolean;
+		/**
+		 * The aria-label for the clear button.
+		 */
+		clearButtonLabel?: string;
+	};
+
+export type SearchableChipSelectControlProps = SearchableChipSelectProps & {
+	className?: string;
+	label: ReactNode;
+	description?: ReactNode;
+};
+
+export type SearchableChipSelectItemProps = ComboboxItemProps & {
+	children?: ReactNode;
+};
+
+export type SearchableChipSelectChipWithRemoveProps = Omit<
+	ComboboxChipProps,
+	'prefix'
+> & {
+	children?: ReactNode;
+	/**
+	 * Circular element to render before the chip content.
+	 */
+	prefix?: ReactNode;
+};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3c4c0309a75..4f6f21044e5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1698,6 +1698,9 @@ importers:

   packages/js/experimental-products-app:
     dependencies:
+      '@base-ui/react':
+        specifier: 1.4.1
+        version: 1.4.1(@date-fns/tz@1.4.1)(@types/react@18.3.28)(date-fns@4.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
       '@dnd-kit/react':
         specifier: 0.4.0
         version: 0.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -32843,7 +32846,7 @@ snapshots:
   '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.97.1)':
     dependencies:
       webpack: 5.97.1(@swc/core@1.15.24)(webpack-cli@5.1.4)
-      webpack-cli: 5.1.4(webpack-bundle-analyzer@4.9.1)(webpack-dev-server@4.15.2)(webpack@5.97.1)
+      webpack-cli: 5.1.4(webpack@5.97.1)

   '@webpack-cli/info@1.5.0(webpack-cli@4.10.0(webpack-bundle-analyzer@4.9.1)(webpack@5.97.1))':
     dependencies:
@@ -32853,7 +32856,7 @@ snapshots:
   '@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.97.1)':
     dependencies:
       webpack: 5.97.1(@swc/core@1.15.24)(webpack-cli@5.1.4)
-      webpack-cli: 5.1.4(webpack-bundle-analyzer@4.9.1)(webpack-dev-server@4.15.2)(webpack@5.97.1)
+      webpack-cli: 5.1.4(webpack@5.97.1)

   '@webpack-cli/serve@1.7.0(webpack-cli@4.10.0(webpack-bundle-analyzer@4.9.1)(webpack@5.97.1))':
     dependencies:
@@ -35578,7 +35581,7 @@ snapshots:
   '@wordpress/dependency-extraction-webpack-plugin@6.43.1-next.v.202604091042.0(webpack@5.97.1)':
     dependencies:
       json2php: 0.0.7
-      webpack: 5.97.1(@swc/core@1.15.24)(esbuild@0.18.20)(webpack-cli@5.1.4)
+      webpack: 5.97.1(@swc/core@1.15.24)(webpack-cli@5.1.4)

   '@wordpress/dependency-extraction-webpack-plugin@6.44.0(webpack@5.97.1)':
     dependencies:
@@ -54721,7 +54724,7 @@ snapshots:
       watchpack: 2.5.1
       webpack-sources: 3.3.4
     optionalDependencies:
-      webpack-cli: 5.1.4(webpack-bundle-analyzer@4.9.1)(webpack-dev-server@4.15.2)(webpack@5.97.1)
+      webpack-cli: 5.1.4(webpack@5.97.1)
     transitivePeerDependencies:
       - '@swc/core'
       - esbuild