From 9eaae4cd573522e22bd00e1d417e86f108eaa289 Mon Sep 17 00:00:00 2001 From: Ravi Suhag Date: Wed, 7 Oct 2026 17:33:37 -0500 Subject: [PATCH 1/4] feat: [combobox] add createItems for id-based values --- .../content/docs/components/combobox/demo.ts | 37 ++++++++++++ .../docs/components/combobox/index.mdx | 9 +++ .../content/docs/components/combobox/props.ts | 15 +++++ .../combobox/__tests__/combobox.test.tsx | 57 +++++++++++++++++++ .../components/combobox/combobox-content.tsx | 15 +++-- .../components/combobox/combobox-root.tsx | 30 +++++----- .../react/components/combobox/combobox.tsx | 3 +- 7 files changed, 145 insertions(+), 21 deletions(-) diff --git a/apps/www/src/content/docs/components/combobox/demo.ts b/apps/www/src/content/docs/components/combobox/demo.ts index 39c350ea7..f77374d33 100644 --- a/apps/www/src/content/docs/components/combobox/demo.ts +++ b/apps/www/src/content/docs/components/combobox/demo.ts @@ -149,3 +149,40 @@ export const controlledDemo = { ); }` }; + +export const createItemsDemo = { + type: 'code', + code: ` + function CreateItemsDemo() { + const users = [ + { id: "u1", name: "Jane Doe" }, + { id: "u2", name: "John Smith" }, + { id: "u3", name: "Ada Lovelace" } + ]; + const items = React.useMemo( + () => + Combobox.createItems(users, { + getValue: user => user.id, + getLabel: user => user.name + }), + [] + ); + const [value, setValue] = React.useState(null); + + return ( + + Selected ID: {value || "None"} + + + + {user => ( + + {user.name} + + )} + + + + ); + }` +}; diff --git a/apps/www/src/content/docs/components/combobox/index.mdx b/apps/www/src/content/docs/components/combobox/index.mdx index d30e80a97..7d9effd36 100644 --- a/apps/www/src/content/docs/components/combobox/index.mdx +++ b/apps/www/src/content/docs/components/combobox/index.mdx @@ -12,6 +12,7 @@ import { iconDemo, withFieldDemo, controlledDemo, + createItemsDemo, } from "./demo.ts"; @@ -69,6 +70,14 @@ Use `value` and `onValueChange` for controlled behavior. +### Using IDs as values + +When your app stores IDs instead of item objects, wrap the data with `Combobox.createItems`. `getValue` returns the value that `value` and `onValueChange` use. `getLabel` returns the text shown in the input and used for filtering. Pass a function as the child of `Combobox.Content` to render each item that matches the input. + +Create the collection at module scope for static data. For data that changes, wrap it in `React.useMemo` keyed by the data. + + + ## API Reference Root and input first, then the parts that make up the popup list. diff --git a/apps/www/src/content/docs/components/combobox/props.ts b/apps/www/src/content/docs/components/combobox/props.ts index 6b60a7236..5b9631f06 100644 --- a/apps/www/src/content/docs/components/combobox/props.ts +++ b/apps/www/src/content/docs/components/combobox/props.ts @@ -49,6 +49,14 @@ export interface ComboboxRootProps { * @default false */ modal?: boolean; + + /** + * The items to filter. Accepts an array of items or a collection from `Combobox.createItems`. + * When set, the root filters the items and `Combobox.Content` can render them with a function child. + * + * @remarks `unknown[] | ReturnType` + */ + items?: unknown[]; } export interface ComboboxInputProps { @@ -96,6 +104,13 @@ export interface ComboboxContentProps { * @defaultValue The theme's `radius` */ radius?: 'none' | 'small' | 'medium' | 'large' | 'full'; + + /** + * The list content. Pass a function to render each item that matches the input when `items` is set on the root. + */ + children?: + | React.ReactNode + | ((item: unknown, index: number) => React.ReactNode); } export interface ComboboxItemProps { diff --git a/packages/react/components/combobox/__tests__/combobox.test.tsx b/packages/react/components/combobox/__tests__/combobox.test.tsx index 57617598e..ab4010613 100644 --- a/packages/react/components/combobox/__tests__/combobox.test.tsx +++ b/packages/react/components/combobox/__tests__/combobox.test.tsx @@ -484,3 +484,60 @@ describe('Combobox', () => { }); }); }); + +describe('Combobox.createItems', () => { + const USERS = [ + { id: 'u1', name: 'Jane Doe' }, + { id: 'u2', name: 'John Smith' }, + { id: 'u3', name: 'Ada Lovelace' } + ]; + const USER_ITEMS = Combobox.createItems(USERS, { + getValue: user => user.id, + getLabel: user => user.name + }); + + const UserCombobox = (props: { + defaultValue?: string | null; + onValueChange?: (value: string | null) => void; + }) => ( + + + + {(user: (typeof USERS)[number]) => ( + + {user.name} + + )} + + + ); + + it('calls onValueChange with the derived value', async () => { + const user = userEvent.setup(); + const onValueChange = vi.fn(); + render(); + + await user.click(screen.getByRole('combobox')); + await clickOption(await screen.findByText('John Smith')); + + expect(onValueChange).toHaveBeenCalledWith('u2'); + }); + + it('shows the derived label for the selected value', () => { + render(); + expect(screen.getByRole('combobox')).toHaveValue('Ada Lovelace'); + }); + + it('filters by the derived label', async () => { + const user = userEvent.setup(); + render(); + + await user.type(screen.getByRole('combobox'), 'jo'); + + await waitFor(() => { + expect(screen.getByText('John Smith')).toBeInTheDocument(); + expect(screen.queryByText('Jane Doe')).not.toBeInTheDocument(); + expect(screen.queryByText('Ada Lovelace')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/packages/react/components/combobox/combobox-content.tsx b/packages/react/components/combobox/combobox-content.tsx index ba86a4ae9..83cce3a7a 100644 --- a/packages/react/components/combobox/combobox-content.tsx +++ b/packages/react/components/combobox/combobox-content.tsx @@ -2,22 +2,27 @@ import { Combobox as ComboboxPrimitive } from '@base-ui/react'; import { cx } from 'class-variance-authority'; +import type { ReactNode } from 'react'; import { type Radius, radiusStyle } from '../../shared/radius'; import { useThemeInjection } from '../theme/portal'; import styles from './combobox.module.css'; import { useComboboxContext } from './combobox-root'; -export interface ComboboxContentProps +export interface ComboboxContentProps extends Omit< ComboboxPrimitive.Positioner.Props, - 'render' | 'className' | 'style' + 'render' | 'className' | 'style' | 'children' >, - ComboboxPrimitive.Popup.Props { + Omit { /** Corner radius for this popup only. Overrides the theme's `radius`. */ radius?: Radius; + /** + * The list content. Pass a function to render each item that matches the input when `items` is set on the root. + */ + children?: ReactNode | ((item: Item, index: number) => ReactNode); } -export const ComboboxContent = ({ +export const ComboboxContent = ({ ref, className, children, @@ -28,7 +33,7 @@ export const ComboboxContent = ({ sideOffset = 4, radius, ...positionerProps -}: ComboboxContentProps) => { +}: ComboboxContentProps) => { const { inputContainerRef } = useComboboxContext(); const theme = useThemeInjection(); return ( diff --git a/packages/react/components/combobox/combobox-root.tsx b/packages/react/components/combobox/combobox-root.tsx index d031e0f40..537a3ff8c 100644 --- a/packages/react/components/combobox/combobox-root.tsx +++ b/packages/react/components/combobox/combobox-root.tsx @@ -37,35 +37,35 @@ export const useComboboxContext = < return context as ComboboxContextValue; }; -export interface BaseComboboxRootProps +export interface BaseComboboxRootProps extends Omit< - ComboboxPrimitive.Root.Props, + ComboboxPrimitive.Root.Props, 'onValueChange' | 'onInputValueChange' | 'multiple' > { onInputValueChange?: (inputValue: string) => void; } -export interface SingleComboboxProps - extends BaseComboboxRootProps { +export interface SingleComboboxProps + extends BaseComboboxRootProps { multiple?: false; value?: Value | null; defaultValue?: Value | null; onValueChange?: (value: Value | null) => void; } -export interface MultipleComboboxProps - extends BaseComboboxRootProps { +export interface MultipleComboboxProps + extends BaseComboboxRootProps { multiple: true; value?: Value[]; defaultValue?: Value[]; onValueChange?: (value: Value[]) => void; } -export type ComboboxRootProps = - | SingleComboboxProps - | MultipleComboboxProps; +export type ComboboxRootProps = + | SingleComboboxProps + | MultipleComboboxProps; -export const ComboboxRoot = ({ +export const ComboboxRoot = ({ multiple = false, children, onValueChange, @@ -75,7 +75,7 @@ export const ComboboxRoot = ({ items, required, ...props -}: ComboboxRootProps) => { +}: ComboboxRootProps) => { const fieldContext = useFieldContext(); const resolvedRequired = required ?? fieldContext?.required; @@ -105,11 +105,11 @@ export const ComboboxRoot = ({ ) => { setInternalValue(value); if (multiple) { - (onValueChange as MultipleComboboxProps['onValueChange'])?.( - value as Value[] - ); + ( + onValueChange as MultipleComboboxProps['onValueChange'] + )?.(value as Value[]); } else { - (onValueChange as SingleComboboxProps['onValueChange'])?.( + (onValueChange as SingleComboboxProps['onValueChange'])?.( value as Value | null ); } diff --git a/packages/react/components/combobox/combobox.tsx b/packages/react/components/combobox/combobox.tsx index a79db22a0..d557fc42f 100644 --- a/packages/react/components/combobox/combobox.tsx +++ b/packages/react/components/combobox/combobox.tsx @@ -17,5 +17,6 @@ export const Combobox = Object.assign(ComboboxRoot, { Label: ComboboxLabel, Separator: ComboboxSeparator, useFilter: ComboboxPrimitive.useFilter, - useFilteredItems: ComboboxPrimitive.useFilteredItems + useFilteredItems: ComboboxPrimitive.useFilteredItems, + createItems: ComboboxPrimitive.createItems }); From 8f4c27c959d17053da2fa748c199451eded39423 Mon Sep 17 00:00:00 2001 From: Ravi Suhag Date: Wed, 7 Oct 2026 18:57:14 -0500 Subject: [PATCH 2/4] fix: [combobox] show item labels on multi-select chips --- .../combobox/__tests__/combobox.test.tsx | 28 +++++++++++++++ .../components/combobox/combobox-input.tsx | 4 +-- .../components/combobox/combobox-items.ts | 35 +++++++++++++++++++ .../components/combobox/combobox-root.tsx | 5 ++- .../react/components/combobox/combobox.tsx | 3 +- 5 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 packages/react/components/combobox/combobox-items.ts diff --git a/packages/react/components/combobox/__tests__/combobox.test.tsx b/packages/react/components/combobox/__tests__/combobox.test.tsx index ab4010613..b60ac6e58 100644 --- a/packages/react/components/combobox/__tests__/combobox.test.tsx +++ b/packages/react/components/combobox/__tests__/combobox.test.tsx @@ -541,3 +541,31 @@ describe('Combobox.createItems', () => { }); }); }); + +describe('Combobox.createItems chips', () => { + const USERS = [ + { id: 'u1', name: 'Jane Doe' }, + { id: 'u2', name: 'John Smith' } + ]; + const USER_ITEMS = Combobox.createItems(USERS, { + getValue: user => user.id, + getLabel: user => user.name + }); + + it('labels chips with the derived label', () => { + render( + + + + {(user: (typeof USERS)[number]) => ( + + {user.name} + + )} + + + ); + expect(screen.getByText('John Smith')).toBeInTheDocument(); + expect(screen.queryByText('u2')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/react/components/combobox/combobox-input.tsx b/packages/react/components/combobox/combobox-input.tsx index 4bd7913c9..a8399ef4c 100644 --- a/packages/react/components/combobox/combobox-input.tsx +++ b/packages/react/components/combobox/combobox-input.tsx @@ -13,7 +13,7 @@ export interface ComboboxInputProps > {} export const ComboboxInput = ({ ref, ...props }: ComboboxInputProps) => { - const { multiple, inputContainerRef, value, onValueChange } = + const { multiple, inputContainerRef, value, onValueChange, getLabel } = useComboboxContext(); return ( { chips={ multiple && Array.isArray(value) ? value.map(val => ({ - label: val, + label: getLabel(val), onRemove: () => onValueChange?.((value as string[])?.filter(v => v !== val)) })) diff --git a/packages/react/components/combobox/combobox-items.ts b/packages/react/components/combobox/combobox-items.ts new file mode 100644 index 000000000..d45f96123 --- /dev/null +++ b/packages/react/components/combobox/combobox-items.ts @@ -0,0 +1,35 @@ +import { Combobox as ComboboxPrimitive } from '@base-ui/react'; + +type LabelLookup = (value: unknown) => string | undefined; + +const labelLookups = new WeakMap(); + +/** + * Wraps Base UI's `createItems` and records how to label each value, so chips + * can show labels instead of raw values. + */ +export const createItems: typeof ComboboxPrimitive.createItems = ( + data, + options +) => { + const collection = ComboboxPrimitive.createItems(data, options); + labelLookups.set(collection, value => { + for (const entry of data ?? []) { + const items = + typeof entry === 'object' && entry !== null && 'items' in entry + ? entry.items + : [entry]; + for (const item of items) { + if (options.getValue(item) === value) return options.getLabel(item); + } + } + return undefined; + }); + return collection; +}; + +/** Returns the label for `value` when `items` came from `createItems`. */ +export function getItemLabel(items: unknown, value: unknown) { + if (typeof items !== 'object' || items === null) return undefined; + return labelLookups.get(items)?.(value); +} diff --git a/packages/react/components/combobox/combobox-root.tsx b/packages/react/components/combobox/combobox-root.tsx index 537a3ff8c..bf252f7c5 100644 --- a/packages/react/components/combobox/combobox-root.tsx +++ b/packages/react/components/combobox/combobox-root.tsx @@ -11,6 +11,7 @@ import { useState } from 'react'; import { useFieldContext } from '../field'; +import { getItemLabel } from './combobox-items'; interface ComboboxContextValue { multiple: boolean; @@ -19,6 +20,7 @@ interface ComboboxContextValue { inputContainerRef: RefObject; value: Value | Value[] | null | undefined; onValueChange?: (value: Value | Value[] | null) => void; + getLabel: (value: Value) => string; } const ComboboxContext = createContext< @@ -124,7 +126,8 @@ export const ComboboxRoot = ({ hasItems: !!items, inputContainerRef, value: computedValue, - onValueChange: handleValueChange + onValueChange: handleValueChange, + getLabel: (value: Value) => getItemLabel(items, value) ?? String(value) }), [multiple, inputValue, items, computedValue, handleValueChange] ); diff --git a/packages/react/components/combobox/combobox.tsx b/packages/react/components/combobox/combobox.tsx index d557fc42f..6b6f66ecc 100644 --- a/packages/react/components/combobox/combobox.tsx +++ b/packages/react/components/combobox/combobox.tsx @@ -2,6 +2,7 @@ import { Combobox as ComboboxPrimitive } from '@base-ui/react'; import { ComboboxContent } from './combobox-content'; import { ComboboxInput } from './combobox-input'; import { ComboboxItem } from './combobox-item'; +import { createItems } from './combobox-items'; import { ComboboxGroup, ComboboxLabel, @@ -18,5 +19,5 @@ export const Combobox = Object.assign(ComboboxRoot, { Separator: ComboboxSeparator, useFilter: ComboboxPrimitive.useFilter, useFilteredItems: ComboboxPrimitive.useFilteredItems, - createItems: ComboboxPrimitive.createItems + createItems }); From f7f1d1cdc27efd1751dc8764be437f7dd62ef83f Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Thu, 15 Oct 2026 23:37:53 +0530 Subject: [PATCH 3/4] refactor: [combobox] use base ui createItems and label chips with itemToStringLabel Export Base UI's createItems directly and delete the wrapper and its WeakMap. Chips take their label from itemToStringLabel, then from the label field of { value, label } values. --- .../content/docs/components/combobox/demo.ts | 24 ++++---- .../docs/components/combobox/index.mdx | 6 +- .../content/docs/components/combobox/props.ts | 3 + .../combobox/__tests__/combobox.test.tsx | 57 ++++++++----------- .../components/combobox/combobox-items.ts | 35 ------------ .../components/combobox/combobox-root.tsx | 27 ++++++--- .../react/components/combobox/combobox.tsx | 3 +- 7 files changed, 63 insertions(+), 92 deletions(-) delete mode 100644 packages/react/components/combobox/combobox-items.ts diff --git a/apps/www/src/content/docs/components/combobox/demo.ts b/apps/www/src/content/docs/components/combobox/demo.ts index f77374d33..71fb48d4b 100644 --- a/apps/www/src/content/docs/components/combobox/demo.ts +++ b/apps/www/src/content/docs/components/combobox/demo.ts @@ -154,19 +154,17 @@ export const createItemsDemo = { type: 'code', code: ` function CreateItemsDemo() { - const users = [ - { id: "u1", name: "Jane Doe" }, - { id: "u2", name: "John Smith" }, - { id: "u3", name: "Ada Lovelace" } - ]; - const items = React.useMemo( - () => - Combobox.createItems(users, { - getValue: user => user.id, - getLabel: user => user.name - }), - [] - ); + const items = React.useMemo(() => { + const users = [ + { id: "u1", name: "Jane Doe" }, + { id: "u2", name: "John Smith" }, + { id: "u3", name: "Ada Lovelace" } + ]; + return Combobox.createItems(users, { + getValue: user => user.id, + getLabel: user => user.name + }); + }, []); const [value, setValue] = React.useState(null); return ( diff --git a/apps/www/src/content/docs/components/combobox/index.mdx b/apps/www/src/content/docs/components/combobox/index.mdx index 7d9effd36..ad9c2c0be 100644 --- a/apps/www/src/content/docs/components/combobox/index.mdx +++ b/apps/www/src/content/docs/components/combobox/index.mdx @@ -62,6 +62,8 @@ Organize items into groups with labels and visual separators. Groups and labels Pass the `multiple` prop to enable multi-select. Selected values appear as chips in the input. Items display checkboxes in multiple mode. +Each chip shows its value as text. To show a label instead, pass `itemToStringLabel` to the root. Values in the `{ value, label }` shape show their `label` without it. + ### Controlled @@ -72,10 +74,12 @@ Use `value` and `onValueChange` for controlled behavior. ### Using IDs as values -When your app stores IDs instead of item objects, wrap the data with `Combobox.createItems`. `getValue` returns the value that `value` and `onValueChange` use. `getLabel` returns the text shown in the input and used for filtering. Pass a function as the child of `Combobox.Content` to render each item that matches the input. +When your app stores IDs instead of item objects, wrap the data with `Combobox.createItems`. `getValue` returns the value that `value` and `onValueChange` use. `getLabel` returns the text shown in the input and used for filtering. Pass a function as the child of `Combobox.Content` to render each item that matches the input. In TypeScript, set the item type on that function, for example `(user: User) => ...`. Create the collection at module scope for static data. For data that changes, wrap it in `React.useMemo` keyed by the data. +With `multiple`, also pass `itemToStringLabel`, because chips do not read `getLabel`. + ## API Reference diff --git a/apps/www/src/content/docs/components/combobox/props.ts b/apps/www/src/content/docs/components/combobox/props.ts index 5b9631f06..6908fd723 100644 --- a/apps/www/src/content/docs/components/combobox/props.ts +++ b/apps/www/src/content/docs/components/combobox/props.ts @@ -57,6 +57,9 @@ export interface ComboboxRootProps { * @remarks `unknown[] | ReturnType` */ items?: unknown[]; + + /** Converts a value to the text shown in the input and in multi-select chips. */ + itemToStringLabel?: (value: string) => string; } export interface ComboboxInputProps { diff --git a/packages/react/components/combobox/__tests__/combobox.test.tsx b/packages/react/components/combobox/__tests__/combobox.test.tsx index b60ac6e58..dc85703d2 100644 --- a/packages/react/components/combobox/__tests__/combobox.test.tsx +++ b/packages/react/components/combobox/__tests__/combobox.test.tsx @@ -230,6 +230,29 @@ describe('Combobox', () => { expect(input).toHaveAttribute('placeholder', PLACEHOLDER_TEXT); }); }); + + it('labels chips with itemToStringLabel', () => { + render( + + FRUIT_OPTIONS.find(option => option.value === value)?.label ?? value + } + /> + ); + expect(screen.getByText('Banana')).toBeInTheDocument(); + expect(screen.queryByText('banana')).not.toBeInTheDocument(); + }); + + it('labels chips with the label of { value, label } values', () => { + render( + + + + ); + expect(screen.getByText('Banana')).toBeInTheDocument(); + }); }); describe('Keyboard Navigation', () => { @@ -497,7 +520,6 @@ describe('Combobox.createItems', () => { }); const UserCombobox = (props: { - defaultValue?: string | null; onValueChange?: (value: string | null) => void; }) => ( @@ -523,11 +545,6 @@ describe('Combobox.createItems', () => { expect(onValueChange).toHaveBeenCalledWith('u2'); }); - it('shows the derived label for the selected value', () => { - render(); - expect(screen.getByRole('combobox')).toHaveValue('Ada Lovelace'); - }); - it('filters by the derived label', async () => { const user = userEvent.setup(); render(); @@ -541,31 +558,3 @@ describe('Combobox.createItems', () => { }); }); }); - -describe('Combobox.createItems chips', () => { - const USERS = [ - { id: 'u1', name: 'Jane Doe' }, - { id: 'u2', name: 'John Smith' } - ]; - const USER_ITEMS = Combobox.createItems(USERS, { - getValue: user => user.id, - getLabel: user => user.name - }); - - it('labels chips with the derived label', () => { - render( - - - - {(user: (typeof USERS)[number]) => ( - - {user.name} - - )} - - - ); - expect(screen.getByText('John Smith')).toBeInTheDocument(); - expect(screen.queryByText('u2')).not.toBeInTheDocument(); - }); -}); diff --git a/packages/react/components/combobox/combobox-items.ts b/packages/react/components/combobox/combobox-items.ts deleted file mode 100644 index d45f96123..000000000 --- a/packages/react/components/combobox/combobox-items.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Combobox as ComboboxPrimitive } from '@base-ui/react'; - -type LabelLookup = (value: unknown) => string | undefined; - -const labelLookups = new WeakMap(); - -/** - * Wraps Base UI's `createItems` and records how to label each value, so chips - * can show labels instead of raw values. - */ -export const createItems: typeof ComboboxPrimitive.createItems = ( - data, - options -) => { - const collection = ComboboxPrimitive.createItems(data, options); - labelLookups.set(collection, value => { - for (const entry of data ?? []) { - const items = - typeof entry === 'object' && entry !== null && 'items' in entry - ? entry.items - : [entry]; - for (const item of items) { - if (options.getValue(item) === value) return options.getLabel(item); - } - } - return undefined; - }); - return collection; -}; - -/** Returns the label for `value` when `items` came from `createItems`. */ -export function getItemLabel(items: unknown, value: unknown) { - if (typeof items !== 'object' || items === null) return undefined; - return labelLookups.get(items)?.(value); -} diff --git a/packages/react/components/combobox/combobox-root.tsx b/packages/react/components/combobox/combobox-root.tsx index bf252f7c5..86b50cd4f 100644 --- a/packages/react/components/combobox/combobox-root.tsx +++ b/packages/react/components/combobox/combobox-root.tsx @@ -11,7 +11,6 @@ import { useState } from 'react'; import { useFieldContext } from '../field'; -import { getItemLabel } from './combobox-items'; interface ComboboxContextValue { multiple: boolean; @@ -76,6 +75,7 @@ export const ComboboxRoot = ({ defaultValue, items, required, + itemToStringLabel, ...props }: ComboboxRootProps) => { const fieldContext = useFieldContext(); @@ -107,11 +107,11 @@ export const ComboboxRoot = ({ ) => { setInternalValue(value); if (multiple) { - ( - onValueChange as MultipleComboboxProps['onValueChange'] - )?.(value as Value[]); + (onValueChange as MultipleComboboxProps['onValueChange'])?.( + value as Value[] + ); } else { - (onValueChange as SingleComboboxProps['onValueChange'])?.( + (onValueChange as SingleComboboxProps['onValueChange'])?.( value as Value | null ); } @@ -127,9 +127,21 @@ export const ComboboxRoot = ({ inputContainerRef, value: computedValue, onValueChange: handleValueChange, - getLabel: (value: Value) => getItemLabel(items, value) ?? String(value) + getLabel: (value: Value) => { + if (itemToStringLabel) return itemToStringLabel(value); + if (typeof value === 'object' && value !== null && 'label' in value) + return String(value.label); + return String(value); + } }), - [multiple, inputValue, items, computedValue, handleValueChange] + [ + multiple, + inputValue, + items, + computedValue, + handleValueChange, + itemToStringLabel + ] ); return ( @@ -139,6 +151,7 @@ export const ComboboxRoot = ({ onValueChange={handleValueChange} onInputValueChange={handleInputValueChange} items={items} + itemToStringLabel={itemToStringLabel} value={providedValue} defaultValue={defaultValue} required={resolvedRequired} diff --git a/packages/react/components/combobox/combobox.tsx b/packages/react/components/combobox/combobox.tsx index 6b6f66ecc..d557fc42f 100644 --- a/packages/react/components/combobox/combobox.tsx +++ b/packages/react/components/combobox/combobox.tsx @@ -2,7 +2,6 @@ import { Combobox as ComboboxPrimitive } from '@base-ui/react'; import { ComboboxContent } from './combobox-content'; import { ComboboxInput } from './combobox-input'; import { ComboboxItem } from './combobox-item'; -import { createItems } from './combobox-items'; import { ComboboxGroup, ComboboxLabel, @@ -19,5 +18,5 @@ export const Combobox = Object.assign(ComboboxRoot, { Separator: ComboboxSeparator, useFilter: ComboboxPrimitive.useFilter, useFilteredItems: ComboboxPrimitive.useFilteredItems, - createItems + createItems: ComboboxPrimitive.createItems }); From 89f11478a26603f957dcef90e665397394059aea Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Thu, 15 Oct 2026 23:56:02 +0530 Subject: [PATCH 4/4] fix: [combobox] label chips in the same order as base ui --- .../combobox/__tests__/combobox.test.tsx | 9 ++++++ .../components/combobox/combobox-root.tsx | 29 ++++++++++--------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/packages/react/components/combobox/__tests__/combobox.test.tsx b/packages/react/components/combobox/__tests__/combobox.test.tsx index dc85703d2..19e28f859 100644 --- a/packages/react/components/combobox/__tests__/combobox.test.tsx +++ b/packages/react/components/combobox/__tests__/combobox.test.tsx @@ -253,6 +253,15 @@ describe('Combobox', () => { ); expect(screen.getByText('Banana')).toBeInTheDocument(); }); + + it('labels chips with the value of { value } values', () => { + render( + + + + ); + expect(screen.getByText('banana')).toBeInTheDocument(); + }); }); describe('Keyboard Navigation', () => { diff --git a/packages/react/components/combobox/combobox-root.tsx b/packages/react/components/combobox/combobox-root.tsx index 86b50cd4f..5f27a616d 100644 --- a/packages/react/components/combobox/combobox-root.tsx +++ b/packages/react/components/combobox/combobox-root.tsx @@ -119,6 +119,19 @@ export const ComboboxRoot = ({ [onValueChange, multiple] ); + // Same fallback order that Base UI uses for the input text. + const getLabel = useCallback( + (value: Value) => { + if (itemToStringLabel) return itemToStringLabel(value); + if (typeof value !== 'object' || value === null) + return String(value ?? ''); + if ('label' in value && value.label != null) return String(value.label); + if ('value' in value) return String(value.value); + return JSON.stringify(value); + }, + [itemToStringLabel] + ); + const contextValue = useMemo( () => ({ multiple, @@ -127,21 +140,9 @@ export const ComboboxRoot = ({ inputContainerRef, value: computedValue, onValueChange: handleValueChange, - getLabel: (value: Value) => { - if (itemToStringLabel) return itemToStringLabel(value); - if (typeof value === 'object' && value !== null && 'label' in value) - return String(value.label); - return String(value); - } + getLabel }), - [ - multiple, - inputValue, - items, - computedValue, - handleValueChange, - itemToStringLabel - ] + [multiple, inputValue, items, computedValue, handleValueChange, getLabel] ); return (