From ff51b9b259feefb53f609fd3702f390661154efd Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Fri, 31 Jul 2026 00:22:36 +0900 Subject: [PATCH 01/16] Add SearchableChipSelectControl to @wordpress/ui --- packages/ui/src/form/index.ts | 1 + .../searchable-chip-select-control/index.ts | 24 ++++++ .../searchable-chip-select-control.tsx | 37 ++++++++++ .../stories/index.story.tsx | 70 ++++++++++++++++++ .../test/index.test.tsx | 74 +++++++++++++++++++ .../searchable-chip-select-control/types.ts | 10 +++ 6 files changed, 216 insertions(+) create mode 100644 packages/ui/src/form/searchable-chip-select-control/index.ts create mode 100644 packages/ui/src/form/searchable-chip-select-control/searchable-chip-select-control.tsx create mode 100644 packages/ui/src/form/searchable-chip-select-control/stories/index.story.tsx create mode 100644 packages/ui/src/form/searchable-chip-select-control/test/index.test.tsx create mode 100644 packages/ui/src/form/searchable-chip-select-control/types.ts diff --git a/packages/ui/src/form/index.ts b/packages/ui/src/form/index.ts index 500b83f687719e..9e6ae3f3a922e3 100644 --- a/packages/ui/src/form/index.ts +++ b/packages/ui/src/form/index.ts @@ -2,3 +2,4 @@ export * from './primitives'; export * from './input-control'; export * from './select-control'; +export { SearchableChipSelectControl } from './searchable-chip-select-control'; diff --git a/packages/ui/src/form/searchable-chip-select-control/index.ts b/packages/ui/src/form/searchable-chip-select-control/index.ts new file mode 100644 index 00000000000000..9c29cb60be2b08 --- /dev/null +++ b/packages/ui/src/form/searchable-chip-select-control/index.ts @@ -0,0 +1,24 @@ +import { ChipWithRemove } from '../primitives/combobox/chip-with-remove'; +import { Item } from '../primitives/combobox/item'; +import { SearchableChipSelectControl as _SearchableChipSelectControl } from './searchable-chip-select-control'; + +Item.displayName = 'SearchableChipSelectControl.Item'; +ChipWithRemove.displayName = 'SearchableChipSelectControl.ChipWithRemove'; + +/** + * A complete searchable multi-select field with chips, integrated label, + * and description. + */ +export const SearchableChipSelectControl = Object.assign( + _SearchableChipSelectControl, + { + /** + * An item rendered inside a `SearchableChipSelectControl` popup. + */ + Item, + /** + * A chip rendered inside a `SearchableChipSelectControl`. + */ + ChipWithRemove, + } +); diff --git a/packages/ui/src/form/searchable-chip-select-control/searchable-chip-select-control.tsx b/packages/ui/src/form/searchable-chip-select-control/searchable-chip-select-control.tsx new file mode 100644 index 00000000000000..12e858c3befe37 --- /dev/null +++ b/packages/ui/src/form/searchable-chip-select-control/searchable-chip-select-control.tsx @@ -0,0 +1,37 @@ +import { forwardRef } from '@wordpress/element'; +import { Field, SearchableChipSelect } from '../primitives'; +import type { SearchableChipSelectControlProps } from './types'; + +/** + * A complete searchable multi-select field with chips, integrated label, + * and description. + */ +export const SearchableChipSelectControl = forwardRef< + HTMLDivElement, + SearchableChipSelectControlProps +>( function SearchableChipSelectControl( + { + className, + label, + description, + details, + hideLabelFromVision, + ...restProps + }, + ref +) { + return ( + + + { label } + + } + /> + { description && ( + { description } + ) } + { details && { details } } + + ); +} ); diff --git a/packages/ui/src/form/searchable-chip-select-control/stories/index.story.tsx b/packages/ui/src/form/searchable-chip-select-control/stories/index.story.tsx new file mode 100644 index 00000000000000..f7d1c8fa7251a2 --- /dev/null +++ b/packages/ui/src/form/searchable-chip-select-control/stories/index.story.tsx @@ -0,0 +1,70 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { fn } from 'storybook/test'; +import { SearchableChipSelectControl } from '../'; +import * as SearchableChipSelectStories from '../../primitives/searchable-chip-select/stories/index.story'; + +const meta: Meta< typeof SearchableChipSelectControl > = { + title: 'Design System/Components/Form/SearchableChipSelectControl', + component: SearchableChipSelectControl, + subcomponents: { + 'SearchableChipSelectControl.Item': SearchableChipSelectControl.Item, + 'SearchableChipSelectControl.ChipWithRemove': + SearchableChipSelectControl.ChipWithRemove, + }, + argTypes: { + items: { control: false }, + onValueChange: { action: fn() }, + }, + parameters: { + componentStatus: { + status: 'use-with-caution', + whereUsed: 'global', + notes: 'Not yet recommended for use alongside components from `@wordpress/components`, pending review of style consistency with `@wordpress/components`, overlays compatibility, and component set completeness. See [WordPress/gutenberg#76135](https://github.com/WordPress/gutenberg/issues/76135).', + }, + }, +}; + +export default meta; + +type Story = StoryObj< typeof SearchableChipSelectControl >; + +export const Default: Story = { + ...SearchableChipSelectStories.Default, + args: { + ...SearchableChipSelectStories.Default.args, + label: 'Label', + description: 'This is a description.', + }, +}; + +export const Creatable: Story = { + ...SearchableChipSelectStories.Creatable, + args: { + ...Default.args, + ...SearchableChipSelectStories.Creatable.args, + }, +}; + +export const WithCustomChipsAndItems: Story = { + ...SearchableChipSelectStories.WithCustomChipsAndItems, + args: { + ...Default.args, + ...SearchableChipSelectStories.WithCustomChipsAndItems.args, + }, +}; + +export const WithCustomEmptyContent: Story = { + ...SearchableChipSelectStories.WithCustomEmptyContent, + args: { + ...Default.args, + ...SearchableChipSelectStories.WithCustomEmptyContent.args, + }, +}; + +export const WithoutClearButton: Story = { + ...SearchableChipSelectStories.WithoutClearButton, + args: { + ...Default.args, + ...SearchableChipSelectStories.WithoutClearButton.args, + }, +}; diff --git a/packages/ui/src/form/searchable-chip-select-control/test/index.test.tsx b/packages/ui/src/form/searchable-chip-select-control/test/index.test.tsx new file mode 100644 index 00000000000000..ec4083bdc6873a --- /dev/null +++ b/packages/ui/src/form/searchable-chip-select-control/test/index.test.tsx @@ -0,0 +1,74 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { createRef } from '@wordpress/element'; +import { SearchableChipSelectControl } from '../index'; + +describe( 'SearchableChipSelectControl', () => { + const mockItems = [ + { value: 'apple', label: 'Apple' }, + { value: 'banana', label: 'Banana' }, + { value: 'cherry', label: 'Cherry' }, + ]; + + it( 'forwards ref', async () => { + const user = userEvent.setup(); + const ref = createRef< HTMLDivElement >(); + const itemRef = createRef< HTMLDivElement >(); + const chipRef = createRef< HTMLDivElement >(); + + render( + + value.map( ( item ) => ( + + { item.label } + + ) ) + } + > + { ( item ) => ( + + { item.label } + + ) } + + ); + + expect( ref.current ).toBeInstanceOf( HTMLDivElement ); + expect( chipRef.current ).toBeInstanceOf( HTMLDivElement ); + + await user.click( screen.getByRole( 'combobox' ) ); + + await waitFor( () => { + expect( itemRef.current ).toBeInstanceOf( HTMLDivElement ); + } ); + } ); + + it( 'renders accessible label and description', () => { + render( + + ); + + expect( + screen.getByRole( 'combobox', { + name: 'Fruits', + description: 'Choose your favorite fruits', + } ) + ).toBeVisible(); + } ); +} ); diff --git a/packages/ui/src/form/searchable-chip-select-control/types.ts b/packages/ui/src/form/searchable-chip-select-control/types.ts new file mode 100644 index 00000000000000..9e71a170902250 --- /dev/null +++ b/packages/ui/src/form/searchable-chip-select-control/types.ts @@ -0,0 +1,10 @@ +import type { ControlProps } from '../types'; +import type { SearchableChipSelectProps } from '../primitives/searchable-chip-select/types'; + +export type SearchableChipSelectControlProps = SearchableChipSelectProps & + ControlProps & { + /** + * CSS class to apply. + */ + className?: string; + }; From 71f6065d3a71592035b506c3515ebb72e2034c45 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Fri, 31 Jul 2026 00:23:04 +0900 Subject: [PATCH 02/16] Add changelog --- packages/ui/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 47ee27046d0dc2..d7b47545184baa 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -4,6 +4,7 @@ ### Enhancements +- Add `SearchableChipSelectControl` form control ([#80980](https://github.com/WordPress/gutenberg/pull/80980)). - Add `SearchableChipSelect` primitive ([#80779](https://github.com/WordPress/gutenberg/pull/80779)). - Add `Combobox.InputGroup` primitive ([#80869](https://github.com/WordPress/gutenberg/pull/80869)). - `Input`: Hide native browser spin controls for `type="number"` inputs by default ([#80646](https://github.com/WordPress/gutenberg/pull/80646)). From d085b270d8db60168d6797a2d79e728e2447ee9f Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Fri, 31 Jul 2026 00:51:06 +0900 Subject: [PATCH 03/16] Tweak changelog --- packages/ui/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index d7b47545184baa..ed315b43f91d04 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -4,7 +4,7 @@ ### Enhancements -- Add `SearchableChipSelectControl` form control ([#80980](https://github.com/WordPress/gutenberg/pull/80980)). +- Add `SearchableChipSelectControl` component ([#80980](https://github.com/WordPress/gutenberg/pull/80980)). - Add `SearchableChipSelect` primitive ([#80779](https://github.com/WordPress/gutenberg/pull/80779)). - Add `Combobox.InputGroup` primitive ([#80869](https://github.com/WordPress/gutenberg/pull/80869)). - `Input`: Hide native browser spin controls for `type="number"` inputs by default ([#80646](https://github.com/WordPress/gutenberg/pull/80646)). From 40ab5bb4d5d9b4ab3971574e810d8e1e657c0961 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Fri, 31 Jul 2026 02:52:48 +0900 Subject: [PATCH 04/16] SearchableChipSelectControl: Add grouped subcomponents and story. --- .../searchable-chip-select-control/index.ts | 22 +++++++++++++++++-- .../stories/index.story.tsx | 22 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/form/searchable-chip-select-control/index.ts b/packages/ui/src/form/searchable-chip-select-control/index.ts index 9c29cb60be2b08..63f576d8770ca0 100644 --- a/packages/ui/src/form/searchable-chip-select-control/index.ts +++ b/packages/ui/src/form/searchable-chip-select-control/index.ts @@ -1,7 +1,12 @@ -import { ChipWithRemove } from '../primitives/combobox/chip-with-remove'; -import { Item } from '../primitives/combobox/item'; import { SearchableChipSelectControl as _SearchableChipSelectControl } from './searchable-chip-select-control'; +import { Group } from '../primitives/combobox/group'; +import { GroupLabel } from '../primitives/combobox/group-label'; +import { Item } from '../primitives/combobox/item'; +import { ChipWithRemove } from '../primitives/combobox/chip-with-remove'; +import { Collection } from '../primitives/combobox/collection'; +Group.displayName = 'SearchableChipSelectControl.Group'; +GroupLabel.displayName = 'SearchableChipSelectControl.GroupLabel'; Item.displayName = 'SearchableChipSelectControl.Item'; ChipWithRemove.displayName = 'SearchableChipSelectControl.ChipWithRemove'; @@ -12,6 +17,15 @@ ChipWithRemove.displayName = 'SearchableChipSelectControl.ChipWithRemove'; export const SearchableChipSelectControl = Object.assign( _SearchableChipSelectControl, { + /** + * Groups related items together with an associated label rendered by + * `SearchableChipSelectControl.GroupLabel`. + */ + Group, + /** + * Renders a label for a `SearchableChipSelectControl.Group`. + */ + GroupLabel, /** * An item rendered inside a `SearchableChipSelectControl` popup. */ @@ -20,5 +34,9 @@ export const SearchableChipSelectControl = Object.assign( * A chip rendered inside a `SearchableChipSelectControl`. */ ChipWithRemove, + /** + * Iterates over the items in a `SearchableChipSelectControl.Group`. + */ + Collection, } ); diff --git a/packages/ui/src/form/searchable-chip-select-control/stories/index.story.tsx b/packages/ui/src/form/searchable-chip-select-control/stories/index.story.tsx index f7d1c8fa7251a2..58d57a4e417b99 100644 --- a/packages/ui/src/form/searchable-chip-select-control/stories/index.story.tsx +++ b/packages/ui/src/form/searchable-chip-select-control/stories/index.story.tsx @@ -7,9 +7,14 @@ const meta: Meta< typeof SearchableChipSelectControl > = { title: 'Design System/Components/Form/SearchableChipSelectControl', component: SearchableChipSelectControl, subcomponents: { + 'SearchableChipSelectControl.Group': SearchableChipSelectControl.Group, + 'SearchableChipSelectControl.GroupLabel': + SearchableChipSelectControl.GroupLabel, 'SearchableChipSelectControl.Item': SearchableChipSelectControl.Item, 'SearchableChipSelectControl.ChipWithRemove': SearchableChipSelectControl.ChipWithRemove, + 'SearchableChipSelectControl.Collection': + SearchableChipSelectControl.Collection, }, argTypes: { items: { control: false }, @@ -68,3 +73,20 @@ export const WithoutClearButton: Story = { ...SearchableChipSelectStories.WithoutClearButton.args, }, }; + +/** + * Options can be organized into labeled groups with + * `SearchableChipSelectControl.Group`, `SearchableChipSelectControl.GroupLabel`, + * and `SearchableChipSelectControl.Collection`. Pass an array of groups to + * `items` (each with `label` and `items` properties), and use `children` to + * render each group. + */ +export const Grouped: Story = { + ...SearchableChipSelectStories.Grouped, + args: { + ...Default.args, + ...SearchableChipSelectStories.Grouped.args, + label: 'Fruit', + description: 'Choose your favorite fruits.', + }, +}; From 98e87507cfc6bd0756cd00f22cccf3f0d4045e1d Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Sat, 22 Aug 2026 02:21:47 +0900 Subject: [PATCH 05/16] SearchableChipSelectControl: Mark recommended and add to manifest. --- .../searchable-chip-select/index.ts | 26 ++- .../searchable-chip-select.tsx | 6 +- .../stories/index.story.tsx | 9 +- .../stories/index.story.tsx | 9 +- storybook/components-manifest.yml | 177 ++++++++++++++++++ storybook/prop-description-allowlist.json | 16 ++ 6 files changed, 235 insertions(+), 8 deletions(-) diff --git a/packages/ui/src/form/primitives/searchable-chip-select/index.ts b/packages/ui/src/form/primitives/searchable-chip-select/index.ts index afe74a66ecbc5b..170dff568fd924 100644 --- a/packages/ui/src/form/primitives/searchable-chip-select/index.ts +++ b/packages/ui/src/form/primitives/searchable-chip-select/index.ts @@ -10,10 +10,32 @@ ChipWithRemove.displayName = 'SearchableChipSelect.ChipWithRemove'; Group.displayName = 'SearchableChipSelect.Group'; GroupLabel.displayName = 'SearchableChipSelect.GroupLabel'; +/** + * A low-level primitive for a searchable multi-selection field with chips, with + * support for a footer item to create new items. + * + * Prefer `SearchableChipSelectControl` when using with a standard label and description. + */ export const SearchableChipSelect = Object.assign( _SearchableChipSelect, { - Item, - ChipWithRemove, + /** + * Groups related items together with an associated label rendered by + * `SearchableChipSelect.GroupLabel`. + */ Group, + /** + * Renders a label for a `SearchableChipSelect.Group`. + */ GroupLabel, + /** + * An item rendered inside a `SearchableChipSelect` popup. + */ + Item, + /** + * A chip rendered inside a `SearchableChipSelect`. + */ + ChipWithRemove, + /** + * Iterates over the items in a `SearchableChipSelect.Group`. + */ Collection, } ); diff --git a/packages/ui/src/form/primitives/searchable-chip-select/searchable-chip-select.tsx b/packages/ui/src/form/primitives/searchable-chip-select/searchable-chip-select.tsx index 03afe61c4501a4..773aa773cd4479 100644 --- a/packages/ui/src/form/primitives/searchable-chip-select/searchable-chip-select.tsx +++ b/packages/ui/src/form/primitives/searchable-chip-select/searchable-chip-select.tsx @@ -17,8 +17,10 @@ import { } from './types'; /** - * A searchable multi-selection component with chips, with support for - * a footer item to create new items. + * A low-level primitive for a searchable multi-selection field with chips, with + * support for a footer item to create new items. + * + * Prefer `SearchableChipSelectControl` when using with a standard label and description. */ export const SearchableChipSelect = forwardRef< HTMLDivElement, diff --git a/packages/ui/src/form/primitives/searchable-chip-select/stories/index.story.tsx b/packages/ui/src/form/primitives/searchable-chip-select/stories/index.story.tsx index db031736d0338c..5bafa888271f04 100644 --- a/packages/ui/src/form/primitives/searchable-chip-select/stories/index.story.tsx +++ b/packages/ui/src/form/primitives/searchable-chip-select/stories/index.story.tsx @@ -10,8 +10,14 @@ import { import { ITEMS } from './fixtures'; const meta: Meta< typeof SearchableChipSelect > = { + tags: [ 'manifest' ], title: 'Design System/Components/Form/Primitives/SearchableChipSelect', component: SearchableChipSelect, + // Temporary: Due to an upstream bug, render the root explicitly so the + // components manifest extractor can resolve props from the JSX. + // + // See: https://github.com/storybookjs/storybook/issues/34877 + render: ( args ) => , subcomponents: { 'SearchableChipSelect.Group': SearchableChipSelect.Group, 'SearchableChipSelect.GroupLabel': SearchableChipSelect.GroupLabel, @@ -26,9 +32,8 @@ const meta: Meta< typeof SearchableChipSelect > = { }, parameters: { componentStatus: { - status: 'use-with-caution', + status: 'recommended', whereUsed: 'global', - notes: 'Not yet recommended for use alongside components from `@wordpress/components`, pending review of style consistency with `@wordpress/components`, overlays compatibility, and component set completeness. See [WordPress/gutenberg#76135](https://github.com/WordPress/gutenberg/issues/76135).', }, }, }; diff --git a/packages/ui/src/form/searchable-chip-select-control/stories/index.story.tsx b/packages/ui/src/form/searchable-chip-select-control/stories/index.story.tsx index 58d57a4e417b99..e8c49a740dd6bc 100644 --- a/packages/ui/src/form/searchable-chip-select-control/stories/index.story.tsx +++ b/packages/ui/src/form/searchable-chip-select-control/stories/index.story.tsx @@ -4,8 +4,14 @@ import { SearchableChipSelectControl } from '../'; import * as SearchableChipSelectStories from '../../primitives/searchable-chip-select/stories/index.story'; const meta: Meta< typeof SearchableChipSelectControl > = { + tags: [ 'manifest' ], title: 'Design System/Components/Form/SearchableChipSelectControl', component: SearchableChipSelectControl, + // Temporary: Due to an upstream bug, render the root explicitly so the + // components manifest extractor can resolve props from the JSX. + // + // See: https://github.com/storybookjs/storybook/issues/34877 + render: ( args ) => , subcomponents: { 'SearchableChipSelectControl.Group': SearchableChipSelectControl.Group, 'SearchableChipSelectControl.GroupLabel': @@ -22,9 +28,8 @@ const meta: Meta< typeof SearchableChipSelectControl > = { }, parameters: { componentStatus: { - status: 'use-with-caution', + status: 'recommended', whereUsed: 'global', - notes: 'Not yet recommended for use alongside components from `@wordpress/components`, pending review of style consistency with `@wordpress/components`, overlays compatibility, and component set completeness. See [WordPress/gutenberg#76135](https://github.com/WordPress/gutenberg/issues/76135).', }, }, }; diff --git a/storybook/components-manifest.yml b/storybook/components-manifest.yml index 31eda548928a6f..aa58dfc0f9b7c8 100644 --- a/storybook/components-manifest.yml +++ b/storybook/components-manifest.yml @@ -1056,6 +1056,183 @@ - type - name: ScrollLock props: [] +- name: SearchableChipSelect + props: + - actionsRef + - aria-describedby + - aria-label + - aria-labelledby + - autoComplete + - autoHighlight + - children + - chipsContent + - clearButtonLabel + - defaultInputValue + - defaultOpen + - defaultValue + - disabled + - emptyContent + - filter + - filteredItems + - form + - grid + - highlightItemOnHover + - id + - inline + - inputRef + - inputValue + - isItemEqualToValue + - items + - itemToStringLabel + - itemToStringValue + - key + - limit + - locale + - loopFocus + - modal + - name + - onInputValueChange + - onItemHighlighted + - onOpenChange + - onOpenChangeComplete + - onValueChange + - open + - openOnInputClick + - readOnly + - ref + - required + - searchPlaceholder + - showClearButton + - value + - virtualized + subcomponents: + - name: SearchableChipSelect.ChipWithRemove + props: + - children + - className + - prefix + - removeLabel + - render + - style + - name: SearchableChipSelect.Collection + props: + - children + - name: SearchableChipSelect.Group + props: + - children + - className + - items + - render + - style + - name: SearchableChipSelect.GroupLabel + props: + - children + - className + - render + - style + - name: SearchableChipSelect.Item + props: + - children + - className + - disabled + - index + - nativeButton + - onClick + - render + - style + - value + - variant +- name: SearchableChipSelectControl + props: + - actionsRef + - aria-describedby + - aria-label + - aria-labelledby + - autoComplete + - autoHighlight + - children + - chipsContent + - className + - clearButtonLabel + - defaultInputValue + - defaultOpen + - defaultValue + - description + - details + - disabled + - emptyContent + - filter + - filteredItems + - form + - grid + - hideLabelFromVision + - highlightItemOnHover + - id + - inline + - inputRef + - inputValue + - isItemEqualToValue + - items + - itemToStringLabel + - itemToStringValue + - key + - label + - limit + - locale + - loopFocus + - modal + - name + - onInputValueChange + - onItemHighlighted + - onOpenChange + - onOpenChangeComplete + - onValueChange + - open + - openOnInputClick + - readOnly + - ref + - required + - searchPlaceholder + - showClearButton + - value + - virtualized + subcomponents: + - name: SearchableChipSelectControl.ChipWithRemove + props: + - children + - className + - prefix + - removeLabel + - render + - style + - name: SearchableChipSelectControl.Collection + props: + - children + - name: SearchableChipSelectControl.Group + props: + - children + - className + - items + - render + - style + - name: SearchableChipSelectControl.GroupLabel + props: + - children + - className + - render + - style + - name: SearchableChipSelectControl.Item + props: + - children + - className + - disabled + - index + - nativeButton + - onClick + - render + - style + - value + - variant - name: SearchControl props: - __next40pxDefaultSize diff --git a/storybook/prop-description-allowlist.json b/storybook/prop-description-allowlist.json index 1f4a1abdd84d13..4bf1497e8235f1 100644 --- a/storybook/prop-description-allowlist.json +++ b/storybook/prop-description-allowlist.json @@ -121,6 +121,22 @@ "ResizableBox:snap", "ResizableBox:snapGap", "ResizableBox:style", + "SearchableChipSelect:key", + "SearchableChipSelect>SearchableChipSelect.ChipWithRemove:children", + "SearchableChipSelect>SearchableChipSelect.Collection", + "SearchableChipSelect>SearchableChipSelect.Collection:children", + "SearchableChipSelect>SearchableChipSelect.Group:children", + "SearchableChipSelect>SearchableChipSelect.GroupLabel:children", + "SearchableChipSelect>SearchableChipSelect.Item", + "SearchableChipSelect>SearchableChipSelect.Item:children", + "SearchableChipSelectControl:key", + "SearchableChipSelectControl>SearchableChipSelectControl.ChipWithRemove:children", + "SearchableChipSelectControl>SearchableChipSelectControl.Collection", + "SearchableChipSelectControl>SearchableChipSelectControl.Collection:children", + "SearchableChipSelectControl>SearchableChipSelectControl.Group:children", + "SearchableChipSelectControl>SearchableChipSelectControl.GroupLabel:children", + "SearchableChipSelectControl>SearchableChipSelectControl.Item", + "SearchableChipSelectControl>SearchableChipSelectControl.Item:children", "SearchControl:__next40pxDefaultSize", "SearchControl:onDrag", "SearchControl:onDragEnd", From 84d3cc6efc5504cc4f60aa6bb8c78f8fb35ca00f Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Sat, 22 Aug 2026 02:24:23 +0900 Subject: [PATCH 06/16] Remove redundant subcomponent JSDocs from SearchableChipSelect. --- .../searchable-chip-select/index.ts | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/packages/ui/src/form/primitives/searchable-chip-select/index.ts b/packages/ui/src/form/primitives/searchable-chip-select/index.ts index 170dff568fd924..b66a5776044f7f 100644 --- a/packages/ui/src/form/primitives/searchable-chip-select/index.ts +++ b/packages/ui/src/form/primitives/searchable-chip-select/index.ts @@ -17,25 +17,9 @@ GroupLabel.displayName = 'SearchableChipSelect.GroupLabel'; * Prefer `SearchableChipSelectControl` when using with a standard label and description. */ export const SearchableChipSelect = Object.assign( _SearchableChipSelect, { - /** - * Groups related items together with an associated label rendered by - * `SearchableChipSelect.GroupLabel`. - */ - Group, - /** - * Renders a label for a `SearchableChipSelect.Group`. - */ - GroupLabel, - /** - * An item rendered inside a `SearchableChipSelect` popup. - */ Item, - /** - * A chip rendered inside a `SearchableChipSelect`. - */ ChipWithRemove, - /** - * Iterates over the items in a `SearchableChipSelect.Group`. - */ + Group, + GroupLabel, Collection, } ); From 508ee57667fc02c1750d72ccbfaced27a2de0379 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Sat, 22 Aug 2026 02:26:07 +0900 Subject: [PATCH 07/16] Remove redundant subcomponent JSDocs from SearchableChipSelectControl. --- .../form/searchable-chip-select-control/index.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/packages/ui/src/form/searchable-chip-select-control/index.ts b/packages/ui/src/form/searchable-chip-select-control/index.ts index 63f576d8770ca0..a6d15f2dde6ca0 100644 --- a/packages/ui/src/form/searchable-chip-select-control/index.ts +++ b/packages/ui/src/form/searchable-chip-select-control/index.ts @@ -17,26 +17,10 @@ ChipWithRemove.displayName = 'SearchableChipSelectControl.ChipWithRemove'; export const SearchableChipSelectControl = Object.assign( _SearchableChipSelectControl, { - /** - * Groups related items together with an associated label rendered by - * `SearchableChipSelectControl.GroupLabel`. - */ Group, - /** - * Renders a label for a `SearchableChipSelectControl.Group`. - */ GroupLabel, - /** - * An item rendered inside a `SearchableChipSelectControl` popup. - */ Item, - /** - * A chip rendered inside a `SearchableChipSelectControl`. - */ ChipWithRemove, - /** - * Iterates over the items in a `SearchableChipSelectControl.Group`. - */ Collection, } ); From 7a0e47ab03f6b7b464683d0d912378ad6f8bcde3 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Sat, 22 Aug 2026 02:26:33 +0900 Subject: [PATCH 08/16] Add changelog --- packages/ui/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 97c1fc297155bb..47c06a09383941 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -8,6 +8,7 @@ - Add a `Menu` component. ([#79560](https://github.com/WordPress/gutenberg/pull/79560)) - Add `ControlWithError` component, ported from `@wordpress/components` and `@wordpress/dataviews` ([#81230](https://github.com/WordPress/gutenberg/issues/81230)) ([#81574](https://github.com/WordPress/gutenberg/pull/81574)). - Export `ValidityIndicator`, so custom validated controls can render the same validity message as `ControlWithError` ([#81230](https://github.com/WordPress/gutenberg/issues/81230)) ([#81574](https://github.com/WordPress/gutenberg/pull/81574)). +- Add `SearchableChipSelectControl` component ([#80980](https://github.com/WordPress/gutenberg/pull/80980)). ### Breaking Changes @@ -17,7 +18,6 @@ ### Enhancements -- Add `SearchableChipSelectControl` component ([#80980](https://github.com/WordPress/gutenberg/pull/80980)). - Add `TextareaControl` component ([#81359](https://github.com/WordPress/gutenberg/pull/81359)). - Export shared keyboard shortcut presentation and accessibility utilities. ([#81826](https://github.com/WordPress/gutenberg/pull/81826)) - `Menu`: Add shortcut display and accessibility metadata support. ([#79560](https://github.com/WordPress/gutenberg/pull/79560)) From 776f1e580d0c9d892c3db5fa218a82cd46c0f6dc Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Sat, 22 Aug 2026 02:28:46 +0900 Subject: [PATCH 09/16] Allow SearchableChipSelect components in use-recommended-components. --- packages/eslint-plugin/CHANGELOG.md | 1 + packages/eslint-plugin/rules/use-recommended-components.js | 2 ++ 2 files changed, 3 insertions(+) diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index 72a4a49e1817b6..191d56625060e0 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -4,6 +4,7 @@ ### Enhancements +- Update `use-recommended-components` rule to mark `SearchableChipSelect` and `SearchableChipSelectControl` from `@wordpress/ui` as recommended ([#80980](https://github.com/WordPress/gutenberg/pull/80980)). - Update `use-recommended-components` rule to mark `Autocomplete`, `Field`, and `Fieldset` from `@wordpress/ui` as recommended ([#80636](https://github.com/WordPress/gutenberg/pull/80636)). - Allow the public `@wordpress/ui` keyboard shortcut utilities in the `use-recommended-components` rule. ([#81826](https://github.com/WordPress/gutenberg/pull/81826)) - Update `use-recommended-components` rule to mark `Input`, `InputControl`, and `InputLayout` from `@wordpress/ui` as recommended, and to prefer `@wordpress/ui` `InputControl` over legacy `@wordpress/components` `InputControl` and `TextControl` ([#81658](https://github.com/WordPress/gutenberg/pull/81658)). diff --git a/packages/eslint-plugin/rules/use-recommended-components.js b/packages/eslint-plugin/rules/use-recommended-components.js index 25201aa1faa267..43cd6ef09aad6f 100644 --- a/packages/eslint-plugin/rules/use-recommended-components.js +++ b/packages/eslint-plugin/rules/use-recommended-components.js @@ -34,6 +34,8 @@ const ALLOWLIST = { 'KeyboardShortcutDisplay', 'Link', 'RangeCalendar', + 'SearchableChipSelect', + 'SearchableChipSelectControl', 'Skeleton', 'Spinner', 'Stack', From 04511fe62572139f4b79f3720adf163e86c0d888 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Sat, 22 Aug 2026 02:35:33 +0900 Subject: [PATCH 10/16] SearchableChipSelectControl: Add missing control-level stories. --- .../stories/index.story.tsx | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/packages/ui/src/form/searchable-chip-select-control/stories/index.story.tsx b/packages/ui/src/form/searchable-chip-select-control/stories/index.story.tsx index e8c49a740dd6bc..3ebc06f0f79f2f 100644 --- a/packages/ui/src/form/searchable-chip-select-control/stories/index.story.tsx +++ b/packages/ui/src/form/searchable-chip-select-control/stories/index.story.tsx @@ -2,6 +2,10 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { fn } from 'storybook/test'; import { SearchableChipSelectControl } from '../'; import * as SearchableChipSelectStories from '../../primitives/searchable-chip-select/stories/index.story'; +import { + WITH_DETAILS_DESCRIPTION, + DETAILS_EXAMPLE, +} from '../../stories/shared'; const meta: Meta< typeof SearchableChipSelectControl > = { tags: [ 'manifest' ], @@ -47,6 +51,59 @@ export const Default: Story = { }, }; +export const VisuallyHiddenLabel: Story = { + args: { + ...Default.args, + hideLabelFromVision: true, + }, +}; + +export const WithDetails: Story = { + parameters: { + docs: { description: { story: WITH_DETAILS_DESCRIPTION } }, + }, + args: { + ...Default.args, + description: undefined, + details: DETAILS_EXAMPLE, + }, +}; + +/** + * Use the `searchPlaceholder` prop to customize the search input placeholder + * text. Prefer a concise label without a trailing ellipsis. + */ +export const WithCustomSearchPlaceholder: Story = { + args: { + ...Default.args, + searchPlaceholder: 'Search fruit', + }, +}; + +const disabledOptionItems = [ + { + value: 'apple', + label: 'Apple', + }, + { + value: 'banana', + label: 'Banana', + disabled: true, + }, + { + value: 'cherry', + label: 'Cherry', + }, +]; + +export const WithDisabledOption: Story = { + args: { + ...Default.args, + items: disabledOptionItems, + defaultValue: [ disabledOptionItems[ 0 ] ], + }, +}; + export const Creatable: Story = { ...SearchableChipSelectStories.Creatable, args: { @@ -95,3 +152,12 @@ export const Grouped: Story = { description: 'Choose your favorite fruits.', }, }; + +export const GroupedCreatable: Story = { + ...SearchableChipSelectStories.GroupedCreatable, + args: { + ...Default.args, + label: 'Fruit', + description: 'Choose your favorite fruits.', + }, +}; From 74d0e9fd57ede7e07f9dc4df32139ab7f31bca93 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Mon, 24 Aug 2026 18:54:55 +0900 Subject: [PATCH 11/16] Update Storybook manifest snapshot. --- storybook/prop-description-allowlist.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/storybook/prop-description-allowlist.json b/storybook/prop-description-allowlist.json index 4bf1497e8235f1..f403b05c840e1a 100644 --- a/storybook/prop-description-allowlist.json +++ b/storybook/prop-description-allowlist.json @@ -121,6 +121,10 @@ "ResizableBox:snap", "ResizableBox:snapGap", "ResizableBox:style", + "SearchControl:__next40pxDefaultSize", + "SearchControl:onDrag", + "SearchControl:onDragEnd", + "SearchControl:onDragStart", "SearchableChipSelect:key", "SearchableChipSelect>SearchableChipSelect.ChipWithRemove:children", "SearchableChipSelect>SearchableChipSelect.Collection", @@ -137,10 +141,6 @@ "SearchableChipSelectControl>SearchableChipSelectControl.GroupLabel:children", "SearchableChipSelectControl>SearchableChipSelectControl.Item", "SearchableChipSelectControl>SearchableChipSelectControl.Item:children", - "SearchControl:__next40pxDefaultSize", - "SearchControl:onDrag", - "SearchControl:onDragEnd", - "SearchControl:onDragStart", "SelectControl:ref", "Slot>Fill", "Slot>SlotFillProvider", From c788e999cec56ce7c00e0e37e626b3985692a1ff Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Mon, 24 Aug 2026 19:15:52 +0900 Subject: [PATCH 12/16] Render SearchableChipSelect directly under Field.Root. --- .../searchable-chip-select-control.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/ui/src/form/searchable-chip-select-control/searchable-chip-select-control.tsx b/packages/ui/src/form/searchable-chip-select-control/searchable-chip-select-control.tsx index 12e858c3befe37..42cb624792dce3 100644 --- a/packages/ui/src/form/searchable-chip-select-control/searchable-chip-select-control.tsx +++ b/packages/ui/src/form/searchable-chip-select-control/searchable-chip-select-control.tsx @@ -25,9 +25,7 @@ export const SearchableChipSelectControl = forwardRef< { label } - } - /> + { description && ( { description } ) } From 80d7b2d3af5c059bfcbb6e4b42c1de2264483e76 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Mon, 24 Aug 2026 22:53:27 +0900 Subject: [PATCH 13/16] Remove SearchableChipSelectControl subcomponent displayNames. --- packages/ui/src/form/searchable-chip-select-control/index.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/ui/src/form/searchable-chip-select-control/index.ts b/packages/ui/src/form/searchable-chip-select-control/index.ts index a6d15f2dde6ca0..fa5dca21d2d731 100644 --- a/packages/ui/src/form/searchable-chip-select-control/index.ts +++ b/packages/ui/src/form/searchable-chip-select-control/index.ts @@ -5,11 +5,6 @@ import { Item } from '../primitives/combobox/item'; import { ChipWithRemove } from '../primitives/combobox/chip-with-remove'; import { Collection } from '../primitives/combobox/collection'; -Group.displayName = 'SearchableChipSelectControl.Group'; -GroupLabel.displayName = 'SearchableChipSelectControl.GroupLabel'; -Item.displayName = 'SearchableChipSelectControl.Item'; -ChipWithRemove.displayName = 'SearchableChipSelectControl.ChipWithRemove'; - /** * A complete searchable multi-select field with chips, integrated label, * and description. From 792adb1c8050f1dfebac1851c1d182462b7c100f Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Mon, 24 Aug 2026 22:53:32 +0900 Subject: [PATCH 14/16] Move creatable stories to SearchableChipSelectControl. --- .../stories/index.story.tsx | 125 ------------------ .../stories/index.story.tsx | 125 +++++++++++++++++- 2 files changed, 122 insertions(+), 128 deletions(-) diff --git a/packages/ui/src/form/primitives/searchable-chip-select/stories/index.story.tsx b/packages/ui/src/form/primitives/searchable-chip-select/stories/index.story.tsx index 5bafa888271f04..0287537ee39cad 100644 --- a/packages/ui/src/form/primitives/searchable-chip-select/stories/index.story.tsx +++ b/packages/ui/src/form/primitives/searchable-chip-select/stories/index.story.tsx @@ -1,5 +1,4 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; -import { useState } from '@wordpress/element'; import { fn } from 'storybook/test'; import { SearchableChipSelect } from '../'; import { @@ -48,58 +47,6 @@ export const Default: Story = { }, }; -/** - * Mark a creatable action with `creatable: true` on an item in `items`. - * It renders in the list footer and is excluded from the main list - * automatically. Handle creation in `onValueChange`. - */ -export const Creatable: Story = { - args: { - ...Default.args, - }, - render: function Template( args ) { - const [ inputValue, setInputValue ] = useState( '' ); - const [ value, setValue ] = useState< typeof ITEMS >( [ - ITEMS[ 0 ], - ITEMS[ 1 ], - ] ); - const creatableItem = { - value: '__create__', - label: - 'Create new item' + ( inputValue ? `: ${ inputValue }` : '' ), - creatable: true, - }; - - return ( - { - if ( - values.some( - ( item ) => item.value === creatableItem.value - ) - ) { - // eslint-disable-next-line no-alert - alert( `Create new item: '${ inputValue }'` ); - setValue( - values.filter( - ( item ) => item.value !== creatableItem.value - ) - ); - } else { - setValue( values ); - } - args.onValueChange?.( values, event ); - } } - /> - ); - }, -}; - /** * To customize what is rendered inside the chips, pass a * render function to the `chipsContent` prop that returns an array of `ChipWithRemove` subcomponents. @@ -177,78 +124,6 @@ export const Grouped: Story = { }, }; -/** - * Grouped items with a creatable footer item. Include the creatable item in - * `items` as a creatable-only group and handle creation in `onValueChange`. - */ -export const GroupedCreatable: Story = { - render: function Template( args ) { - const [ inputValue, setInputValue ] = useState( '' ); - const [ value, setValue ] = useState< FixtureItem[] >( [ - GROUPED_ITEMS[ 0 ].items[ 0 ], - GROUPED_ITEMS[ 1 ].items[ 0 ], - ] ); - const creatableItem = { - value: '__create__', - label: - 'Create new item' + ( inputValue ? `: ${ inputValue }` : '' ), - creatable: true, - }; - const items = [ - ...GROUPED_ITEMS, - { label: '', items: [ creatableItem ] }, - ]; - - return ( - { - if ( - values.some( - ( item ) => item.value === creatableItem.value - ) - ) { - // eslint-disable-next-line no-alert - alert( `Create new item: '${ inputValue }'` ); - setValue( - values.filter( - ( item ) => item.value !== creatableItem.value - ) - ); - } else { - setValue( values ); - } - args.onValueChange?.( values, event ); - } } - children={ ( group: FixtureGroup ) => ( - - - { group.label } - - - { ( item: FixtureItem ) => ( - - { item.label } - - ) } - - - ) } - /> - ); - }, -}; - /** * Use the `showClearButton` prop to hide the clear button. */ diff --git a/packages/ui/src/form/searchable-chip-select-control/stories/index.story.tsx b/packages/ui/src/form/searchable-chip-select-control/stories/index.story.tsx index 3ebc06f0f79f2f..13ddda4408323a 100644 --- a/packages/ui/src/form/searchable-chip-select-control/stories/index.story.tsx +++ b/packages/ui/src/form/searchable-chip-select-control/stories/index.story.tsx @@ -1,6 +1,13 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; +import { useState } from '@wordpress/element'; import { fn } from 'storybook/test'; import { SearchableChipSelectControl } from '../'; +import { + GROUPED_ITEMS, + type FixtureGroup, + type FixtureItem, +} from '../../primitives/combobox/stories/fixtures'; +import { ITEMS } from '../../primitives/searchable-chip-select/stories/fixtures'; import * as SearchableChipSelectStories from '../../primitives/searchable-chip-select/stories/index.story'; import { WITH_DETAILS_DESCRIPTION, @@ -104,11 +111,55 @@ export const WithDisabledOption: Story = { }, }; +/** + * Mark a creatable action with `creatable: true` on an item in `items`. + * It renders in the list footer and is excluded from the main list + * automatically. Handle creation in `onValueChange`. + */ export const Creatable: Story = { - ...SearchableChipSelectStories.Creatable, args: { ...Default.args, - ...SearchableChipSelectStories.Creatable.args, + }, + render: function Template( args ) { + const [ inputValue, setInputValue ] = useState( '' ); + const [ value, setValue ] = useState< typeof ITEMS >( [ + ITEMS[ 0 ], + ITEMS[ 1 ], + ] ); + const creatableItem = { + value: '__create__', + label: + 'Create new item' + ( inputValue ? `: ${ inputValue }` : '' ), + creatable: true, + }; + + return ( + { + if ( + values.some( + ( item ) => item.value === creatableItem.value + ) + ) { + // eslint-disable-next-line no-alert + alert( `Create new item: '${ inputValue }'` ); + setValue( + values.filter( + ( item ) => item.value !== creatableItem.value + ) + ); + } else { + setValue( values ); + } + args.onValueChange?.( values, event ); + } } + /> + ); }, }; @@ -153,11 +204,79 @@ export const Grouped: Story = { }, }; +/** + * Grouped items with a creatable footer item. Include the creatable item in + * `items` as a creatable-only group and handle creation in `onValueChange`. + */ export const GroupedCreatable: Story = { - ...SearchableChipSelectStories.GroupedCreatable, args: { ...Default.args, label: 'Fruit', description: 'Choose your favorite fruits.', }, + render: function Template( args ) { + const [ inputValue, setInputValue ] = useState( '' ); + const [ value, setValue ] = useState< FixtureItem[] >( [ + GROUPED_ITEMS[ 0 ].items[ 0 ], + GROUPED_ITEMS[ 1 ].items[ 0 ], + ] ); + const creatableItem = { + value: '__create__', + label: + 'Create new item' + ( inputValue ? `: ${ inputValue }` : '' ), + creatable: true, + }; + const items = [ + ...GROUPED_ITEMS, + { label: '', items: [ creatableItem ] }, + ]; + + return ( + { + if ( + values.some( + ( item ) => item.value === creatableItem.value + ) + ) { + // eslint-disable-next-line no-alert + alert( `Create new item: '${ inputValue }'` ); + setValue( + values.filter( + ( item ) => item.value !== creatableItem.value + ) + ); + } else { + setValue( values ); + } + args.onValueChange?.( values, event ); + } } + children={ ( group: FixtureGroup ) => ( + + + { group.label } + + + { ( item: FixtureItem ) => ( + + { item.label } + + ) } + + + ) } + /> + ); + }, }; From 2787468c08a185aa9cee267cd35ecc447619faac Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Wed, 26 Aug 2026 03:36:03 +0900 Subject: [PATCH 15/16] SearchableChipSelectControl: Fix Grouped story defaultValue --- .../searchable-chip-select-control/stories/index.story.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/form/searchable-chip-select-control/stories/index.story.tsx b/packages/ui/src/form/searchable-chip-select-control/stories/index.story.tsx index 13ddda4408323a..2e3a3997c03e75 100644 --- a/packages/ui/src/form/searchable-chip-select-control/stories/index.story.tsx +++ b/packages/ui/src/form/searchable-chip-select-control/stories/index.story.tsx @@ -197,8 +197,11 @@ export const WithoutClearButton: Story = { export const Grouped: Story = { ...SearchableChipSelectStories.Grouped, args: { - ...Default.args, ...SearchableChipSelectStories.Grouped.args, + defaultValue: [ + GROUPED_ITEMS[ 0 ].items[ 0 ], + GROUPED_ITEMS[ 1 ].items[ 0 ], + ], label: 'Fruit', description: 'Choose your favorite fruits.', }, From 6233fb412e61dec46afbce77a7f71e90b6c64027 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Wed, 26 Aug 2026 03:41:53 +0900 Subject: [PATCH 16/16] SearchableChipSelect: Forward ref to search input --- packages/ui/CHANGELOG.md | 1 + .../searchable-chip-select.tsx | 4 +- .../test/index.test.tsx | 20 ++++++-- .../searchable-chip-select-control.tsx | 2 +- .../test/index.test.tsx | 46 +++++-------------- 5 files changed, 30 insertions(+), 43 deletions(-) diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 850deb1e70caac..20b993ae2c6279 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -39,6 +39,7 @@ - `Card`: Use the normal neutral surface stroke for the default border. ([#81746](https://github.com/WordPress/gutenberg/pull/81746)) - `Calendar`, `RangeCalendar`: Preserve day focus when a controlled month change removes the focused day. ([#81635](https://github.com/WordPress/gutenberg/pull/81635)) - `Button`: Fix the loading spinner being off-center in RTL ([#81901](https://github.com/WordPress/gutenberg/pull/81901)). +- `SearchableChipSelect`, `SearchableChipSelectControl`: Forward ref to the search input instead of the chips container ([#80980](https://github.com/WordPress/gutenberg/pull/80980)). ### Documentation diff --git a/packages/ui/src/form/primitives/searchable-chip-select/searchable-chip-select.tsx b/packages/ui/src/form/primitives/searchable-chip-select/searchable-chip-select.tsx index 773aa773cd4479..2f7c17d5f41d06 100644 --- a/packages/ui/src/form/primitives/searchable-chip-select/searchable-chip-select.tsx +++ b/packages/ui/src/form/primitives/searchable-chip-select/searchable-chip-select.tsx @@ -23,7 +23,7 @@ import { * Prefer `SearchableChipSelectControl` when using with a standard label and description. */ export const SearchableChipSelect = forwardRef< - HTMLDivElement, + HTMLInputElement, SearchableChipSelectProps >( function SearchableChipSelect( { @@ -65,7 +65,6 @@ export const SearchableChipSelect = forwardRef< visuallyDisabled={ disabled } /> } - ref={ ref } > { ( value: Item[] ) => ( @@ -104,6 +103,7 @@ export const SearchableChipSelect = forwardRef< { mockedWarning.mockClear(); } ); - it( 'forwards ref', () => { - const ref = createRef< HTMLDivElement >(); + it( 'forwards ref to the search input', () => { + const ref = createRef< HTMLInputElement >(); - render( ); + render( + + ); - expect( ref.current ).toBeInstanceOf( HTMLDivElement ); + const input = screen.getByRole( 'combobox', { + name: 'Select options', + } ); + + expect( ref.current ).toBe( input ); + act( () => { + ref.current?.focus(); + } ); + expect( input ).toHaveFocus(); } ); it( 'passes aria-label and aria-describedby props to the appropriate components', () => { diff --git a/packages/ui/src/form/searchable-chip-select-control/searchable-chip-select-control.tsx b/packages/ui/src/form/searchable-chip-select-control/searchable-chip-select-control.tsx index 42cb624792dce3..6c06be97c473c4 100644 --- a/packages/ui/src/form/searchable-chip-select-control/searchable-chip-select-control.tsx +++ b/packages/ui/src/form/searchable-chip-select-control/searchable-chip-select-control.tsx @@ -7,7 +7,7 @@ import type { SearchableChipSelectControlProps } from './types'; * and description. */ export const SearchableChipSelectControl = forwardRef< - HTMLDivElement, + HTMLInputElement, SearchableChipSelectControlProps >( function SearchableChipSelectControl( { diff --git a/packages/ui/src/form/searchable-chip-select-control/test/index.test.tsx b/packages/ui/src/form/searchable-chip-select-control/test/index.test.tsx index ec4083bdc6873a..92dd6007ca82b4 100644 --- a/packages/ui/src/form/searchable-chip-select-control/test/index.test.tsx +++ b/packages/ui/src/form/searchable-chip-select-control/test/index.test.tsx @@ -1,5 +1,4 @@ -import { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { act, render, screen } from '@testing-library/react'; import { createRef } from '@wordpress/element'; import { SearchableChipSelectControl } from '../index'; @@ -10,49 +9,26 @@ describe( 'SearchableChipSelectControl', () => { { value: 'cherry', label: 'Cherry' }, ]; - it( 'forwards ref', async () => { - const user = userEvent.setup(); - const ref = createRef< HTMLDivElement >(); - const itemRef = createRef< HTMLDivElement >(); - const chipRef = createRef< HTMLDivElement >(); + it( 'forwards ref to the search input', () => { + const ref = createRef< HTMLInputElement >(); render( - value.map( ( item ) => ( - - { item.label } - - ) ) - } - > - { ( item ) => ( - - { item.label } - - ) } - + /> ); - expect( ref.current ).toBeInstanceOf( HTMLDivElement ); - expect( chipRef.current ).toBeInstanceOf( HTMLDivElement ); - - await user.click( screen.getByRole( 'combobox' ) ); + const input = screen.getByRole( 'combobox', { + name: 'Select options', + } ); - await waitFor( () => { - expect( itemRef.current ).toBeInstanceOf( HTMLDivElement ); + expect( ref.current ).toBe( input ); + act( () => { + ref.current?.focus(); } ); + expect( input ).toHaveFocus(); } ); it( 'renders accessible label and description', () => {