Skip to content

SearchableChipSelect: Add grouped items support - #80989

Merged
mirka merged 20 commits into
trunkfrom
update-ui-searchable-chip-select
Aug 21, 2026
Merged

SearchableChipSelect: Add grouped items support#80989
mirka merged 20 commits into
trunkfrom
update-ui-searchable-chip-select

Conversation

@mirka

@mirka mirka commented Jul 30, 2026

Copy link
Copy Markdown
Member

What?

Enhances the SearchableChipSelect primitive with grouped items support and a creatable: true item marker for footer create actions.

Why?

Grouped options are a common combobox pattern. Creatable items need to participate in keyboard navigation while staying out of the main list UI.

How?

  • Export Group, GroupLabel, and Collection subcomponents from SearchableChipSelect.
  • Fix the default collection renderer so custom children are passed directly to Combobox.Collection (required for grouped rendering).
  • Add ItemGroup typing and isItem / isItemGroup helpers for grouped items.
  • Replace the creatableItem prop with a creatable: true marker on an item in items. The component finds the creatable item, excludes it from the main list, moves it to the end of flat items for keyboard order, and renders it in the list footer.
  • Add development warnings for common misconfigurations (grouped items without children, multiple creatable items, creatable not last in flat lists, creatable mixed with regular items in a group).
  • Add Grouped, GroupedCreatable stories and unit tests for grouped, creatable, and warning behavior.

Testing Instructions

  1. Run Storybook and open Design System / Components / Form / Primitives / SearchableChipSelect.
  2. Check the Grouped story: open the popup and confirm items appear under group labels.
  3. Check the Creatable story: confirm the create action appears in the footer, not in the main list.
  4. Check the GroupedCreatable story: type a search query, use the keyboard to select the create action, and confirm it works.
  5. Run unit tests: npm run test:unit -- packages/ui/src/form/primitives/searchable-chip-select/test/index.test.tsx

Screenshots

Grouped items

Grouped items

@mirka mirka self-assigned this Jul 30, 2026
@github-actions github-actions Bot added the [Package] UI /packages/ui label Jul 30, 2026
@mirka mirka added the [Type] Enhancement A suggestion for improvement. label Jul 30, 2026
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

Size Change: 0 B

Total Size: 7.77 MB

📦 View Changed
Filename Size Change
build/scripts/edit-site/index.min.js 339 kB -3 B (0%)
build/scripts/media-utils/index.min.js 158 kB +3 B (0%)

compressed-size-action

Comment on lines 8 to +11
Item.displayName = 'SearchableChipSelect.Item';
ChipWithRemove.displayName = 'SearchableChipSelect.ChipWithRemove';
Group.displayName = 'SearchableChipSelect.Group';
GroupLabel.displayName = 'SearchableChipSelect.GroupLabel';

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.

Why aren't we setting a display name for Collection?

Short answer: I realized we have some systemic problems with the way we assign new display names on re-exports. This is especially noticeable for the Collection subcomponent, but already effects other subcomponents as well.

Longer answer

When higher-level components re-export combobox subcomponents (Item, Group, Collection, etc.) and assign displayName in each layer's index file, two issues stack up:

1. TypeScript vs runtime
forwardRef subcomponents (Item, Group, …) allow displayName assignment. Plain functions like Collection don't — TypeScript errors unless you suppress or cast. forwardRef on Collection doesn't work because base-ui's Collection doesn't take a ref.

2. Shared singleton references
Re-exporting the same imported component and setting displayName in both SearchableChipSelect and SearchableChipSelectControl mutates one function object — only the last assignment wins. That's already true for Item, Group, etc., not just Collection.

Distinct names per layer requires a new component reference each time — a wrapper function or a dedicated file (like SelectControl.Item, which wraps rather than re-exports).

Decision: Skip Collection.displayName for now; treat the singleton / multi-layer naming problem as systemic, not worth solving piecemeal in this PR.

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

Flaky tests detected in cb5302f.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/31649300042
📝 Reported tests:

Should insert content using the global inserter in /test/e2e/specs/widgets/editing-widgets.spec.js, passed after 1 failed attempt.

@mirka
mirka marked this pull request as ready for review August 3, 2026 17:23
@mirka
mirka requested a review from a team as a code owner August 3, 2026 17:23
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: mirka <0mirka00@git.wordpress.org>
Co-authored-by: ciampo <mciampini@git.wordpress.org>
Co-authored-by: Mamaduka <mamaduka@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

Comment thread packages/ui/src/form/primitives/searchable-chip-select/searchable-chip-select.tsx Outdated
* array of groups instead of a flat list of items.
*/
items?: Item[];
items?: Item[] | ItemGroup[];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

According to the new types, items accepts grouped data while children remains optional.

But it also looks like the default renderer skips group objects. But Base UI will still consider the list non-empty, and therefore the empty state will be hidden and the popup appear blank.

Hope it makes sense 😅

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.

Valid observation, but this is also another conscious decision similar to the point above. We want to avoid conditional types, so the types are kept simple. And the failure mode of forgetting to pass children in the grouped case is non-subtle (i.e. easy to catch). The intent is for Storybook documentation to cover this, rather than contort the internal logic to handle it magically.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Gotcha. Instead of conditional types, what about a props union like

type FlatItemsProps = {
	items?: Item[];
	children?: ( item: Item, index: number ) => ReactNode;
};

type GroupedItemsProps = {
	items: ItemGroup[];
	children: ( group: ItemGroup, index: number ) => ReactNode;
};

type ItemsProps = FlatItemsProps | GroupedItemsProps;

export type SearchableChipSelectProps = Omit<
	ComboboxRootProps< Item, true >,
	'children' | 'items' | 'multiple'
> &
	ItemsProps & {
		// Existing common props…
	};

? Would that prevent the valid-looking blank popup without adding runtime logic without adding too much complexity to the types?

In general, and similarly to what was discussed above, I'd feel better if we tightened this (kind of implicit) contract with redundancy in JSDocs and (dev) runtime errors when the props are not configured in the correct way. These errors may also help agents when building / debugging.

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.

Conditional shapes in the broad sense like that also complicate type management on the consumer side, often involving type casts once you aren't passing full literal values. With this specifically, it did require extra type casting in the Storybook file, so it's likely that similar inconveniences will be encountered in consumer usage.

I hope the added console warning will suffice for now?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Right, we can probably keep the logic as-is for now, since the failure will be quite obvious too,.

Maybe we can improve the docs around it?

-items prop: “Grouped items require a custom children renderer.”
-children prop: “Required when items contains groups.”
-Grouped Storybook example: mention that groups have no default renderer

@mirka
mirka requested a review from ciampo August 6, 2026 19:13
@mirka mirka added [Type] Breaking Change For PRs that introduce a change that will break existing functionality and removed [Type] Enhancement A suggestion for improvement. labels Aug 7, 2026

@ciampo ciampo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pre-approving for the sake of speed of iteration.

Good to merge once last round of feedback is addressed 🚀

Comment on lines +70 to +86
export function shouldSkipCollectionEntry(
entry: Item | ItemGroup,
creatableItem: CreatableItem | undefined
): boolean {
if ( ! creatableItem ) {
return false;
}

if ( isItem( entry ) ) {
return isCreatableItem( entry );
}

return (
entry.items.length > 0 &&
entry.items.every( ( item ) => isCreatableItem( item ) )
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We currently skip a group only when every item is creatable. I think I identified a small edge case.

If a group contains [ Apple, Create ], children renders Create in the group, and the footer will render it again, meaning there will be two Create options.

Should we remove creatable items from their original groups during normalization? (potentially add a test for this)

* array of groups instead of a flat list of items.
*/
items?: Item[];
items?: Item[] | ItemGroup[];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Right, we can probably keep the logic as-is for now, since the failure will be quite obvious too,.

Maybe we can improve the docs around it?

-items prop: “Grouped items require a custom children renderer.”
-children prop: “Required when items contains groups.”
-Grouped Storybook example: mention that groups have no default renderer

Comment on lines +34 to +53
if ( ! hasGroupedItems( items ) ) {
const flatItems = items as Item[];
let lastCreatableIndex = -1;

for ( let index = flatItems.length - 1; index >= 0; index-- ) {
if ( isCreatableItem( flatItems[ index ] ) ) {
lastCreatableIndex = index;
break;
}
}

if (
lastCreatableIndex >= 0 &&
lastCreatableIndex !== flatItems.length - 1
) {
warning(
'SearchableChipSelect: the creatable item should be last in `items` for predictable keyboard navigation.'
);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we still need this warning branch? normalizeRootItems() should take care of this, there's a chance we don't really need to surface this error to the consumers of the component?

@mirka
mirka enabled auto-merge (squash) August 21, 2026 13:41
@mirka
mirka merged commit 047dc27 into trunk Aug 21, 2026
60 checks passed
@mirka
mirka deleted the update-ui-searchable-chip-select branch August 21, 2026 14:32
@github-actions github-actions Bot added this to the Gutenberg 23.9 milestone Aug 21, 2026
@MaggieCabrera MaggieCabrera added the [Type] Enhancement A suggestion for improvement. label Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] UI /packages/ui [Type] Breaking Change For PRs that introduce a change that will break existing functionality [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants