Skip to content

Commit 0d2f63c

Browse files
committed
[combobox] Support keyed values with object items
1 parent 1e7473e commit 0d2f63c

11 files changed

Lines changed: 859 additions & 67 deletions

File tree

docs/src/app/(docs)/react/components/combobox/page.mdx

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,18 +71,98 @@ import { Combobox } from '@base-ui/react/combobox';
7171
</Combobox.Root>;
7272
```
7373

74-
## TypeScript
74+
## Value modes
7575

76-
Combobox infers the item type from the `defaultValue` or `value` props passed to `<Combobox.Root>`.
77-
The type of items held in the `items` array must also match the `value` prop type passed to `<Combobox.Item>`.
76+
Combobox supports two value modes:
77+
78+
- **Item values**: `value` / `defaultValue` has the same shape as the entries in the `items` array.
79+
- **Keyed values**: `value` / `defaultValue` is a primitive while `items` use the `{ value, label }` object shape.
80+
81+
In the keyed mode, the combobox uses `item.label` for display and filtering, and `item.value` as the selected value. Other object shapes such as `{ id, name }` are not supported by the keyed mode; use item-shaped values instead.
82+
83+
### Item values
84+
85+
Use object-shaped values when the selected value should keep the full item data instead of a primitive key.
86+
87+
- Use `itemToStringLabel` when the object does not already have a `label` field, or when you want to override the default `label` text shown in the input or trigger.
88+
- Use `itemToStringValue` when form submission should serialize the object as a string, such as an `id`.
89+
- Use `isItemEqualToValue` when items are re-created across renders or loaded asynchronously and object identity is not stable.
90+
91+
```tsx title="Object-shaped values" {13-15} "value={user}"
92+
interface User {
93+
id: number;
94+
name: string;
95+
}
96+
97+
const users: User[] = [
98+
{ id: 1, name: 'Alice' },
99+
{ id: 2, name: 'Bob' },
100+
];
101+
102+
<Combobox.Root
103+
items={users}
104+
itemToStringLabel={(user) => user.name}
105+
itemToStringValue={(user) => String(user.id)}
106+
isItemEqualToValue={(item, value) => item.id === value.id}
107+
>
108+
<Combobox.Input />
109+
<Combobox.Portal>
110+
<Combobox.Positioner>
111+
<Combobox.Popup>
112+
<Combobox.List>
113+
{(user: User) => (
114+
<Combobox.Item key={user.id} value={user}>
115+
{user.name}
116+
</Combobox.Item>
117+
)}
118+
</Combobox.List>
119+
</Combobox.Popup>
120+
</Combobox.Positioner>
121+
</Combobox.Portal>
122+
</Combobox.Root>;
123+
```
124+
125+
### Keyed values
126+
127+
Use keyed values when `items` already have a `{ value, label }` shape and the selected value should be the primitive `item.value`.
128+
Closed-trigger typeahead is only available once a primitive keyed value has already been selected.
129+
130+
```tsx title="Keyed values" "value"2,3,5 "defaultValue"
131+
interface FruitItem {
132+
value: string;
133+
label: string;
134+
}
135+
136+
const items: FruitItem[] = [
137+
{ value: 'apple', label: 'Apple' },
138+
{ value: 'banana', label: 'Banana' },
139+
];
140+
141+
<Combobox.Root items={items} defaultValue="banana">
142+
<Combobox.Input />
143+
<Combobox.Portal>
144+
<Combobox.Positioner>
145+
<Combobox.Popup>
146+
<Combobox.List>
147+
{(item) => (
148+
<Combobox.Item key={item.value} value={item.value}>
149+
{item.label}
150+
</Combobox.Item>
151+
)}
152+
</Combobox.List>
153+
</Combobox.Popup>
154+
</Combobox.Positioner>
155+
</Combobox.Portal>
156+
</Combobox.Root>;
157+
```
78158

79159
## Examples
80160

81161
### Typed wrapper component
82162

83-
The following example shows a typed wrapper around the Combobox component with correct type inference and type safety:
163+
Use a two-generic wrapper when the selected value has the same shape as the `items` entries:
84164

85-
```tsx title="Specifying generic type parameters"
165+
```tsx title="Wrapper for item-shaped values"
86166
import * as React from 'react';
87167
import { Combobox } from '@base-ui/react/combobox';
88168

@@ -93,6 +173,8 @@ export function MyCombobox<Value, Multiple extends boolean | undefined = false>(
93173
}
94174
```
95175

176+
If the wrapper should also support keyed `{ value, label }` items, forward the third `Items` generic as well.
177+
96178
### Multiple select
97179

98180
The combobox can allow multiple selections by adding the `multiple` prop to `<Combobox.Root>`.

docs/src/app/(docs)/react/components/combobox/types.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ Doesn't render its own HTML element.
1414
| Prop | Type | Default | Description |
1515
| :------------------- | :------------------------------------------------------------------------------------------------------ | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1616
| name | `string` | - | Identifies the field when a form is submitted. |
17-
| defaultValue | `Value[] \| Value \| null` | - | The uncontrolled selected value of the combobox when it's initially rendered. To render a controlled combobox, use the `value` prop instead. |
18-
| value | `Value[] \| Value \| null` | - | The selected value of the combobox. Use when controlled. |
17+
| defaultValue | `Value[] \| Value \| null` | - | The uncontrolled selected value of the combobox when it's initially rendered.&#xA;Must match the item value shape, except when `items` use the `{ value, label }` shape, in which case a primitive `item.value` is also supported. To render a controlled combobox, use the `value` prop instead. |
18+
| value | `Value[] \| Value \| null` | - | The selected value of the combobox. Use when controlled.&#xA;Must match the item value shape, except when `items` use the `{ value, label }` shape, in which case a primitive `item.value` is also supported. |
1919
| onValueChange | `((value: Value[] \| Value \| null, eventDetails: Combobox.Root.ChangeEventDetails) => void)` | - | Event handler called when the selected value of the combobox changes. |
2020
| defaultInputValue | `string \| number \| string[]` | - | The uncontrolled input value when initially rendered. To render a controlled input, use the `inputValue` prop instead. |
2121
| inputValue | `string \| string[] \| number` | - | The input value of the combobox. Use when controlled. |
@@ -33,9 +33,9 @@ Doesn't render its own HTML element.
3333
| grid | `boolean` | `false` | Whether list items are presented in a grid layout.&#xA;When enabled, arrow keys navigate across rows and columns inferred from DOM rows. |
3434
| inline | `boolean` | `false` | Whether the list is rendered inline without using the popup. |
3535
| isItemEqualToValue | `((itemValue: Value, value: Value) => boolean)` | - | Custom comparison logic used to determine if a combobox item value matches the current selected value. Useful when item values are objects without matching referentially.&#xA;Defaults to `Object.is` comparison. |
36-
| itemToStringLabel | `((itemValue: Value) => string)` | - | When the item values are objects (`<Combobox.Item value={object}>`), this function converts the object value to a string representation for display in the input.&#xA;If the shape of the object is `{ value, label }`, the label will be used automatically without needing to specify this prop. |
36+
| itemToStringLabel | `((itemValue: Value) => string)` | - | When the item values are objects (`<Combobox.Item value={object}>`), this function converts the object value to a string representation for display in the input.&#xA;If the shape of the object is `{ value, label }`, the label will be used automatically for display and filtering without needing to specify this prop.&#xA;In that keyed mode, this prop is bypassed for the `{ value, label }` item array itself and only applies as a fallback when resolving other value shapes. |
3737
| itemToStringValue | `((itemValue: Value) => string)` | - | When the item values are objects (`<Combobox.Item value={object}>`), this function converts the object value to a string representation for form submission.&#xA;If the shape of the object is `{ value, label }`, the value will be used automatically without needing to specify this prop. |
38-
| items | `any[] \| Group[]` | - | The items to be displayed in the list.&#xA;Can be either a flat array of items or an array of groups with items. |
38+
| items | `any[] \| Group[]` | - | The items to be displayed in the list.&#xA;Can be either a flat array of items or an array of groups with items.&#xA;Primitive `value`/`defaultValue` is only supported implicitly when each item has the shape `{ value, label }`. |
3939
| limit | `number` | `-1` | The maximum number of items to display in the list. |
4040
| locale | `Intl.LocalesArgument` | - | The locale to use for string comparison.&#xA;Defaults to the user's runtime locale. |
4141
| loopFocus | `boolean` | `true` | Whether to loop keyboard focus back to the input when the end of the list is reached while using the arrow keys. The first item can then be reached by pressing ArrowDown again from the input, or the last item can be reached by pressing ArrowUp from the input.&#xA;The input is always included in the focus loop per [ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/).&#xA;When disabled, focus does not move when on the last element and the user presses ArrowDown, or when on the first element and the user presses ArrowUp. |

0 commit comments

Comments
 (0)