@jobber/components 9.6.0 → 9.6.2

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 (187) hide show
  1. package/dist/ActivityIndicator-cjs.js +1 -0
  2. package/dist/ActivityIndicator-es.js +1 -0
  3. package/dist/Autocomplete/index.cjs +2 -0
  4. package/dist/Autocomplete/index.mjs +2 -0
  5. package/dist/Autocomplete-cjs.js +6 -4
  6. package/dist/Autocomplete-es.js +3 -1
  7. package/dist/Banner-cjs.js +1 -0
  8. package/dist/Banner-es.js +1 -0
  9. package/dist/Card/index.cjs +2 -1
  10. package/dist/Card/index.mjs +2 -1
  11. package/dist/Chip/index.cjs +2 -0
  12. package/dist/Chip/index.mjs +2 -0
  13. package/dist/Chip-cjs.js +1 -0
  14. package/dist/Chip-es.js +1 -0
  15. package/dist/Chips/InternalChipDismissible/hooks/index.cjs +2 -0
  16. package/dist/Chips/InternalChipDismissible/hooks/index.mjs +2 -0
  17. package/dist/Chips/InternalChipDismissible/index.cjs +2 -0
  18. package/dist/Chips/InternalChipDismissible/index.mjs +2 -0
  19. package/dist/Chips/index.cjs +2 -0
  20. package/dist/Chips/index.mjs +2 -0
  21. package/dist/Combobox/index.cjs +4 -2
  22. package/dist/Combobox/index.mjs +4 -2
  23. package/dist/{ComboboxSeparator-cjs.js → ComboboxChipRemove-cjs.js} +455 -501
  24. package/dist/{ComboboxSeparator-es.js → ComboboxChipRemove-es.js} +456 -500
  25. package/dist/ComboboxPrimitive-cjs.js +30 -29
  26. package/dist/ComboboxPrimitive-es.js +3 -2
  27. package/dist/ConfirmationModal/index.cjs +2 -0
  28. package/dist/ConfirmationModal/index.mjs +2 -0
  29. package/dist/ConfirmationModal-cjs.js +1 -0
  30. package/dist/ConfirmationModal-es.js +1 -0
  31. package/dist/DataDump/index.cjs +2 -1
  32. package/dist/DataDump/index.mjs +2 -1
  33. package/dist/DataDump-cjs.js +1 -0
  34. package/dist/DataDump-es.js +1 -0
  35. package/dist/DataList/components/DataListActions/index.cjs +2 -0
  36. package/dist/DataList/components/DataListActions/index.mjs +2 -0
  37. package/dist/DataList/components/DataListBulkActions/index.cjs +2 -0
  38. package/dist/DataList/components/DataListBulkActions/index.mjs +2 -0
  39. package/dist/DataList/components/DataListFilters/components/DataListSort/index.cjs +2 -0
  40. package/dist/DataList/components/DataListFilters/components/DataListSort/index.mjs +2 -0
  41. package/dist/DataList/components/DataListFilters/index.cjs +2 -0
  42. package/dist/DataList/components/DataListFilters/index.mjs +2 -0
  43. package/dist/DataList/components/DataListHeader/index.cjs +2 -0
  44. package/dist/DataList/components/DataListHeader/index.mjs +2 -0
  45. package/dist/DataList/components/DataListItem/index.cjs +2 -0
  46. package/dist/DataList/components/DataListItem/index.mjs +2 -0
  47. package/dist/DataList/components/DataListItemActions/index.cjs +2 -0
  48. package/dist/DataList/components/DataListItemActions/index.mjs +2 -0
  49. package/dist/DataList/components/DataListItemActionsOverflow/index.cjs +2 -0
  50. package/dist/DataList/components/DataListItemActionsOverflow/index.mjs +2 -0
  51. package/dist/DataList/components/DataListItems/index.cjs +2 -0
  52. package/dist/DataList/components/DataListItems/index.mjs +2 -0
  53. package/dist/DataList/components/DataListLayout/index.cjs +2 -0
  54. package/dist/DataList/components/DataListLayout/index.mjs +2 -0
  55. package/dist/DataList/components/DataListLayoutActions/index.cjs +2 -0
  56. package/dist/DataList/components/DataListLayoutActions/index.mjs +2 -0
  57. package/dist/DataList/index.cjs +2 -0
  58. package/dist/DataList/index.mjs +2 -0
  59. package/dist/DataTable-cjs.js +1 -0
  60. package/dist/DataTable-es.js +1 -0
  61. package/dist/DatePicker/index.cjs +2 -0
  62. package/dist/DatePicker/index.mjs +2 -0
  63. package/dist/DatePicker-cjs.js +8 -7
  64. package/dist/DatePicker-es.js +6 -5
  65. package/dist/Dialog/index.cjs +2 -1
  66. package/dist/Dialog/index.mjs +2 -1
  67. package/dist/DrawerDescription-cjs.js +99 -268
  68. package/dist/DrawerDescription-es.js +103 -270
  69. package/dist/DrawerGrid-cjs.js +1 -0
  70. package/dist/DrawerGrid-es.js +1 -0
  71. package/dist/DrawerTitle-cjs.js +6 -2
  72. package/dist/DrawerTitle-es.js +7 -3
  73. package/dist/FeatureSwitch-cjs.js +1 -0
  74. package/dist/FeatureSwitch-es.js +1 -0
  75. package/dist/FieldDescription-cjs.js +181 -184
  76. package/dist/FieldDescription-es.js +184 -187
  77. package/dist/FilterPicker/components/FilterPickerActivator/index.cjs +2 -0
  78. package/dist/FilterPicker/components/FilterPickerActivator/index.mjs +2 -0
  79. package/dist/FilterPicker/components/FilterPickerContent/index.cjs +2 -0
  80. package/dist/FilterPicker/components/FilterPickerContent/index.mjs +2 -0
  81. package/dist/FilterPicker/components/FilterPickerTrigger/index.cjs +2 -0
  82. package/dist/FilterPicker/components/FilterPickerTrigger/index.mjs +2 -0
  83. package/dist/FilterPicker/index.cjs +2 -0
  84. package/dist/FilterPicker/index.mjs +2 -0
  85. package/dist/FilterPickerContent-cjs.js +5 -4
  86. package/dist/FilterPickerContent-es.js +2 -1
  87. package/dist/FormFieldWrapper-cjs.js +1 -0
  88. package/dist/FormFieldWrapper-es.js +1 -0
  89. package/dist/FormatFile/index.cjs +2 -0
  90. package/dist/FormatFile/index.mjs +2 -0
  91. package/dist/FormatFile-cjs.js +1 -0
  92. package/dist/FormatFile-es.js +1 -0
  93. package/dist/Gallery/index.cjs +2 -0
  94. package/dist/Gallery/index.mjs +2 -0
  95. package/dist/InputAvatar-cjs.js +1 -0
  96. package/dist/InputAvatar-es.js +1 -0
  97. package/dist/InputDate/index.cjs +2 -0
  98. package/dist/InputDate/index.mjs +2 -0
  99. package/dist/InputDate-cjs.js +1 -0
  100. package/dist/InputDate-es.js +1 -0
  101. package/dist/InputNumber/index.cjs +2 -1
  102. package/dist/InputNumber/index.mjs +2 -1
  103. package/dist/InternalChipDismissible-cjs.js +1 -0
  104. package/dist/InternalChipDismissible-es.js +1 -0
  105. package/dist/LightBox/index.cjs +2 -0
  106. package/dist/LightBox/index.mjs +2 -0
  107. package/dist/LightBox-cjs.js +1 -0
  108. package/dist/LightBox-es.js +1 -0
  109. package/dist/Menu/index.cjs +2 -1
  110. package/dist/Menu/index.mjs +2 -1
  111. package/dist/Menu-cjs.js +6 -4
  112. package/dist/Menu-es.js +3 -1
  113. package/dist/MenuSubmenuTrigger-cjs.js +164 -206
  114. package/dist/MenuSubmenuTrigger-es.js +152 -194
  115. package/dist/Modal/index.cjs +3 -1
  116. package/dist/Modal/index.mjs +4 -2
  117. package/dist/NumberFieldInput-cjs.js +308 -187
  118. package/dist/NumberFieldInput-es.js +308 -187
  119. package/dist/Page/index.cjs +2 -1
  120. package/dist/Page/index.mjs +2 -1
  121. package/dist/Page-cjs.js +1 -0
  122. package/dist/Page-es.js +1 -0
  123. package/dist/Popover/index.cjs +2 -0
  124. package/dist/Popover/index.mjs +2 -0
  125. package/dist/Popover-cjs.js +9 -7
  126. package/dist/Popover-es.js +3 -1
  127. package/dist/ProgressBar-cjs.js +1 -0
  128. package/dist/ProgressBar-es.js +1 -0
  129. package/dist/ScrollAreaViewport-cjs.js +1093 -747
  130. package/dist/ScrollAreaViewport-es.js +1081 -734
  131. package/dist/Select/index.cjs +2 -1
  132. package/dist/Select/index.mjs +2 -1
  133. package/dist/SelectGroupLabel-cjs.js +306 -207
  134. package/dist/SelectGroupLabel-es.js +302 -203
  135. package/dist/SideDrawer-cjs.js +1 -0
  136. package/dist/SideDrawer-es.js +1 -0
  137. package/dist/Spinner-cjs.js +1 -0
  138. package/dist/Spinner-es.js +1 -0
  139. package/dist/Tooltip/index.cjs +2 -0
  140. package/dist/Tooltip/index.mjs +2 -0
  141. package/dist/Tooltip-cjs.js +7 -6
  142. package/dist/Tooltip-es.js +2 -1
  143. package/dist/dialogReturnFocus-cjs.js +1 -1
  144. package/dist/dialogReturnFocus-es.js +2 -2
  145. package/dist/floating-ui.react-cjs.js +192 -2564
  146. package/dist/floating-ui.react-dom-cjs.js +2103 -0
  147. package/dist/floating-ui.react-dom-es.js +2072 -0
  148. package/dist/floating-ui.react-es.js +156 -2519
  149. package/dist/floating-ui.utils-cjs.js +342 -0
  150. package/dist/floating-ui.utils-es.js +300 -0
  151. package/dist/index.cjs +4 -3
  152. package/dist/index.mjs +4 -3
  153. package/dist/primitives/BottomSheet/index.cjs +1 -1
  154. package/dist/primitives/BottomSheet/index.mjs +1 -1
  155. package/dist/primitives/ComboboxPrimitive/index.cjs +4 -2
  156. package/dist/primitives/ComboboxPrimitive/index.mjs +4 -2
  157. package/dist/primitives/SelectPrimitive/index.cjs +2 -0
  158. package/dist/primitives/SelectPrimitive/index.mjs +2 -0
  159. package/dist/primitives/index.cjs +3 -2
  160. package/dist/primitives/index.mjs +3 -2
  161. package/dist/scrollEdges-cjs.js +11 -1
  162. package/dist/scrollEdges-es.js +11 -1
  163. package/dist/sharedHelpers/i18n/index.cjs +1 -0
  164. package/dist/sharedHelpers/i18n/index.d.ts +1 -0
  165. package/dist/sharedHelpers/i18n/index.mjs +1 -1
  166. package/dist/sharedHelpers/i18n/releasedLanguages.d.ts +12 -0
  167. package/dist/sharedHelpers/i18n/useReleasedLocale.d.ts +6 -0
  168. package/dist/showToast-cjs.js +1 -1
  169. package/dist/showToast-es.js +1 -1
  170. package/dist/testUtils/releasedLanguages.d.ts +16 -0
  171. package/dist/unstyledPrimitives/index.cjs +478 -720
  172. package/dist/unstyledPrimitives/index.mjs +421 -663
  173. package/dist/useAtlantisI18n-cjs.js +44 -11
  174. package/dist/useAtlantisI18n-es.js +44 -12
  175. package/dist/useCompositeListItem-cjs.js +361 -2108
  176. package/dist/useCompositeListItem-es.js +331 -2076
  177. package/dist/useLabel-cjs.js +3 -2
  178. package/dist/useLabel-es.js +3 -2
  179. package/dist/useRenderElement-cjs.js +7 -9
  180. package/dist/useRenderElement-es.js +7 -9
  181. package/dist/useScrollLock-cjs.js +344 -819
  182. package/dist/useScrollLock-es.js +296 -732
  183. package/dist/useScrollToActive-cjs.js +5 -4
  184. package/dist/useScrollToActive-es.js +2 -1
  185. package/package.json +3 -3
  186. package/dist/scrollable-cjs.js +0 -58
  187. package/dist/scrollable-es.js +0 -54
@@ -1,12 +1,12 @@
1
1
  import * as React from 'react';
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';
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';
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, 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';
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';
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';
9
8
  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: state => state.id,
86
- labelId: state => state.labelId,
87
- items: state => state.items,
88
- selectedValue: state => state.selectedValue,
89
- hasSelectionChips: state => {
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 => {
90
90
  const selectedValue = state.selectedValue;
91
91
  return Array.isArray(selectedValue) && selectedValue.length > 0;
92
- },
93
- hasSelectedValue: state => {
92
+ }),
93
+ hasSelectedValue: createSelector(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: (state, enabled) => {
105
+ }),
106
+ hasNullItemLabel: createSelector((state, enabled) => {
107
107
  return enabled ? hasNullItemLabel(state.items) : false;
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) => {
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) => {
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: 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
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)
152
152
  };
153
153
 
154
154
  /**
@@ -171,7 +171,8 @@ function createCollatorItemFilter(collatorFilter, itemToStringLabel) {
171
171
  if (item == null) {
172
172
  return false;
173
173
  }
174
- return collatorFilter.contains(item, query, itemToStringLabel);
174
+ const itemString = stringifyAsLabel(item, itemToStringLabel);
175
+ return collatorFilter.contains(itemString, query);
175
176
  };
176
177
  }
177
178
 
@@ -187,13 +188,14 @@ function createSingleSelectionCollatorFilter(collatorFilter, itemToStringLabel,
187
188
  if (!query) {
188
189
  return true;
189
190
  }
191
+ const itemString = stringifyAsLabel(item, itemToStringLabel);
190
192
  const selectedString = selectedValue != null ? stringifyAsLabel(selectedValue, itemToStringLabel) : '';
191
193
 
192
194
  // Handle case-insensitive matching consistently
193
195
  if (selectedString && collatorFilter.contains(selectedString, query) && selectedString.length === query.length) {
194
196
  return true;
195
197
  }
196
- return collatorFilter.contains(item, query, itemToStringLabel);
198
+ return collatorFilter.contains(itemString, query);
197
199
  };
198
200
  }
199
201
 
@@ -265,8 +267,9 @@ function useComboboxFilter(options = {}) {
265
267
  return createSingleSelectionCollatorFilter(coreFilter, itemToString, value)(item, query);
266
268
  }, [coreFilter, value, multiple]);
267
269
  return React.useMemo(() => ({
268
- ...coreFilter,
269
- contains
270
+ contains,
271
+ startsWith: coreFilter.startsWith,
272
+ endsWith: coreFilter.endsWith
270
273
  }), [contains, coreFilter]);
271
274
  }
272
275
 
@@ -287,7 +290,7 @@ function AriaCombobox(props) {
287
290
  inputValue: inputValueProp,
288
291
  open: openProp,
289
292
  defaultOpen = false,
290
- selectionMode,
293
+ selectionMode = 'none',
291
294
  onItemHighlighted: onItemHighlightedProp,
292
295
  name: nameProp,
293
296
  form,
@@ -360,10 +363,12 @@ function AriaCombobox(props) {
360
363
  */
361
364
  const valuesRef = React.useRef([]);
362
365
  /**
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.
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.
365
370
  */
366
- const pointerDownItemRef = React.useRef(null);
371
+ const allValuesRef = React.useRef([]);
367
372
  const disabled = fieldDisabled || disabledProp;
368
373
  const name = fieldName ?? nameProp;
369
374
  const multiple = selectionMode === 'multiple';
@@ -390,10 +395,11 @@ function AriaCombobox(props) {
390
395
  if (filterProp !== undefined) {
391
396
  return filterProp;
392
397
  }
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.
398
+ if (single && !queryChangedAfterOpen) {
399
+ return createSingleSelectionCollatorFilter(collatorFilter, itemToStringLabel, selectedValue);
400
+ }
395
401
  return createCollatorItemFilter(collatorFilter, itemToStringLabel);
396
- }, [filterProp, collatorFilter, itemToStringLabel]);
402
+ }, [filterProp, single, selectedValue, queryChangedAfterOpen, collatorFilter, itemToStringLabel]);
397
403
 
398
404
  // If neither inputValue nor defaultInputValue are provided, derive it from the
399
405
  // selected value for single mode so the input reflects the selection on mount.
@@ -419,9 +425,9 @@ function AriaCombobox(props) {
419
425
  state: 'open'
420
426
  });
421
427
  const isGrouped = isGroupedItems(items);
422
- const query = closeQuery ?? String(inputValue).trim();
428
+ const query = closeQuery ?? (inputValue === '' ? '' : String(inputValue).trim());
423
429
  const selectedLabelString = single ? stringifyAsLabel(selectedValue, itemToStringLabel) : '';
424
- const shouldBypassFiltering = single && !queryChangedAfterOpen && query !== '' && selectedLabelString.length === query.length && collatorFilter.contains(selectedLabelString, query);
430
+ const shouldBypassFiltering = single && !queryChangedAfterOpen && query !== '' && selectedLabelString !== '' && selectedLabelString.length === query.length && collatorFilter.contains(selectedLabelString, query);
425
431
  const filterQuery = shouldBypassFiltering ? '' : query;
426
432
  const shouldIgnoreExternalFiltering = hasItems && hasFilteredItemsProp && shouldBypassFiltering;
427
433
  const flatItems = React.useMemo(() => {
@@ -448,18 +454,12 @@ function AriaCombobox(props) {
448
454
  if (limit > -1 && currentCount >= limit) {
449
455
  break;
450
456
  }
451
- const remainingLimit = limit > -1 ? limit - currentCount : Infinity;
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
- }
457
+ const candidateItems = filterQuery === '' ? group.items : group.items.filter(item => filter(item, filterQuery, itemToStringLabel));
458
+ if (candidateItems.length === 0) {
459
+ continue;
462
460
  }
461
+ const remainingLimit = limit > -1 ? limit - currentCount : Infinity;
462
+ const itemsToTake = candidateItems.slice(0, remainingLimit);
463
463
  if (itemsToTake.length > 0) {
464
464
  const newGroup = {
465
465
  ...group,
@@ -499,85 +499,76 @@ function AriaCombobox(props) {
499
499
  }
500
500
  return filteredItems;
501
501
  }, [filteredItems, isGrouped]);
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;
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;
581
572
  const fieldRawValue = selectionMode === 'none' ? inputValue : selectedValue;
582
573
  const fieldStringValue = React.useMemo(() => {
583
574
  if (selectionMode === 'none') {
@@ -620,38 +611,35 @@ function AriaCombobox(props) {
620
611
  store.set('forceMounted', true);
621
612
  }
622
613
  });
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
- };
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();
639
619
  }
640
- onItemHighlighted(value, createGenericEventDetails(type, undefined, {
641
- index
642
- }));
643
- });
620
+ }, [forceMount, selectedValue]);
644
621
  const setIndices = useStableCallback(options => {
645
622
  store.update(options);
646
- const activeIndexOption = options.activeIndex;
647
- if (activeIndexOption === undefined) {
623
+ const type = options.type || 'none';
624
+ if (options.activeIndex === undefined) {
648
625
  return;
649
626
  }
650
- const type = options.type || none;
651
- if (activeIndexOption === null) {
652
- emitHighlight(undefined, -1, type);
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
+ }
653
634
  } else {
654
- emitHighlight(valuesRef.current[activeIndexOption], activeIndexOption, type);
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
+ }));
655
643
  }
656
644
  });
657
645
  const setInputValue = useStableCallback((next, eventDetails) => {
@@ -664,11 +652,6 @@ function AriaCombobox(props) {
664
652
  // If user is typing, ensure we don't auto-highlight on open due to a race
665
653
  // with the post-open effect that sets this flag.
666
654
  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
- }
672
655
  const event = eventDetails.event;
673
656
  const inputType = event.inputType;
674
657
  // Treat composition commits as typed input; autofill may omit `inputType` or
@@ -684,34 +667,10 @@ function AriaCombobox(props) {
684
667
  pendingQueryHighlightRef.current = {
685
668
  hasQuery
686
669
  };
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)) {
670
+ if (hasQuery && autoHighlightMode && store.state.activeIndex == null) {
705
671
  store.set('activeIndex', 0);
706
672
  }
707
673
  }
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
- };
715
674
  }
716
675
  setInputValueUnwrapped(next);
717
676
  });
@@ -723,7 +682,7 @@ function AriaCombobox(props) {
723
682
  // If the `Empty` component is not used, the positioner or popup should be hidden
724
683
  // with CSS. In this case, allow the Escape key to bubble to close a parent popup
725
684
  // if there are no items to show.
726
- if (eventDetails.reason === escapeKey && hasItems && flatFilteredItems.length === 0 && !emptyRef.current) {
685
+ if (eventDetails.reason === 'escape-key' && hasItems && flatFilteredItems.length === 0 && !store.state.emptyRef.current) {
727
686
  eventDetails.allowPropagation();
728
687
  }
729
688
  props.onOpenChange?.(nextOpen, eventDetails);
@@ -733,10 +692,10 @@ function AriaCombobox(props) {
733
692
 
734
693
  // If reopening interrupts the close animation, handleUnmount won't run to clear the
735
694
  // frozen closeQuery and pending popup input.
736
- if (nextOpen && inputInsidePopup && !inline && closeQuery !== null) {
695
+ if (nextOpen && multiple && inputInsidePopup && !inline && closeQuery !== null) {
737
696
  setQueryChangedAfterOpen(false);
738
697
  setCloseQuery(null);
739
- if (inputValue !== '' && eventDetails.reason !== inputChange) {
698
+ if (inputValue !== '') {
740
699
  setInputValue('', createChangeEventDetails(inputClear, eventDetails.event));
741
700
  }
742
701
  }
@@ -790,8 +749,18 @@ function AriaCombobox(props) {
790
749
  if (shouldFillInput) {
791
750
  setInputValue(stringifyAsLabel(nextValue, itemToStringLabel), createChangeEventDetails(eventDetails.reason, eventDetails.event));
792
751
  }
752
+ if (single && nextValue != null && eventDetails.reason !== inputChange && queryChangedAfterOpen && !inline) {
753
+ setCloseQuery(query);
754
+ }
793
755
  });
794
- const handleSelection = useStableCallback((event, itemValue) => {
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
+ }
795
764
  const targetEl = getTarget(event);
796
765
  const overrideEvent = selectionEventRef.current ?? event;
797
766
  selectionEventRef.current = null;
@@ -807,8 +776,8 @@ function AriaCombobox(props) {
807
776
  }
808
777
  if (multiple) {
809
778
  const currentSelectedValue = Array.isArray(selectedValue) ? selectedValue : [];
810
- const isCurrentlySelected = selectedValueIncludes(currentSelectedValue, itemValue, isItemEqualToValue);
811
- const nextValue = isCurrentlySelected ? removeItem(currentSelectedValue, itemValue, isItemEqualToValue) : [...currentSelectedValue, itemValue];
779
+ const isCurrentlySelected = selectedValueIncludes(currentSelectedValue, itemValue, store.state.isItemEqualToValue);
780
+ const nextValue = isCurrentlySelected ? removeItem(currentSelectedValue, itemValue, store.state.isItemEqualToValue) : [...currentSelectedValue, itemValue];
812
781
  setSelectedValue(nextValue, eventDetails);
813
782
  if (eventDetails.isCanceled) {
814
783
  return;
@@ -831,6 +800,9 @@ function AriaCombobox(props) {
831
800
  }
832
801
  });
833
802
  const requestSubmit = useStableCallback(() => {
803
+ if (!store.state.submitOnItemClick) {
804
+ return;
805
+ }
834
806
  const formElement = validation.inputRef.current?.form ?? store.state.inputElement?.form;
835
807
  if (formElement && typeof formElement.requestSubmit === 'function') {
836
808
  formElement.requestSubmit();
@@ -903,29 +875,25 @@ function AriaCombobox(props) {
903
875
  React.useImperativeHandle(props.actionsRef, () => ({
904
876
  unmount: handleUnmount
905
877
  }), [handleUnmount]);
906
- useIsoLayoutEffect(function syncClosedState() {
907
- if (open) {
878
+ useIsoLayoutEffect(function syncSelectedIndex() {
879
+ if (open || selectionMode === 'none') {
908
880
  return;
909
881
  }
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;
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
+ });
916
895
  }
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]);
896
+ }, [open, selectedValue, items, selectionMode, flatItems, multiple, isItemEqualToValue, setIndices]);
929
897
  useIsoLayoutEffect(() => {
930
898
  if (items) {
931
899
  valuesRef.current = flatFilteredItems;
@@ -935,52 +903,14 @@ function AriaCombobox(props) {
935
903
  useIsoLayoutEffect(() => {
936
904
  const pendingHighlight = pendingQueryHighlightRef.current;
937
905
  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;
941
906
  if (pendingHighlight.hasQuery) {
942
- if (autoHighlightMode && listIsNavigable) {
907
+ if (autoHighlightMode) {
943
908
  store.set('activeIndex', 0);
944
909
  }
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
- }
910
+ } else if (autoHighlightMode === 'always') {
911
+ store.set('activeIndex', 0);
983
912
  }
913
+ pendingQueryHighlightRef.current = null;
984
914
  }
985
915
  if (!open && !inline) {
986
916
  return;
@@ -993,11 +923,21 @@ function AriaCombobox(props) {
993
923
  store.set('activeIndex', 0);
994
924
  return;
995
925
  }
996
- emitHighlight(undefined, -1, none);
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
+ }
997
932
  return;
998
933
  }
999
934
  if (storeActiveIndex >= candidateItems.length) {
1000
- emitHighlight(undefined, -1, none);
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
+ }
1001
941
  store.set('activeIndex', null);
1002
942
  return;
1003
943
  }
@@ -1005,12 +945,15 @@ function AriaCombobox(props) {
1005
945
  const previouslyHighlightedItemValue = lastHighlightRef.current.value;
1006
946
  const isSameItem = previouslyHighlightedItemValue !== NO_ACTIVE_VALUE && compareItemEquality(itemValue, previouslyHighlightedItemValue, store.state.isItemEqualToValue);
1007
947
  if (lastHighlightRef.current.index !== storeActiveIndex || !isSameItem) {
1008
- emitHighlight(itemValue, storeActiveIndex, none);
948
+ lastHighlightRef.current = {
949
+ value: itemValue,
950
+ index: storeActiveIndex
951
+ };
952
+ store.state.onItemHighlighted(itemValue, createGenericEventDetails(none, undefined, {
953
+ index: storeActiveIndex
954
+ }));
1009
955
  }
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]);
956
+ }, [activeIndex, autoHighlightMode, hasFilteredItemsProp, hasItems, flatFilteredItems, inline, open, store]);
1014
957
  useIsoLayoutEffect(() => {
1015
958
  if (selectionMode === 'none') {
1016
959
  setFilled(String(inputValue) !== '');
@@ -1041,12 +984,6 @@ function AriaCombobox(props) {
1041
984
  }
1042
985
  setQueryChangedAfterOpen(true);
1043
986
  });
1044
- function syncInputToSelectedLabel() {
1045
- const nextInputValue = stringifyAsLabel(selectedValue, itemToStringLabel);
1046
- if (inputValue !== nextInputValue) {
1047
- setInputValue(nextInputValue, createChangeEventDetails(none));
1048
- }
1049
- }
1050
987
  useValueChanged(selectedValue, () => {
1051
988
  if (selectionMode === 'none') {
1052
989
  return;
@@ -1055,7 +992,10 @@ function AriaCombobox(props) {
1055
992
  setDirty(isSelectedValueDirty(selectedValue));
1056
993
  validation.change(selectedValue);
1057
994
  if (single && !hasInputValue && !inputInsidePopup) {
1058
- syncInputToSelectedLabel();
995
+ const nextInputValue = stringifyAsLabel(selectedValue, itemToStringLabel);
996
+ if (inputValue !== nextInputValue) {
997
+ setInputValue(nextInputValue, createChangeEventDetails(none));
998
+ }
1059
999
  }
1060
1000
  });
1061
1001
  useValueChanged(inputValue, () => {
@@ -1070,7 +1010,10 @@ function AriaCombobox(props) {
1070
1010
  if (!single || hasInputValue || inputInsidePopup || queryChangedAfterOpen) {
1071
1011
  return;
1072
1012
  }
1073
- syncInputToSelectedLabel();
1013
+ const nextInputValue = stringifyAsLabel(selectedValue, itemToStringLabel);
1014
+ if (inputValue !== nextInputValue) {
1015
+ setInputValue(nextInputValue, createChangeEventDetails(none));
1016
+ }
1074
1017
  });
1075
1018
  const floatingRootContext = useFloatingRootContext({
1076
1019
  open: inline ? true : open,
@@ -1080,19 +1023,18 @@ function AriaCombobox(props) {
1080
1023
  floating: positionerElement
1081
1024
  }
1082
1025
  });
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';
1026
+ let ariaHasPopup;
1027
+ let ariaExpanded;
1028
+ if (!inline) {
1029
+ ariaHasPopup = grid ? 'grid' : 'listbox';
1030
+ ariaExpanded = open ? 'true' : 'false';
1031
+ }
1088
1032
  const role = React.useMemo(() => {
1089
1033
  const isPlainInput = inputElement?.tagName === 'INPUT';
1090
1034
  // During SSR and initial hydration, the input ref is not available yet.
1091
1035
  // Assume an input-like control so combobox ARIA attributes are present.
1092
1036
  const shouldTreatAsInput = inputElement == null || isPlainInput;
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;
1037
+ const shouldApplyAria = shouldTreatAsInput || open;
1096
1038
  const reference = shouldTreatAsInput ? {
1097
1039
  autoComplete: 'off',
1098
1040
  spellCheck: 'false',
@@ -1103,7 +1045,7 @@ function AriaCombobox(props) {
1103
1045
  reference.role = 'combobox';
1104
1046
  reference['aria-expanded'] = ariaExpanded;
1105
1047
  reference['aria-haspopup'] = ariaHasPopup;
1106
- reference['aria-controls'] = expanded ? listElement?.id : undefined;
1048
+ reference['aria-controls'] = open ? listElement?.id : undefined;
1107
1049
  reference['aria-autocomplete'] = autoComplete;
1108
1050
  }
1109
1051
  return {
@@ -1112,7 +1054,7 @@ function AriaCombobox(props) {
1112
1054
  role: 'presentation'
1113
1055
  }
1114
1056
  };
1115
- }, [inputElement, expanded, ariaExpanded, ariaHasPopup, listElement?.id, autoComplete]);
1057
+ }, [inputElement, open, ariaExpanded, ariaHasPopup, listElement?.id, autoComplete]);
1116
1058
  const click = useClick(floatingRootContext, {
1117
1059
  enabled: !readOnly && !disabled && openOnInputClick,
1118
1060
  event: 'mousedown-only',
@@ -1166,7 +1108,7 @@ function AriaCombobox(props) {
1166
1108
  } else {
1167
1109
  setIndices({
1168
1110
  activeIndex: nextActiveIndex,
1169
- type: keyboardActiveRef.current ? keyboard : pointer
1111
+ type: keyboardActiveRef.current ? 'keyboard' : 'pointer'
1170
1112
  });
1171
1113
  }
1172
1114
  }
@@ -1181,8 +1123,7 @@ function AriaCombobox(props) {
1181
1123
  }
1182
1124
  }
1183
1125
  }, dismiss.reference, click.reference, role.reference), [listNavigation.reference, dismiss.reference, click.reference, role.reference, grid, store]);
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]);
1126
+ const popupProps = React.useMemo(() => mergeProps(FOCUSABLE_POPUP_PROPS, listNavigation.floating, dismiss.floating, role.floating), [listNavigation.floating, dismiss.floating, role.floating]);
1186
1127
  const itemProps = React.useMemo(() => {
1187
1128
  const listNavigationItemProps = listNavigation.item;
1188
1129
  if (!listNavigationItemProps) {
@@ -1200,7 +1141,6 @@ function AriaCombobox(props) {
1200
1141
  store.update({
1201
1142
  inline: inlineProp,
1202
1143
  popupProps,
1203
- listProps,
1204
1144
  inputProps,
1205
1145
  triggerProps,
1206
1146
  itemProps,
@@ -1208,10 +1148,10 @@ function AriaCombobox(props) {
1208
1148
  setInputValue,
1209
1149
  setSelectedValue,
1210
1150
  setIndices,
1151
+ onItemHighlighted,
1211
1152
  handleSelection,
1212
1153
  forceMount,
1213
- requestSubmit,
1214
- onOpenChangeComplete
1154
+ requestSubmit
1215
1155
  });
1216
1156
  });
1217
1157
  useIsoLayoutEffect(() => {
@@ -1224,7 +1164,6 @@ function AriaCombobox(props) {
1224
1164
  items,
1225
1165
  inline: inlineProp,
1226
1166
  popupProps,
1227
- listProps,
1228
1167
  inputProps,
1229
1168
  triggerProps,
1230
1169
  openMethod,
@@ -1236,7 +1175,9 @@ function AriaCombobox(props) {
1236
1175
  readOnly,
1237
1176
  required,
1238
1177
  grid,
1178
+ isGrouped,
1239
1179
  virtualized,
1180
+ onOpenChangeComplete,
1240
1181
  openOnInputClick,
1241
1182
  itemToStringLabel,
1242
1183
  modal,
@@ -1244,9 +1185,10 @@ function AriaCombobox(props) {
1244
1185
  isItemEqualToValue,
1245
1186
  submitOnItemClick,
1246
1187
  hasInputValue,
1188
+ requestSubmit,
1247
1189
  inputOwnsFormValue: selectionMode === 'none' && (inlineProp || !store.state.inputInsidePopup)
1248
1190
  });
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]);
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]);
1250
1192
  const hiddenInputRef = useMergedRefs(inputRefProp, validation.inputRef);
1251
1193
  const itemsContextValue = React.useMemo(() => ({
1252
1194
  query,
@@ -1290,7 +1232,7 @@ function AriaCombobox(props) {
1290
1232
  },
1291
1233
  // Handle browser autofill.
1292
1234
  onChange(event) {
1293
- // Workaround for https://github.com/react/react/issues/9023
1235
+ // Workaround for https://github.com/facebook/react/issues/9023
1294
1236
  if (event.nativeEvent.defaultPrevented || disabled || readOnly) {
1295
1237
  return;
1296
1238
  }
@@ -1385,51 +1327,7 @@ const triggerStateAttributesMapping = {
1385
1327
  } : null
1386
1328
  };
1387
1329
 
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
- }
1330
+ const BOUNDARY_OFFSET = 2;
1433
1331
 
1434
1332
  /**
1435
1333
  * A button that opens the popup.
@@ -1459,14 +1357,20 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
1459
1357
  labelId: fieldLabelId
1460
1358
  } = useLabelableContext();
1461
1359
  const store = useComboboxRootContext();
1360
+ const {
1361
+ filteredItems
1362
+ } = useComboboxDerivedItemsContext();
1462
1363
  const selectionMode = useStore(store, selectors.selectionMode);
1463
1364
  const comboboxDisabled = useStore(store, selectors.disabled);
1464
1365
  const readOnly = useStore(store, selectors.readOnly);
1465
1366
  const required = useStore(store, selectors.required);
1367
+ const mounted = useStore(store, selectors.mounted);
1368
+ const popupSideValue = useStore(store, selectors.popupSide);
1466
1369
  const positionerElement = useStore(store, selectors.positionerElement);
1467
1370
  const listElement = useStore(store, selectors.listElement);
1468
1371
  const storedPopupId = useStore(store, selectors.popupId);
1469
1372
  const triggerProps = useStore(store, selectors.triggerProps);
1373
+ const triggerElement = useStore(store, selectors.triggerElement);
1470
1374
  const inputInsidePopup = useStore(store, selectors.inputInsidePopup);
1471
1375
  const rootId = useStore(store, selectors.id);
1472
1376
  const comboboxLabelId = useStore(store, selectors.labelId);
@@ -1479,8 +1383,8 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
1479
1383
  const inputValue = useComboboxInputValueContext();
1480
1384
  const focusTimeout = useTimeout();
1481
1385
  const disabled = fieldDisabled || comboboxDisabled || disabledProp;
1482
- const listEmpty = useListEmpty();
1483
- const popupSide = usePopupSide(store);
1386
+ const listEmpty = filteredItems.length === 0;
1387
+ const popupSide = mounted && positionerElement ? popupSideValue : null;
1484
1388
  useLabelableId({
1485
1389
  id: inputInsidePopup ? idProp : undefined
1486
1390
  });
@@ -1498,6 +1402,18 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
1498
1402
  function trackPointerType(event) {
1499
1403
  currentPointerTypeRef.current = event.pointerType;
1500
1404
  }
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]);
1501
1417
  const {
1502
1418
  reference: triggerTypeaheadProps
1503
1419
  } = useTypeahead(floatingRootContext, {
@@ -1508,7 +1424,7 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
1508
1424
  onMatch(index) {
1509
1425
  const nextSelectedValue = store.state.valuesRef.current[index];
1510
1426
  if (nextSelectedValue !== undefined) {
1511
- store.state.setSelectedValue(nextSelectedValue, createChangeEventDetails(none));
1427
+ store.state.setSelectedValue(nextSelectedValue, createChangeEventDetails('none'));
1512
1428
  }
1513
1429
  }
1514
1430
  });
@@ -1543,7 +1459,7 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
1543
1459
  id,
1544
1460
  tabIndex: inputInsidePopup ? 0 : -1,
1545
1461
  role: inputInsidePopup ? 'combobox' : undefined,
1546
- 'aria-expanded': open,
1462
+ 'aria-expanded': open ? 'true' : 'false',
1547
1463
  'aria-haspopup': inputInsidePopup ? 'dialog' : 'listbox',
1548
1464
  'aria-controls': ariaControls,
1549
1465
  'aria-required': inputInsidePopup ? required || undefined : undefined,
@@ -1590,20 +1506,22 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
1590
1506
  }
1591
1507
  const doc = ownerDocument(event.currentTarget);
1592
1508
  function handleMouseUp(mouseEvent) {
1593
- const currentTriggerElement = store.state.triggerElement;
1594
- if (!currentTriggerElement) {
1509
+ if (!triggerElement) {
1595
1510
  return;
1596
1511
  }
1597
1512
  const mouseUpTarget = getTarget(mouseEvent);
1598
1513
  const positioner = store.state.positionerElement;
1599
1514
  const list = store.state.listElement;
1600
- if (contains(currentTriggerElement, mouseUpTarget) || contains(positioner, mouseUpTarget) || contains(list, mouseUpTarget)) {
1515
+ if (contains(triggerElement, mouseUpTarget) || contains(positioner, mouseUpTarget) || contains(list, mouseUpTarget) || mouseUpTarget === triggerElement) {
1601
1516
  return;
1602
1517
  }
1603
- if (isMouseWithinBounds(mouseEvent, currentTriggerElement)) {
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) {
1604
1522
  return;
1605
1523
  }
1606
- store.state.setOpen(false, createChangeEventDetails(cancelOpen, mouseEvent));
1524
+ store.state.setOpen(false, createChangeEventDetails('cancel-open', mouseEvent));
1607
1525
  }
1608
1526
  if (inputInsidePopup) {
1609
1527
  doc.addEventListener('mouseup', handleMouseUp, {
@@ -1612,7 +1530,7 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
1612
1530
  }
1613
1531
  },
1614
1532
  onKeyDown(event) {
1615
- if (readOnly) {
1533
+ if (disabled || readOnly) {
1616
1534
  return;
1617
1535
  }
1618
1536
  if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
@@ -1621,7 +1539,7 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
1621
1539
  store.state.inputRef.current?.focus();
1622
1540
  }
1623
1541
  }
1624
- }, validation.getValidationProps(disabled, elementProps), getButtonProps],
1542
+ }, validation ? validation.getValidationProps(disabled, elementProps) : elementProps, getButtonProps],
1625
1543
  stateAttributesMapping: triggerStateAttributesMapping
1626
1544
  });
1627
1545
  return element;
@@ -1696,6 +1614,9 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1696
1614
  const positioning = useComboboxPositionerContext(true);
1697
1615
  const hasPositionerParent = Boolean(positioning);
1698
1616
  const store = useComboboxRootContext();
1617
+ const {
1618
+ filteredItems
1619
+ } = useComboboxDerivedItemsContext();
1699
1620
  // `inputValue` can't be placed in the store.
1700
1621
  // https://github.com/mui/base-ui/issues/2703
1701
1622
  const inputValue = useComboboxInputValueContext();
@@ -1712,16 +1633,19 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1712
1633
  const open = useStore(store, selectors.open);
1713
1634
  const mounted = useStore(store, selectors.mounted);
1714
1635
  const selectedValue = useStore(store, selectors.selectedValue);
1636
+ const popupSideValue = useStore(store, selectors.popupSide);
1637
+ const positionerElement = useStore(store, selectors.positionerElement);
1715
1638
  const rootId = useStore(store, selectors.id);
1716
1639
  const inline = useStore(store, selectors.inline);
1717
1640
  const modal = useStore(store, selectors.modal);
1718
1641
  const autoHighlightEnabled = Boolean(autoHighlightMode);
1719
- const popupSide = usePopupSide(store);
1642
+ const popupSide = mounted && positionerElement ? popupSideValue : null;
1720
1643
  const disabled = fieldDisabled || comboboxDisabled || disabledProp;
1721
- const listEmpty = useListEmpty();
1644
+ const listEmpty = filteredItems.length === 0;
1722
1645
  const isInsidePopup = hasPositionerParent || inline;
1723
1646
  const focusManagerModal = !isInsidePopup || modal;
1724
1647
  const id = useBaseUiId(idProp ?? (!isInsidePopup ? rootId : undefined));
1648
+ const ariaLabelledBy = resolveAriaLabelledBy(fieldLabelId, undefined);
1725
1649
  const fieldStateForInput = hasPositionerParent ? DEFAULT_FIELD_STATE_ATTRIBUTES : fieldState;
1726
1650
  const [composingValue, setComposingValue] = React.useState(null);
1727
1651
  const isComposingRef = React.useRef(false);
@@ -1739,17 +1663,7 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1739
1663
  inputOwnsFormValue
1740
1664
  });
1741
1665
  });
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
- }
1666
+ const validationProps = hasPositionerParent || !validation ? elementProps : validation.getValidationProps(disabled, elementProps);
1753
1667
  const state = {
1754
1668
  ...fieldStateForInput,
1755
1669
  open,
@@ -1767,7 +1681,9 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1767
1681
  highlightedChipIndex
1768
1682
  } = comboboxChipsContext;
1769
1683
  const renderedChipsCount = comboboxChipsContext.chipsRef.current.length;
1770
- const [previousChipKey, nextChipKey] = getChipNavigationKeys(direction);
1684
+ const isRtl = direction === 'rtl';
1685
+ const previousChipKey = isRtl ? 'ArrowRight' : 'ArrowLeft';
1686
+ const nextChipKey = isRtl ? 'ArrowLeft' : 'ArrowRight';
1771
1687
  if (highlightedChipIndex !== undefined) {
1772
1688
  if (event.key === previousChipKey) {
1773
1689
  event.preventDefault();
@@ -1786,8 +1702,14 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1786
1702
  } else if (event.key === 'Backspace' || event.key === 'Delete') {
1787
1703
  event.preventDefault();
1788
1704
  // Move highlight appropriately after removal.
1789
- nextIndex = getIndexAfterChipRemoval(highlightedChipIndex, selectedValue.length);
1790
- clearHighlight();
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
+ });
1791
1713
  }
1792
1714
  return nextIndex;
1793
1715
  }
@@ -1796,6 +1718,13 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1796
1718
  if (event.key === previousChipKey && (event.currentTarget.selectionStart ?? 0) === 0 && selectedValue.length > 0) {
1797
1719
  event.preventDefault();
1798
1720
  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();
1799
1728
  }
1800
1729
  return nextIndex;
1801
1730
  }
@@ -1803,10 +1732,11 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1803
1732
  state,
1804
1733
  ref: [forwardedRef, store.state.inputRef, setInputElement],
1805
1734
  props: [inputProps, triggerProps, {
1806
- value: composingValue ?? inputValue,
1735
+ type: 'text',
1736
+ value: componentProps.value ?? composingValue ?? inputValue,
1807
1737
  'aria-readonly': readOnly || undefined,
1808
1738
  'aria-required': required || undefined,
1809
- 'aria-labelledby': fieldLabelId,
1739
+ 'aria-labelledby': ariaLabelledBy,
1810
1740
  disabled,
1811
1741
  readOnly,
1812
1742
  required: selectionMode === 'none' ? required : undefined,
@@ -1861,23 +1791,11 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1861
1791
  store.state.setInputValue(next, createChangeEventDetails(inputChange, event.nativeEvent));
1862
1792
  },
1863
1793
  onChange(event) {
1864
- const nativeEvent = event.nativeEvent;
1865
1794
  // Autofill may not provide `inputType` (Chrome) or may report
1866
1795
  // `insertReplacementText` (Firefox).
1867
- const inputType = nativeEvent.inputType;
1796
+ const inputType = event.nativeEvent.inputType;
1868
1797
  const autofillLikeInput = !inputType || inputType === 'insertReplacementText';
1869
- // During composition the input is always considered typed into.
1870
1798
  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
- }
1881
1799
 
1882
1800
  // During IME composition, avoid propagating controlled updates to prevent
1883
1801
  // filtering the options prematurely so `Empty` won't show incorrectly.
@@ -1889,23 +1807,38 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1889
1807
  const nextVal = event.currentTarget.value;
1890
1808
  setComposingValue(nextVal);
1891
1809
  if (nextVal === '' && !store.state.openOnInputClick && !store.state.inputInsidePopup) {
1892
- store.state.setOpen(false, createChangeEventDetails(inputClear, nativeEvent));
1810
+ store.state.setOpen(false, createChangeEventDetails(inputClear, event.nativeEvent));
1893
1811
  }
1894
1812
  const trimmed = nextVal.trim();
1895
1813
  const shouldMaintainHighlight = autoHighlightEnabled && trimmed !== '';
1896
- maybeOpenOnInput(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
+ }
1897
1826
  if (open && store.state.activeIndex !== null && !shouldMaintainHighlight) {
1898
- clearHighlight();
1827
+ store.state.setIndices({
1828
+ activeIndex: null,
1829
+ selectedIndex: null,
1830
+ type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
1831
+ });
1899
1832
  }
1900
1833
  return;
1901
1834
  }
1902
- const inputChangeDetails = createChangeEventDetails(inputChange, nativeEvent);
1835
+ const inputChangeDetails = createChangeEventDetails(inputChange, event.nativeEvent);
1903
1836
  store.state.setInputValue(event.currentTarget.value, inputChangeDetails);
1904
1837
  if (inputChangeDetails.isCanceled) {
1905
1838
  return;
1906
1839
  }
1907
1840
  const empty = event.currentTarget.value === '';
1908
- const clearDetails = createChangeEventDetails(inputClear, nativeEvent);
1841
+ const clearDetails = createChangeEventDetails(inputClear, event.nativeEvent);
1909
1842
  if (empty && !store.state.inputInsidePopup) {
1910
1843
  if (selectionMode === 'single') {
1911
1844
  store.state.setSelectedValue(null, clearDetails);
@@ -1914,13 +1847,30 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1914
1847
  store.state.setOpen(false, clearDetails);
1915
1848
  }
1916
1849
  }
1917
- maybeOpenOnInput(event.currentTarget.value.trim());
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
+ }
1918
1864
 
1919
1865
  // When the user types, ensure the list resets its highlight so that
1920
1866
  // virtual focus returns to the input (aria-activedescendant is
1921
1867
  // cleared).
1922
1868
  if (open && store.state.activeIndex !== null && !autoHighlightEnabled) {
1923
- clearHighlight();
1869
+ store.state.setIndices({
1870
+ activeIndex: null,
1871
+ selectedIndex: null,
1872
+ type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
1873
+ });
1924
1874
  }
1925
1875
  },
1926
1876
  onKeyDown(event) {
@@ -1966,7 +1916,11 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1966
1916
  const removalIndex = renderedChipsCount > 0 ? renderedChipsCount - 1 : selectedValue.length - 1;
1967
1917
  const newValue = selectedValue.filter((_, index) => index !== removalIndex);
1968
1918
  // If the removed item was also the active (highlighted) item, clear highlight
1969
- clearHighlight();
1919
+ store.state.setIndices({
1920
+ activeIndex: null,
1921
+ selectedIndex: null,
1922
+ type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
1923
+ });
1970
1924
  store.state.setSelectedValue(newValue, createChangeEventDetails(none, event.nativeEvent));
1971
1925
  return;
1972
1926
  }
@@ -1996,11 +1950,20 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1996
1950
  return;
1997
1951
  }
1998
1952
  stopEvent(event);
1999
- clickHighlightedItem(store, activeIndex, nativeEvent);
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
+ }
2000
1959
  }
2001
1960
  },
2002
- onPointerMove: markPointerActive,
2003
- onPointerDown: markPointerActive
1961
+ onPointerMove() {
1962
+ store.state.keyboardActiveRef.current = false;
1963
+ },
1964
+ onPointerDown() {
1965
+ store.state.keyboardActiveRef.current = false;
1966
+ }
2004
1967
  }, validationProps],
2005
1968
  stateAttributesMapping: triggerStateAttributesMapping
2006
1969
  });
@@ -2052,14 +2015,20 @@ const ComboboxInputGroup = /*#__PURE__*/React.forwardRef(function ComboboxInputG
2052
2015
  state: fieldState
2053
2016
  } = useFieldRootContext();
2054
2017
  const store = useComboboxRootContext();
2018
+ const {
2019
+ filteredItems
2020
+ } = useComboboxDerivedItemsContext();
2055
2021
  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);
2056
2025
  const comboboxDisabled = useStore(store, selectors.disabled);
2057
2026
  const readOnly = useStore(store, selectors.readOnly);
2058
2027
  const hasSelectedValue = useStore(store, selectors.hasSelectedValue);
2059
2028
  const selectionMode = useStore(store, selectors.selectionMode);
2060
- const popupSide = usePopupSide(store);
2029
+ const popupSide = mounted && positionerElement ? popupSideValue : null;
2061
2030
  const disabled = comboboxDisabled;
2062
- const listEmpty = useListEmpty();
2031
+ const listEmpty = filteredItems.length === 0;
2063
2032
  const placeholder = selectionMode === 'none' ? false : !hasSelectedValue;
2064
2033
  const state = {
2065
2034
  ...fieldState,
@@ -2195,21 +2164,19 @@ const ComboboxClear = /*#__PURE__*/React.forwardRef(function ComboboxClear(compo
2195
2164
  if (disabled || readOnly) {
2196
2165
  return;
2197
2166
  }
2198
- const type = store.state.keyboardActiveRef.current ? keyboard : pointer;
2167
+ const keyboardActiveRef = store.state.keyboardActiveRef;
2199
2168
  store.state.setInputValue('', createChangeEventDetails(clearPress, event.nativeEvent));
2200
2169
  if (selectionMode !== 'none') {
2201
2170
  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.
2204
2171
  store.state.setIndices({
2205
2172
  activeIndex: null,
2206
2173
  selectedIndex: null,
2207
- type
2174
+ type: keyboardActiveRef.current ? 'keyboard' : 'pointer'
2208
2175
  });
2209
2176
  } else {
2210
2177
  store.state.setIndices({
2211
2178
  activeIndex: null,
2212
- type
2179
+ type: keyboardActiveRef.current ? 'keyboard' : 'pointer'
2213
2180
  });
2214
2181
  }
2215
2182
  store.state.inputRef.current?.focus();
@@ -2253,6 +2220,9 @@ function ComboboxCollection(props) {
2253
2220
  } = useComboboxDerivedItemsContext();
2254
2221
  const groupContext = useGroupCollectionContext();
2255
2222
  const itemsToRender = groupContext ? groupContext.items : filteredItems;
2223
+ if (!itemsToRender) {
2224
+ return null;
2225
+ }
2256
2226
  return /*#__PURE__*/jsx(React.Fragment, {
2257
2227
  children: itemsToRender.map(children)
2258
2228
  });
@@ -2276,7 +2246,7 @@ const ComboboxList = /*#__PURE__*/React.forwardRef(function ComboboxList(compone
2276
2246
  } = useComboboxDerivedItemsContext();
2277
2247
  const selectionMode = useStore(store, selectors.selectionMode);
2278
2248
  const grid = useStore(store, selectors.grid);
2279
- const listProps = useStore(store, selectors.listProps);
2249
+ const popupProps = useStore(store, selectors.popupProps);
2280
2250
  const virtualized = useStore(store, selectors.virtualized);
2281
2251
  const forceMounted = useStore(store, selectors.forceMounted);
2282
2252
  const multiple = selectionMode === 'multiple';
@@ -2290,7 +2260,7 @@ const ComboboxList = /*#__PURE__*/React.forwardRef(function ComboboxList(compone
2290
2260
 
2291
2261
  // Support "closed template" API: if children is a function, implicitly wrap it
2292
2262
  // with a Combobox.Collection that reads items from context/root.
2293
- // Ensures this component's `listProps` subscription does not cause <Combobox.Item>
2263
+ // Ensures this component's `popupProps` subscription does not cause <Combobox.Item>
2294
2264
  // to re-render on every active index change.
2295
2265
  const resolvedChildren = React.useMemo(() => {
2296
2266
  if (typeof children === 'function') {
@@ -2307,7 +2277,7 @@ const ComboboxList = /*#__PURE__*/React.forwardRef(function ComboboxList(compone
2307
2277
  const element = useRenderElement('div', componentProps, {
2308
2278
  state,
2309
2279
  ref: [forwardedRef, setListElement, hasPositionerContext ? null : setPositionerElement],
2310
- props: [listProps, {
2280
+ props: [popupProps, {
2311
2281
  children: resolvedChildren,
2312
2282
  tabIndex: -1,
2313
2283
  id: floatingId,
@@ -2324,7 +2294,13 @@ const ComboboxList = /*#__PURE__*/React.forwardRef(function ComboboxList(compone
2324
2294
  return;
2325
2295
  }
2326
2296
  stopEvent(event);
2327
- clickHighlightedItem(store, activeIndex, event.nativeEvent);
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
+ }
2328
2304
  }
2329
2305
  },
2330
2306
  onKeyDownCapture() {
@@ -2383,7 +2359,7 @@ function useInitialLiveRegionTextMutation() {
2383
2359
  if (textNode == null) {
2384
2360
  return undefined;
2385
2361
  }
2386
- const originalValue = textNode.data;
2362
+ const originalValue = textNode.nodeValue ?? '';
2387
2363
  const markedValue = `${originalValue}${LIVE_REGION_MARKER}`;
2388
2364
  textNode.nodeValue = markedValue;
2389
2365
  timeout.start(INITIAL_LIVE_REGION_TEXT_MUTATION_RESET_DELAY, () => {
@@ -2512,23 +2488,24 @@ const ComboboxPositioner = /*#__PURE__*/React.forwardRef(function ComboboxPositi
2512
2488
  render,
2513
2489
  className,
2514
2490
  anchor,
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,
2491
+ positionMethod = 'absolute',
2492
+ side = 'bottom',
2493
+ align = 'center',
2494
+ sideOffset = 0,
2495
+ alignOffset = 0,
2522
2496
  collisionBoundary = 'clipping-ancestors',
2523
- collisionPadding,
2524
- arrowPadding,
2525
- sticky,
2497
+ collisionPadding = 5,
2498
+ arrowPadding = 5,
2499
+ sticky = false,
2526
2500
  disableAnchorTracking = false,
2527
2501
  collisionAvoidance = DROPDOWN_COLLISION_AVOIDANCE,
2528
2502
  style: styleProp,
2529
2503
  ...elementProps
2530
2504
  } = componentProps;
2531
2505
  const store = useComboboxRootContext();
2506
+ const {
2507
+ filteredItems
2508
+ } = useComboboxDerivedItemsContext();
2532
2509
  const floatingRootContext = useComboboxFloatingContext();
2533
2510
  const keepMounted = useComboboxPortalContext();
2534
2511
  const modal = useStore(store, selectors.modal);
@@ -2541,7 +2518,7 @@ const ComboboxPositioner = /*#__PURE__*/React.forwardRef(function ComboboxPositi
2541
2518
  const inputGroupElement = useStore(store, selectors.inputGroupElement);
2542
2519
  const inputInsidePopup = useStore(store, selectors.inputInsidePopup);
2543
2520
  const transitionStatus = useStore(store, selectors.transitionStatus);
2544
- const empty = useListEmpty();
2521
+ const empty = filteredItems.length === 0;
2545
2522
  const resolvedAnchor = anchor ?? (inputInsidePopup ? triggerElement : inputGroupElement ?? inputElement);
2546
2523
  const positioning = useAnchorPositioning({
2547
2524
  anchor: resolvedAnchor,
@@ -2616,16 +2593,18 @@ const ComboboxPopup = /*#__PURE__*/React.forwardRef(function ComboboxPopup(compo
2616
2593
  const store = useComboboxRootContext();
2617
2594
  const positioning = useComboboxPositionerContext();
2618
2595
  const floatingRootContext = useComboboxFloatingContext();
2596
+ const {
2597
+ filteredItems
2598
+ } = useComboboxDerivedItemsContext();
2619
2599
  const mounted = useStore(store, selectors.mounted);
2620
2600
  const open = useStore(store, selectors.open);
2621
2601
  const openMethod = useStore(store, selectors.openMethod);
2622
- const popupProps = useStore(store, selectors.popupProps);
2623
2602
  const transitionStatus = useStore(store, selectors.transitionStatus);
2624
2603
  const inputInsidePopup = useStore(store, selectors.inputInsidePopup);
2625
2604
  const inputElement = useStore(store, selectors.inputElement);
2626
2605
  const modal = useStore(store, selectors.modal);
2627
2606
  const rootId = useStore(store, selectors.id);
2628
- const empty = useListEmpty();
2607
+ const empty = filteredItems.length === 0;
2629
2608
  const popupId = elementProps.id ?? (inputInsidePopup ? getComboboxPopupId(rootId) : undefined);
2630
2609
  useIsoLayoutEffect(() => {
2631
2610
  // Prefer the rendered DOM id, which a `render` prop element or function may override.
@@ -2654,9 +2633,10 @@ const ComboboxPopup = /*#__PURE__*/React.forwardRef(function ComboboxPopup(compo
2654
2633
  const element = useRenderElement('div', componentProps, {
2655
2634
  state,
2656
2635
  ref: [forwardedRef, store.state.popupRef],
2657
- props: [popupProps, {
2636
+ props: [{
2658
2637
  id: popupId,
2659
2638
  role: inputInsidePopup ? 'dialog' : 'presentation',
2639
+ tabIndex: -1,
2660
2640
  onFocus(event) {
2661
2641
  const target = getTarget(event.nativeEvent);
2662
2642
  if (openMethod !== 'touch' && (contains(store.state.listElement, target) || target === event.currentTarget)) {
@@ -2803,7 +2783,7 @@ const ComboboxGroupLabel = /*#__PURE__*/React.forwardRef(function ComboboxGroupL
2803
2783
  useIsoLayoutEffect(() => {
2804
2784
  setLabelId(id);
2805
2785
  return () => {
2806
- setLabelId(currentId => currentId === id ? undefined : currentId);
2786
+ setLabelId(undefined);
2807
2787
  };
2808
2788
  }, [id, setLabelId]);
2809
2789
  const element = useRenderElement('div', componentProps, {
@@ -2845,27 +2825,27 @@ function ComboboxItemInner(props) {
2845
2825
  style,
2846
2826
  value: itemValue = null,
2847
2827
  index: indexProp,
2848
- disabled: disabledProp = false,
2828
+ disabled = false,
2849
2829
  nativeButton = false,
2850
2830
  ...elementProps
2851
2831
  } = componentProps;
2832
+ const didPointerDownRef = React.useRef(false);
2852
2833
  const textRef = React.useRef(null);
2853
2834
  const listItem = useCompositeListItem({
2854
- guess: true,
2855
2835
  index: indexProp,
2856
- textRef
2836
+ textRef,
2837
+ indexGuessBehavior: IndexGuessBehavior.GuessFromOrder
2857
2838
  });
2858
2839
  const store = useComboboxRootContext();
2859
2840
  const isRow = useComboboxRowContext();
2860
2841
  const hasItems = useComboboxHasItemsContext();
2842
+ const open = useStore(store, selectors.open);
2861
2843
  const selectionMode = useStore(store, selectors.selectionMode);
2862
- const rootDisabled = useStore(store, selectors.disabled);
2863
2844
  const readOnly = useStore(store, selectors.readOnly);
2864
2845
  const isItemEqualToValue = useStore(store, selectors.isItemEqualToValue);
2865
- const disabled = rootDisabled || disabledProp;
2866
2846
  const selectable = selectionMode !== 'none';
2867
- const index = indexProp ?? indexFromFilter ?? listItem.index;
2868
- const hasRegistered = index !== -1;
2847
+ const index = indexProp ?? (virtualized ? indexFromFilter ?? -1 : listItem.index);
2848
+ const hasRegistered = listItem.index !== -1;
2869
2849
  const rootId = useStore(store, selectors.id);
2870
2850
  const highlighted = useStore(store, selectors.isActive, index);
2871
2851
  const matchesSelectedValue = useStore(store, selectors.isSelected, itemValue);
@@ -2890,24 +2870,31 @@ function ComboboxItemInner(props) {
2890
2870
  }
2891
2871
  const visibleMap = store.state.valuesRef.current;
2892
2872
  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
+ }
2893
2880
  return () => {
2894
2881
  delete visibleMap[index];
2895
2882
  };
2896
- }, [hasRegistered, hasItems, index, itemValue, store]);
2883
+ }, [hasRegistered, hasItems, index, itemValue, store, selectionMode]);
2897
2884
  useIsoLayoutEffect(() => {
2885
+ if (!open) {
2886
+ didPointerDownRef.current = false;
2887
+ return;
2888
+ }
2898
2889
  if (!hasRegistered || hasItems) {
2899
2890
  return;
2900
2891
  }
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.
2905
2892
  const selectedValue = store.state.selectedValue;
2906
2893
  const lastSelectedValue = Array.isArray(selectedValue) ? selectedValue[selectedValue.length - 1] : selectedValue;
2907
2894
  if (compareItemEquality(itemValue, lastSelectedValue, isItemEqualToValue)) {
2908
2895
  store.set('selectedIndex', index);
2909
2896
  }
2910
- }, [hasRegistered, hasItems, store, index, itemValue, isItemEqualToValue]);
2897
+ }, [hasRegistered, hasItems, open, store, index, itemValue, isItemEqualToValue]);
2911
2898
  const {
2912
2899
  getButtonProps,
2913
2900
  buttonRef
@@ -2942,12 +2929,7 @@ function ComboboxItemInner(props) {
2942
2929
  // as it should be a `<div>` instead.
2943
2930
  tabIndex: undefined,
2944
2931
  onPointerDownCapture(event) {
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
- }
2932
+ didPointerDownRef.current = true;
2951
2933
  event.preventDefault();
2952
2934
  },
2953
2935
  onMouseDown(event) {
@@ -2962,8 +2944,8 @@ function ComboboxItemInner(props) {
2962
2944
  commitSelection(event.nativeEvent);
2963
2945
  },
2964
2946
  onMouseUp(event) {
2965
- const pointerStartedOnItem = store.state.pointerDownItemRef.current === event.currentTarget;
2966
- store.state.pointerDownItemRef.current = null;
2947
+ const pointerStartedOnItem = didPointerDownRef.current;
2948
+ didPointerDownRef.current = false;
2967
2949
  if (disabled || readOnly || event.button !== 0 || pointerStartedOnItem || !highlighted) {
2968
2950
  return;
2969
2951
  }
@@ -3099,33 +3081,6 @@ const ComboboxEmpty = /*#__PURE__*/React.forwardRef(function ComboboxEmpty(compo
3099
3081
  });
3100
3082
  if (process.env.NODE_ENV !== "production") ComboboxEmpty.displayName = "ComboboxEmpty";
3101
3083
 
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
-
3129
3084
  /**
3130
3085
  * Returns the internally filtered items.
3131
3086
  */
@@ -3192,8 +3147,7 @@ const ComboboxLabel = /*#__PURE__*/React.forwardRef(function ComboboxLabel(compo
3192
3147
  id: defaultLabelId,
3193
3148
  fallbackControlId: localControlId,
3194
3149
  setLabelId(nextLabelId) {
3195
- const resolvedLabelId = typeof nextLabelId === 'function' ? nextLabelId(store.state.labelId) : nextLabelId;
3196
- store.set('labelId', resolvedLabelId);
3150
+ store.set('labelId', nextLabelId);
3197
3151
  }
3198
3152
  });
3199
3153
  return useRenderElement('div', componentProps, {
@@ -3236,10 +3190,11 @@ function ComboboxValue(props) {
3236
3190
  }
3237
3191
 
3238
3192
  const ComboboxItemIndicator = /*#__PURE__*/React.forwardRef(function ComboboxItemIndicator(componentProps, forwardedRef) {
3193
+ const keepMounted = componentProps.keepMounted ?? false;
3239
3194
  const {
3240
3195
  selected
3241
3196
  } = useComboboxItemContext();
3242
- const shouldRender = componentProps.keepMounted || selected;
3197
+ const shouldRender = keepMounted || selected;
3243
3198
  if (!shouldRender) {
3244
3199
  return null;
3245
3200
  }
@@ -3369,7 +3324,9 @@ const ComboboxChip = /*#__PURE__*/React.forwardRef(function ComboboxChip(compone
3369
3324
  } = useCompositeListItem();
3370
3325
  function handleKeyDown(event) {
3371
3326
  let nextIndex = index;
3372
- const [previousChipKey, nextChipKey] = getChipNavigationKeys(direction);
3327
+ const isRtl = direction === 'rtl';
3328
+ const previousChipKey = isRtl ? 'ArrowRight' : 'ArrowLeft';
3329
+ const nextChipKey = isRtl ? 'ArrowLeft' : 'ArrowRight';
3373
3330
  if (event.key === previousChipKey) {
3374
3331
  event.preventDefault();
3375
3332
  if (index > 0) {
@@ -3385,12 +3342,13 @@ const ComboboxChip = /*#__PURE__*/React.forwardRef(function ComboboxChip(compone
3385
3342
  nextIndex = undefined;
3386
3343
  }
3387
3344
  } else if (event.key === 'Backspace' || event.key === 'Delete') {
3388
- nextIndex = getIndexAfterChipRemoval(index, selectedValue.length);
3345
+ const computedNextIndex = index >= selectedValue.length - 1 ? selectedValue.length - 2 : index;
3346
+ nextIndex = computedNextIndex >= 0 ? computedNextIndex : undefined;
3389
3347
  stopEvent(event);
3390
3348
  store.state.setIndices({
3391
3349
  activeIndex: null,
3392
3350
  selectedIndex: null,
3393
- type: keyboard
3351
+ type: 'keyboard'
3394
3352
  });
3395
3353
  store.state.setSelectedValue(selectedValue.filter((_, i) => i !== index), createChangeEventDetails(none, event.nativeEvent));
3396
3354
  } else if (event.key === 'Enter' || event.key === ' ') {
@@ -3490,7 +3448,7 @@ const ComboboxChipRemove = /*#__PURE__*/React.forwardRef(function ComboboxChipRe
3490
3448
  if (removedIndex !== -1 && activeIndex === removedIndex) {
3491
3449
  store.state.setIndices({
3492
3450
  activeIndex: null,
3493
- type: store.state.keyboardActiveRef.current ? keyboard : pointer
3451
+ type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
3494
3452
  });
3495
3453
  }
3496
3454
  }
@@ -3511,12 +3469,18 @@ const ComboboxChipRemove = /*#__PURE__*/React.forwardRef(function ComboboxChipRe
3511
3469
  event.preventDefault();
3512
3470
  },
3513
3471
  onClick(event) {
3472
+ if (disabled || readOnly) {
3473
+ return;
3474
+ }
3514
3475
  const eventDetails = removeChip(event);
3515
3476
  if (!eventDetails.isPropagationAllowed) {
3516
3477
  event.stopPropagation();
3517
3478
  }
3518
3479
  },
3519
3480
  onKeyDown(event) {
3481
+ if (disabled || readOnly) {
3482
+ return;
3483
+ }
3520
3484
  if (event.key === 'Enter' || event.key === ' ') {
3521
3485
  const eventDetails = removeChip(event);
3522
3486
  if (!eventDetails.isPropagationAllowed) {
@@ -3530,12 +3494,4 @@ const ComboboxChipRemove = /*#__PURE__*/React.forwardRef(function ComboboxChipRe
3530
3494
  });
3531
3495
  if (process.env.NODE_ENV !== "production") ComboboxChipRemove.displayName = "ComboboxChipRemove";
3532
3496
 
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 };
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 };