@jobber/components 9.5.3 → 9.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/Combobox.d.ts +1 -0
  2. package/Combobox.js +17 -0
  3. package/dist/Autocomplete/index.cjs +0 -2
  4. package/dist/Autocomplete/index.mjs +0 -2
  5. package/dist/Autocomplete-cjs.js +4 -5
  6. package/dist/Autocomplete-es.js +1 -2
  7. package/dist/Card/index.cjs +2 -2
  8. package/dist/Card/index.mjs +2 -2
  9. package/dist/Chip/index.cjs +0 -2
  10. package/dist/Chip/index.mjs +0 -2
  11. package/dist/Chips/InternalChipDismissible/hooks/index.cjs +0 -2
  12. package/dist/Chips/InternalChipDismissible/hooks/index.mjs +0 -2
  13. package/dist/Chips/InternalChipDismissible/index.cjs +0 -2
  14. package/dist/Chips/InternalChipDismissible/index.mjs +0 -2
  15. package/dist/Chips/index.cjs +0 -2
  16. package/dist/Chips/index.mjs +0 -2
  17. package/dist/Combobox/Combobox.contentSlots.d.ts +5 -0
  18. package/dist/Combobox/Combobox.context.d.ts +34 -0
  19. package/dist/Combobox/Combobox.d.ts +69 -0
  20. package/dist/Combobox/Combobox.footer.d.ts +7 -0
  21. package/dist/Combobox/Combobox.items.d.ts +25 -0
  22. package/dist/Combobox/Combobox.responsive.d.ts +54 -0
  23. package/dist/Combobox/Combobox.selectAll.d.ts +4 -0
  24. package/dist/Combobox/Combobox.slotFactory.d.ts +7 -0
  25. package/dist/Combobox/Combobox.testUtils.d.ts +12 -0
  26. package/dist/Combobox/Combobox.types.d.ts +166 -0
  27. package/dist/Combobox/index.cjs +35 -0
  28. package/dist/Combobox/index.d.ts +2 -0
  29. package/dist/Combobox/index.mjs +29 -0
  30. package/dist/Combobox-cjs.js +675 -0
  31. package/dist/Combobox-es.js +673 -0
  32. package/dist/ComboboxPrimitive-cjs.js +88 -47
  33. package/dist/ComboboxPrimitive-es.js +39 -23
  34. package/dist/{ComboboxChipRemove-cjs.js → ComboboxSeparator-cjs.js} +501 -455
  35. package/dist/{ComboboxChipRemove-es.js → ComboboxSeparator-es.js} +500 -456
  36. package/dist/ConfirmationModal/index.cjs +0 -2
  37. package/dist/ConfirmationModal/index.mjs +0 -2
  38. package/dist/ContentBlock/index.cjs +1 -0
  39. package/dist/ContentBlock/index.mjs +1 -0
  40. package/dist/ContentBlock-cjs.js +2 -18
  41. package/dist/ContentBlock-es.js +1 -17
  42. package/dist/DataDump/index.cjs +2 -2
  43. package/dist/DataDump/index.mjs +2 -2
  44. package/dist/DataList/components/DataListActions/index.cjs +0 -2
  45. package/dist/DataList/components/DataListActions/index.mjs +0 -2
  46. package/dist/DataList/components/DataListBulkActions/index.cjs +0 -2
  47. package/dist/DataList/components/DataListBulkActions/index.mjs +0 -2
  48. package/dist/DataList/components/DataListFilters/components/DataListSort/index.cjs +0 -2
  49. package/dist/DataList/components/DataListFilters/components/DataListSort/index.mjs +0 -2
  50. package/dist/DataList/components/DataListFilters/index.cjs +0 -2
  51. package/dist/DataList/components/DataListFilters/index.mjs +0 -2
  52. package/dist/DataList/components/DataListHeader/index.cjs +0 -2
  53. package/dist/DataList/components/DataListHeader/index.mjs +0 -2
  54. package/dist/DataList/components/DataListItem/index.cjs +0 -2
  55. package/dist/DataList/components/DataListItem/index.mjs +0 -2
  56. package/dist/DataList/components/DataListItemActions/index.cjs +0 -2
  57. package/dist/DataList/components/DataListItemActions/index.mjs +0 -2
  58. package/dist/DataList/components/DataListItemActionsOverflow/index.cjs +0 -2
  59. package/dist/DataList/components/DataListItemActionsOverflow/index.mjs +0 -2
  60. package/dist/DataList/components/DataListItems/index.cjs +0 -2
  61. package/dist/DataList/components/DataListItems/index.mjs +0 -2
  62. package/dist/DataList/components/DataListLayout/index.cjs +0 -2
  63. package/dist/DataList/components/DataListLayout/index.mjs +0 -2
  64. package/dist/DataList/components/DataListLayoutActions/index.cjs +0 -2
  65. package/dist/DataList/components/DataListLayoutActions/index.mjs +0 -2
  66. package/dist/DataList/index.cjs +0 -2
  67. package/dist/DataList/index.mjs +0 -2
  68. package/dist/DatePicker/index.cjs +0 -2
  69. package/dist/DatePicker/index.mjs +0 -2
  70. package/dist/DatePicker-cjs.js +4 -5
  71. package/dist/DatePicker-es.js +1 -2
  72. package/dist/Dialog/index.cjs +2 -2
  73. package/dist/Dialog/index.mjs +2 -2
  74. package/dist/DrawerDescription-cjs.js +268 -99
  75. package/dist/DrawerDescription-es.js +270 -103
  76. package/dist/DrawerTitle-cjs.js +2 -6
  77. package/dist/DrawerTitle-es.js +3 -7
  78. package/dist/FieldDescription-cjs.js +184 -181
  79. package/dist/FieldDescription-es.js +187 -184
  80. package/dist/FilterPicker/components/FilterPickerActivator/index.cjs +0 -2
  81. package/dist/FilterPicker/components/FilterPickerActivator/index.mjs +0 -2
  82. package/dist/FilterPicker/components/FilterPickerContent/index.cjs +0 -2
  83. package/dist/FilterPicker/components/FilterPickerContent/index.mjs +0 -2
  84. package/dist/FilterPicker/components/FilterPickerTrigger/index.cjs +0 -2
  85. package/dist/FilterPicker/components/FilterPickerTrigger/index.mjs +0 -2
  86. package/dist/FilterPicker/index.cjs +0 -2
  87. package/dist/FilterPicker/index.mjs +0 -2
  88. package/dist/FilterPickerContent-cjs.js +4 -5
  89. package/dist/FilterPickerContent-es.js +1 -2
  90. package/dist/FormatFile/index.cjs +0 -2
  91. package/dist/FormatFile/index.mjs +0 -2
  92. package/dist/Gallery/index.cjs +0 -2
  93. package/dist/Gallery/index.mjs +0 -2
  94. package/dist/InputDate/index.cjs +0 -2
  95. package/dist/InputDate/index.mjs +0 -2
  96. package/dist/InputNumber/index.cjs +2 -2
  97. package/dist/InputNumber/index.mjs +2 -2
  98. package/dist/InputNumber/types.d.ts +5 -0
  99. package/dist/InputNumber-cjs.js +2 -2
  100. package/dist/InputNumber-es.js +2 -2
  101. package/dist/LightBox/index.cjs +0 -2
  102. package/dist/LightBox/index.mjs +0 -2
  103. package/dist/Menu/index.cjs +2 -2
  104. package/dist/Menu/index.mjs +2 -2
  105. package/dist/Menu-cjs.js +4 -5
  106. package/dist/Menu-es.js +1 -2
  107. package/dist/MenuSubmenuTrigger-cjs.js +206 -164
  108. package/dist/MenuSubmenuTrigger-es.js +194 -152
  109. package/dist/Modal/index.cjs +0 -2
  110. package/dist/Modal/index.mjs +1 -3
  111. package/dist/NumberFieldInput-cjs.js +187 -308
  112. package/dist/NumberFieldInput-es.js +187 -308
  113. package/dist/Page/index.cjs +2 -2
  114. package/dist/Page/index.mjs +2 -2
  115. package/dist/Popover/index.cjs +0 -2
  116. package/dist/Popover/index.mjs +0 -2
  117. package/dist/Popover-cjs.js +7 -8
  118. package/dist/Popover-es.js +1 -2
  119. package/dist/ScrollAreaViewport-cjs.js +747 -1093
  120. package/dist/ScrollAreaViewport-es.js +734 -1081
  121. package/dist/Select/index.cjs +2 -2
  122. package/dist/Select/index.mjs +2 -2
  123. package/dist/SelectGroupLabel-cjs.js +207 -306
  124. package/dist/SelectGroupLabel-es.js +203 -302
  125. package/dist/SelectPrimitive-cjs.js +11 -17
  126. package/dist/SelectPrimitive-es.js +1 -7
  127. package/dist/Tooltip/index.cjs +0 -2
  128. package/dist/Tooltip/index.mjs +0 -2
  129. package/dist/Tooltip-cjs.js +6 -7
  130. package/dist/Tooltip-es.js +1 -2
  131. package/dist/buttonRenderAdapter-cjs.js +2 -1
  132. package/dist/buttonRenderAdapter-es.js +2 -1
  133. package/dist/dialogReturnFocus-cjs.js +1 -1
  134. package/dist/dialogReturnFocus-es.js +2 -2
  135. package/dist/docs/InputNumber/InputNumber.md +2 -2
  136. package/dist/docs/SelectPrimitive/SelectPrimitive.md +1 -1
  137. package/dist/floating-ui.react-cjs.js +2557 -185
  138. package/dist/floating-ui.react-es.js +2512 -149
  139. package/dist/getMappedBreakpointWidth-cjs.js +21 -0
  140. package/dist/getMappedBreakpointWidth-es.js +18 -0
  141. package/dist/index.cjs +15 -10
  142. package/dist/index.d.mts +1 -0
  143. package/dist/index.d.ts +1 -0
  144. package/dist/index.mjs +14 -10
  145. package/dist/mergeClassName-cjs.js +12 -0
  146. package/dist/mergeClassName-es.js +10 -0
  147. package/dist/primitives/BottomSheet/index.cjs +1 -1
  148. package/dist/primitives/BottomSheet/index.mjs +1 -1
  149. package/dist/primitives/ComboboxPrimitive/ComboboxPrimitive.d.ts +2 -2
  150. package/dist/primitives/ComboboxPrimitive/index.cjs +4 -5
  151. package/dist/primitives/ComboboxPrimitive/index.mjs +4 -5
  152. package/dist/primitives/SelectPrimitive/index.cjs +1 -2
  153. package/dist/primitives/SelectPrimitive/index.mjs +1 -2
  154. package/dist/primitives/index.cjs +3 -3
  155. package/dist/primitives/index.mjs +3 -3
  156. package/dist/scrollEdges-cjs.js +1 -11
  157. package/dist/scrollEdges-es.js +1 -11
  158. package/dist/scrollable-cjs.js +58 -0
  159. package/dist/scrollable-es.js +54 -0
  160. package/dist/styles.css +759 -42
  161. package/dist/unstyledPrimitives/index.cjs +720 -478
  162. package/dist/unstyledPrimitives/index.mjs +663 -421
  163. package/dist/useCompositeListItem-cjs.js +2108 -361
  164. package/dist/useCompositeListItem-es.js +2076 -331
  165. package/dist/useLabel-cjs.js +2 -3
  166. package/dist/useLabel-es.js +2 -3
  167. package/dist/useRenderElement-cjs.js +9 -7
  168. package/dist/useRenderElement-es.js +9 -7
  169. package/dist/useScrollLock-cjs.js +819 -344
  170. package/dist/useScrollLock-es.js +732 -296
  171. package/dist/useScrollToActive-cjs.js +4 -5
  172. package/dist/useScrollToActive-es.js +1 -2
  173. package/dist/utils/meta/meta.json +43 -1
  174. package/package.json +8 -3
  175. package/dist/floating-ui.react-dom-cjs.js +0 -2103
  176. package/dist/floating-ui.react-dom-es.js +0 -2072
  177. package/dist/floating-ui.utils-cjs.js +0 -342
  178. package/dist/floating-ui.utils-es.js +0 -300
@@ -1,12 +1,12 @@
1
1
  import * as React from 'react';
2
- import { bi as getGridNavigatedIndex, ag as isIndexOutOfListBounds, N as createSelector, a4 as useDirection, n as useControlled, V as Store, b as useStableCallback, P as useStore, q as useValueAsRef, m as useTransitionStatus, O as useOpenInteractionType, a as useIsoLayoutEffect, E as createGenericEventDetails, z as inputClear, y as inputChange, d as createChangeEventDetails, W as focusOut, X as outsidePress, g as getTarget, a9 as itemPress, f as useOpenChangeComplete, K as useValueChanged, v as none, Q as useClick, bj as inputPress, R as useDismiss, _ as contains, F as FOCUSABLE_POPUP_PROPS, U as useOnFirstRender, w as visuallyHiddenInput, x as visuallyHidden, Y as pressableTriggerOpenStateMapping, k as useTimeout, c as useButton, o as ownerDocument, L as stopEvent, aj as listNavigation, e as closePress, u as useBaseUiId, bd as android, bk as gecko, aF as escapeKey, aG as isInteractiveElement, bl as clearPress, t as transitionStatusMapping, b8 as triggerOpenStateMapping, r as ios, a0 as FloatingPortal, p as popupStateMapping, a1 as InternalBackdrop, a2 as inertValue, a3 as DROPDOWN_COLLISION_AVOIDANCE, i as FloatingFocusManager, S as SafeReact, l as error, bm as chipRemovePress } from './useScrollLock-es.js';
2
+ import { bU as getGridNavigatedIndex, aO as isIndexOutOfListBounds, a7 as useDirection, v as useControlled, bV as Store, d as useStableCallback, U as useStore, w as useValueAsRef, s as useTransitionStatus, R as useOpenInteractionType, G as createGenericEventDetails, z as none, M as inputClear, O as inputChange, aE as getOverflowAncestors, i as isHTMLElement, a0 as contains, bu as escapeKey, h as createChangeEventDetails, Y as focusOut, Z as outsidePress, e as getTarget, ad as itemPress, l as useOpenChangeComplete, c as useIsoLayoutEffect, L as useValueChanged, V as useClick, bW as inputPress, W as useDismiss, F as FOCUSABLE_POPUP_PROPS, B as visuallyHiddenInput, C as visuallyHidden, P as keyboard, bX as pointer, _ as pressableTriggerOpenStateMapping, q as useTimeout, f as useButton, o as ownerDocument, aR as stopEvent, aS as listNavigation, a1 as cancelOpen, j as closePress, u as useBaseUiId, bg as android, bY as gecko, au as isElement, bv as isInteractiveElement, bZ as clearPress, t as transitionStatusMapping, be as triggerOpenStateMapping, x as ios, a2 as FloatingPortal, a6 as popupStateMapping, a3 as InternalBackdrop, a4 as inertValue, a5 as DROPDOWN_COLLISION_AVOIDANCE, n as FloatingFocusManager, S as SafeReact, r as error, b_ as chipRemovePress } from './useScrollLock-es.js';
3
3
  import { f as formatErrorMessage, c as EMPTY_ARRAY, a as useRefWithInit, E as EMPTY_OBJECT, N as NOOP, m as mergeProps, b as useMergedRefs, u as useRenderElement } from './useRenderElement-es.js';
4
- import { h as hasNullItemLabel, e as compareItemEquality, m as stringifyAsLabel, a as useFormContext, b as useFieldRootContext, c as useLabelableId, E as isGroupedItems, s as stringifyAsValue, d as useRegisterFieldControl, x as selectedValueIncludes, A as removeItem, g as findItemIndex, i as useFloatingRootContext, j as useListNavigation, l as defaultItemEquality, n as areArraysEqual, f as fieldValidityMapping, u as useLabelableContext, k as useTypeahead, r as resolveAriaLabelledBy, o as getPseudoElementBounds, F as FieldRootContext, G as DEFAULT_FIELD_ROOT_CONTEXT, H as DEFAULT_FIELD_STATE_ATTRIBUTES, C as CompositeList, v as useAnchorPositioning, t as useAnchoredPopupScrollLock, w as usePositioner, y as getDisabledMountTransitionStyles, z as useCompositeListItem, I as IndexGuessBehavior, J as getDefaultLabelId, p as resolveMultipleLabels, q as resolveSelectedLabel } from './useCompositeListItem-es.js';
4
+ import { h as hasNullItemLabel, e as compareItemEquality, n as stringifyAsLabel, a as useFormContext, b as useFieldRootContext, c as useLabelableId, E as isGroupedItems, G as findSelectionIndex, s as stringifyAsValue, d as useRegisterFieldControl, H as selectedValueIncludes, B as removeItem, i as useFloatingRootContext, j as useListNavigation, l as useOnFirstRender, m as defaultItemEquality, o as areArraysEqual, f as fieldValidityMapping, u as useLabelableContext, k as useTypeahead, r as resolveAriaLabelledBy, p as isMouseWithinBounds, F as FieldRootContext, I as DEFAULT_FIELD_ROOT_CONTEXT, J as DEFAULT_FIELD_STATE_ATTRIBUTES, C as CompositeList, w as useAnchorPositioning, v as useAnchoredPopupScrollLock, x as usePositioner, z as getDisabledMountTransitionStyles, A as useCompositeListItem, g as findItemIndex, K as getDefaultLabelId, q as resolveMultipleLabels, t as resolveSelectedLabel } from './useCompositeListItem-es.js';
5
5
  import { jsx, jsxs } from 'react/jsx-runtime';
6
6
  import * as ReactDOM from 'react-dom';
7
7
  import { s as stringifyLocale } from './stringifyLocale-es.js';
8
+ import { i as isScrollableY } from './scrollable-es.js';
8
9
  import { u as useLabel } from './useLabel-es.js';
9
- import { b as isElement } from './floating-ui.utils-es.js';
10
10
 
11
11
  /**
12
12
  * Positional arguments are deliberate: property names of an options object
@@ -82,15 +82,15 @@ function useComboboxHasItemsContext() {
82
82
  }
83
83
 
84
84
  const selectors = {
85
- id: createSelector(state => state.id),
86
- labelId: createSelector(state => state.labelId),
87
- items: createSelector(state => state.items),
88
- selectedValue: createSelector(state => state.selectedValue),
89
- hasSelectionChips: createSelector(state => {
85
+ id: state => state.id,
86
+ labelId: state => state.labelId,
87
+ items: state => state.items,
88
+ selectedValue: state => state.selectedValue,
89
+ hasSelectionChips: state => {
90
90
  const selectedValue = state.selectedValue;
91
91
  return Array.isArray(selectedValue) && selectedValue.length > 0;
92
- }),
93
- hasSelectedValue: createSelector(state => {
92
+ },
93
+ hasSelectedValue: state => {
94
94
  const {
95
95
  selectedValue,
96
96
  selectionMode
@@ -102,53 +102,53 @@ const selectors = {
102
102
  return selectedValue.length > 0;
103
103
  }
104
104
  return true;
105
- }),
106
- hasNullItemLabel: createSelector((state, enabled) => {
105
+ },
106
+ hasNullItemLabel: (state, enabled) => {
107
107
  return enabled ? hasNullItemLabel(state.items) : false;
108
- }),
109
- open: createSelector(state => state.open),
110
- mounted: createSelector(state => state.mounted),
111
- forceMounted: createSelector(state => state.forceMounted),
112
- inline: createSelector(state => state.inline),
113
- activeIndex: createSelector(state => state.activeIndex),
114
- selectedIndex: createSelector(state => state.selectedIndex),
115
- isActive: createSelector((state, index) => state.activeIndex === index),
116
- isSelected: createSelector((state, itemValue) => {
108
+ },
109
+ open: state => state.open,
110
+ mounted: state => state.mounted,
111
+ forceMounted: state => state.forceMounted,
112
+ inline: state => state.inline,
113
+ activeIndex: state => state.activeIndex,
114
+ selectedIndex: state => state.selectedIndex,
115
+ isActive: (state, index) => state.activeIndex === index,
116
+ isSelected: (state, itemValue) => {
117
117
  const comparer = state.isItemEqualToValue;
118
118
  const selectedValue = state.selectedValue;
119
119
  if (Array.isArray(selectedValue)) {
120
120
  return selectedValue.some(selectedItem => compareItemEquality(itemValue, selectedItem, comparer));
121
121
  }
122
122
  return compareItemEquality(itemValue, selectedValue, comparer);
123
- }),
124
- transitionStatus: createSelector(state => state.transitionStatus),
125
- popupProps: createSelector(state => state.popupProps),
126
- inputProps: createSelector(state => state.inputProps),
127
- triggerProps: createSelector(state => state.triggerProps),
128
- itemProps: createSelector(state => state.itemProps),
129
- positionerElement: createSelector(state => state.positionerElement),
130
- listElement: createSelector(state => state.listElement),
131
- popupId: createSelector(state => state.popupId),
132
- triggerElement: createSelector(state => state.triggerElement),
133
- inputElement: createSelector(state => state.inputElement),
134
- inputGroupElement: createSelector(state => state.inputGroupElement),
135
- popupSide: createSelector(state => state.popupSide),
136
- openMethod: createSelector(state => state.openMethod),
137
- inputInsidePopup: createSelector(state => state.inputInsidePopup),
138
- inputOwnsFormValue: createSelector(state => state.inputOwnsFormValue),
139
- selectionMode: createSelector(state => state.selectionMode),
140
- name: createSelector(state => state.name),
141
- form: createSelector(state => state.form),
142
- disabled: createSelector(state => state.disabled),
143
- readOnly: createSelector(state => state.readOnly),
144
- required: createSelector(state => state.required),
145
- grid: createSelector(state => state.grid),
146
- virtualized: createSelector(state => state.virtualized),
147
- itemToStringLabel: createSelector(state => state.itemToStringLabel),
148
- isItemEqualToValue: createSelector(state => state.isItemEqualToValue),
149
- modal: createSelector(state => state.modal),
150
- autoHighlight: createSelector(state => state.autoHighlight),
151
- submitOnItemClick: createSelector(state => state.submitOnItemClick)
123
+ },
124
+ transitionStatus: state => state.transitionStatus,
125
+ popupProps: state => state.popupProps,
126
+ listProps: state => state.listProps,
127
+ inputProps: state => state.inputProps,
128
+ triggerProps: state => state.triggerProps,
129
+ itemProps: state => state.itemProps,
130
+ positionerElement: state => state.positionerElement,
131
+ listElement: state => state.listElement,
132
+ popupId: state => state.popupId,
133
+ triggerElement: state => state.triggerElement,
134
+ inputElement: state => state.inputElement,
135
+ inputGroupElement: state => state.inputGroupElement,
136
+ popupSide: state => state.popupSide,
137
+ openMethod: state => state.openMethod,
138
+ inputInsidePopup: state => state.inputInsidePopup,
139
+ inputOwnsFormValue: state => state.inputOwnsFormValue,
140
+ selectionMode: state => state.selectionMode,
141
+ name: state => state.name,
142
+ form: state => state.form,
143
+ disabled: state => state.disabled,
144
+ readOnly: state => state.readOnly,
145
+ required: state => state.required,
146
+ grid: state => state.grid,
147
+ virtualized: state => state.virtualized,
148
+ itemToStringLabel: state => state.itemToStringLabel,
149
+ isItemEqualToValue: state => state.isItemEqualToValue,
150
+ modal: state => state.modal,
151
+ autoHighlight: state => state.autoHighlight
152
152
  };
153
153
 
154
154
  /**
@@ -171,8 +171,7 @@ function createCollatorItemFilter(collatorFilter, itemToStringLabel) {
171
171
  if (item == null) {
172
172
  return false;
173
173
  }
174
- const itemString = stringifyAsLabel(item, itemToStringLabel);
175
- return collatorFilter.contains(itemString, query);
174
+ return collatorFilter.contains(item, query, itemToStringLabel);
176
175
  };
177
176
  }
178
177
 
@@ -188,14 +187,13 @@ function createSingleSelectionCollatorFilter(collatorFilter, itemToStringLabel,
188
187
  if (!query) {
189
188
  return true;
190
189
  }
191
- const itemString = stringifyAsLabel(item, itemToStringLabel);
192
190
  const selectedString = selectedValue != null ? stringifyAsLabel(selectedValue, itemToStringLabel) : '';
193
191
 
194
192
  // Handle case-insensitive matching consistently
195
193
  if (selectedString && collatorFilter.contains(selectedString, query) && selectedString.length === query.length) {
196
194
  return true;
197
195
  }
198
- return collatorFilter.contains(itemString, query);
196
+ return collatorFilter.contains(item, query, itemToStringLabel);
199
197
  };
200
198
  }
201
199
 
@@ -267,9 +265,8 @@ function useComboboxFilter(options = {}) {
267
265
  return createSingleSelectionCollatorFilter(coreFilter, itemToString, value)(item, query);
268
266
  }, [coreFilter, value, multiple]);
269
267
  return React.useMemo(() => ({
270
- contains,
271
- startsWith: coreFilter.startsWith,
272
- endsWith: coreFilter.endsWith
268
+ ...coreFilter,
269
+ contains
273
270
  }), [contains, coreFilter]);
274
271
  }
275
272
 
@@ -290,7 +287,7 @@ function AriaCombobox(props) {
290
287
  inputValue: inputValueProp,
291
288
  open: openProp,
292
289
  defaultOpen = false,
293
- selectionMode = 'none',
290
+ selectionMode,
294
291
  onItemHighlighted: onItemHighlightedProp,
295
292
  name: nameProp,
296
293
  form,
@@ -363,12 +360,10 @@ function AriaCombobox(props) {
363
360
  */
364
361
  const valuesRef = React.useRef([]);
365
362
  /**
366
- * Contains all item values in a stable, unfiltered order.
367
- * This is only used when `items` prop is not provided.
368
- * It accumulates values on first mount and does not remove them on unmount due to
369
- * filtering, providing a stable index for selected value tracking.
363
+ * The item element that received the last `pointerdown`, used to detect whether a
364
+ * `mouseup` on an item belongs to a drag-select gesture that started elsewhere.
370
365
  */
371
- const allValuesRef = React.useRef([]);
366
+ const pointerDownItemRef = React.useRef(null);
372
367
  const disabled = fieldDisabled || disabledProp;
373
368
  const name = fieldName ?? nameProp;
374
369
  const multiple = selectionMode === 'multiple';
@@ -395,11 +390,10 @@ function AriaCombobox(props) {
395
390
  if (filterProp !== undefined) {
396
391
  return filterProp;
397
392
  }
398
- if (single && !queryChangedAfterOpen) {
399
- return createSingleSelectionCollatorFilter(collatorFilter, itemToStringLabel, selectedValue);
400
- }
393
+ // `shouldBypassFiltering` already empties the query whenever a single selection's label
394
+ // matches it exactly, so the filter never needs a selection-aware variant here.
401
395
  return createCollatorItemFilter(collatorFilter, itemToStringLabel);
402
- }, [filterProp, single, selectedValue, queryChangedAfterOpen, collatorFilter, itemToStringLabel]);
396
+ }, [filterProp, collatorFilter, itemToStringLabel]);
403
397
 
404
398
  // If neither inputValue nor defaultInputValue are provided, derive it from the
405
399
  // selected value for single mode so the input reflects the selection on mount.
@@ -425,9 +419,9 @@ function AriaCombobox(props) {
425
419
  state: 'open'
426
420
  });
427
421
  const isGrouped = isGroupedItems(items);
428
- const query = closeQuery ?? (inputValue === '' ? '' : String(inputValue).trim());
422
+ const query = closeQuery ?? String(inputValue).trim();
429
423
  const selectedLabelString = single ? stringifyAsLabel(selectedValue, itemToStringLabel) : '';
430
- const shouldBypassFiltering = single && !queryChangedAfterOpen && query !== '' && selectedLabelString !== '' && selectedLabelString.length === query.length && collatorFilter.contains(selectedLabelString, query);
424
+ const shouldBypassFiltering = single && !queryChangedAfterOpen && query !== '' && selectedLabelString.length === query.length && collatorFilter.contains(selectedLabelString, query);
431
425
  const filterQuery = shouldBypassFiltering ? '' : query;
432
426
  const shouldIgnoreExternalFiltering = hasItems && hasFilteredItemsProp && shouldBypassFiltering;
433
427
  const flatItems = React.useMemo(() => {
@@ -454,12 +448,18 @@ function AriaCombobox(props) {
454
448
  if (limit > -1 && currentCount >= limit) {
455
449
  break;
456
450
  }
457
- const candidateItems = filterQuery === '' ? group.items : group.items.filter(item => filter(item, filterQuery, itemToStringLabel));
458
- if (candidateItems.length === 0) {
459
- continue;
460
- }
461
451
  const remainingLimit = limit > -1 ? limit - currentCount : Infinity;
462
- const itemsToTake = candidateItems.slice(0, remainingLimit);
452
+ const itemsToTake = filterQuery === '' ? group.items.slice(0, remainingLimit) : [];
453
+ if (filterQuery !== '') {
454
+ for (const item of group.items) {
455
+ if (itemsToTake.length >= remainingLimit) {
456
+ break;
457
+ }
458
+ if (filter(item, filterQuery, itemToStringLabel)) {
459
+ itemsToTake.push(item);
460
+ }
461
+ }
462
+ }
463
463
  if (itemsToTake.length > 0) {
464
464
  const newGroup = {
465
465
  ...group,
@@ -499,76 +499,85 @@ function AriaCombobox(props) {
499
499
  }
500
500
  return filteredItems;
501
501
  }, [filteredItems, isGrouped]);
502
- const store = useRefWithInit(() => new Store({
503
- id,
504
- labelId: undefined,
505
- selectedValue,
506
- open,
507
- filter,
508
- query,
509
- items,
510
- selectionMode,
511
- listRef,
512
- labelsRef,
513
- popupRef,
514
- emptyRef,
515
- inputRef,
516
- startDismissRef,
517
- endDismissRef,
518
- keyboardActiveRef,
519
- chipsContainerRef,
520
- clearRef,
521
- valuesRef,
522
- allValuesRef,
523
- selectionEventRef,
524
- name,
525
- form,
526
- disabled,
527
- readOnly,
528
- required,
529
- grid,
530
- isGrouped,
531
- virtualized,
532
- openOnInputClick,
533
- itemToStringLabel,
534
- isItemEqualToValue,
535
- modal,
536
- autoHighlight: autoHighlightMode,
537
- submitOnItemClick,
538
- hasInputValue,
539
- mounted: false,
540
- forceMounted: false,
541
- transitionStatus: 'idle',
542
- inline: inlineProp,
543
- activeIndex: null,
544
- selectedIndex: null,
545
- popupProps: {},
546
- inputProps: {},
547
- triggerProps: {},
548
- itemProps: EMPTY_OBJECT,
549
- positionerElement: null,
550
- listElement: null,
551
- popupId: undefined,
552
- triggerElement: null,
553
- inputElement: null,
554
- inputGroupElement: null,
555
- popupSide: null,
556
- openMethod: null,
557
- inputInsidePopup: true,
558
- // Avoid duplicate names in the server HTML. Popup inputs aren't rendered
559
- // until after hydration, so the hidden input takes over then if needed.
560
- inputOwnsFormValue: selectionMode === 'none',
561
- onOpenChangeComplete: onOpenChangeCompleteProp || NOOP,
562
- // Placeholder callbacks replaced on first render
563
- setOpen: NOOP,
564
- setInputValue: NOOP,
565
- setSelectedValue: NOOP,
566
- setIndices: NOOP,
567
- onItemHighlighted: NOOP,
568
- handleSelection: NOOP,
569
- forceMount: NOOP,
570
- requestSubmit: NOOP
571
- })).current;
502
+ const store = useRefWithInit(() => {
503
+ // An inline list open on the first render never gets a closed pass of the closed-state
504
+ // sync effect below, and `items`-prop lists don't self-register their index the way
505
+ // individually rendered `<Combobox.Item>`s do, so the selected item was never highlighted.
506
+ // Seeding the index here lets list navigation highlight and scroll to the selection on
507
+ // mount. Computed once by construction, so a selection or list that resolves after mount
508
+ // doesn't move an existing highlight or scroll the list away.
509
+ let initialSelectedIndex = null;
510
+ if (inlineProp && open && hasItems && selectionMode !== 'none') {
511
+ initialSelectedIndex = findSelectionIndex(flatFilteredItems, selectedValue, isItemEqualToValue, multiple);
512
+ }
513
+ return new Store({
514
+ id,
515
+ labelId: undefined,
516
+ selectedValue,
517
+ open,
518
+ items,
519
+ selectionMode,
520
+ listRef,
521
+ labelsRef,
522
+ popupRef,
523
+ emptyRef,
524
+ inputRef,
525
+ startDismissRef,
526
+ endDismissRef,
527
+ keyboardActiveRef,
528
+ chipsContainerRef,
529
+ clearRef,
530
+ valuesRef,
531
+ pointerDownItemRef,
532
+ selectionEventRef,
533
+ name,
534
+ form,
535
+ disabled,
536
+ readOnly,
537
+ required,
538
+ grid,
539
+ virtualized,
540
+ openOnInputClick,
541
+ itemToStringLabel,
542
+ isItemEqualToValue,
543
+ modal,
544
+ autoHighlight: autoHighlightMode,
545
+ submitOnItemClick,
546
+ hasInputValue,
547
+ mounted: false,
548
+ forceMounted: false,
549
+ transitionStatus: 'idle',
550
+ inline: inlineProp,
551
+ activeIndex: null,
552
+ selectedIndex: initialSelectedIndex,
553
+ popupProps: {},
554
+ listProps: {},
555
+ inputProps: {},
556
+ triggerProps: {},
557
+ itemProps: EMPTY_OBJECT,
558
+ positionerElement: null,
559
+ listElement: null,
560
+ popupId: undefined,
561
+ triggerElement: null,
562
+ inputElement: null,
563
+ inputGroupElement: null,
564
+ popupSide: null,
565
+ openMethod: null,
566
+ inputInsidePopup: true,
567
+ // Avoid duplicate names in the server HTML. Popup inputs aren't rendered
568
+ // until after hydration, so the hidden input takes over then if needed.
569
+ inputOwnsFormValue: selectionMode === 'none',
570
+ // Placeholder callbacks replaced on first render
571
+ onOpenChangeComplete: NOOP,
572
+ setOpen: NOOP,
573
+ setInputValue: NOOP,
574
+ setSelectedValue: NOOP,
575
+ setIndices: NOOP,
576
+ handleSelection: NOOP,
577
+ forceMount: NOOP,
578
+ requestSubmit: NOOP
579
+ });
580
+ }).current;
572
581
  const fieldRawValue = selectionMode === 'none' ? inputValue : selectedValue;
573
582
  const fieldStringValue = React.useMemo(() => {
574
583
  if (selectionMode === 'none') {
@@ -611,35 +620,38 @@ function AriaCombobox(props) {
611
620
  store.set('forceMounted', true);
612
621
  }
613
622
  });
614
- const initialSelectedValueRef = React.useRef(selectedValue);
615
- useIsoLayoutEffect(() => {
616
- // Ensure the values and labels are registered for programmatic value changes.
617
- if (selectedValue !== initialSelectedValueRef.current) {
618
- forceMount();
623
+
624
+ /**
625
+ * Emits `onItemHighlighted` for the item at `index`, or clears the highlight when `index` is `-1`
626
+ * (a no-op if nothing was highlighted). Keeps `lastHighlightRef` in sync with what was emitted.
627
+ */
628
+ const emitHighlight = useStableCallback((value, index, type) => {
629
+ if (index === -1) {
630
+ if (lastHighlightRef.current === INITIAL_LAST_HIGHLIGHT) {
631
+ return;
632
+ }
633
+ lastHighlightRef.current = INITIAL_LAST_HIGHLIGHT;
634
+ } else {
635
+ lastHighlightRef.current = {
636
+ value,
637
+ index
638
+ };
619
639
  }
620
- }, [forceMount, selectedValue]);
640
+ onItemHighlighted(value, createGenericEventDetails(type, undefined, {
641
+ index
642
+ }));
643
+ });
621
644
  const setIndices = useStableCallback(options => {
622
645
  store.update(options);
623
- const type = options.type || 'none';
624
- if (options.activeIndex === undefined) {
646
+ const activeIndexOption = options.activeIndex;
647
+ if (activeIndexOption === undefined) {
625
648
  return;
626
649
  }
627
- if (options.activeIndex === null) {
628
- if (lastHighlightRef.current !== INITIAL_LAST_HIGHLIGHT) {
629
- lastHighlightRef.current = INITIAL_LAST_HIGHLIGHT;
630
- onItemHighlighted(undefined, createGenericEventDetails(type, undefined, {
631
- index: -1
632
- }));
633
- }
650
+ const type = options.type || none;
651
+ if (activeIndexOption === null) {
652
+ emitHighlight(undefined, -1, type);
634
653
  } else {
635
- const activeValue = valuesRef.current[options.activeIndex];
636
- lastHighlightRef.current = {
637
- value: activeValue,
638
- index: options.activeIndex
639
- };
640
- onItemHighlighted(activeValue, createGenericEventDetails(type, undefined, {
641
- index: options.activeIndex
642
- }));
654
+ emitHighlight(valuesRef.current[activeIndexOption], activeIndexOption, type);
643
655
  }
644
656
  });
645
657
  const setInputValue = useStableCallback((next, eventDetails) => {
@@ -652,6 +664,11 @@ function AriaCombobox(props) {
652
664
  // If user is typing, ensure we don't auto-highlight on open due to a race
653
665
  // with the post-open effect that sets this flag.
654
666
  if (eventDetails.reason === inputChange) {
667
+ // A controlled popup may ignore a close request. Resuming input proves the popup
668
+ // is remaining open, so release the query captured for an exit animation.
669
+ if (open && closeQuery !== null) {
670
+ setCloseQuery(null);
671
+ }
655
672
  const event = eventDetails.event;
656
673
  const inputType = event.inputType;
657
674
  // Treat composition commits as typed input; autofill may omit `inputType` or
@@ -667,10 +684,34 @@ function AriaCombobox(props) {
667
684
  pendingQueryHighlightRef.current = {
668
685
  hasQuery
669
686
  };
670
- if (hasQuery && autoHighlightMode && store.state.activeIndex == null) {
687
+
688
+ // Virtualized lists own their scroller. Reset regular lists directly so a stale
689
+ // composite registry cannot select a reordered item and scrolling cannot escape
690
+ // the popup.
691
+ const list = store.state.listElement;
692
+ if (!store.state.virtualized && list) {
693
+ const popup = popupRef.current;
694
+ for (const ancestor of getOverflowAncestors(list.firstElementChild ?? list)) {
695
+ if (!isHTMLElement(ancestor) || (popup ? !contains(popup, ancestor) : ancestor.getAttribute('role') === 'dialog')) {
696
+ break;
697
+ }
698
+ if (isScrollableY(ancestor)) {
699
+ ancestor.scrollTop = 0;
700
+ break;
701
+ }
702
+ }
703
+ }
704
+ if (hasQuery && autoHighlightMode && store.state.activeIndex == null && (open || inline)) {
671
705
  store.set('activeIndex', 0);
672
706
  }
673
707
  }
708
+ } else if (eventDetails.reason === inputClear && next === '' && store.state.inputInsidePopup) {
709
+ // A programmatic clear of an active query (e.g. after selecting an item with the
710
+ // input inside the popup): restore the highlight to the selected item.
711
+ pendingQueryHighlightRef.current = {
712
+ hasQuery: false,
713
+ selection: true
714
+ };
674
715
  }
675
716
  setInputValueUnwrapped(next);
676
717
  });
@@ -682,7 +723,7 @@ function AriaCombobox(props) {
682
723
  // If the `Empty` component is not used, the positioner or popup should be hidden
683
724
  // with CSS. In this case, allow the Escape key to bubble to close a parent popup
684
725
  // if there are no items to show.
685
- if (eventDetails.reason === 'escape-key' && hasItems && flatFilteredItems.length === 0 && !store.state.emptyRef.current) {
726
+ if (eventDetails.reason === escapeKey && hasItems && flatFilteredItems.length === 0 && !emptyRef.current) {
686
727
  eventDetails.allowPropagation();
687
728
  }
688
729
  props.onOpenChange?.(nextOpen, eventDetails);
@@ -692,10 +733,10 @@ function AriaCombobox(props) {
692
733
 
693
734
  // If reopening interrupts the close animation, handleUnmount won't run to clear the
694
735
  // frozen closeQuery and pending popup input.
695
- if (nextOpen && multiple && inputInsidePopup && !inline && closeQuery !== null) {
736
+ if (nextOpen && inputInsidePopup && !inline && closeQuery !== null) {
696
737
  setQueryChangedAfterOpen(false);
697
738
  setCloseQuery(null);
698
- if (inputValue !== '') {
739
+ if (inputValue !== '' && eventDetails.reason !== inputChange) {
699
740
  setInputValue('', createChangeEventDetails(inputClear, eventDetails.event));
700
741
  }
701
742
  }
@@ -749,18 +790,8 @@ function AriaCombobox(props) {
749
790
  if (shouldFillInput) {
750
791
  setInputValue(stringifyAsLabel(nextValue, itemToStringLabel), createChangeEventDetails(eventDetails.reason, eventDetails.event));
751
792
  }
752
- if (single && nextValue != null && eventDetails.reason !== inputChange && queryChangedAfterOpen && !inline) {
753
- setCloseQuery(query);
754
- }
755
793
  });
756
- const handleSelection = useStableCallback((event, passedValue) => {
757
- let itemValue = passedValue;
758
- if (itemValue === undefined) {
759
- if (activeIndex === null) {
760
- return;
761
- }
762
- itemValue = valuesRef.current[activeIndex];
763
- }
794
+ const handleSelection = useStableCallback((event, itemValue) => {
764
795
  const targetEl = getTarget(event);
765
796
  const overrideEvent = selectionEventRef.current ?? event;
766
797
  selectionEventRef.current = null;
@@ -776,8 +807,8 @@ function AriaCombobox(props) {
776
807
  }
777
808
  if (multiple) {
778
809
  const currentSelectedValue = Array.isArray(selectedValue) ? selectedValue : [];
779
- const isCurrentlySelected = selectedValueIncludes(currentSelectedValue, itemValue, store.state.isItemEqualToValue);
780
- const nextValue = isCurrentlySelected ? removeItem(currentSelectedValue, itemValue, store.state.isItemEqualToValue) : [...currentSelectedValue, itemValue];
810
+ const isCurrentlySelected = selectedValueIncludes(currentSelectedValue, itemValue, isItemEqualToValue);
811
+ const nextValue = isCurrentlySelected ? removeItem(currentSelectedValue, itemValue, isItemEqualToValue) : [...currentSelectedValue, itemValue];
781
812
  setSelectedValue(nextValue, eventDetails);
782
813
  if (eventDetails.isCanceled) {
783
814
  return;
@@ -800,9 +831,6 @@ function AriaCombobox(props) {
800
831
  }
801
832
  });
802
833
  const requestSubmit = useStableCallback(() => {
803
- if (!store.state.submitOnItemClick) {
804
- return;
805
- }
806
834
  const formElement = validation.inputRef.current?.form ?? store.state.inputElement?.form;
807
835
  if (formElement && typeof formElement.requestSubmit === 'function') {
808
836
  formElement.requestSubmit();
@@ -875,25 +903,29 @@ function AriaCombobox(props) {
875
903
  React.useImperativeHandle(props.actionsRef, () => ({
876
904
  unmount: handleUnmount
877
905
  }), [handleUnmount]);
878
- useIsoLayoutEffect(function syncSelectedIndex() {
879
- if (open || selectionMode === 'none') {
906
+ useIsoLayoutEffect(function syncClosedState() {
907
+ if (open) {
880
908
  return;
881
909
  }
882
- const registry = items ? flatItems : allValuesRef.current;
883
- if (multiple) {
884
- const currentValue = Array.isArray(selectedValue) ? selectedValue : [];
885
- const lastValue = currentValue[currentValue.length - 1];
886
- const lastIndex = findItemIndex(registry, lastValue, isItemEqualToValue);
887
- setIndices({
888
- selectedIndex: lastIndex === -1 ? null : lastIndex
889
- });
890
- } else {
891
- const index = findItemIndex(registry, selectedValue, isItemEqualToValue);
892
- setIndices({
893
- selectedIndex: index === -1 ? null : index
894
- });
910
+
911
+ // State-driven (not tied to the internal event path) so controlled closes
912
+ // also clear a pointerdown that never received a matching item mouseup.
913
+ pointerDownItemRef.current = null;
914
+ if (selectionMode === 'none') {
915
+ return;
895
916
  }
896
- }, [open, selectedValue, items, selectionMode, flatItems, multiple, isItemEqualToValue, setIndices]);
917
+
918
+ // Without `items`, look the selection up in the live registry of mounted item
919
+ // values (the list stays mounted while closed when closed-state features need
920
+ // it — trigger interaction and rendered-label autofill force-mount it). Mounted
921
+ // items re-assert the index themselves when their registration moves; when
922
+ // nothing is mounted the lookup resolves to `null` and each item re-registers
923
+ // the index on the next open.
924
+ const registry = hasItems ? flatItems : valuesRef.current;
925
+ setIndices({
926
+ selectedIndex: findSelectionIndex(registry, selectedValue, isItemEqualToValue, multiple)
927
+ });
928
+ }, [open, selectedValue, selectionMode, multiple, hasItems, flatItems, isItemEqualToValue, setIndices]);
897
929
  useIsoLayoutEffect(() => {
898
930
  if (items) {
899
931
  valuesRef.current = flatFilteredItems;
@@ -903,14 +935,52 @@ function AriaCombobox(props) {
903
935
  useIsoLayoutEffect(() => {
904
936
  const pendingHighlight = pendingQueryHighlightRef.current;
905
937
  if (pendingHighlight) {
938
+ // A directly rendered list remains visible when the popup state is closed, while a
939
+ // kept-mounted Positioner is hidden and should stay inert.
940
+ const listIsNavigable = open || inline || store.state.positionerElement?.hidden === false;
906
941
  if (pendingHighlight.hasQuery) {
907
- if (autoHighlightMode) {
942
+ if (autoHighlightMode && listIsNavigable) {
908
943
  store.set('activeIndex', 0);
909
944
  }
910
- } else if (autoHighlightMode === 'always') {
911
- store.set('activeIndex', 0);
945
+ pendingQueryHighlightRef.current = null;
946
+ } else if (String(inputValue).trim() === '') {
947
+ // Only handle the clear once it has committed (a controlled input may reject it),
948
+ // so a restore cannot fire while a query is still active.
949
+ pendingQueryHighlightRef.current = null;
950
+ if (listIsNavigable) {
951
+ const clearedBySelection = pendingHighlight.selection;
952
+ if (autoHighlightMode === 'always' && !clearedBySelection && store.state.selectionMode === 'none') {
953
+ // There is no selection to restore in Autocomplete. Keep the first-item reset
954
+ // synchronous so list navigation sees it before a directly rendered list closes.
955
+ store.set('activeIndex', 0);
956
+ }
957
+
958
+ // Items re-mounted by the clear publish their composite indices in a follow-up
959
+ // commit, so the item registries are mid-update here. Defer past React's cascade.
960
+ queueMicrotask(() => {
961
+ if (!store.state.open && !store.state.inline || inputRef.current && inputRef.current.value.trim() !== '') {
962
+ return;
963
+ }
964
+
965
+ // Return the highlight to the selected item, the same anchor the popup uses
966
+ // when it first opens. Read the selection through the store so consumers can
967
+ // pass an inline `isItemEqualToValue` or a fresh `selectedValue` array without
968
+ // re-running this effect on every render.
969
+ const currentSelectedValue = store.state.selectedValue;
970
+ const isMultiple = store.state.selectionMode === 'multiple';
971
+ const lastSelectedValue = isMultiple && Array.isArray(currentSelectedValue) ? currentSelectedValue[currentSelectedValue.length - 1] : currentSelectedValue;
972
+ const hasSelection = store.state.selectionMode !== 'none' && lastSelectedValue != null;
973
+ if (hasSelection || clearedBySelection) {
974
+ const registry = hasItems || hasFilteredItemsProp ? flatFilteredItems : valuesRef.current;
975
+ // A selection that is no longer in the list drops the highlight rather than
976
+ // leaving it on whichever item now occupies that index.
977
+ store.set('activeIndex', hasSelection ? findSelectionIndex(registry, currentSelectedValue, store.state.isItemEqualToValue, isMultiple) : null);
978
+ } else if (autoHighlightMode === 'always') {
979
+ store.set('activeIndex', 0);
980
+ }
981
+ });
982
+ }
912
983
  }
913
- pendingQueryHighlightRef.current = null;
914
984
  }
915
985
  if (!open && !inline) {
916
986
  return;
@@ -923,21 +993,11 @@ function AriaCombobox(props) {
923
993
  store.set('activeIndex', 0);
924
994
  return;
925
995
  }
926
- if (lastHighlightRef.current !== INITIAL_LAST_HIGHLIGHT) {
927
- lastHighlightRef.current = INITIAL_LAST_HIGHLIGHT;
928
- store.state.onItemHighlighted(undefined, createGenericEventDetails(none, undefined, {
929
- index: -1
930
- }));
931
- }
996
+ emitHighlight(undefined, -1, none);
932
997
  return;
933
998
  }
934
999
  if (storeActiveIndex >= candidateItems.length) {
935
- if (lastHighlightRef.current !== INITIAL_LAST_HIGHLIGHT) {
936
- lastHighlightRef.current = INITIAL_LAST_HIGHLIGHT;
937
- store.state.onItemHighlighted(undefined, createGenericEventDetails(none, undefined, {
938
- index: -1
939
- }));
940
- }
1000
+ emitHighlight(undefined, -1, none);
941
1001
  store.set('activeIndex', null);
942
1002
  return;
943
1003
  }
@@ -945,15 +1005,12 @@ function AriaCombobox(props) {
945
1005
  const previouslyHighlightedItemValue = lastHighlightRef.current.value;
946
1006
  const isSameItem = previouslyHighlightedItemValue !== NO_ACTIVE_VALUE && compareItemEquality(itemValue, previouslyHighlightedItemValue, store.state.isItemEqualToValue);
947
1007
  if (lastHighlightRef.current.index !== storeActiveIndex || !isSameItem) {
948
- lastHighlightRef.current = {
949
- value: itemValue,
950
- index: storeActiveIndex
951
- };
952
- store.state.onItemHighlighted(itemValue, createGenericEventDetails(none, undefined, {
953
- index: storeActiveIndex
954
- }));
1008
+ emitHighlight(itemValue, storeActiveIndex, none);
955
1009
  }
956
- }, [activeIndex, autoHighlightMode, hasFilteredItemsProp, hasItems, flatFilteredItems, inline, open, store]);
1010
+ }, [activeIndex, autoHighlightMode, emitHighlight, hasFilteredItemsProp, hasItems, flatFilteredItems, inline, open, store,
1011
+ // Reruns the effect when the query changes without affecting the deps above, such as
1012
+ // clearing the input when no items are filtered out (individually rendered items).
1013
+ inputValue]);
957
1014
  useIsoLayoutEffect(() => {
958
1015
  if (selectionMode === 'none') {
959
1016
  setFilled(String(inputValue) !== '');
@@ -984,6 +1041,12 @@ function AriaCombobox(props) {
984
1041
  }
985
1042
  setQueryChangedAfterOpen(true);
986
1043
  });
1044
+ function syncInputToSelectedLabel() {
1045
+ const nextInputValue = stringifyAsLabel(selectedValue, itemToStringLabel);
1046
+ if (inputValue !== nextInputValue) {
1047
+ setInputValue(nextInputValue, createChangeEventDetails(none));
1048
+ }
1049
+ }
987
1050
  useValueChanged(selectedValue, () => {
988
1051
  if (selectionMode === 'none') {
989
1052
  return;
@@ -992,10 +1055,7 @@ function AriaCombobox(props) {
992
1055
  setDirty(isSelectedValueDirty(selectedValue));
993
1056
  validation.change(selectedValue);
994
1057
  if (single && !hasInputValue && !inputInsidePopup) {
995
- const nextInputValue = stringifyAsLabel(selectedValue, itemToStringLabel);
996
- if (inputValue !== nextInputValue) {
997
- setInputValue(nextInputValue, createChangeEventDetails(none));
998
- }
1058
+ syncInputToSelectedLabel();
999
1059
  }
1000
1060
  });
1001
1061
  useValueChanged(inputValue, () => {
@@ -1010,10 +1070,7 @@ function AriaCombobox(props) {
1010
1070
  if (!single || hasInputValue || inputInsidePopup || queryChangedAfterOpen) {
1011
1071
  return;
1012
1072
  }
1013
- const nextInputValue = stringifyAsLabel(selectedValue, itemToStringLabel);
1014
- if (inputValue !== nextInputValue) {
1015
- setInputValue(nextInputValue, createChangeEventDetails(none));
1016
- }
1073
+ syncInputToSelectedLabel();
1017
1074
  });
1018
1075
  const floatingRootContext = useFloatingRootContext({
1019
1076
  open: inline ? true : open,
@@ -1023,18 +1080,19 @@ function AriaCombobox(props) {
1023
1080
  floating: positionerElement
1024
1081
  }
1025
1082
  });
1026
- let ariaHasPopup;
1027
- let ariaExpanded;
1028
- if (!inline) {
1029
- ariaHasPopup = grid ? 'grid' : 'listbox';
1030
- ariaExpanded = open ? 'true' : 'false';
1031
- }
1083
+ const ariaHasPopup = grid ? 'grid' : 'listbox';
1084
+ // An inline list isn't gated on `open`: it renders for as long as it's in the tree, so the
1085
+ // combobox is permanently expanded even while the internal open state is `false`.
1086
+ const expanded = open || inline;
1087
+ const ariaExpanded = expanded ? 'true' : 'false';
1032
1088
  const role = React.useMemo(() => {
1033
1089
  const isPlainInput = inputElement?.tagName === 'INPUT';
1034
1090
  // During SSR and initial hydration, the input ref is not available yet.
1035
1091
  // Assume an input-like control so combobox ARIA attributes are present.
1036
1092
  const shouldTreatAsInput = inputElement == null || isPlainInput;
1037
- const shouldApplyAria = shouldTreatAsInput || open;
1093
+ // A non-input control only takes on combobox semantics while the list is exposed, which for
1094
+ // an inline list is the whole time.
1095
+ const shouldApplyAria = shouldTreatAsInput || expanded;
1038
1096
  const reference = shouldTreatAsInput ? {
1039
1097
  autoComplete: 'off',
1040
1098
  spellCheck: 'false',
@@ -1045,7 +1103,7 @@ function AriaCombobox(props) {
1045
1103
  reference.role = 'combobox';
1046
1104
  reference['aria-expanded'] = ariaExpanded;
1047
1105
  reference['aria-haspopup'] = ariaHasPopup;
1048
- reference['aria-controls'] = open ? listElement?.id : undefined;
1106
+ reference['aria-controls'] = expanded ? listElement?.id : undefined;
1049
1107
  reference['aria-autocomplete'] = autoComplete;
1050
1108
  }
1051
1109
  return {
@@ -1054,7 +1112,7 @@ function AriaCombobox(props) {
1054
1112
  role: 'presentation'
1055
1113
  }
1056
1114
  };
1057
- }, [inputElement, open, ariaExpanded, ariaHasPopup, listElement?.id, autoComplete]);
1115
+ }, [inputElement, expanded, ariaExpanded, ariaHasPopup, listElement?.id, autoComplete]);
1058
1116
  const click = useClick(floatingRootContext, {
1059
1117
  enabled: !readOnly && !disabled && openOnInputClick,
1060
1118
  event: 'mousedown-only',
@@ -1108,7 +1166,7 @@ function AriaCombobox(props) {
1108
1166
  } else {
1109
1167
  setIndices({
1110
1168
  activeIndex: nextActiveIndex,
1111
- type: keyboardActiveRef.current ? 'keyboard' : 'pointer'
1169
+ type: keyboardActiveRef.current ? keyboard : pointer
1112
1170
  });
1113
1171
  }
1114
1172
  }
@@ -1123,7 +1181,8 @@ function AriaCombobox(props) {
1123
1181
  }
1124
1182
  }
1125
1183
  }, dismiss.reference, click.reference, role.reference), [listNavigation.reference, dismiss.reference, click.reference, role.reference, grid, store]);
1126
- const popupProps = React.useMemo(() => mergeProps(FOCUSABLE_POPUP_PROPS, listNavigation.floating, dismiss.floating, role.floating), [listNavigation.floating, dismiss.floating, role.floating]);
1184
+ const popupProps = React.useMemo(() => mergeProps(FOCUSABLE_POPUP_PROPS, dismiss.floating), [dismiss.floating]);
1185
+ const listProps = React.useMemo(() => mergeProps(listNavigation.floating, role.floating), [listNavigation.floating, role.floating]);
1127
1186
  const itemProps = React.useMemo(() => {
1128
1187
  const listNavigationItemProps = listNavigation.item;
1129
1188
  if (!listNavigationItemProps) {
@@ -1141,6 +1200,7 @@ function AriaCombobox(props) {
1141
1200
  store.update({
1142
1201
  inline: inlineProp,
1143
1202
  popupProps,
1203
+ listProps,
1144
1204
  inputProps,
1145
1205
  triggerProps,
1146
1206
  itemProps,
@@ -1148,10 +1208,10 @@ function AriaCombobox(props) {
1148
1208
  setInputValue,
1149
1209
  setSelectedValue,
1150
1210
  setIndices,
1151
- onItemHighlighted,
1152
1211
  handleSelection,
1153
1212
  forceMount,
1154
- requestSubmit
1213
+ requestSubmit,
1214
+ onOpenChangeComplete
1155
1215
  });
1156
1216
  });
1157
1217
  useIsoLayoutEffect(() => {
@@ -1164,6 +1224,7 @@ function AriaCombobox(props) {
1164
1224
  items,
1165
1225
  inline: inlineProp,
1166
1226
  popupProps,
1227
+ listProps,
1167
1228
  inputProps,
1168
1229
  triggerProps,
1169
1230
  openMethod,
@@ -1175,9 +1236,7 @@ function AriaCombobox(props) {
1175
1236
  readOnly,
1176
1237
  required,
1177
1238
  grid,
1178
- isGrouped,
1179
1239
  virtualized,
1180
- onOpenChangeComplete,
1181
1240
  openOnInputClick,
1182
1241
  itemToStringLabel,
1183
1242
  modal,
@@ -1185,10 +1244,9 @@ function AriaCombobox(props) {
1185
1244
  isItemEqualToValue,
1186
1245
  submitOnItemClick,
1187
1246
  hasInputValue,
1188
- requestSubmit,
1189
1247
  inputOwnsFormValue: selectionMode === 'none' && (inlineProp || !store.state.inputInsidePopup)
1190
1248
  });
1191
- }, [store, id, selectedValue, open, mounted, transitionStatus, items, popupProps, inputProps, itemProps, openMethod, triggerProps, selectionMode, name, disabled, readOnly, required, validation, grid, isGrouped, virtualized, onOpenChangeComplete, openOnInputClick, itemToStringLabel, modal, isItemEqualToValue, submitOnItemClick, hasInputValue, inlineProp, requestSubmit, autoHighlightMode, form]);
1249
+ }, [store, id, selectedValue, open, mounted, transitionStatus, items, popupProps, listProps, inputProps, itemProps, openMethod, triggerProps, selectionMode, name, disabled, readOnly, required, grid, virtualized, openOnInputClick, itemToStringLabel, modal, isItemEqualToValue, submitOnItemClick, hasInputValue, inlineProp, autoHighlightMode, form]);
1192
1250
  const hiddenInputRef = useMergedRefs(inputRefProp, validation.inputRef);
1193
1251
  const itemsContextValue = React.useMemo(() => ({
1194
1252
  query,
@@ -1232,7 +1290,7 @@ function AriaCombobox(props) {
1232
1290
  },
1233
1291
  // Handle browser autofill.
1234
1292
  onChange(event) {
1235
- // Workaround for https://github.com/facebook/react/issues/9023
1293
+ // Workaround for https://github.com/react/react/issues/9023
1236
1294
  if (event.nativeEvent.defaultPrevented || disabled || readOnly) {
1237
1295
  return;
1238
1296
  }
@@ -1327,7 +1385,51 @@ const triggerStateAttributesMapping = {
1327
1385
  } : null
1328
1386
  };
1329
1387
 
1330
- const BOUNDARY_OFFSET = 2;
1388
+ /**
1389
+ * The popup side is only meaningful while the positioner is mounted, as the store retains the
1390
+ * last resolved side after the popup unmounts.
1391
+ */
1392
+ function usePopupSide(store) {
1393
+ const mounted = useStore(store, selectors.mounted);
1394
+ const popupSide = useStore(store, selectors.popupSide);
1395
+ const positionerElement = useStore(store, selectors.positionerElement);
1396
+ return mounted && positionerElement ? popupSide : null;
1397
+ }
1398
+
1399
+ /**
1400
+ * Whether the filtered list has no items to show.
1401
+ */
1402
+ function useListEmpty() {
1403
+ return useComboboxDerivedItemsContext().filteredItems.length === 0;
1404
+ }
1405
+
1406
+ /**
1407
+ * The arrow keys that move the chip highlight backwards and forwards, in that order.
1408
+ */
1409
+ function getChipNavigationKeys(direction) {
1410
+ return direction === 'rtl' ? ['ArrowRight', 'ArrowLeft'] : ['ArrowLeft', 'ArrowRight'];
1411
+ }
1412
+
1413
+ /**
1414
+ * Where the highlight lands once the chip at `index` is removed, or `undefined` for no highlight.
1415
+ */
1416
+ function getIndexAfterChipRemoval(index, chipCount) {
1417
+ const nextIndex = index >= chipCount - 1 ? chipCount - 2 : index;
1418
+ return nextIndex >= 0 ? nextIndex : undefined;
1419
+ }
1420
+
1421
+ /**
1422
+ * Commits the highlighted item by clicking it, tagging the originating event so the item's
1423
+ * handler can attribute the selection to it.
1424
+ */
1425
+ function clickHighlightedItem(store, activeIndex, nativeEvent) {
1426
+ const listItem = store.state.listRef.current[activeIndex];
1427
+ if (listItem) {
1428
+ store.state.selectionEventRef.current = nativeEvent;
1429
+ listItem.click();
1430
+ store.state.selectionEventRef.current = null;
1431
+ }
1432
+ }
1331
1433
 
1332
1434
  /**
1333
1435
  * A button that opens the popup.
@@ -1357,20 +1459,14 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
1357
1459
  labelId: fieldLabelId
1358
1460
  } = useLabelableContext();
1359
1461
  const store = useComboboxRootContext();
1360
- const {
1361
- filteredItems
1362
- } = useComboboxDerivedItemsContext();
1363
1462
  const selectionMode = useStore(store, selectors.selectionMode);
1364
1463
  const comboboxDisabled = useStore(store, selectors.disabled);
1365
1464
  const readOnly = useStore(store, selectors.readOnly);
1366
1465
  const required = useStore(store, selectors.required);
1367
- const mounted = useStore(store, selectors.mounted);
1368
- const popupSideValue = useStore(store, selectors.popupSide);
1369
1466
  const positionerElement = useStore(store, selectors.positionerElement);
1370
1467
  const listElement = useStore(store, selectors.listElement);
1371
1468
  const storedPopupId = useStore(store, selectors.popupId);
1372
1469
  const triggerProps = useStore(store, selectors.triggerProps);
1373
- const triggerElement = useStore(store, selectors.triggerElement);
1374
1470
  const inputInsidePopup = useStore(store, selectors.inputInsidePopup);
1375
1471
  const rootId = useStore(store, selectors.id);
1376
1472
  const comboboxLabelId = useStore(store, selectors.labelId);
@@ -1383,8 +1479,8 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
1383
1479
  const inputValue = useComboboxInputValueContext();
1384
1480
  const focusTimeout = useTimeout();
1385
1481
  const disabled = fieldDisabled || comboboxDisabled || disabledProp;
1386
- const listEmpty = filteredItems.length === 0;
1387
- const popupSide = mounted && positionerElement ? popupSideValue : null;
1482
+ const listEmpty = useListEmpty();
1483
+ const popupSide = usePopupSide(store);
1388
1484
  useLabelableId({
1389
1485
  id: inputInsidePopup ? idProp : undefined
1390
1486
  });
@@ -1402,18 +1498,6 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
1402
1498
  function trackPointerType(event) {
1403
1499
  currentPointerTypeRef.current = event.pointerType;
1404
1500
  }
1405
- const domReference = floatingRootContext.useState('domReferenceElement');
1406
-
1407
- // Update the floating root context to use the trigger element when it differs from the current reference.
1408
- // This ensures useClick and useTypeahead attach handlers to the correct element.
1409
- React.useEffect(() => {
1410
- if (!inputInsidePopup) {
1411
- return;
1412
- }
1413
- if (triggerElement && triggerElement !== domReference) {
1414
- floatingRootContext.set('domReferenceElement', triggerElement);
1415
- }
1416
- }, [triggerElement, domReference, floatingRootContext, inputInsidePopup]);
1417
1501
  const {
1418
1502
  reference: triggerTypeaheadProps
1419
1503
  } = useTypeahead(floatingRootContext, {
@@ -1424,7 +1508,7 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
1424
1508
  onMatch(index) {
1425
1509
  const nextSelectedValue = store.state.valuesRef.current[index];
1426
1510
  if (nextSelectedValue !== undefined) {
1427
- store.state.setSelectedValue(nextSelectedValue, createChangeEventDetails('none'));
1511
+ store.state.setSelectedValue(nextSelectedValue, createChangeEventDetails(none));
1428
1512
  }
1429
1513
  }
1430
1514
  });
@@ -1459,7 +1543,7 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
1459
1543
  id,
1460
1544
  tabIndex: inputInsidePopup ? 0 : -1,
1461
1545
  role: inputInsidePopup ? 'combobox' : undefined,
1462
- 'aria-expanded': open ? 'true' : 'false',
1546
+ 'aria-expanded': open,
1463
1547
  'aria-haspopup': inputInsidePopup ? 'dialog' : 'listbox',
1464
1548
  'aria-controls': ariaControls,
1465
1549
  'aria-required': inputInsidePopup ? required || undefined : undefined,
@@ -1506,22 +1590,20 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
1506
1590
  }
1507
1591
  const doc = ownerDocument(event.currentTarget);
1508
1592
  function handleMouseUp(mouseEvent) {
1509
- if (!triggerElement) {
1593
+ const currentTriggerElement = store.state.triggerElement;
1594
+ if (!currentTriggerElement) {
1510
1595
  return;
1511
1596
  }
1512
1597
  const mouseUpTarget = getTarget(mouseEvent);
1513
1598
  const positioner = store.state.positionerElement;
1514
1599
  const list = store.state.listElement;
1515
- if (contains(triggerElement, mouseUpTarget) || contains(positioner, mouseUpTarget) || contains(list, mouseUpTarget) || mouseUpTarget === triggerElement) {
1600
+ if (contains(currentTriggerElement, mouseUpTarget) || contains(positioner, mouseUpTarget) || contains(list, mouseUpTarget)) {
1516
1601
  return;
1517
1602
  }
1518
- const bounds = getPseudoElementBounds(triggerElement);
1519
- const withinHorizontal = mouseEvent.clientX >= bounds.left - BOUNDARY_OFFSET && mouseEvent.clientX <= bounds.right + BOUNDARY_OFFSET;
1520
- const withinVertical = mouseEvent.clientY >= bounds.top - BOUNDARY_OFFSET && mouseEvent.clientY <= bounds.bottom + BOUNDARY_OFFSET;
1521
- if (withinHorizontal && withinVertical) {
1603
+ if (isMouseWithinBounds(mouseEvent, currentTriggerElement)) {
1522
1604
  return;
1523
1605
  }
1524
- store.state.setOpen(false, createChangeEventDetails('cancel-open', mouseEvent));
1606
+ store.state.setOpen(false, createChangeEventDetails(cancelOpen, mouseEvent));
1525
1607
  }
1526
1608
  if (inputInsidePopup) {
1527
1609
  doc.addEventListener('mouseup', handleMouseUp, {
@@ -1530,7 +1612,7 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
1530
1612
  }
1531
1613
  },
1532
1614
  onKeyDown(event) {
1533
- if (disabled || readOnly) {
1615
+ if (readOnly) {
1534
1616
  return;
1535
1617
  }
1536
1618
  if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
@@ -1539,7 +1621,7 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
1539
1621
  store.state.inputRef.current?.focus();
1540
1622
  }
1541
1623
  }
1542
- }, validation ? validation.getValidationProps(disabled, elementProps) : elementProps, getButtonProps],
1624
+ }, validation.getValidationProps(disabled, elementProps), getButtonProps],
1543
1625
  stateAttributesMapping: triggerStateAttributesMapping
1544
1626
  });
1545
1627
  return element;
@@ -1614,9 +1696,6 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1614
1696
  const positioning = useComboboxPositionerContext(true);
1615
1697
  const hasPositionerParent = Boolean(positioning);
1616
1698
  const store = useComboboxRootContext();
1617
- const {
1618
- filteredItems
1619
- } = useComboboxDerivedItemsContext();
1620
1699
  // `inputValue` can't be placed in the store.
1621
1700
  // https://github.com/mui/base-ui/issues/2703
1622
1701
  const inputValue = useComboboxInputValueContext();
@@ -1633,19 +1712,16 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1633
1712
  const open = useStore(store, selectors.open);
1634
1713
  const mounted = useStore(store, selectors.mounted);
1635
1714
  const selectedValue = useStore(store, selectors.selectedValue);
1636
- const popupSideValue = useStore(store, selectors.popupSide);
1637
- const positionerElement = useStore(store, selectors.positionerElement);
1638
1715
  const rootId = useStore(store, selectors.id);
1639
1716
  const inline = useStore(store, selectors.inline);
1640
1717
  const modal = useStore(store, selectors.modal);
1641
1718
  const autoHighlightEnabled = Boolean(autoHighlightMode);
1642
- const popupSide = mounted && positionerElement ? popupSideValue : null;
1719
+ const popupSide = usePopupSide(store);
1643
1720
  const disabled = fieldDisabled || comboboxDisabled || disabledProp;
1644
- const listEmpty = filteredItems.length === 0;
1721
+ const listEmpty = useListEmpty();
1645
1722
  const isInsidePopup = hasPositionerParent || inline;
1646
1723
  const focusManagerModal = !isInsidePopup || modal;
1647
1724
  const id = useBaseUiId(idProp ?? (!isInsidePopup ? rootId : undefined));
1648
- const ariaLabelledBy = resolveAriaLabelledBy(fieldLabelId, undefined);
1649
1725
  const fieldStateForInput = hasPositionerParent ? DEFAULT_FIELD_STATE_ATTRIBUTES : fieldState;
1650
1726
  const [composingValue, setComposingValue] = React.useState(null);
1651
1727
  const isComposingRef = React.useRef(false);
@@ -1663,7 +1739,17 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1663
1739
  inputOwnsFormValue
1664
1740
  });
1665
1741
  });
1666
- const validationProps = hasPositionerParent || !validation ? elementProps : validation.getValidationProps(disabled, elementProps);
1742
+ const validationProps = hasPositionerParent ? elementProps : validation.getValidationProps(disabled, elementProps);
1743
+ function clearHighlight() {
1744
+ store.state.setIndices({
1745
+ activeIndex: null,
1746
+ selectedIndex: null,
1747
+ type: store.state.keyboardActiveRef.current ? keyboard : pointer
1748
+ });
1749
+ }
1750
+ function markPointerActive() {
1751
+ store.state.keyboardActiveRef.current = false;
1752
+ }
1667
1753
  const state = {
1668
1754
  ...fieldStateForInput,
1669
1755
  open,
@@ -1681,9 +1767,7 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1681
1767
  highlightedChipIndex
1682
1768
  } = comboboxChipsContext;
1683
1769
  const renderedChipsCount = comboboxChipsContext.chipsRef.current.length;
1684
- const isRtl = direction === 'rtl';
1685
- const previousChipKey = isRtl ? 'ArrowRight' : 'ArrowLeft';
1686
- const nextChipKey = isRtl ? 'ArrowLeft' : 'ArrowRight';
1770
+ const [previousChipKey, nextChipKey] = getChipNavigationKeys(direction);
1687
1771
  if (highlightedChipIndex !== undefined) {
1688
1772
  if (event.key === previousChipKey) {
1689
1773
  event.preventDefault();
@@ -1702,14 +1786,8 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1702
1786
  } else if (event.key === 'Backspace' || event.key === 'Delete') {
1703
1787
  event.preventDefault();
1704
1788
  // Move highlight appropriately after removal.
1705
- const computedNextIndex = highlightedChipIndex >= selectedValue.length - 1 ? selectedValue.length - 2 : highlightedChipIndex;
1706
- // If the computed index is negative, treat it as no highlight.
1707
- nextIndex = computedNextIndex >= 0 ? computedNextIndex : undefined;
1708
- store.state.setIndices({
1709
- activeIndex: null,
1710
- selectedIndex: null,
1711
- type: 'keyboard'
1712
- });
1789
+ nextIndex = getIndexAfterChipRemoval(highlightedChipIndex, selectedValue.length);
1790
+ clearHighlight();
1713
1791
  }
1714
1792
  return nextIndex;
1715
1793
  }
@@ -1718,13 +1796,6 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1718
1796
  if (event.key === previousChipKey && (event.currentTarget.selectionStart ?? 0) === 0 && selectedValue.length > 0) {
1719
1797
  event.preventDefault();
1720
1798
  nextIndex = renderedChipsCount > 0 ? renderedChipsCount - 1 : undefined;
1721
- } else if (event.key === 'Backspace' && event.currentTarget.value === '' && selectedValue.length > 0) {
1722
- store.state.setIndices({
1723
- activeIndex: null,
1724
- selectedIndex: null,
1725
- type: 'keyboard'
1726
- });
1727
- event.preventDefault();
1728
1799
  }
1729
1800
  return nextIndex;
1730
1801
  }
@@ -1732,11 +1803,10 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1732
1803
  state,
1733
1804
  ref: [forwardedRef, store.state.inputRef, setInputElement],
1734
1805
  props: [inputProps, triggerProps, {
1735
- type: 'text',
1736
- value: componentProps.value ?? composingValue ?? inputValue,
1806
+ value: composingValue ?? inputValue,
1737
1807
  'aria-readonly': readOnly || undefined,
1738
1808
  'aria-required': required || undefined,
1739
- 'aria-labelledby': ariaLabelledBy,
1809
+ 'aria-labelledby': fieldLabelId,
1740
1810
  disabled,
1741
1811
  readOnly,
1742
1812
  required: selectionMode === 'none' ? required : undefined,
@@ -1791,11 +1861,23 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1791
1861
  store.state.setInputValue(next, createChangeEventDetails(inputChange, event.nativeEvent));
1792
1862
  },
1793
1863
  onChange(event) {
1864
+ const nativeEvent = event.nativeEvent;
1794
1865
  // Autofill may not provide `inputType` (Chrome) or may report
1795
1866
  // `insertReplacementText` (Firefox).
1796
- const inputType = event.nativeEvent.inputType;
1867
+ const inputType = nativeEvent.inputType;
1797
1868
  const autofillLikeInput = !inputType || inputType === 'insertReplacementText';
1869
+ // During composition the input is always considered typed into.
1798
1870
  const shouldOpenOnInput = isComposingRef.current || !autofillLikeInput;
1871
+ function maybeOpenOnInput(trimmed) {
1872
+ if (readOnly || disabled || !trimmed || !shouldOpenOnInput) {
1873
+ return;
1874
+ }
1875
+ store.state.setOpen(true, createChangeEventDetails(inputChange, nativeEvent));
1876
+ // When autoHighlight is enabled, keep the highlight (will be set to 0 in root).
1877
+ if (!autoHighlightEnabled) {
1878
+ clearHighlight();
1879
+ }
1880
+ }
1799
1881
 
1800
1882
  // During IME composition, avoid propagating controlled updates to prevent
1801
1883
  // filtering the options prematurely so `Empty` won't show incorrectly.
@@ -1807,38 +1889,23 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1807
1889
  const nextVal = event.currentTarget.value;
1808
1890
  setComposingValue(nextVal);
1809
1891
  if (nextVal === '' && !store.state.openOnInputClick && !store.state.inputInsidePopup) {
1810
- store.state.setOpen(false, createChangeEventDetails(inputClear, event.nativeEvent));
1892
+ store.state.setOpen(false, createChangeEventDetails(inputClear, nativeEvent));
1811
1893
  }
1812
1894
  const trimmed = nextVal.trim();
1813
1895
  const shouldMaintainHighlight = autoHighlightEnabled && trimmed !== '';
1814
- if (!readOnly && !disabled && trimmed) {
1815
- if (shouldOpenOnInput) {
1816
- store.state.setOpen(true, createChangeEventDetails(inputChange, event.nativeEvent));
1817
- if (!autoHighlightEnabled) {
1818
- store.state.setIndices({
1819
- activeIndex: null,
1820
- selectedIndex: null,
1821
- type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
1822
- });
1823
- }
1824
- }
1825
- }
1896
+ maybeOpenOnInput(trimmed);
1826
1897
  if (open && store.state.activeIndex !== null && !shouldMaintainHighlight) {
1827
- store.state.setIndices({
1828
- activeIndex: null,
1829
- selectedIndex: null,
1830
- type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
1831
- });
1898
+ clearHighlight();
1832
1899
  }
1833
1900
  return;
1834
1901
  }
1835
- const inputChangeDetails = createChangeEventDetails(inputChange, event.nativeEvent);
1902
+ const inputChangeDetails = createChangeEventDetails(inputChange, nativeEvent);
1836
1903
  store.state.setInputValue(event.currentTarget.value, inputChangeDetails);
1837
1904
  if (inputChangeDetails.isCanceled) {
1838
1905
  return;
1839
1906
  }
1840
1907
  const empty = event.currentTarget.value === '';
1841
- const clearDetails = createChangeEventDetails(inputClear, event.nativeEvent);
1908
+ const clearDetails = createChangeEventDetails(inputClear, nativeEvent);
1842
1909
  if (empty && !store.state.inputInsidePopup) {
1843
1910
  if (selectionMode === 'single') {
1844
1911
  store.state.setSelectedValue(null, clearDetails);
@@ -1847,30 +1914,13 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1847
1914
  store.state.setOpen(false, clearDetails);
1848
1915
  }
1849
1916
  }
1850
- const trimmed = event.currentTarget.value.trim();
1851
- if (!readOnly && !disabled && trimmed) {
1852
- if (shouldOpenOnInput) {
1853
- store.state.setOpen(true, createChangeEventDetails(inputChange, event.nativeEvent));
1854
- // When autoHighlight is enabled, keep the highlight (will be set to 0 in root).
1855
- if (!autoHighlightEnabled) {
1856
- store.state.setIndices({
1857
- activeIndex: null,
1858
- selectedIndex: null,
1859
- type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
1860
- });
1861
- }
1862
- }
1863
- }
1917
+ maybeOpenOnInput(event.currentTarget.value.trim());
1864
1918
 
1865
1919
  // When the user types, ensure the list resets its highlight so that
1866
1920
  // virtual focus returns to the input (aria-activedescendant is
1867
1921
  // cleared).
1868
1922
  if (open && store.state.activeIndex !== null && !autoHighlightEnabled) {
1869
- store.state.setIndices({
1870
- activeIndex: null,
1871
- selectedIndex: null,
1872
- type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
1873
- });
1923
+ clearHighlight();
1874
1924
  }
1875
1925
  },
1876
1926
  onKeyDown(event) {
@@ -1916,11 +1966,7 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1916
1966
  const removalIndex = renderedChipsCount > 0 ? renderedChipsCount - 1 : selectedValue.length - 1;
1917
1967
  const newValue = selectedValue.filter((_, index) => index !== removalIndex);
1918
1968
  // If the removed item was also the active (highlighted) item, clear highlight
1919
- store.state.setIndices({
1920
- activeIndex: null,
1921
- selectedIndex: null,
1922
- type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
1923
- });
1969
+ clearHighlight();
1924
1970
  store.state.setSelectedValue(newValue, createChangeEventDetails(none, event.nativeEvent));
1925
1971
  return;
1926
1972
  }
@@ -1950,20 +1996,11 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1950
1996
  return;
1951
1997
  }
1952
1998
  stopEvent(event);
1953
- const listItem = store.state.listRef.current[activeIndex];
1954
- if (listItem) {
1955
- store.state.selectionEventRef.current = nativeEvent;
1956
- listItem.click();
1957
- store.state.selectionEventRef.current = null;
1958
- }
1999
+ clickHighlightedItem(store, activeIndex, nativeEvent);
1959
2000
  }
1960
2001
  },
1961
- onPointerMove() {
1962
- store.state.keyboardActiveRef.current = false;
1963
- },
1964
- onPointerDown() {
1965
- store.state.keyboardActiveRef.current = false;
1966
- }
2002
+ onPointerMove: markPointerActive,
2003
+ onPointerDown: markPointerActive
1967
2004
  }, validationProps],
1968
2005
  stateAttributesMapping: triggerStateAttributesMapping
1969
2006
  });
@@ -2015,20 +2052,14 @@ const ComboboxInputGroup = /*#__PURE__*/React.forwardRef(function ComboboxInputG
2015
2052
  state: fieldState
2016
2053
  } = useFieldRootContext();
2017
2054
  const store = useComboboxRootContext();
2018
- const {
2019
- filteredItems
2020
- } = useComboboxDerivedItemsContext();
2021
2055
  const open = useStore(store, selectors.open);
2022
- const mounted = useStore(store, selectors.mounted);
2023
- const popupSideValue = useStore(store, selectors.popupSide);
2024
- const positionerElement = useStore(store, selectors.positionerElement);
2025
2056
  const comboboxDisabled = useStore(store, selectors.disabled);
2026
2057
  const readOnly = useStore(store, selectors.readOnly);
2027
2058
  const hasSelectedValue = useStore(store, selectors.hasSelectedValue);
2028
2059
  const selectionMode = useStore(store, selectors.selectionMode);
2029
- const popupSide = mounted && positionerElement ? popupSideValue : null;
2060
+ const popupSide = usePopupSide(store);
2030
2061
  const disabled = comboboxDisabled;
2031
- const listEmpty = filteredItems.length === 0;
2062
+ const listEmpty = useListEmpty();
2032
2063
  const placeholder = selectionMode === 'none' ? false : !hasSelectedValue;
2033
2064
  const state = {
2034
2065
  ...fieldState,
@@ -2164,19 +2195,21 @@ const ComboboxClear = /*#__PURE__*/React.forwardRef(function ComboboxClear(compo
2164
2195
  if (disabled || readOnly) {
2165
2196
  return;
2166
2197
  }
2167
- const keyboardActiveRef = store.state.keyboardActiveRef;
2198
+ const type = store.state.keyboardActiveRef.current ? keyboard : pointer;
2168
2199
  store.state.setInputValue('', createChangeEventDetails(clearPress, event.nativeEvent));
2169
2200
  if (selectionMode !== 'none') {
2170
2201
  store.state.setSelectedValue(Array.isArray(selectedValue) ? [] : null, createChangeEventDetails(clearPress, event.nativeEvent));
2202
+ // A distinct object shape: `Store.update` iterates own keys, so passing an explicit
2203
+ // `selectedIndex: undefined` would overwrite the state instead of leaving it alone.
2171
2204
  store.state.setIndices({
2172
2205
  activeIndex: null,
2173
2206
  selectedIndex: null,
2174
- type: keyboardActiveRef.current ? 'keyboard' : 'pointer'
2207
+ type
2175
2208
  });
2176
2209
  } else {
2177
2210
  store.state.setIndices({
2178
2211
  activeIndex: null,
2179
- type: keyboardActiveRef.current ? 'keyboard' : 'pointer'
2212
+ type
2180
2213
  });
2181
2214
  }
2182
2215
  store.state.inputRef.current?.focus();
@@ -2220,9 +2253,6 @@ function ComboboxCollection(props) {
2220
2253
  } = useComboboxDerivedItemsContext();
2221
2254
  const groupContext = useGroupCollectionContext();
2222
2255
  const itemsToRender = groupContext ? groupContext.items : filteredItems;
2223
- if (!itemsToRender) {
2224
- return null;
2225
- }
2226
2256
  return /*#__PURE__*/jsx(React.Fragment, {
2227
2257
  children: itemsToRender.map(children)
2228
2258
  });
@@ -2246,7 +2276,7 @@ const ComboboxList = /*#__PURE__*/React.forwardRef(function ComboboxList(compone
2246
2276
  } = useComboboxDerivedItemsContext();
2247
2277
  const selectionMode = useStore(store, selectors.selectionMode);
2248
2278
  const grid = useStore(store, selectors.grid);
2249
- const popupProps = useStore(store, selectors.popupProps);
2279
+ const listProps = useStore(store, selectors.listProps);
2250
2280
  const virtualized = useStore(store, selectors.virtualized);
2251
2281
  const forceMounted = useStore(store, selectors.forceMounted);
2252
2282
  const multiple = selectionMode === 'multiple';
@@ -2260,7 +2290,7 @@ const ComboboxList = /*#__PURE__*/React.forwardRef(function ComboboxList(compone
2260
2290
 
2261
2291
  // Support "closed template" API: if children is a function, implicitly wrap it
2262
2292
  // with a Combobox.Collection that reads items from context/root.
2263
- // Ensures this component's `popupProps` subscription does not cause <Combobox.Item>
2293
+ // Ensures this component's `listProps` subscription does not cause <Combobox.Item>
2264
2294
  // to re-render on every active index change.
2265
2295
  const resolvedChildren = React.useMemo(() => {
2266
2296
  if (typeof children === 'function') {
@@ -2277,7 +2307,7 @@ const ComboboxList = /*#__PURE__*/React.forwardRef(function ComboboxList(compone
2277
2307
  const element = useRenderElement('div', componentProps, {
2278
2308
  state,
2279
2309
  ref: [forwardedRef, setListElement, hasPositionerContext ? null : setPositionerElement],
2280
- props: [popupProps, {
2310
+ props: [listProps, {
2281
2311
  children: resolvedChildren,
2282
2312
  tabIndex: -1,
2283
2313
  id: floatingId,
@@ -2294,13 +2324,7 @@ const ComboboxList = /*#__PURE__*/React.forwardRef(function ComboboxList(compone
2294
2324
  return;
2295
2325
  }
2296
2326
  stopEvent(event);
2297
- const nativeEvent = event.nativeEvent;
2298
- const listItem = store.state.listRef.current[activeIndex];
2299
- if (listItem) {
2300
- store.state.selectionEventRef.current = nativeEvent;
2301
- listItem.click();
2302
- store.state.selectionEventRef.current = null;
2303
- }
2327
+ clickHighlightedItem(store, activeIndex, event.nativeEvent);
2304
2328
  }
2305
2329
  },
2306
2330
  onKeyDownCapture() {
@@ -2359,7 +2383,7 @@ function useInitialLiveRegionTextMutation() {
2359
2383
  if (textNode == null) {
2360
2384
  return undefined;
2361
2385
  }
2362
- const originalValue = textNode.nodeValue ?? '';
2386
+ const originalValue = textNode.data;
2363
2387
  const markedValue = `${originalValue}${LIVE_REGION_MARKER}`;
2364
2388
  textNode.nodeValue = markedValue;
2365
2389
  timeout.start(INITIAL_LIVE_REGION_TEXT_MUTATION_RESET_DELAY, () => {
@@ -2488,24 +2512,23 @@ const ComboboxPositioner = /*#__PURE__*/React.forwardRef(function ComboboxPositi
2488
2512
  render,
2489
2513
  className,
2490
2514
  anchor,
2491
- positionMethod = 'absolute',
2492
- side = 'bottom',
2493
- align = 'center',
2494
- sideOffset = 0,
2495
- alignOffset = 0,
2515
+ // `useAnchorPositioning` applies the same defaults to the undefined values; the names
2516
+ // remain destructured to exclude the props from `elementProps`.
2517
+ positionMethod,
2518
+ side,
2519
+ align,
2520
+ sideOffset,
2521
+ alignOffset,
2496
2522
  collisionBoundary = 'clipping-ancestors',
2497
- collisionPadding = 5,
2498
- arrowPadding = 5,
2499
- sticky = false,
2523
+ collisionPadding,
2524
+ arrowPadding,
2525
+ sticky,
2500
2526
  disableAnchorTracking = false,
2501
2527
  collisionAvoidance = DROPDOWN_COLLISION_AVOIDANCE,
2502
2528
  style: styleProp,
2503
2529
  ...elementProps
2504
2530
  } = componentProps;
2505
2531
  const store = useComboboxRootContext();
2506
- const {
2507
- filteredItems
2508
- } = useComboboxDerivedItemsContext();
2509
2532
  const floatingRootContext = useComboboxFloatingContext();
2510
2533
  const keepMounted = useComboboxPortalContext();
2511
2534
  const modal = useStore(store, selectors.modal);
@@ -2518,7 +2541,7 @@ const ComboboxPositioner = /*#__PURE__*/React.forwardRef(function ComboboxPositi
2518
2541
  const inputGroupElement = useStore(store, selectors.inputGroupElement);
2519
2542
  const inputInsidePopup = useStore(store, selectors.inputInsidePopup);
2520
2543
  const transitionStatus = useStore(store, selectors.transitionStatus);
2521
- const empty = filteredItems.length === 0;
2544
+ const empty = useListEmpty();
2522
2545
  const resolvedAnchor = anchor ?? (inputInsidePopup ? triggerElement : inputGroupElement ?? inputElement);
2523
2546
  const positioning = useAnchorPositioning({
2524
2547
  anchor: resolvedAnchor,
@@ -2593,18 +2616,16 @@ const ComboboxPopup = /*#__PURE__*/React.forwardRef(function ComboboxPopup(compo
2593
2616
  const store = useComboboxRootContext();
2594
2617
  const positioning = useComboboxPositionerContext();
2595
2618
  const floatingRootContext = useComboboxFloatingContext();
2596
- const {
2597
- filteredItems
2598
- } = useComboboxDerivedItemsContext();
2599
2619
  const mounted = useStore(store, selectors.mounted);
2600
2620
  const open = useStore(store, selectors.open);
2601
2621
  const openMethod = useStore(store, selectors.openMethod);
2622
+ const popupProps = useStore(store, selectors.popupProps);
2602
2623
  const transitionStatus = useStore(store, selectors.transitionStatus);
2603
2624
  const inputInsidePopup = useStore(store, selectors.inputInsidePopup);
2604
2625
  const inputElement = useStore(store, selectors.inputElement);
2605
2626
  const modal = useStore(store, selectors.modal);
2606
2627
  const rootId = useStore(store, selectors.id);
2607
- const empty = filteredItems.length === 0;
2628
+ const empty = useListEmpty();
2608
2629
  const popupId = elementProps.id ?? (inputInsidePopup ? getComboboxPopupId(rootId) : undefined);
2609
2630
  useIsoLayoutEffect(() => {
2610
2631
  // Prefer the rendered DOM id, which a `render` prop element or function may override.
@@ -2633,10 +2654,9 @@ const ComboboxPopup = /*#__PURE__*/React.forwardRef(function ComboboxPopup(compo
2633
2654
  const element = useRenderElement('div', componentProps, {
2634
2655
  state,
2635
2656
  ref: [forwardedRef, store.state.popupRef],
2636
- props: [{
2657
+ props: [popupProps, {
2637
2658
  id: popupId,
2638
2659
  role: inputInsidePopup ? 'dialog' : 'presentation',
2639
- tabIndex: -1,
2640
2660
  onFocus(event) {
2641
2661
  const target = getTarget(event.nativeEvent);
2642
2662
  if (openMethod !== 'touch' && (contains(store.state.listElement, target) || target === event.currentTarget)) {
@@ -2783,7 +2803,7 @@ const ComboboxGroupLabel = /*#__PURE__*/React.forwardRef(function ComboboxGroupL
2783
2803
  useIsoLayoutEffect(() => {
2784
2804
  setLabelId(id);
2785
2805
  return () => {
2786
- setLabelId(undefined);
2806
+ setLabelId(currentId => currentId === id ? undefined : currentId);
2787
2807
  };
2788
2808
  }, [id, setLabelId]);
2789
2809
  const element = useRenderElement('div', componentProps, {
@@ -2825,27 +2845,27 @@ function ComboboxItemInner(props) {
2825
2845
  style,
2826
2846
  value: itemValue = null,
2827
2847
  index: indexProp,
2828
- disabled = false,
2848
+ disabled: disabledProp = false,
2829
2849
  nativeButton = false,
2830
2850
  ...elementProps
2831
2851
  } = componentProps;
2832
- const didPointerDownRef = React.useRef(false);
2833
2852
  const textRef = React.useRef(null);
2834
2853
  const listItem = useCompositeListItem({
2854
+ guess: true,
2835
2855
  index: indexProp,
2836
- textRef,
2837
- indexGuessBehavior: IndexGuessBehavior.GuessFromOrder
2856
+ textRef
2838
2857
  });
2839
2858
  const store = useComboboxRootContext();
2840
2859
  const isRow = useComboboxRowContext();
2841
2860
  const hasItems = useComboboxHasItemsContext();
2842
- const open = useStore(store, selectors.open);
2843
2861
  const selectionMode = useStore(store, selectors.selectionMode);
2862
+ const rootDisabled = useStore(store, selectors.disabled);
2844
2863
  const readOnly = useStore(store, selectors.readOnly);
2845
2864
  const isItemEqualToValue = useStore(store, selectors.isItemEqualToValue);
2865
+ const disabled = rootDisabled || disabledProp;
2846
2866
  const selectable = selectionMode !== 'none';
2847
- const index = indexProp ?? (virtualized ? indexFromFilter ?? -1 : listItem.index);
2848
- const hasRegistered = listItem.index !== -1;
2867
+ const index = indexProp ?? indexFromFilter ?? listItem.index;
2868
+ const hasRegistered = index !== -1;
2849
2869
  const rootId = useStore(store, selectors.id);
2850
2870
  const highlighted = useStore(store, selectors.isActive, index);
2851
2871
  const matchesSelectedValue = useStore(store, selectors.isSelected, itemValue);
@@ -2870,31 +2890,24 @@ function ComboboxItemInner(props) {
2870
2890
  }
2871
2891
  const visibleMap = store.state.valuesRef.current;
2872
2892
  visibleMap[index] = itemValue;
2873
-
2874
- // Stable registry that doesn't depend on filtering. Assume that no
2875
- // filtering had occurred at this point; otherwise, an `items` prop is
2876
- // required.
2877
- if (selectionMode !== 'none') {
2878
- store.state.allValuesRef.current.push(itemValue);
2879
- }
2880
2893
  return () => {
2881
2894
  delete visibleMap[index];
2882
2895
  };
2883
- }, [hasRegistered, hasItems, index, itemValue, store, selectionMode]);
2896
+ }, [hasRegistered, hasItems, index, itemValue, store]);
2884
2897
  useIsoLayoutEffect(() => {
2885
- if (!open) {
2886
- didPointerDownRef.current = false;
2887
- return;
2888
- }
2889
2898
  if (!hasRegistered || hasItems) {
2890
2899
  return;
2891
2900
  }
2901
+
2902
+ // Runs while closed as well (the list can stay mounted via `keepMounted` or a
2903
+ // force-mount) so the index tracks the item's composite position, keeping features
2904
+ // like closed-trigger typeahead in sync when the rendered order changes.
2892
2905
  const selectedValue = store.state.selectedValue;
2893
2906
  const lastSelectedValue = Array.isArray(selectedValue) ? selectedValue[selectedValue.length - 1] : selectedValue;
2894
2907
  if (compareItemEquality(itemValue, lastSelectedValue, isItemEqualToValue)) {
2895
2908
  store.set('selectedIndex', index);
2896
2909
  }
2897
- }, [hasRegistered, hasItems, open, store, index, itemValue, isItemEqualToValue]);
2910
+ }, [hasRegistered, hasItems, store, index, itemValue, isItemEqualToValue]);
2898
2911
  const {
2899
2912
  getButtonProps,
2900
2913
  buttonRef
@@ -2929,7 +2942,12 @@ function ComboboxItemInner(props) {
2929
2942
  // as it should be a `<div>` instead.
2930
2943
  tabIndex: undefined,
2931
2944
  onPointerDownCapture(event) {
2932
- didPointerDownRef.current = true;
2945
+ // The compat `mouseup` only fires for the primary pointer, so a non-primary
2946
+ // touch must not overwrite the shared ref — a mismatch would make the primary
2947
+ // pointer's release read as a drag-select and commit a second time after `click`.
2948
+ if (event.isPrimary) {
2949
+ store.state.pointerDownItemRef.current = event.currentTarget;
2950
+ }
2933
2951
  event.preventDefault();
2934
2952
  },
2935
2953
  onMouseDown(event) {
@@ -2944,8 +2962,8 @@ function ComboboxItemInner(props) {
2944
2962
  commitSelection(event.nativeEvent);
2945
2963
  },
2946
2964
  onMouseUp(event) {
2947
- const pointerStartedOnItem = didPointerDownRef.current;
2948
- didPointerDownRef.current = false;
2965
+ const pointerStartedOnItem = store.state.pointerDownItemRef.current === event.currentTarget;
2966
+ store.state.pointerDownItemRef.current = null;
2949
2967
  if (disabled || readOnly || event.button !== 0 || pointerStartedOnItem || !highlighted) {
2950
2968
  return;
2951
2969
  }
@@ -3081,6 +3099,33 @@ const ComboboxEmpty = /*#__PURE__*/React.forwardRef(function ComboboxEmpty(compo
3081
3099
  });
3082
3100
  if (process.env.NODE_ENV !== "production") ComboboxEmpty.displayName = "ComboboxEmpty";
3083
3101
 
3102
+ /**
3103
+ * A visual separator between items.
3104
+ * Renders a `<div>` element.
3105
+ *
3106
+ * @internal
3107
+ */
3108
+ const ListboxSeparator = /*#__PURE__*/React.forwardRef(function ListboxSeparator(componentProps, forwardedRef) {
3109
+ const {
3110
+ className,
3111
+ render,
3112
+ orientation = 'horizontal',
3113
+ style,
3114
+ ...elementProps
3115
+ } = componentProps;
3116
+ const state = {
3117
+ orientation
3118
+ };
3119
+ return useRenderElement('div', componentProps, {
3120
+ state,
3121
+ ref: forwardedRef,
3122
+ props: [{
3123
+ role: 'presentation'
3124
+ }, elementProps]
3125
+ });
3126
+ });
3127
+ if (process.env.NODE_ENV !== "production") ListboxSeparator.displayName = "ListboxSeparator";
3128
+
3084
3129
  /**
3085
3130
  * Returns the internally filtered items.
3086
3131
  */
@@ -3147,7 +3192,8 @@ const ComboboxLabel = /*#__PURE__*/React.forwardRef(function ComboboxLabel(compo
3147
3192
  id: defaultLabelId,
3148
3193
  fallbackControlId: localControlId,
3149
3194
  setLabelId(nextLabelId) {
3150
- store.set('labelId', nextLabelId);
3195
+ const resolvedLabelId = typeof nextLabelId === 'function' ? nextLabelId(store.state.labelId) : nextLabelId;
3196
+ store.set('labelId', resolvedLabelId);
3151
3197
  }
3152
3198
  });
3153
3199
  return useRenderElement('div', componentProps, {
@@ -3190,11 +3236,10 @@ function ComboboxValue(props) {
3190
3236
  }
3191
3237
 
3192
3238
  const ComboboxItemIndicator = /*#__PURE__*/React.forwardRef(function ComboboxItemIndicator(componentProps, forwardedRef) {
3193
- const keepMounted = componentProps.keepMounted ?? false;
3194
3239
  const {
3195
3240
  selected
3196
3241
  } = useComboboxItemContext();
3197
- const shouldRender = keepMounted || selected;
3242
+ const shouldRender = componentProps.keepMounted || selected;
3198
3243
  if (!shouldRender) {
3199
3244
  return null;
3200
3245
  }
@@ -3324,9 +3369,7 @@ const ComboboxChip = /*#__PURE__*/React.forwardRef(function ComboboxChip(compone
3324
3369
  } = useCompositeListItem();
3325
3370
  function handleKeyDown(event) {
3326
3371
  let nextIndex = index;
3327
- const isRtl = direction === 'rtl';
3328
- const previousChipKey = isRtl ? 'ArrowRight' : 'ArrowLeft';
3329
- const nextChipKey = isRtl ? 'ArrowLeft' : 'ArrowRight';
3372
+ const [previousChipKey, nextChipKey] = getChipNavigationKeys(direction);
3330
3373
  if (event.key === previousChipKey) {
3331
3374
  event.preventDefault();
3332
3375
  if (index > 0) {
@@ -3342,13 +3385,12 @@ const ComboboxChip = /*#__PURE__*/React.forwardRef(function ComboboxChip(compone
3342
3385
  nextIndex = undefined;
3343
3386
  }
3344
3387
  } else if (event.key === 'Backspace' || event.key === 'Delete') {
3345
- const computedNextIndex = index >= selectedValue.length - 1 ? selectedValue.length - 2 : index;
3346
- nextIndex = computedNextIndex >= 0 ? computedNextIndex : undefined;
3388
+ nextIndex = getIndexAfterChipRemoval(index, selectedValue.length);
3347
3389
  stopEvent(event);
3348
3390
  store.state.setIndices({
3349
3391
  activeIndex: null,
3350
3392
  selectedIndex: null,
3351
- type: 'keyboard'
3393
+ type: keyboard
3352
3394
  });
3353
3395
  store.state.setSelectedValue(selectedValue.filter((_, i) => i !== index), createChangeEventDetails(none, event.nativeEvent));
3354
3396
  } else if (event.key === 'Enter' || event.key === ' ') {
@@ -3448,7 +3490,7 @@ const ComboboxChipRemove = /*#__PURE__*/React.forwardRef(function ComboboxChipRe
3448
3490
  if (removedIndex !== -1 && activeIndex === removedIndex) {
3449
3491
  store.state.setIndices({
3450
3492
  activeIndex: null,
3451
- type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
3493
+ type: store.state.keyboardActiveRef.current ? keyboard : pointer
3452
3494
  });
3453
3495
  }
3454
3496
  }
@@ -3469,18 +3511,12 @@ const ComboboxChipRemove = /*#__PURE__*/React.forwardRef(function ComboboxChipRe
3469
3511
  event.preventDefault();
3470
3512
  },
3471
3513
  onClick(event) {
3472
- if (disabled || readOnly) {
3473
- return;
3474
- }
3475
3514
  const eventDetails = removeChip(event);
3476
3515
  if (!eventDetails.isPropagationAllowed) {
3477
3516
  event.stopPropagation();
3478
3517
  }
3479
3518
  },
3480
3519
  onKeyDown(event) {
3481
- if (disabled || readOnly) {
3482
- return;
3483
- }
3484
3520
  if (event.key === 'Enter' || event.key === ' ') {
3485
3521
  const eventDetails = removeChip(event);
3486
3522
  if (!eventDetails.isPropagationAllowed) {
@@ -3494,4 +3530,12 @@ const ComboboxChipRemove = /*#__PURE__*/React.forwardRef(function ComboboxChipRe
3494
3530
  });
3495
3531
  if (process.env.NODE_ENV !== "production") ComboboxChipRemove.displayName = "ComboboxChipRemove";
3496
3532
 
3497
- export { useCoreFilter as A, AriaCombobox as B, ComboboxRoot as C, useComboboxInputValueContext as D, ComboboxLabel as a, ComboboxValue as b, ComboboxInput as c, ComboboxInputGroup as d, ComboboxTrigger as e, ComboboxList as f, ComboboxStatus as g, ComboboxPortal as h, ComboboxBackdrop as i, ComboboxPositioner as j, ComboboxPopup as k, ComboboxArrow as l, ComboboxIcon as m, ComboboxGroup as n, ComboboxGroupLabel as o, ComboboxItem as p, ComboboxItemIndicator as q, ComboboxChips as r, ComboboxChip as s, ComboboxChipRemove as t, ComboboxRow as u, ComboboxCollection as v, ComboboxEmpty as w, ComboboxClear as x, useComboboxFilter as y, useFilteredItems as z };
3533
+ /**
3534
+ * A visual separator between items or groups.
3535
+ * Renders a `<div>` element.
3536
+ *
3537
+ * Documentation: [Base UI Combobox](https://base-ui.com/react/components/combobox)
3538
+ */
3539
+ const ComboboxSeparator = ListboxSeparator;
3540
+
3541
+ export { useFilteredItems as A, useCoreFilter as B, ComboboxRoot as C, AriaCombobox as D, useComboboxInputValueContext as E, ListboxSeparator as L, ComboboxLabel as a, ComboboxValue as b, ComboboxInput as c, ComboboxInputGroup as d, ComboboxTrigger as e, ComboboxList as f, ComboboxStatus as g, ComboboxPortal as h, ComboboxBackdrop as i, ComboboxPositioner as j, ComboboxPopup as k, ComboboxArrow as l, ComboboxIcon as m, ComboboxGroup as n, ComboboxGroupLabel as o, ComboboxItem as p, ComboboxItemIndicator as q, ComboboxChips as r, ComboboxChip as s, ComboboxChipRemove as t, ComboboxRow as u, ComboboxCollection as v, ComboboxEmpty as w, ComboboxClear as x, ComboboxSeparator as y, useComboboxFilter as z };