Skip to content

Commit efe70d4

Browse files
committed
[combobox] Infer labels only for primitive projected values
1 parent 36e4a50 commit efe70d4

5 files changed

Lines changed: 11 additions & 24 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ To select a primitive field such as an ID, `itemToValue` is required:
8585

8686
Render functions still receive full objects, while selection state and callbacks use the primitive key. When rendering items from `<Combobox.List>` or `<Combobox.Collection>`, omit `<Combobox.Item value>`. For manually rendered virtualized items, provide `index`.
8787

88+
Labels are inferred automatically for primitive keys. If `itemToValue` returns an object, provide `itemToStringLabel` to display its label in the input.
89+
8890
## Examples
8991

9092
### Typed wrapper component

packages/react/src/combobox/root/AriaCombobox.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,10 +276,9 @@ export function AriaCombobox<Value = any, Mode extends SelectionMode = 'none'>(
276276
value,
277277
labelItems as Parameters<typeof resolveLabelString>[1],
278278
itemToStringLabel,
279-
isItemEqualToValue,
280279
)
281280
: stringifyAsLabel(value, itemToStringLabel),
282-
[getItemValue, labelItems, itemToStringLabel, isItemEqualToValue],
281+
[getItemValue, labelItems, itemToStringLabel],
283282
);
284283

285284
// If neither inputValue nor defaultInputValue are provided, derive it from the

packages/react/src/combobox/root/ComboboxRoot.test.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3718,13 +3718,17 @@ describe('<Combobox.Root />', () => {
37183718
{ value: { code: 'us' }, label: 'United States' },
37193719
{ value: { code: 'ca' }, label: 'Canada' },
37203720
];
3721+
const labels: Record<string, string> = { us: 'United States', ca: 'Canada' };
37213722

37223723
const { user } = await render(
37233724
<Combobox.Root
37243725
items={items}
37253726
itemToValue={(item) => item.value}
37263727
defaultValue={{ code: 'ca' }}
37273728
isItemEqualToValue={(a: { code: string }, b: { code: string }) => a.code === b.code}
3729+
// Object projections resolve their text through `itemToStringLabel`; automatic
3730+
// label inference only matches primitive projected values.
3731+
itemToStringLabel={(value: { code: string }) => labels[value.code]}
37283732
>
37293733
<Combobox.Input data-testid="input" />
37303734
<span data-testid="value">

packages/react/src/combobox/value/ComboboxValue.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,6 @@ export function ComboboxValue(props: ComboboxValue.Props): React.ReactElement {
1717
const store = useComboboxRootContext();
1818

1919
const itemToStringLabel = useStore(store, selectors.itemToStringLabel);
20-
const hasItemToValue = useStore(store, selectors.hasItemToValue);
21-
const isItemEqualToValue = useStore(store, selectors.isItemEqualToValue);
22-
// With `itemToValue`, the store `items` hold projected values, which may be objects that
23-
// need the custom comparer to match. Without it, the resolvers' own matching is correct.
24-
const equality = hasItemToValue ? isItemEqualToValue : undefined;
2520
const selectedValue = useStore(store, selectors.selectedValue);
2621
const items = useStore(store, selectors.items);
2722
const multiple = useStore(store, selectors.selectionMode) === 'multiple';
@@ -38,9 +33,9 @@ export function ComboboxValue(props: ComboboxValue.Props): React.ReactElement {
3833
} else if (!hasSelectedValue && placeholder != null && !hasNullLabel) {
3934
children = placeholder;
4035
} else if (multiple && Array.isArray(selectedValue)) {
41-
children = resolveMultipleLabels(selectedValue, items, itemToStringLabel, equality);
36+
children = resolveMultipleLabels(selectedValue, items, itemToStringLabel);
4237
} else {
43-
children = resolveSelectedLabel(selectedValue, items, itemToStringLabel, equality);
38+
children = resolveSelectedLabel(selectedValue, items, itemToStringLabel);
4439
}
4540

4641
return <React.Fragment>{children}</React.Fragment>;

packages/react/src/internals/resolveValueLabel.tsx

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
'use client';
22
import * as React from 'react';
33
import { serializeValue } from './serializeValue';
4-
import { compareItemEquality, type ItemEqualityComparer } from './itemEquality';
54

65
type ItemRecord = Record<string, React.ReactNode>;
76
type ItemsInput = ItemRecord | ReadonlyArray<LabeledItem> | ReadonlyArray<Group<any>> | undefined;
@@ -91,7 +90,6 @@ export function resolveSelectedLabel(
9190
value: any,
9291
items: ItemsInput,
9392
itemToStringLabel?: (item: any) => string,
94-
isItemEqualToValue?: ItemEqualityComparer,
9593
): React.ReactNode {
9694
function fallback() {
9795
return stringifyAsLabel(value, itemToStringLabel);
@@ -118,15 +116,6 @@ export function resolveSelectedLabel(
118116
? arrayItems.flatMap((group) => group.items)
119117
: arrayItems;
120118

121-
if (isItemEqualToValue) {
122-
const match = flatItems.find(
123-
(item) => item && compareItemEquality(item.value, value, isItemEqualToValue),
124-
);
125-
if (match && match.label != null) {
126-
return match.label;
127-
}
128-
}
129-
130119
if (value == null || typeof value !== 'object') {
131120
const match = flatItems.find((item) => item.value === value);
132121
if (match && match.label != null) {
@@ -156,9 +145,8 @@ export function resolveLabelString(
156145
value: any,
157146
items: ItemsInput,
158147
itemToStringLabel?: (item: any) => string,
159-
isItemEqualToValue?: ItemEqualityComparer,
160148
): string {
161-
const label = resolveSelectedLabel(value, items, itemToStringLabel, isItemEqualToValue);
149+
const label = resolveSelectedLabel(value, items, itemToStringLabel);
162150

163151
if (typeof label === 'string' || typeof label === 'number') {
164152
return String(label);
@@ -173,15 +161,14 @@ export function resolveMultipleLabels(
173161
values: any[],
174162
items: ItemsInput,
175163
itemToStringLabel?: (item: any) => string,
176-
isItemEqualToValue?: ItemEqualityComparer,
177164
): React.ReactNode {
178165
return values.reduce((acc, value, index) => {
179166
if (index > 0) {
180167
acc.push(', ');
181168
}
182169
acc.push(
183170
<React.Fragment key={index}>
184-
{resolveSelectedLabel(value, items, itemToStringLabel, isItemEqualToValue)}
171+
{resolveSelectedLabel(value, items, itemToStringLabel)}
185172
</React.Fragment>,
186173
);
187174
return acc;

0 commit comments

Comments
 (0)