@@ -176,6 +176,12 @@ export function AriaCombobox<Value = any, Mode extends SelectionMode = 'none'>(
176176 const single = selectionMode === 'single' ;
177177 const hasInputValue = inputValueProp !== undefined || defaultInputValueProp !== undefined ;
178178 const hasItems = items !== undefined ;
179+
180+ // Resolving a primitive selected value to its `{ value, label }` item's label is only
181+ // supported for non-virtualized lists (virtualized selection sync matches whole items),
182+ // so virtualized lists resolve labels from the value's own shape to keep the displayed
183+ // label consistent with what can actually be highlighted.
184+ const labelItems = virtualized ? undefined : items ;
179185 const hasFilteredItemsProp = filteredItemsProp !== undefined ;
180186
181187 let autoHighlightMode : false | 'input-change' | 'always' ;
@@ -213,7 +219,7 @@ export function AriaCombobox<Value = any, Mode extends SelectionMode = 'none'>(
213219 return defaultInputValueProp ?? '' ;
214220 }
215221 if ( single ) {
216- return resolveLabelString ( selectedValue , items , itemToStringLabel , isItemEqualToValue ) ;
222+ return resolveLabelString ( selectedValue , labelItems , itemToStringLabel , isItemEqualToValue ) ;
217223 }
218224 return '' ;
219225 } ,
@@ -240,8 +246,10 @@ export function AriaCombobox<Value = any, Mode extends SelectionMode = 'none'>(
240246 // render (e.g. each keystroke) when the selection and items are unchanged.
241247 const selectedLabelString = React . useMemo (
242248 ( ) =>
243- single ? resolveLabelString ( selectedValue , items , itemToStringLabel , isItemEqualToValue ) : '' ,
244- [ single , selectedValue , items , itemToStringLabel , isItemEqualToValue ] ,
249+ single
250+ ? resolveLabelString ( selectedValue , labelItems , itemToStringLabel , isItemEqualToValue )
251+ : '' ,
252+ [ single , selectedValue , labelItems , itemToStringLabel , isItemEqualToValue ] ,
245253 ) ;
246254
247255 const shouldBypassFiltering =
@@ -651,7 +659,7 @@ export function AriaCombobox<Value = any, Mode extends SelectionMode = 'none'>(
651659
652660 if ( shouldFillInput ) {
653661 setInputValue (
654- resolveLabelString ( nextValue , items , itemToStringLabel , isItemEqualToValue ) ,
662+ resolveLabelString ( nextValue , labelItems , itemToStringLabel , isItemEqualToValue ) ,
655663 createChangeEventDetails ( eventDetails . reason , eventDetails . event ) ,
656664 ) ;
657665 }
@@ -1335,27 +1343,38 @@ export function AriaCombobox<Value = any, Mode extends SelectionMode = 'none'>(
13351343 const candidates = items ? flatFilteredItems : valuesRef . current ;
13361344 const matchingIndex = candidates . findIndex ( ( candidate , index ) => {
13371345 const renderedValue = valuesRef . current [ index ] ;
1346+ // The exact value passed to `<Combobox.Item value>`, falling back to the
1347+ // item itself (virtualized) or its inferred value (non-virtualized) for
1348+ // slots not registered by a mounted item. User stringifiers expect this
1349+ // shape, so resolve it before calling them — never the outer
1350+ // `{ value, label }` item.
1351+ let resolvedValue = renderedValue ;
1352+ if ( resolvedValue === undefined ) {
1353+ resolvedValue = virtualized ? candidate : inferItemValue ( candidate ) ;
1354+ }
13381355
13391356 // Try matching by value first (e.g., "US" for country code)
1340- const candidateValue = stringifyAsValue ( candidate , itemToStringValue ) ;
1357+ const candidateValue = stringifyAsValue ( resolvedValue , itemToStringValue ) ;
13411358 if ( candidateValue . toLowerCase ( ) === nextValue . toLowerCase ( ) ) {
13421359 return true ;
13431360 }
13441361 // Also try matching by label for browser autofill compatibility
13451362 // (browsers autofill with displayed text like "United States", not the underlying value)
1346- const candidateLabel = stringifyAsLabel ( candidate , itemToStringLabel ) ;
1363+ const candidateLabel = stringifyAsLabel ( resolvedValue , itemToStringLabel ) ;
13471364 if ( candidateLabel . toLowerCase ( ) === nextValue . toLowerCase ( ) ) {
13481365 return true ;
13491366 }
13501367
1351- if ( renderedValue !== undefined && renderedValue !== candidate ) {
1352- const renderedValueString = stringifyAsValue ( renderedValue , itemToStringValue ) ;
1353- if ( renderedValueString . toLowerCase ( ) === nextValue . toLowerCase ( ) ) {
1368+ // When the rendered value differs from the `{ value, label }` item
1369+ // (primitive inference), also match against the item's own value/label so
1370+ // autofill by displayed label still works — using the callback-free
1371+ // stringifiers, which read `.value`/`.label` without handing the outer item
1372+ // to a user stringifier written for the rendered value.
1373+ if ( resolvedValue !== candidate ) {
1374+ if ( stringifyAsValue ( candidate ) . toLowerCase ( ) === nextValue . toLowerCase ( ) ) {
13541375 return true ;
13551376 }
1356-
1357- const renderedLabel = stringifyAsLabel ( renderedValue , itemToStringLabel ) ;
1358- if ( renderedLabel . toLowerCase ( ) === nextValue . toLowerCase ( ) ) {
1377+ if ( stringifyAsLabel ( candidate ) . toLowerCase ( ) === nextValue . toLowerCase ( ) ) {
13591378 return true ;
13601379 }
13611380 }
@@ -1568,8 +1587,10 @@ interface ComboboxRootProps<ItemValue> {
15681587 * The items to be displayed in the list.
15691588 * Can be either a flat array of items or an array of groups with items.
15701589 * In non-virtualized lists, when items use the `{ value, label }` shape, a primitive
1571- * `<Combobox.Item value>` resolves its text label from the matching item, and
1572- * `<Combobox.Value>` and the input render the label instead of the raw value.
1590+ * `<Combobox.Item value>` resolves its text label from the matching item. In single
1591+ * selection mode `<Combobox.Value>` and the input render that label instead of the raw
1592+ * value; in multiple selection mode the input stays the query field, so only
1593+ * `<Combobox.Value>` renders the selected labels.
15731594 * Virtualized lists do not support this primitive value inference.
15741595 */
15751596 items ?: readonly any [ ] | readonly Group < any > [ ] | undefined ;
0 commit comments