Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ff51b9b
Add SearchableChipSelectControl to @wordpress/ui
mirka Jul 30, 2026
71f6065
Add changelog
mirka Jul 30, 2026
d085b27
Tweak changelog
mirka Jul 30, 2026
40ab5bb
SearchableChipSelectControl: Add grouped subcomponents and story.
mirka Jul 30, 2026
a8dd424
Merge trunk into add-ui-searchable-chip-select-control
mirka Aug 21, 2026
98e8750
SearchableChipSelectControl: Mark recommended and add to manifest.
mirka Aug 21, 2026
84d3cc6
Remove redundant subcomponent JSDocs from SearchableChipSelect.
mirka Aug 21, 2026
508ee57
Remove redundant subcomponent JSDocs from SearchableChipSelectControl.
mirka Aug 21, 2026
7a0e47a
Add changelog
mirka Aug 21, 2026
776f1e5
Allow SearchableChipSelect components in use-recommended-components.
mirka Aug 21, 2026
04511fe
SearchableChipSelectControl: Add missing control-level stories.
mirka Aug 21, 2026
afb39ad
Merge trunk into add-ui-searchable-chip-select-control
mirka Aug 24, 2026
74d0e9f
Update Storybook manifest snapshot.
mirka Aug 24, 2026
c788e99
Render SearchableChipSelect directly under Field.Root.
mirka Aug 24, 2026
80d7b2d
Remove SearchableChipSelectControl subcomponent displayNames.
mirka Aug 24, 2026
792adb1
Move creatable stories to SearchableChipSelectControl.
mirka Aug 24, 2026
f88d7cd
Merge branch 'trunk' into add-ui-searchable-chip-select-control
mirka Aug 24, 2026
e7272d9
Merge branch 'trunk' into add-ui-searchable-chip-select-control
mirka Aug 25, 2026
2787468
SearchableChipSelectControl: Fix Grouped story defaultValue
mirka Aug 25, 2026
6233fb4
SearchableChipSelect: Forward ref to search input
mirka Aug 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/eslint-plugin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,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)).
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/rules/use-recommended-components.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ const ALLOWLIST = {
'KeyboardShortcutDisplay',
'Link',
'RangeCalendar',
'SearchableChipSelect',
'SearchableChipSelectControl',
'Skeleton',
'Spinner',
'Stack',
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,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)).
- Add `ValidatedInputControl` component, an `InputControl` wrapped in `ControlWithError` ([#81627](https://github.com/WordPress/gutenberg/pull/81627)).
- Add `ValidatedTextareaControl` component, a `TextareaControl` wrapped in `ControlWithError` ([#81984](https://github.com/WordPress/gutenberg/pull/81984)).

Expand Down Expand Up @@ -38,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

Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/form/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export * from './primitives';

export * from './input-control';
export * from './select-control';
export { SearchableChipSelectControl } from './searchable-chip-select-control';
export * from './textarea-control';
export * from './with-validation';
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ 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,
HTMLInputElement,
SearchableChipSelectProps
>( function SearchableChipSelect(
{
Expand Down Expand Up @@ -63,7 +65,6 @@ export const SearchableChipSelect = forwardRef<
visuallyDisabled={ disabled }
/>
}
ref={ ref }
>
<Combobox.Value>
{ ( value: Item[] ) => (
Expand Down Expand Up @@ -102,6 +103,7 @@ export const SearchableChipSelect = forwardRef<
</Combobox.Value>

<Combobox.Input
ref={ ref }
render={
<input
className={ clsx(
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -10,8 +9,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 ) => <SearchableChipSelect { ...args } />,
subcomponents: {
'SearchableChipSelect.Group': SearchableChipSelect.Group,
'SearchableChipSelect.GroupLabel': SearchableChipSelect.GroupLabel,
Expand All @@ -26,9 +31,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).',
},
},
};
Expand All @@ -43,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 = {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed these two stories on the lower-level component in favor of keeping them on the higher-level component, since a clean and simple sharing of the code cannot be done due to the render function.

I think this is fine, I don't expect much need for the lower-level version here, unlike the underlying Combobox.

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 (
<SearchableChipSelect
{ ...args }
items={ [ ...ITEMS, creatableItem ] }
inputValue={ inputValue }
onInputValueChange={ setInputValue }
value={ value }
onValueChange={ ( values: typeof ITEMS, event ) => {
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.
Expand Down Expand Up @@ -172,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 (
<SearchableChipSelect
{ ...args }
items={ items }
inputValue={ inputValue }
onInputValueChange={ setInputValue }
value={ value }
onValueChange={ ( values: FixtureItem[], event ) => {
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 ) => (
<SearchableChipSelect.Group
key={ group.label }
items={ group.items }
>
<SearchableChipSelect.GroupLabel>
{ group.label }
</SearchableChipSelect.GroupLabel>
<SearchableChipSelect.Collection>
{ ( item: FixtureItem ) => (
<SearchableChipSelect.Item
key={ item.value }
value={ item }
>
{ item.label }
</SearchableChipSelect.Item>
) }
</SearchableChipSelect.Collection>
</SearchableChipSelect.Group>
) }
/>
);
},
};

/**
* Use the `showClearButton` prop to hide the clear button.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, screen, waitFor } from '@testing-library/react';
import { act, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createRef } from '@wordpress/element';
import warning from '@wordpress/warning';
Expand All @@ -15,12 +15,22 @@ describe( 'SearchableChipSelect', () => {
mockedWarning.mockClear();
} );

it( 'forwards ref', () => {
const ref = createRef< HTMLDivElement >();
it( 'forwards ref to the search input', () => {
const ref = createRef< HTMLInputElement >();

render( <SearchableChipSelect ref={ ref } /> );
render(
<SearchableChipSelect ref={ ref } aria-label="Select options" />
);

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', () => {
Expand Down
21 changes: 21 additions & 0 deletions packages/ui/src/form/searchable-chip-select-control/index.ts

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't add displayName on the exports here, since it would overwrite the names on the lower-level components. Again, this is an issue we need to address package-wide.

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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';

/**
* A complete searchable multi-select field with chips, integrated label,
* and description.
*/
export const SearchableChipSelectControl = Object.assign(
_SearchableChipSelectControl,
{
Group,
GroupLabel,
Item,
ChipWithRemove,
Collection,
}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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<
HTMLInputElement,
SearchableChipSelectControlProps
>( function SearchableChipSelectControl(
{
className,
label,
description,
details,
hideLabelFromVision,
...restProps
},
ref
) {
return (
<Field.Root className={ className }>
<Field.Label hideFromVision={ hideLabelFromVision }>
{ label }
</Field.Label>
<SearchableChipSelect ref={ ref } { ...restProps } />
{ description && (
<Field.Description>{ description }</Field.Description>
) }
{ details && <Field.Details>{ details }</Field.Details> }
</Field.Root>
);
} );
Loading
Loading