@jobber/components 8.23.0 → 8.23.1

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 (134) hide show
  1. package/dist/Autocomplete/index.cjs +1 -1
  2. package/dist/Autocomplete/index.mjs +1 -1
  3. package/dist/Card/index.cjs +4 -4
  4. package/dist/Card/index.mjs +4 -4
  5. package/dist/Chip/index.cjs +1 -1
  6. package/dist/Chip/index.mjs +1 -1
  7. package/dist/Chips/InternalChipDismissible/hooks/index.cjs +1 -1
  8. package/dist/Chips/InternalChipDismissible/hooks/index.mjs +1 -1
  9. package/dist/Chips/InternalChipDismissible/index.cjs +1 -1
  10. package/dist/Chips/InternalChipDismissible/index.mjs +1 -1
  11. package/dist/Chips/index.cjs +1 -1
  12. package/dist/Chips/index.mjs +1 -1
  13. package/dist/ComboboxChipRemove-cjs.js +668 -379
  14. package/dist/ComboboxChipRemove-es.js +508 -219
  15. package/dist/ConfirmationModal/index.cjs +1 -1
  16. package/dist/ConfirmationModal/index.mjs +1 -1
  17. package/dist/DataDump/index.cjs +4 -4
  18. package/dist/DataDump/index.mjs +4 -4
  19. package/dist/DataList/components/DataListActions/index.cjs +1 -1
  20. package/dist/DataList/components/DataListActions/index.mjs +1 -1
  21. package/dist/DataList/components/DataListBulkActions/index.cjs +1 -1
  22. package/dist/DataList/components/DataListBulkActions/index.mjs +1 -1
  23. package/dist/DataList/components/DataListFilters/components/DataListSort/index.cjs +1 -1
  24. package/dist/DataList/components/DataListFilters/components/DataListSort/index.mjs +1 -1
  25. package/dist/DataList/components/DataListFilters/index.cjs +1 -1
  26. package/dist/DataList/components/DataListFilters/index.mjs +1 -1
  27. package/dist/DataList/components/DataListHeader/index.cjs +1 -1
  28. package/dist/DataList/components/DataListHeader/index.mjs +1 -1
  29. package/dist/DataList/components/DataListItem/index.cjs +1 -1
  30. package/dist/DataList/components/DataListItem/index.mjs +1 -1
  31. package/dist/DataList/components/DataListItemActions/index.cjs +1 -1
  32. package/dist/DataList/components/DataListItemActions/index.mjs +1 -1
  33. package/dist/DataList/components/DataListItemActionsOverflow/index.cjs +1 -1
  34. package/dist/DataList/components/DataListItemActionsOverflow/index.mjs +1 -1
  35. package/dist/DataList/components/DataListItems/index.cjs +1 -1
  36. package/dist/DataList/components/DataListItems/index.mjs +1 -1
  37. package/dist/DataList/components/DataListLayout/index.cjs +1 -1
  38. package/dist/DataList/components/DataListLayout/index.mjs +1 -1
  39. package/dist/DataList/components/DataListLayoutActions/index.cjs +1 -1
  40. package/dist/DataList/components/DataListLayoutActions/index.mjs +1 -1
  41. package/dist/DataList/index.cjs +1 -1
  42. package/dist/DataList/index.mjs +1 -1
  43. package/dist/DatePicker/index.cjs +1 -1
  44. package/dist/DatePicker/index.mjs +1 -1
  45. package/dist/Dialog/index.cjs +3 -3
  46. package/dist/Dialog/index.mjs +3 -3
  47. package/dist/DrawerDescription-cjs.js +32 -33
  48. package/dist/DrawerDescription-es.js +19 -20
  49. package/dist/DrawerTitle-cjs.js +7 -5
  50. package/dist/DrawerTitle-es.js +7 -5
  51. package/dist/FilterPicker/components/FilterPickerActivator/index.cjs +1 -1
  52. package/dist/FilterPicker/components/FilterPickerActivator/index.mjs +1 -1
  53. package/dist/FilterPicker/components/FilterPickerContent/index.cjs +1 -1
  54. package/dist/FilterPicker/components/FilterPickerContent/index.mjs +1 -1
  55. package/dist/FilterPicker/components/FilterPickerTrigger/index.cjs +1 -1
  56. package/dist/FilterPicker/components/FilterPickerTrigger/index.mjs +1 -1
  57. package/dist/FilterPicker/index.cjs +1 -1
  58. package/dist/FilterPicker/index.mjs +1 -1
  59. package/dist/FormFieldWrapper-cjs.js +2 -2
  60. package/dist/FormFieldWrapper-es.js +2 -2
  61. package/dist/FormatFile/index.cjs +1 -1
  62. package/dist/FormatFile/index.mjs +1 -1
  63. package/dist/Gallery/index.cjs +1 -1
  64. package/dist/Gallery/index.mjs +1 -1
  65. package/dist/InputDate/index.cjs +1 -1
  66. package/dist/InputDate/index.mjs +1 -1
  67. package/dist/InputNumberExperimental-cjs.js +319 -102
  68. package/dist/InputNumberExperimental-es.js +321 -104
  69. package/dist/LightBox/index.cjs +1 -1
  70. package/dist/LightBox/index.mjs +1 -1
  71. package/dist/Menu/index.cjs +4 -4
  72. package/dist/Menu/index.mjs +4 -4
  73. package/dist/MenuSubmenuTrigger-cjs.js +545 -498
  74. package/dist/MenuSubmenuTrigger-es.js +490 -443
  75. package/dist/Modal/index.cjs +1 -1
  76. package/dist/Modal/index.mjs +1 -1
  77. package/dist/NumberFieldInput-cjs.js +609 -496
  78. package/dist/NumberFieldInput-es.js +612 -498
  79. package/dist/Page/index.cjs +4 -4
  80. package/dist/Page/index.mjs +4 -4
  81. package/dist/Popover/index.cjs +1 -1
  82. package/dist/Popover/index.mjs +1 -1
  83. package/dist/ScrollAreaViewport-cjs.js +326 -601
  84. package/dist/ScrollAreaViewport-es.js +278 -548
  85. package/dist/SelectGroupLabel-cjs.js +494 -528
  86. package/dist/SelectGroupLabel-es.js +411 -445
  87. package/dist/Separator-cjs.js +1 -0
  88. package/dist/Separator-es.js +1 -0
  89. package/dist/Tooltip/index.cjs +1 -1
  90. package/dist/Tooltip/index.mjs +1 -1
  91. package/dist/docs/InputTime/InputTime.md +8 -0
  92. package/dist/docs/SelectPrimitive/SelectPrimitive.md +38 -25
  93. package/dist/floating-ui.react-cjs.js +586 -10
  94. package/dist/floating-ui.react-dom-cjs.js +92 -92
  95. package/dist/floating-ui.react-dom-es.js +1 -1
  96. package/dist/floating-ui.react-es.js +580 -4
  97. package/dist/floating-ui.utils-cjs.js +159 -0
  98. package/dist/floating-ui.utils-es.js +136 -0
  99. package/dist/index.cjs +4 -4
  100. package/dist/index.mjs +4 -4
  101. package/dist/{ToolbarRootContext-cjs.js → isElementDisabled-cjs.js} +5 -0
  102. package/dist/{ToolbarRootContext-es.js → isElementDisabled-es.js} +5 -1
  103. package/dist/primitives/BottomSheet/index.cjs +3 -3
  104. package/dist/primitives/BottomSheet/index.mjs +3 -3
  105. package/dist/primitives/ComboboxPrimitive/index.cjs +2 -2
  106. package/dist/primitives/ComboboxPrimitive/index.mjs +2 -2
  107. package/dist/primitives/InputNumberExperimental/index.cjs +1 -1
  108. package/dist/primitives/InputNumberExperimental/index.mjs +1 -1
  109. package/dist/primitives/SelectPrimitive/index.cjs +5 -5
  110. package/dist/primitives/SelectPrimitive/index.mjs +5 -5
  111. package/dist/primitives/index.cjs +4 -4
  112. package/dist/primitives/index.mjs +4 -4
  113. package/dist/resolveAriaLabelledBy-cjs.js +5 -0
  114. package/dist/resolveAriaLabelledBy-es.js +5 -1
  115. package/dist/{CSPContext-cjs.js → scrollEdges-cjs.js} +30 -2
  116. package/dist/{CSPContext-es.js → scrollEdges-es.js} +28 -3
  117. package/dist/styles.css +9 -3
  118. package/dist/unstyledPrimitives/index.cjs +1217 -639
  119. package/dist/unstyledPrimitives/index.mjs +1163 -585
  120. package/dist/useButton-cjs.js +283 -317
  121. package/dist/useButton-es.js +277 -298
  122. package/dist/useCompositeListItem-cjs.js +328 -279
  123. package/dist/useCompositeListItem-es.js +293 -246
  124. package/dist/useLabel-cjs.js +11 -1
  125. package/dist/useLabel-es.js +11 -2
  126. package/dist/useLabelableId-cjs.js +37 -91
  127. package/dist/useLabelableId-es.js +37 -90
  128. package/dist/useRenderElement-cjs.js +81 -26
  129. package/dist/useRenderElement-es.js +81 -27
  130. package/dist/{InternalBackdrop-cjs.js → useScrollLock-cjs.js} +1286 -1220
  131. package/dist/{InternalBackdrop-es.js → useScrollLock-es.js} +1250 -1207
  132. package/package.json +3 -3
  133. package/dist/index.esm-cjs.js +0 -738
  134. package/dist/index.esm-es.js +0 -712
@@ -1,22 +1,58 @@
1
1
  import * as React from 'react';
2
- import { E as useControlled, b as useStableCallback, n as useValueAsRef, k as useTransitionStatus, a as useIsoLayoutEffect, N as createGenericEventDetails, K as inputClear, J as inputChange, d as createChangeEventDetails, y as focusOut, U as outsidePress, g as getTarget, Z as itemPress, f as useOpenChangeComplete, Q as useValueChanged, G as none, ae as inputPress, r as contains, H as visuallyHiddenInput, I as visuallyHidden, h as useTimeout, c as useButton, o as ownerDocument, s as stopEvent, v as listNavigation, e as closePress, u as useBaseUiId, _ as isAndroid, af as isFirefox, a8 as escapeKey, ag as clearPress, t as transitionStatusMapping, S as SafeReact, j as error, ah as chipRemovePress } from './useButton-es.js';
3
- import { f as formatErrorMessage, c as EMPTY_ARRAY, a as useRefWithInit, N as NOOP, E as EMPTY_OBJECT, b as useMergedRefs, u as useRenderElement } from './useRenderElement-es.js';
4
- import { a as useFormContext, b as useFieldRootContext, c as useLabelableId, d as useField, f as fieldValidityMapping, u as useLabelableContext, e as DEFAULT_FIELD_STATE_ATTRIBUTES } from './useLabelableId-es.js';
5
- import { u as useFloatingRootContext, a as useListNavigation, b as useTypeahead, g as getPseudoElementBounds, C as CompositeList, c as useAnchorPositioning, d as getDisabledMountTransitionStyles, e as useCompositeListItem, I as IndexGuessBehavior } from './useCompositeListItem-es.js';
6
- import { h as hasNullItemLabel, c as compareItemEquality, i as stringifyAsLabel, j as isGroupedItems, s as stringifyAsValue, e as selectedValueIncludes, g as removeItem, f as findItemIndex, d as defaultItemEquality, r as resolveAriaLabelledBy, k as getDefaultLabelId, a as resolveMultipleLabels, b as resolveSelectedLabel } from './resolveAriaLabelledBy-es.js';
7
- import { o as createSelector, S as Store, r as useStore, q as useOpenInteractionType, s as useClick, t as useDismiss, v as useInteractions, w as useOnFirstRender, x as pressableTriggerOpenStateMapping, n as useDirection, G as triggerOpenStateMapping, y as FloatingPortal, p as popupStateMapping, z as useScrollLock, I as InternalBackdrop, A as inertValue, B as DROPDOWN_COLLISION_AVOIDANCE, F as FloatingFocusManager } from './InternalBackdrop-es.js';
2
+ import { z as useControlled, b as useStableCallback, m as useValueAsRef, k as useTransitionStatus, a as useIsoLayoutEffect, J as createGenericEventDetails, G as inputClear, F as inputChange, d as createChangeEventDetails, v as focusOut, Q as outsidePress, g as getTarget, V as itemPress, f as useOpenChangeComplete, O as useValueChanged, C as none, a5 as inputPress, q as contains, D as visuallyHiddenInput, E as visuallyHidden, h as useTimeout, c as useButton, o as ownerDocument, s as stopEvent, r as listNavigation, e as closePress, u as useBaseUiId, W as android, a0 as escapeKey, a6 as clearPress, t as transitionStatusMapping, A as ios, S as SafeReact, j as error, a7 as chipRemovePress } from './useButton-es.js';
3
+ import { f as formatErrorMessage, b as EMPTY_ARRAY, a as useRefWithInit, E as EMPTY_OBJECT, N as NOOP, m as mergeProps, c as useMergedRefs, u as useRenderElement } from './useRenderElement-es.js';
4
+ import { a as useFormContext, b as useFieldRootContext, c as useLabelableId, d as useRegisterFieldControl, f as fieldValidityMapping, u as useLabelableContext, F as FieldRootContext, e as DEFAULT_FIELD_ROOT_CONTEXT, g as DEFAULT_FIELD_STATE_ATTRIBUTES } from './useLabelableId-es.js';
5
+ import { u as useFloatingRootContext, a as useListNavigation, b as useTypeahead, g as getPseudoElementBounds, C as CompositeList, d as useAnchorPositioning, c as useAnchoredPopupScrollLock, e as usePositioner, f as getDisabledMountTransitionStyles, h as useCompositeListItem, I as IndexGuessBehavior } from './useCompositeListItem-es.js';
6
+ import { h as hasNullItemLabel, c as compareItemEquality, a as stringifyAsLabel, k as isGroupedItems, s as stringifyAsValue, i as selectedValueIncludes, j as removeItem, f as findItemIndex, d as defaultItemEquality, b as areArraysEqual, r as resolveAriaLabelledBy, l as getDefaultLabelId, e as resolveMultipleLabels, g as resolveSelectedLabel } from './resolveAriaLabelledBy-es.js';
7
+ import { ak as getGridNavigatedIndex, e as isIndexOutOfListBounds, t as createSelector, r as useDirection, S as Store, w as useStore, v as useOpenInteractionType, x as useClick, y as useDismiss, F as FOCUSABLE_POPUP_PROPS, z as useOnFirstRender, B as pressableTriggerOpenStateMapping, al as gecko, a0 as isInteractiveElement, W as triggerOpenStateMapping, E as FloatingPortal, p as popupStateMapping, I as InternalBackdrop, G as inertValue, H as DROPDOWN_COLLISION_AVOIDANCE, a as FloatingFocusManager } from './useScrollLock-es.js';
8
8
  import { jsx, jsxs } from 'react/jsx-runtime';
9
9
  import * as ReactDOM from 'react-dom';
10
- import { u as useLabel } from './useLabel-es.js';
10
+ import { s as stringifyLocale, u as useLabel } from './useLabel-es.js';
11
+ import { b as isElement } from './floating-ui.utils.dom-es.js';
12
+
13
+ /**
14
+ * Positional arguments are deliberate: property names of an options object
15
+ * don't minify, and the signature is locked to the caller via `typeof` on the
16
+ * `grid` option of `useListNavigation`.
17
+ *
18
+ * The injected grid navigator only ever operates on a uniform 1x1 grid (sizes are
19
+ * always `1x1` and packing is never dense), so the cell-map machinery that supports
20
+ * multi-cell items collapses to an identity transform over the item list. Calling
21
+ * `getGridNavigatedIndex` directly keeps the cell-map helpers
22
+ * (`createGridCellMap`/`getGridCellIndexOfCorner`/`getGridCellIndices`) out of
23
+ * grid-combobox bundles.
24
+ */
25
+ function gridNavigation(event, prevIndex, listRef, orientation, loopFocus, rtl, disabledIndices, minIndex, maxIndex, cols = 2) {
26
+ const nextIndex = getGridNavigatedIndex(listRef.current, {
27
+ event,
28
+ orientation,
29
+ loopFocus,
30
+ rtl,
31
+ cols,
32
+ disabledIndices,
33
+ minIndex,
34
+ maxIndex,
35
+ // An out-of-range previous index falls back to the first enabled item.
36
+ prevIndex: prevIndex > maxIndex ? minIndex : prevIndex,
37
+ stopEvent: true
38
+ });
39
+
40
+ // `getGridNavigatedIndex` can return an out-of-bounds sentinel (e.g. `-1` when there is no
41
+ // previous item to move from); surface that as `undefined` so the caller treats it as
42
+ // "no navigation" rather than highlighting index `-1`.
43
+ return isIndexOutOfListBounds(listRef.current, nextIndex) ? undefined : nextIndex;
44
+ }
11
45
 
12
46
  const ComboboxRootContext = /*#__PURE__*/React.createContext(undefined);
13
47
  if (process.env.NODE_ENV !== "production") ComboboxRootContext.displayName = "ComboboxRootContext";
14
48
  const ComboboxFloatingContext = /*#__PURE__*/React.createContext(undefined);
15
49
  if (process.env.NODE_ENV !== "production") ComboboxFloatingContext.displayName = "ComboboxFloatingContext";
16
50
  const ComboboxDerivedItemsContext = /*#__PURE__*/React.createContext(undefined);
51
+ if (process.env.NODE_ENV !== "production") ComboboxDerivedItemsContext.displayName = "ComboboxDerivedItemsContext";
52
+ const ComboboxHasItemsContext = /*#__PURE__*/React.createContext(false);
17
53
  // `inputValue` can't be placed in the store.
18
54
  // https://github.com/mui/base-ui/issues/2703
19
- if (process.env.NODE_ENV !== "production") ComboboxDerivedItemsContext.displayName = "ComboboxDerivedItemsContext";
55
+ if (process.env.NODE_ENV !== "production") ComboboxHasItemsContext.displayName = "ComboboxHasItemsContext";
20
56
  const ComboboxInputValueContext = /*#__PURE__*/React.createContext('');
21
57
  if (process.env.NODE_ENV !== "production") ComboboxInputValueContext.displayName = "ComboboxInputValueContext";
22
58
  function useComboboxRootContext() {
@@ -43,11 +79,13 @@ function useComboboxDerivedItemsContext() {
43
79
  function useComboboxInputValueContext() {
44
80
  return React.useContext(ComboboxInputValueContext);
45
81
  }
82
+ function useComboboxHasItemsContext() {
83
+ return React.useContext(ComboboxHasItemsContext);
84
+ }
46
85
 
47
86
  const selectors = {
48
87
  id: createSelector(state => state.id),
49
88
  labelId: createSelector(state => state.labelId),
50
- query: createSelector(state => state.query),
51
89
  items: createSelector(state => state.items),
52
90
  selectedValue: createSelector(state => state.selectedValue),
53
91
  hasSelectionChips: createSelector(state => {
@@ -89,35 +127,25 @@ const selectors = {
89
127
  popupProps: createSelector(state => state.popupProps),
90
128
  inputProps: createSelector(state => state.inputProps),
91
129
  triggerProps: createSelector(state => state.triggerProps),
92
- getItemProps: createSelector(state => state.getItemProps),
130
+ itemProps: createSelector(state => state.itemProps),
93
131
  positionerElement: createSelector(state => state.positionerElement),
94
132
  listElement: createSelector(state => state.listElement),
133
+ popupId: createSelector(state => state.popupId),
95
134
  triggerElement: createSelector(state => state.triggerElement),
96
135
  inputElement: createSelector(state => state.inputElement),
97
136
  inputGroupElement: createSelector(state => state.inputGroupElement),
98
137
  popupSide: createSelector(state => state.popupSide),
99
138
  openMethod: createSelector(state => state.openMethod),
100
139
  inputInsidePopup: createSelector(state => state.inputInsidePopup),
140
+ inputOwnsFormValue: createSelector(state => state.inputOwnsFormValue),
101
141
  selectionMode: createSelector(state => state.selectionMode),
102
- listRef: createSelector(state => state.listRef),
103
- labelsRef: createSelector(state => state.labelsRef),
104
- popupRef: createSelector(state => state.popupRef),
105
- emptyRef: createSelector(state => state.emptyRef),
106
- inputRef: createSelector(state => state.inputRef),
107
- keyboardActiveRef: createSelector(state => state.keyboardActiveRef),
108
- chipsContainerRef: createSelector(state => state.chipsContainerRef),
109
- clearRef: createSelector(state => state.clearRef),
110
- valuesRef: createSelector(state => state.valuesRef),
111
- allValuesRef: createSelector(state => state.allValuesRef),
112
142
  name: createSelector(state => state.name),
143
+ form: createSelector(state => state.form),
113
144
  disabled: createSelector(state => state.disabled),
114
145
  readOnly: createSelector(state => state.readOnly),
115
146
  required: createSelector(state => state.required),
116
147
  grid: createSelector(state => state.grid),
117
- isGrouped: createSelector(state => state.isGrouped),
118
148
  virtualized: createSelector(state => state.virtualized),
119
- onOpenChangeComplete: createSelector(state => state.onOpenChangeComplete),
120
- openOnInputClick: createSelector(state => state.openOnInputClick),
121
149
  itemToStringLabel: createSelector(state => state.itemToStringLabel),
122
150
  isItemEqualToValue: createSelector(state => state.isItemEqualToValue),
123
151
  modal: createSelector(state => state.modal),
@@ -125,6 +153,15 @@ const selectors = {
125
153
  submitOnItemClick: createSelector(state => state.submitOnItemClick)
126
154
  };
127
155
 
156
+ /**
157
+ * Derives the default id assigned to `Combobox.Popup` when the input is rendered inside it.
158
+ * Shared by the popup (which applies it) and the trigger (which references it via `aria-controls`)
159
+ * so the convention only lives in one place.
160
+ */
161
+ function getComboboxPopupId(rootId) {
162
+ return rootId == null ? undefined : `${rootId}-popup`;
163
+ }
164
+
128
165
  /**
129
166
  * Enhanced filter using Intl.Collator for more robust string matching.
130
167
  * Uses the provided `itemToStringLabel` function if available, otherwise falls back to:
@@ -165,15 +202,6 @@ function createSingleSelectionCollatorFilter(collatorFilter, itemToStringLabel,
165
202
  }
166
203
 
167
204
  const filterCache = new Map();
168
- function stringifyLocale(locale) {
169
- if (Array.isArray(locale)) {
170
- return locale.map(value => stringifyLocale(value)).join(',');
171
- }
172
- if (locale == null) {
173
- return '';
174
- }
175
- return String(locale);
176
- }
177
205
  function getFilter(options = {}) {
178
206
  const mergedOptions = {
179
207
  usage: 'search',
@@ -262,9 +290,12 @@ function AriaCombobox(props) {
262
290
  onSelectedValueChange,
263
291
  defaultInputValue: defaultInputValueProp,
264
292
  inputValue: inputValueProp,
293
+ open: openProp,
294
+ defaultOpen = false,
265
295
  selectionMode = 'none',
266
296
  onItemHighlighted: onItemHighlightedProp,
267
297
  name: nameProp,
298
+ form,
268
299
  disabled: disabledProp = false,
269
300
  readOnly = false,
270
301
  required = false,
@@ -297,7 +328,6 @@ function AriaCombobox(props) {
297
328
  const {
298
329
  setDirty,
299
330
  validityData,
300
- shouldValidateOnChange,
301
331
  setFilled,
302
332
  name: fieldName,
303
333
  disabled: fieldDisabled,
@@ -306,6 +336,7 @@ function AriaCombobox(props) {
306
336
  validationMode,
307
337
  validation
308
338
  } = useFieldRootContext();
339
+ const direction = useDirection();
309
340
  const id = useLabelableId({
310
341
  id: idProp
311
342
  });
@@ -390,8 +421,8 @@ function AriaCombobox(props) {
390
421
  state: 'inputValue'
391
422
  });
392
423
  const [open, setOpenUnwrapped] = useControlled({
393
- controlled: props.open,
394
- default: props.defaultOpen,
424
+ controlled: openProp,
425
+ default: defaultOpen,
395
426
  name: 'Combobox',
396
427
  state: 'open'
397
428
  });
@@ -493,6 +524,7 @@ function AriaCombobox(props) {
493
524
  allValuesRef,
494
525
  selectionEventRef,
495
526
  name,
527
+ form,
496
528
  disabled,
497
529
  readOnly,
498
530
  required,
@@ -515,14 +547,19 @@ function AriaCombobox(props) {
515
547
  popupProps: {},
516
548
  inputProps: {},
517
549
  triggerProps: {},
550
+ itemProps: EMPTY_OBJECT,
518
551
  positionerElement: null,
519
552
  listElement: null,
553
+ popupId: undefined,
520
554
  triggerElement: null,
521
555
  inputElement: null,
522
556
  inputGroupElement: null,
523
557
  popupSide: null,
524
558
  openMethod: null,
525
559
  inputInsidePopup: true,
560
+ // Avoid duplicate names in the server HTML. Popup inputs aren't rendered
561
+ // until after hydration, so the hidden input takes over then if needed.
562
+ inputOwnsFormValue: selectionMode === 'none',
526
563
  onOpenChangeComplete: onOpenChangeCompleteProp || NOOP,
527
564
  // Placeholder callbacks replaced on first render
528
565
  setOpen: NOOP,
@@ -531,7 +568,6 @@ function AriaCombobox(props) {
531
568
  setIndices: NOOP,
532
569
  onItemHighlighted: NOOP,
533
570
  handleSelection: NOOP,
534
- getItemProps: () => EMPTY_OBJECT,
535
571
  forceMount: NOOP,
536
572
  requestSubmit: NOOP
537
573
  })).current;
@@ -556,6 +592,7 @@ function AriaCombobox(props) {
556
592
  const inputGroupElement = useStore(store, selectors.inputGroupElement);
557
593
  const inline = useStore(store, selectors.inline);
558
594
  const inputInsidePopup = useStore(store, selectors.inputInsidePopup);
595
+ const inputOwnsFormValue = useStore(store, selectors.inputOwnsFormValue);
559
596
  const triggerRef = useValueAsRef(triggerElement);
560
597
  const {
561
598
  mounted,
@@ -566,14 +603,8 @@ function AriaCombobox(props) {
566
603
  openMethod,
567
604
  triggerProps
568
605
  } = useOpenInteractionType(open);
569
- useField({
570
- id,
571
- name,
572
- commit: validation.commit,
573
- value: fieldRawValue,
574
- controlRef: inputInsidePopup ? triggerRef : inputRef,
575
- getValue: () => fieldStringValue
576
- });
606
+ const getStringifiedValueForForm = useStableCallback(() => fieldStringValue);
607
+ useRegisterFieldControl(inputInsidePopup ? triggerRef : inputRef, id, fieldRawValue, getStringifiedValueForForm, !disabled, nameProp);
577
608
  const forceMount = useStableCallback(() => {
578
609
  if (items) {
579
610
  // Ensure typeahead works on a closed list.
@@ -660,6 +691,16 @@ function AriaCombobox(props) {
660
691
  if (eventDetails.isCanceled) {
661
692
  return;
662
693
  }
694
+
695
+ // If reopening interrupts the close animation, handleUnmount won't run to clear the
696
+ // frozen closeQuery and pending popup input.
697
+ if (nextOpen && multiple && inputInsidePopup && !inline && closeQuery !== null) {
698
+ setQueryChangedAfterOpen(false);
699
+ setCloseQuery(null);
700
+ if (inputValue !== '') {
701
+ setInputValue('', createChangeEventDetails(inputClear, eventDetails.event));
702
+ }
703
+ }
663
704
  if (!nextOpen && queryChangedAfterOpen) {
664
705
  if (single) {
665
706
  if (!inline) {
@@ -670,17 +711,22 @@ function AriaCombobox(props) {
670
711
  setQueryChangedAfterOpen(false);
671
712
  }
672
713
  } else if (multiple) {
673
- if (inline || inputInsidePopup) {
714
+ if (!inline) {
715
+ // Freeze the current query so filtering remains stable while exiting.
716
+ setCloseQuery(query);
717
+ }
718
+ if (inputInsidePopup) {
674
719
  setIndices({
675
720
  activeIndex: null
676
721
  });
677
- } else {
678
- // Freeze the current query so filtering remains stable while exiting.
679
- setCloseQuery(query);
680
722
  }
723
+
681
724
  // Clear the input immediately on close while retaining filtering via closeQuery for exit animations
682
- // if the input is outside the popup.
683
- setInputValue('', createChangeEventDetails(inputClear, eventDetails.event));
725
+ // if the input is outside the popup. When the input is inside the popup, defer the clear until
726
+ // unmount so the filtered list doesn't flash to unfiltered during the exit animation.
727
+ if (!inputInsidePopup || inline) {
728
+ setInputValue('', createChangeEventDetails(inputClear, eventDetails.event));
729
+ }
684
730
  }
685
731
  }
686
732
  setOpenUnwrapped(nextOpen);
@@ -735,6 +781,9 @@ function AriaCombobox(props) {
735
781
  const isCurrentlySelected = selectedValueIncludes(currentSelectedValue, itemValue, store.state.isItemEqualToValue);
736
782
  const nextValue = isCurrentlySelected ? removeItem(currentSelectedValue, itemValue, store.state.isItemEqualToValue) : [...currentSelectedValue, itemValue];
737
783
  setSelectedValue(nextValue, eventDetails);
784
+ if (eventDetails.isCanceled) {
785
+ return;
786
+ }
738
787
  const wasFiltering = inputRef.current ? inputRef.current.value.trim() !== '' : false;
739
788
  if (!wasFiltering) {
740
789
  return;
@@ -746,6 +795,9 @@ function AriaCombobox(props) {
746
795
  }
747
796
  } else {
748
797
  setSelectedValue(itemValue, eventDetails);
798
+ if (eventDetails.isCanceled) {
799
+ return;
800
+ }
749
801
  setOpen(false, eventDetails);
750
802
  }
751
803
  });
@@ -753,9 +805,9 @@ function AriaCombobox(props) {
753
805
  if (!store.state.submitOnItemClick) {
754
806
  return;
755
807
  }
756
- const form = store.state.inputElement?.form;
757
- if (form && typeof form.requestSubmit === 'function') {
758
- form.requestSubmit();
808
+ const formElement = validation.inputRef.current?.form ?? store.state.inputElement?.form;
809
+ if (formElement && typeof formElement.requestSubmit === 'function') {
810
+ formElement.requestSubmit();
759
811
  }
760
812
  });
761
813
  const handleUnmount = useStableCallback(() => {
@@ -921,6 +973,13 @@ function AriaCombobox(props) {
921
973
  });
922
974
  }
923
975
  }, [hasItems, autoHighlightMode, flatFilteredItems.length, setIndices]);
976
+ function isSelectedValueDirty(value) {
977
+ const initialValue = validityData.initialValue;
978
+ if (Array.isArray(value) && Array.isArray(initialValue)) {
979
+ return !areArraysEqual(value, initialValue, (itemValue, initialItemValue) => compareItemEquality(itemValue, initialItemValue, isItemEqualToValue));
980
+ }
981
+ return value !== initialValue;
982
+ }
924
983
  useValueChanged(query, () => {
925
984
  if (!open || query === '' || query === String(initialDefaultInputValue)) {
926
985
  return;
@@ -932,12 +991,8 @@ function AriaCombobox(props) {
932
991
  return;
933
992
  }
934
993
  clearErrors(name);
935
- setDirty(selectedValue !== validityData.initialValue);
936
- if (shouldValidateOnChange()) {
937
- validation.commit(selectedValue);
938
- } else {
939
- validation.commit(selectedValue, true);
940
- }
994
+ setDirty(isSelectedValueDirty(selectedValue));
995
+ validation.change(selectedValue);
941
996
  if (single && !hasInputValue && !inputInsidePopup) {
942
997
  const nextInputValue = stringifyAsLabel(selectedValue, itemToStringLabel);
943
998
  if (inputValue !== nextInputValue) {
@@ -951,11 +1006,7 @@ function AriaCombobox(props) {
951
1006
  }
952
1007
  clearErrors(name);
953
1008
  setDirty(inputValue !== validityData.initialValue);
954
- if (shouldValidateOnChange()) {
955
- validation.commit(inputValue);
956
- } else {
957
- validation.commit(inputValue, true);
958
- }
1009
+ validation.change(inputValue);
959
1010
  });
960
1011
  useValueChanged(items, () => {
961
1012
  if (!single || hasInputValue || inputInsidePopup || queryChangedAfterOpen) {
@@ -1010,9 +1061,9 @@ function AriaCombobox(props) {
1010
1061
  enabled: !readOnly && !disabled && openOnInputClick,
1011
1062
  event: 'mousedown-only',
1012
1063
  toggle: false,
1013
- // Apply a small delay for touch to let iOS viewport centering settle.
1064
+ // Apply a small delay for touch to let mobile viewport/keyboard positioning settle.
1014
1065
  // This avoids top-bottom flip flickers if the preferred position is "top" when first tapping.
1015
- touchOpenDelay: inputInsidePopup ? 0 : 50,
1066
+ touchOpenDelay: inputInsidePopup ? 0 : 100,
1016
1067
  reason: inputPress
1017
1068
  });
1018
1069
  const dismiss = useDismiss(floatingRootContext, {
@@ -1043,13 +1094,10 @@ function AriaCombobox(props) {
1043
1094
  focusItemOnOpen: queryChangedAfterOpen || selectionMode === 'none' && !autoHighlightMode ? false : 'auto',
1044
1095
  focusItemOnHover: highlightItemOnHover,
1045
1096
  resetOnPointerLeave: !keepHighlight,
1046
- // `cols` > 1 enables grid navigation.
1047
- // Since <Combobox.Row> infers column sizes (and is required when building a grid),
1048
- // it works correctly even with a value of `2`.
1049
- // Floating UI tests don't require `role="row"` wrappers, so retains the number API.
1050
- cols: grid ? 2 : 1,
1051
1097
  orientation: grid ? 'horizontal' : undefined,
1098
+ rtl: direction === 'rtl',
1052
1099
  disabledIndices: EMPTY_ARRAY,
1100
+ grid: grid ? gridNavigation : undefined,
1053
1101
  onNavigate(nextActiveIndex, event) {
1054
1102
  // Retain the highlight only while actually transitioning out or closed.
1055
1103
  if (!event && !open || transitionStatus === 'ending') {
@@ -1067,18 +1115,37 @@ function AriaCombobox(props) {
1067
1115
  }
1068
1116
  }
1069
1117
  });
1070
- const {
1071
- getReferenceProps,
1072
- getFloatingProps,
1073
- getItemProps
1074
- } = useInteractions([role, click, dismiss, listNavigation]);
1118
+ const inputProps = React.useMemo(() => mergeProps(listNavigation.reference, {
1119
+ onKeyDown(event) {
1120
+ // In grid mode the navigation hook treats ArrowLeft/ArrowRight as horizontal
1121
+ // grid movement. When the input has focus and no item is highlighted the user
1122
+ // is still editing the query, so let the input keep its native caret behavior.
1123
+ if (grid && store.state.activeIndex == null && (event.key === 'ArrowLeft' || event.key === 'ArrowRight')) {
1124
+ event.preventBaseUIHandler();
1125
+ }
1126
+ }
1127
+ }, dismiss.reference, click.reference, role.reference), [listNavigation.reference, dismiss.reference, click.reference, role.reference, grid, store]);
1128
+ const popupProps = React.useMemo(() => mergeProps(FOCUSABLE_POPUP_PROPS, listNavigation.floating, dismiss.floating, role.floating), [listNavigation.floating, dismiss.floating, role.floating]);
1129
+ const itemProps = React.useMemo(() => {
1130
+ const listNavigationItemProps = listNavigation.item;
1131
+ if (!listNavigationItemProps) {
1132
+ return EMPTY_OBJECT;
1133
+ }
1134
+
1135
+ // Combobox keeps focus on the input; item focus would incorrectly sync
1136
+ // list navigation state from DOM focus.
1137
+ return {
1138
+ ...listNavigationItemProps,
1139
+ onFocus: undefined
1140
+ };
1141
+ }, [listNavigation.item]);
1075
1142
  useOnFirstRender(() => {
1076
1143
  store.update({
1077
1144
  inline: inlineProp,
1078
- popupProps: getFloatingProps(),
1079
- inputProps: getReferenceProps(),
1145
+ popupProps,
1146
+ inputProps,
1080
1147
  triggerProps,
1081
- getItemProps,
1148
+ itemProps,
1082
1149
  setOpen,
1083
1150
  setInputValue,
1084
1151
  setSelectedValue,
@@ -1098,13 +1165,14 @@ function AriaCombobox(props) {
1098
1165
  transitionStatus,
1099
1166
  items,
1100
1167
  inline: inlineProp,
1101
- popupProps: getFloatingProps(),
1102
- inputProps: getReferenceProps(),
1168
+ popupProps,
1169
+ inputProps,
1103
1170
  triggerProps,
1104
1171
  openMethod,
1105
- getItemProps,
1172
+ itemProps,
1106
1173
  selectionMode,
1107
1174
  name,
1175
+ form,
1108
1176
  disabled,
1109
1177
  readOnly,
1110
1178
  required,
@@ -1119,9 +1187,10 @@ function AriaCombobox(props) {
1119
1187
  isItemEqualToValue,
1120
1188
  submitOnItemClick,
1121
1189
  hasInputValue,
1122
- requestSubmit
1190
+ requestSubmit,
1191
+ inputOwnsFormValue: selectionMode === 'none' && (inlineProp || !store.state.inputInsidePopup)
1123
1192
  });
1124
- }, [store, id, selectedValue, open, mounted, transitionStatus, items, getFloatingProps, getReferenceProps, getItemProps, openMethod, triggerProps, selectionMode, name, disabled, readOnly, required, validation, grid, isGrouped, virtualized, onOpenChangeComplete, openOnInputClick, itemToStringLabel, modal, isItemEqualToValue, submitOnItemClick, hasInputValue, inlineProp, requestSubmit, autoHighlightMode]);
1193
+ }, [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]);
1125
1194
  const hiddenInputRef = useMergedRefs(inputRefProp, validation.inputRef);
1126
1195
  const itemsContextValue = React.useMemo(() => ({
1127
1196
  query,
@@ -1136,7 +1205,7 @@ function AriaCombobox(props) {
1136
1205
  return stringifyAsValue(fieldRawValue, itemToStringValue);
1137
1206
  }, [fieldRawValue, itemToStringValue]);
1138
1207
  const hasMultipleSelection = multiple && Array.isArray(selectedValue) && selectedValue.length > 0;
1139
- const hiddenInputName = multiple || selectionMode === 'none' ? undefined : name;
1208
+ const hiddenInputName = multiple || selectionMode === 'none' && inputOwnsFormValue ? undefined : name;
1140
1209
  const hiddenInputs = React.useMemo(() => {
1141
1210
  if (!multiple || !Array.isArray(selectedValue) || !name) {
1142
1211
  return null;
@@ -1145,14 +1214,16 @@ function AriaCombobox(props) {
1145
1214
  const currentSerializedValue = stringifyAsValue(value, itemToStringValue);
1146
1215
  return /*#__PURE__*/jsx("input", {
1147
1216
  type: "hidden",
1217
+ form: form,
1148
1218
  name: name,
1149
- value: currentSerializedValue
1219
+ value: currentSerializedValue,
1220
+ disabled: disabled
1150
1221
  }, currentSerializedValue);
1151
1222
  });
1152
- }, [multiple, selectedValue, name, itemToStringValue]);
1223
+ }, [multiple, selectedValue, form, name, itemToStringValue, disabled]);
1153
1224
  const children = /*#__PURE__*/jsxs(React.Fragment, {
1154
1225
  children: [props.children, /*#__PURE__*/jsx("input", {
1155
- ...validation.getInputValidationProps({
1226
+ ...validation.getValidationProps(disabled, {
1156
1227
  // Move focus when the hidden input is focused.
1157
1228
  onFocus() {
1158
1229
  if (inputInsidePopup) {
@@ -1164,48 +1235,58 @@ function AriaCombobox(props) {
1164
1235
  // Handle browser autofill.
1165
1236
  onChange(event) {
1166
1237
  // Workaround for https://github.com/facebook/react/issues/9023
1167
- if (event.nativeEvent.defaultPrevented) {
1238
+ if (event.nativeEvent.defaultPrevented || disabled || readOnly) {
1168
1239
  return;
1169
1240
  }
1170
- const nextValue = event.target.value;
1241
+ const nextValue = event.currentTarget.value;
1242
+ const nextValueLower = nextValue.toLowerCase();
1171
1243
  const details = createChangeEventDetails(none, event.nativeEvent);
1244
+ const findSerializedMatchIndex = () => valuesRef.current.findIndex(candidate => stringifyAsValue(candidate, itemToStringValue).toLowerCase() === nextValueLower || stringifyAsLabel(candidate, itemToStringLabel).toLowerCase() === nextValueLower);
1172
1245
  function handleChange() {
1173
1246
  // Browser autofill only writes a single scalar value.
1174
1247
  if (multiple) {
1175
1248
  return;
1176
1249
  }
1177
1250
  if (selectionMode === 'none') {
1178
- setDirty(nextValue !== validityData.initialValue);
1179
1251
  setInputValue(nextValue, details);
1180
- if (shouldValidateOnChange()) {
1181
- validation.commit(nextValue);
1182
- }
1183
1252
  return;
1184
1253
  }
1185
- const matchingValue = valuesRef.current.find(v => {
1186
- const candidate = stringifyAsValue(v, itemToStringValue);
1187
- if (candidate.toLowerCase() === nextValue.toLowerCase()) {
1188
- return true;
1189
- }
1190
- return false;
1191
- });
1254
+
1255
+ // Preserve the original serialized matching, then fall back to rendered text,
1256
+ // which browsers can autofill for primitive values like `value="US">United States`.
1257
+ let matchingIndex = findSerializedMatchIndex();
1258
+ if (matchingIndex === -1) {
1259
+ matchingIndex = valuesRef.current.findIndex((_, index) => {
1260
+ const renderedLabel = labelsRef.current[index];
1261
+ return renderedLabel != null && renderedLabel.toLowerCase() === nextValueLower;
1262
+ });
1263
+ }
1264
+ const matchingValue = matchingIndex === -1 ? undefined : valuesRef.current[matchingIndex];
1192
1265
  if (matchingValue != null) {
1193
- setDirty(matchingValue !== validityData.initialValue);
1266
+ // `setSelectedValue` may be canceled by `onValueChange`; rely on
1267
+ // `useValueChanged` to mark the field dirty and run validation only
1268
+ // when the value actually changes.
1194
1269
  setSelectedValue?.(matchingValue, details);
1195
- if (shouldValidateOnChange()) {
1196
- validation.commit(matchingValue);
1197
- }
1198
1270
  }
1199
1271
  }
1200
- if (items) {
1201
- handleChange();
1202
- } else {
1272
+
1273
+ // Only single-selection autofill matches against the registered values/labels.
1274
+ // `multiple` ignores autofill and `none` just writes the input value, so avoid the
1275
+ // sticky `forceMounted` mount (which never resets) for those modes.
1276
+ if (single) {
1203
1277
  forceMount();
1204
- queueMicrotask(handleChange);
1278
+ if (items && findSerializedMatchIndex() === -1) {
1279
+ // `forceMount` only refreshes the derived labels for the `items` prop. When
1280
+ // serialized matching misses, also mount the list so rendered labels (which can
1281
+ // differ from the serialized values) are registered for autofill matching.
1282
+ store.set('forceMounted', true);
1283
+ }
1205
1284
  }
1285
+ queueMicrotask(handleChange);
1206
1286
  }
1207
1287
  }),
1208
1288
  id: id && hiddenInputName == null ? `${id}-hidden-input` : undefined,
1289
+ form: form,
1209
1290
  name: hiddenInputName,
1210
1291
  autoComplete: formAutoComplete,
1211
1292
  disabled: disabled,
@@ -1215,18 +1296,22 @@ function AriaCombobox(props) {
1215
1296
  ref: hiddenInputRef,
1216
1297
  style: hiddenInputName ? visuallyHiddenInput : visuallyHidden,
1217
1298
  tabIndex: -1,
1218
- "aria-hidden": true
1299
+ "aria-hidden": true,
1300
+ suppressHydrationWarning: true
1219
1301
  }), hiddenInputs]
1220
1302
  });
1221
1303
  return /*#__PURE__*/jsx(ComboboxRootContext.Provider, {
1222
1304
  value: store,
1223
1305
  children: /*#__PURE__*/jsx(ComboboxFloatingContext.Provider, {
1224
1306
  value: floatingRootContext,
1225
- children: /*#__PURE__*/jsx(ComboboxDerivedItemsContext.Provider, {
1226
- value: itemsContextValue,
1227
- children: /*#__PURE__*/jsx(ComboboxInputValueContext.Provider, {
1228
- value: inputValue,
1229
- children: children
1307
+ children: /*#__PURE__*/jsx(ComboboxHasItemsContext.Provider, {
1308
+ value: hasItems,
1309
+ children: /*#__PURE__*/jsx(ComboboxDerivedItemsContext.Provider, {
1310
+ value: itemsContextValue,
1311
+ children: /*#__PURE__*/jsx(ComboboxInputValueContext.Provider, {
1312
+ value: inputValue,
1313
+ children: children
1314
+ })
1230
1315
  })
1231
1316
  })
1232
1317
  })
@@ -1249,6 +1334,8 @@ const BOUNDARY_OFFSET = 2;
1249
1334
  /**
1250
1335
  * A button that opens the popup.
1251
1336
  * Renders a `<button>` element.
1337
+ *
1338
+ * Documentation: [Base UI Combobox](https://base-ui.com/react/components/combobox)
1252
1339
  */
1253
1340
  const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(componentProps, forwardedRef) {
1254
1341
  const {
@@ -1257,6 +1344,7 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
1257
1344
  nativeButton = true,
1258
1345
  disabled: disabledProp = false,
1259
1346
  id: idProp,
1347
+ style,
1260
1348
  ...elementProps
1261
1349
  } = componentProps;
1262
1350
  const {
@@ -1282,6 +1370,7 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
1282
1370
  const popupSideValue = useStore(store, selectors.popupSide);
1283
1371
  const positionerElement = useStore(store, selectors.positionerElement);
1284
1372
  const listElement = useStore(store, selectors.listElement);
1373
+ const storedPopupId = useStore(store, selectors.popupId);
1285
1374
  const triggerProps = useStore(store, selectors.triggerProps);
1286
1375
  const triggerElement = useStore(store, selectors.triggerElement);
1287
1376
  const inputInsidePopup = useStore(store, selectors.inputInsidePopup);
@@ -1303,6 +1392,14 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
1303
1392
  });
1304
1393
  const id = inputInsidePopup ? idProp ?? rootId : idProp;
1305
1394
  const ariaLabelledBy = resolveAriaLabelledBy(fieldLabelId, comboboxLabelId);
1395
+ let ariaControls;
1396
+ if (open && inputInsidePopup) {
1397
+ // Fall back to the default id while the popup registers its own (custom ids are stored once the
1398
+ // popup mounts), so `aria-controls` is set on the same commit `open` becomes `true`.
1399
+ ariaControls = storedPopupId ?? getComboboxPopupId(rootId);
1400
+ } else if (open) {
1401
+ ariaControls = listElement?.id;
1402
+ }
1306
1403
  const currentPointerTypeRef = React.useRef('');
1307
1404
  function trackPointerType(event) {
1308
1405
  currentPointerTypeRef.current = event.pointerType;
@@ -1352,7 +1449,7 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
1352
1449
  disabled,
1353
1450
  popupSide,
1354
1451
  listEmpty,
1355
- placeholder: !hasSelectedValue
1452
+ placeholder: selectionMode === 'none' ? false : !hasSelectedValue
1356
1453
  };
1357
1454
  const setTriggerElement = useStableCallback(element => {
1358
1455
  store.set('triggerElement', element);
@@ -1366,7 +1463,7 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
1366
1463
  role: inputInsidePopup ? 'combobox' : undefined,
1367
1464
  'aria-expanded': open ? 'true' : 'false',
1368
1465
  'aria-haspopup': inputInsidePopup ? 'dialog' : 'listbox',
1369
- 'aria-controls': open ? listElement?.id : undefined,
1466
+ 'aria-controls': ariaControls,
1370
1467
  'aria-required': inputInsidePopup ? required || undefined : undefined,
1371
1468
  'aria-labelledby': ariaLabelledBy,
1372
1469
  onPointerDown: trackPointerType,
@@ -1444,7 +1541,7 @@ const ComboboxTrigger = /*#__PURE__*/React.forwardRef(function ComboboxTrigger(c
1444
1541
  store.state.inputRef.current?.focus();
1445
1542
  }
1446
1543
  }
1447
- }, validation ? validation.getValidationProps(elementProps) : elementProps, getButtonProps],
1544
+ }, validation ? validation.getValidationProps(disabled, elementProps) : elementProps, getButtonProps],
1448
1545
  stateAttributesMapping: triggerStateAttributesMapping
1449
1546
  });
1450
1547
  return element;
@@ -1479,9 +1576,9 @@ const ComboboxInternalDismissButton = /*#__PURE__*/React.forwardRef(function Com
1479
1576
  native: false
1480
1577
  });
1481
1578
  const mergedRef = useMergedRefs(forwardedRef, buttonRef);
1482
- const handleDismiss = useStableCallback(event => {
1579
+ function handleDismiss(event) {
1483
1580
  store.state.setOpen(false, createChangeEventDetails(closePress, event.nativeEvent, event.currentTarget));
1484
- });
1581
+ }
1485
1582
  const dismissProps = getButtonProps({
1486
1583
  onClick: handleDismiss
1487
1584
  });
@@ -1501,6 +1598,7 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1501
1598
  className,
1502
1599
  disabled: disabledProp = false,
1503
1600
  id: idProp,
1601
+ style,
1504
1602
  ...elementProps
1505
1603
  } = componentProps;
1506
1604
  const {
@@ -1529,6 +1627,7 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1529
1627
  const comboboxDisabled = useStore(store, selectors.disabled);
1530
1628
  const readOnly = useStore(store, selectors.readOnly);
1531
1629
  const name = useStore(store, selectors.name);
1630
+ const form = useStore(store, selectors.form);
1532
1631
  const selectionMode = useStore(store, selectors.selectionMode);
1533
1632
  const autoHighlightMode = useStore(store, selectors.autoHighlight);
1534
1633
  const inputProps = useStore(store, selectors.inputProps);
@@ -1554,6 +1653,7 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1554
1653
  const isComposingRef = React.useRef(false);
1555
1654
  const lastActiveIndexRef = React.useRef(null);
1556
1655
  const shouldRestoreActiveIndexRef = React.useRef(false);
1656
+ const inputOwnsFormValue = selectionMode === 'none' && !hasPositionerParent;
1557
1657
  const setInputElement = useStableCallback(element => {
1558
1658
  const nextIsInsidePopup = hasPositionerParent || store.state.inline;
1559
1659
  if (nextIsInsidePopup && !store.state.hasInputValue) {
@@ -1561,10 +1661,11 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1561
1661
  }
1562
1662
  store.update({
1563
1663
  inputElement: element,
1564
- inputInsidePopup: nextIsInsidePopup
1664
+ inputInsidePopup: nextIsInsidePopup,
1665
+ inputOwnsFormValue
1565
1666
  });
1566
1667
  });
1567
- const validationProps = hasPositionerParent || !validation ? elementProps : validation.getValidationProps(elementProps);
1668
+ const validationProps = hasPositionerParent || !validation ? elementProps : validation.getValidationProps(disabled, elementProps);
1568
1669
  const state = {
1569
1670
  ...fieldStateForInput,
1570
1671
  open,
@@ -1581,17 +1682,21 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1581
1682
  const {
1582
1683
  highlightedChipIndex
1583
1684
  } = comboboxChipsContext;
1685
+ const renderedChipsCount = comboboxChipsContext.chipsRef.current.length;
1686
+ const isRtl = direction === 'rtl';
1687
+ const previousChipKey = isRtl ? 'ArrowRight' : 'ArrowLeft';
1688
+ const nextChipKey = isRtl ? 'ArrowLeft' : 'ArrowRight';
1584
1689
  if (highlightedChipIndex !== undefined) {
1585
- if (event.key === 'ArrowLeft') {
1690
+ if (event.key === previousChipKey) {
1586
1691
  event.preventDefault();
1587
1692
  if (highlightedChipIndex > 0) {
1588
1693
  nextIndex = highlightedChipIndex - 1;
1589
1694
  } else {
1590
1695
  nextIndex = undefined;
1591
1696
  }
1592
- } else if (event.key === 'ArrowRight') {
1697
+ } else if (event.key === nextChipKey) {
1593
1698
  event.preventDefault();
1594
- if (highlightedChipIndex < selectedValue.length - 1) {
1699
+ if (highlightedChipIndex < renderedChipsCount - 1) {
1595
1700
  nextIndex = highlightedChipIndex + 1;
1596
1701
  } else {
1597
1702
  nextIndex = undefined;
@@ -1612,10 +1717,9 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1612
1717
  }
1613
1718
 
1614
1719
  // Handle navigation when no chip is highlighted
1615
- if (event.key === 'ArrowLeft' && (event.currentTarget.selectionStart ?? 0) === 0 && selectedValue.length > 0) {
1720
+ if (event.key === previousChipKey && (event.currentTarget.selectionStart ?? 0) === 0 && selectedValue.length > 0) {
1616
1721
  event.preventDefault();
1617
- const lastChipIndex = Math.max(selectedValue.length - 1, 0);
1618
- nextIndex = lastChipIndex;
1722
+ nextIndex = renderedChipsCount > 0 ? renderedChipsCount - 1 : undefined;
1619
1723
  } else if (event.key === 'Backspace' && event.currentTarget.value === '' && selectedValue.length > 0) {
1620
1724
  store.state.setIndices({
1621
1725
  activeIndex: null,
@@ -1638,7 +1742,8 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1638
1742
  disabled,
1639
1743
  readOnly,
1640
1744
  required: selectionMode === 'none' ? required : undefined,
1641
- ...(selectionMode === 'none' && name && {
1745
+ form,
1746
+ ...(inputOwnsFormValue && name && {
1642
1747
  name
1643
1748
  }),
1644
1749
  id,
@@ -1675,7 +1780,7 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1675
1780
  }
1676
1781
  },
1677
1782
  onCompositionStart(event) {
1678
- if (isAndroid) {
1783
+ if (android) {
1679
1784
  return;
1680
1785
  }
1681
1786
  isComposingRef.current = true;
@@ -1729,7 +1834,11 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1729
1834
  }
1730
1835
  return;
1731
1836
  }
1732
- store.state.setInputValue(event.currentTarget.value, createChangeEventDetails(inputChange, event.nativeEvent));
1837
+ const inputChangeDetails = createChangeEventDetails(inputChange, event.nativeEvent);
1838
+ store.state.setInputValue(event.currentTarget.value, inputChangeDetails);
1839
+ if (inputChangeDetails.isCanceled) {
1840
+ return;
1841
+ }
1733
1842
  const empty = event.currentTarget.value === '';
1734
1843
  const clearDetails = createChangeEventDetails(inputClear, event.nativeEvent);
1735
1844
  if (empty && !store.state.inputInsidePopup) {
@@ -1779,14 +1888,14 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1779
1888
  const isRTL = direction === 'rtl';
1780
1889
  if (event.key === 'Home') {
1781
1890
  stopEvent(event);
1782
- const cursor = isFirefox && isRTL ? input.value.length : 0;
1891
+ const cursor = gecko && isRTL ? input.value.length : 0;
1783
1892
  input.setSelectionRange(cursor, cursor);
1784
1893
  input.scrollLeft = 0;
1785
1894
  return;
1786
1895
  }
1787
1896
  if (event.key === 'End') {
1788
1897
  stopEvent(event);
1789
- const cursor = isFirefox && isRTL ? 0 : input.value.length;
1898
+ const cursor = gecko && isRTL ? 0 : input.value.length;
1790
1899
  input.setSelectionRange(cursor, cursor);
1791
1900
  input.scrollLeft = isRTL ? -scrollAmount : scrollAmount;
1792
1901
  return;
@@ -1805,7 +1914,9 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1805
1914
 
1806
1915
  // Handle deletion when no chip is highlighted and the input is empty.
1807
1916
  if (comboboxChipsContext && event.key === 'Backspace' && input.value === '' && comboboxChipsContext.highlightedChipIndex === undefined && Array.isArray(selectedValue) && selectedValue.length > 0) {
1808
- const newValue = selectedValue.slice(0, -1);
1917
+ const renderedChipsCount = comboboxChipsContext.chipsRef.current.length;
1918
+ const removalIndex = renderedChipsCount > 0 ? renderedChipsCount - 1 : selectedValue.length - 1;
1919
+ const newValue = selectedValue.filter((_, index) => index !== removalIndex);
1809
1920
  // If the removed item was also the active (highlighted) item, clear highlight
1810
1921
  store.state.setIndices({
1811
1922
  activeIndex: null,
@@ -1858,27 +1969,52 @@ const ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(compo
1858
1969
  }, validationProps],
1859
1970
  stateAttributesMapping: triggerStateAttributesMapping
1860
1971
  });
1972
+ const renderedInput = hasPositionerParent ? /*#__PURE__*/jsx(FieldRootContext.Provider, {
1973
+ value: DEFAULT_FIELD_ROOT_CONTEXT,
1974
+ children: element
1975
+ }) : element;
1861
1976
  return /*#__PURE__*/jsxs(React.Fragment, {
1862
1977
  children: [open && focusManagerModal && /*#__PURE__*/jsx(ComboboxInternalDismissButton, {
1863
1978
  ref: store.state.startDismissRef
1864
- }), element]
1979
+ }), renderedInput]
1865
1980
  });
1866
1981
  });
1867
1982
  if (process.env.NODE_ENV !== "production") ComboboxInput.displayName = "ComboboxInput";
1868
1983
 
1984
+ function handleInputPress(event, store, disabled, readOnly, shouldIgnoreTarget) {
1985
+ if (event.baseUIHandlerPrevented || readOnly) {
1986
+ return;
1987
+ }
1988
+ const target = getTarget(event.nativeEvent);
1989
+ const targetElement = isElement(target) ? target : null;
1990
+ if (targetElement !== event.currentTarget && (shouldIgnoreTarget?.(targetElement) || isInteractiveElement(targetElement))) {
1991
+ return;
1992
+ }
1993
+ event.preventDefault();
1994
+ if (disabled) {
1995
+ return;
1996
+ }
1997
+ store.state.inputRef.current?.focus();
1998
+ if (store.state.openOnInputClick) {
1999
+ store.state.setOpen(true, createChangeEventDetails(inputPress, event.nativeEvent));
2000
+ }
2001
+ }
2002
+
1869
2003
  /**
1870
2004
  * A wrapper for the input and its associated controls.
1871
2005
  * Renders a `<div>` element.
2006
+ *
2007
+ * Documentation: [Base UI Combobox](https://base-ui.com/react/components/combobox)
1872
2008
  */
1873
2009
  const ComboboxInputGroup = /*#__PURE__*/React.forwardRef(function ComboboxInputGroup(componentProps, forwardedRef) {
1874
2010
  const {
1875
2011
  render,
1876
2012
  className,
2013
+ style,
1877
2014
  ...elementProps
1878
2015
  } = componentProps;
1879
2016
  const {
1880
- state: fieldState,
1881
- disabled: fieldDisabled
2017
+ state: fieldState
1882
2018
  } = useFieldRootContext();
1883
2019
  const store = useComboboxRootContext();
1884
2020
  const {
@@ -1893,7 +2029,7 @@ const ComboboxInputGroup = /*#__PURE__*/React.forwardRef(function ComboboxInputG
1893
2029
  const hasSelectedValue = useStore(store, selectors.hasSelectedValue);
1894
2030
  const selectionMode = useStore(store, selectors.selectionMode);
1895
2031
  const popupSide = mounted && positionerElement ? popupSideValue : null;
1896
- const disabled = fieldDisabled || comboboxDisabled;
2032
+ const disabled = comboboxDisabled;
1897
2033
  const listEmpty = filteredItems.length === 0;
1898
2034
  const placeholder = selectionMode === 'none' ? false : !hasSelectedValue;
1899
2035
  const state = {
@@ -1911,7 +2047,12 @@ const ComboboxInputGroup = /*#__PURE__*/React.forwardRef(function ComboboxInputG
1911
2047
  return useRenderElement('div', componentProps, {
1912
2048
  ref: [forwardedRef, setInputGroupElement],
1913
2049
  props: [{
1914
- role: 'group'
2050
+ role: 'group',
2051
+ onMouseDown(event) {
2052
+ handleInputPress(event, store, disabled, readOnly, target => {
2053
+ return contains(store.state.chipsContainerRef.current, target);
2054
+ });
2055
+ }
1915
2056
  }, elementProps],
1916
2057
  state,
1917
2058
  stateAttributesMapping: triggerStateAttributesMapping
@@ -1922,11 +2063,14 @@ if (process.env.NODE_ENV !== "production") ComboboxInputGroup.displayName = "Com
1922
2063
  /**
1923
2064
  * An icon that indicates that the trigger button opens the popup.
1924
2065
  * Renders a `<span>` element.
2066
+ *
2067
+ * Documentation: [Base UI Combobox](https://base-ui.com/react/components/combobox)
1925
2068
  */
1926
2069
  const ComboboxIcon = /*#__PURE__*/React.forwardRef(function ComboboxIcon(componentProps, forwardedRef) {
1927
2070
  const {
1928
- className,
1929
2071
  render,
2072
+ className,
2073
+ style,
1930
2074
  ...elementProps
1931
2075
  } = componentProps;
1932
2076
  const element = useRenderElement('span', componentProps, {
@@ -1948,6 +2092,8 @@ const stateAttributesMapping$2 = {
1948
2092
  /**
1949
2093
  * Clears the value when clicked.
1950
2094
  * Renders a `<button>` element.
2095
+ *
2096
+ * Documentation: [Base UI Combobox](https://base-ui.com/react/components/combobox)
1951
2097
  */
1952
2098
  const ComboboxClear = /*#__PURE__*/React.forwardRef(function ComboboxClear(componentProps, forwardedRef) {
1953
2099
  const {
@@ -1956,6 +2102,7 @@ const ComboboxClear = /*#__PURE__*/React.forwardRef(function ComboboxClear(compo
1956
2102
  disabled: disabledProp = false,
1957
2103
  nativeButton = true,
1958
2104
  keepMounted = false,
2105
+ style,
1959
2106
  ...elementProps
1960
2107
  } = componentProps;
1961
2108
  const {
@@ -1992,6 +2139,7 @@ const ComboboxClear = /*#__PURE__*/React.forwardRef(function ComboboxClear(compo
1992
2139
  } = useTransitionStatus(visible);
1993
2140
  const state = {
1994
2141
  disabled,
2142
+ visible,
1995
2143
  open,
1996
2144
  transitionStatus
1997
2145
  };
@@ -2087,6 +2235,7 @@ const ComboboxList = /*#__PURE__*/React.forwardRef(function ComboboxList(compone
2087
2235
  const {
2088
2236
  render,
2089
2237
  className,
2238
+ style,
2090
2239
  children,
2091
2240
  ...elementProps
2092
2241
  } = componentProps;
@@ -2094,17 +2243,14 @@ const ComboboxList = /*#__PURE__*/React.forwardRef(function ComboboxList(compone
2094
2243
  const floatingRootContext = useComboboxFloatingContext();
2095
2244
  const hasPositionerContext = Boolean(useComboboxPositionerContext(true));
2096
2245
  const {
2097
- filteredItems
2246
+ filteredItems,
2247
+ hasItems
2098
2248
  } = useComboboxDerivedItemsContext();
2099
- const items = useStore(store, selectors.items);
2100
- const labelsRef = useStore(store, selectors.labelsRef);
2101
- const listRef = useStore(store, selectors.listRef);
2102
2249
  const selectionMode = useStore(store, selectors.selectionMode);
2103
2250
  const grid = useStore(store, selectors.grid);
2104
2251
  const popupProps = useStore(store, selectors.popupProps);
2105
- const disabled = useStore(store, selectors.disabled);
2106
- const readOnly = useStore(store, selectors.readOnly);
2107
2252
  const virtualized = useStore(store, selectors.virtualized);
2253
+ const forceMounted = useStore(store, selectors.forceMounted);
2108
2254
  const multiple = selectionMode === 'multiple';
2109
2255
  const empty = filteredItems.length === 0;
2110
2256
  const setPositionerElement = useStableCallback(element => {
@@ -2140,7 +2286,7 @@ const ComboboxList = /*#__PURE__*/React.forwardRef(function ComboboxList(compone
2140
2286
  role: grid ? 'grid' : 'listbox',
2141
2287
  'aria-multiselectable': multiple ? 'true' : undefined,
2142
2288
  onKeyDown(event) {
2143
- if (disabled || readOnly) {
2289
+ if (store.state.disabled || store.state.readOnly) {
2144
2290
  return;
2145
2291
  }
2146
2292
  if (event.key === 'Enter') {
@@ -2170,28 +2316,93 @@ const ComboboxList = /*#__PURE__*/React.forwardRef(function ComboboxList(compone
2170
2316
  if (virtualized) {
2171
2317
  return element;
2172
2318
  }
2319
+
2320
+ // With the `items` prop, typeahead labels are derived from the items so they survive the list
2321
+ // unmounting (unmounting clears the registered labels). Rendered labels only need to be
2322
+ // registered when the list is force-mounted to match browser autofill against rendered text.
2323
+ const labelsRef = hasItems && !forceMounted ? undefined : store.state.labelsRef;
2173
2324
  return /*#__PURE__*/jsx(CompositeList, {
2174
- elementsRef: listRef,
2175
- labelsRef: items ? undefined : labelsRef,
2325
+ elementsRef: store.state.listRef,
2326
+ labelsRef: labelsRef,
2176
2327
  children: element
2177
2328
  });
2178
2329
  });
2179
2330
  if (process.env.NODE_ENV !== "production") ComboboxList.displayName = "ComboboxList";
2180
2331
 
2332
+ // Word Joiner is invisible and zero-width, so it forces a text mutation without shifting layout.
2333
+ const LIVE_REGION_MARKER = '\u2060';
2334
+ // Safari VoiceOver needed roughly 200ms to reliably notice the initial polite live-region change.
2335
+ const INITIAL_LIVE_REGION_TEXT_MUTATION_RESET_DELAY = 200;
2336
+ function findLastTextNode(root) {
2337
+ const walker = root.ownerDocument.createTreeWalker(root, NodeFilter.SHOW_TEXT);
2338
+ let lastTextNode = null;
2339
+ while (walker.nextNode()) {
2340
+ const textNode = walker.currentNode;
2341
+ if (textNode.nodeValue !== '') {
2342
+ lastTextNode = textNode;
2343
+ }
2344
+ }
2345
+ return lastTextNode;
2346
+ }
2347
+ function useInitialLiveRegionTextMutation() {
2348
+ const timeout = useTimeout();
2349
+ const rootRef = React.useRef(null);
2350
+
2351
+ // Only the initial mounted announcement needs the marker; later text updates announce naturally.
2352
+ React.useEffect(() => {
2353
+ if (ios) {
2354
+ return undefined;
2355
+ }
2356
+ const root = rootRef.current;
2357
+ if (root == null) {
2358
+ return undefined;
2359
+ }
2360
+ const textNode = findLastTextNode(root);
2361
+ if (textNode == null) {
2362
+ return undefined;
2363
+ }
2364
+ const originalValue = textNode.nodeValue ?? '';
2365
+ const markedValue = `${originalValue}${LIVE_REGION_MARKER}`;
2366
+ textNode.nodeValue = markedValue;
2367
+ timeout.start(INITIAL_LIVE_REGION_TEXT_MUTATION_RESET_DELAY, () => {
2368
+ if (textNode.nodeValue === markedValue) {
2369
+ textNode.nodeValue = originalValue;
2370
+ }
2371
+ });
2372
+ return () => {
2373
+ timeout.clear();
2374
+ if (textNode.nodeValue === markedValue) {
2375
+ textNode.nodeValue = originalValue;
2376
+ }
2377
+ };
2378
+ }, [rootRef, timeout]);
2379
+ return rootRef;
2380
+ }
2381
+
2181
2382
  /**
2182
2383
  * Displays a status message whose content changes are announced politely to screen readers.
2183
2384
  * Useful for conveying the status of an asynchronously loaded list.
2385
+ * This component's root element must remain mounted in the DOM to announce
2386
+ * changes consistently across screen readers. Avoid hiding or removing the
2387
+ * component itself with `display: none`, `hidden`, `aria-hidden`, or conditional
2388
+ * rendering. Prefer updating or conditionally rendering its children instead.
2184
2389
  * Renders a `<div>` element.
2390
+ *
2391
+ * Documentation: [Base UI Combobox](https://base-ui.com/react/components/combobox)
2185
2392
  */
2186
2393
  const ComboboxStatus = /*#__PURE__*/React.forwardRef(function ComboboxStatus(componentProps, forwardedRef) {
2187
2394
  const {
2188
2395
  render,
2189
2396
  className,
2397
+ style,
2398
+ children: childrenProp,
2190
2399
  ...elementProps
2191
2400
  } = componentProps;
2401
+ const statusRef = useInitialLiveRegionTextMutation();
2192
2402
  return useRenderElement('div', componentProps, {
2193
- ref: forwardedRef,
2403
+ ref: [forwardedRef, statusRef],
2194
2404
  props: [{
2405
+ children: childrenProp,
2195
2406
  role: 'status',
2196
2407
  'aria-live': 'polite',
2197
2408
  'aria-atomic': true
@@ -2240,11 +2451,14 @@ const stateAttributesMapping$1 = {
2240
2451
  /**
2241
2452
  * An overlay displayed beneath the popup.
2242
2453
  * Renders a `<div>` element.
2454
+ *
2455
+ * Documentation: [Base UI Combobox](https://base-ui.com/react/components/combobox)
2243
2456
  */
2244
2457
  const ComboboxBackdrop = /*#__PURE__*/React.forwardRef(function ComboboxBackdrop(componentProps, forwardedRef) {
2245
2458
  const {
2246
- className,
2247
2459
  render,
2460
+ className,
2461
+ style,
2248
2462
  ...elementProps
2249
2463
  } = componentProps;
2250
2464
  const store = useComboboxRootContext();
@@ -2287,6 +2501,7 @@ const ComboboxPositioner = /*#__PURE__*/React.forwardRef(function ComboboxPositi
2287
2501
  sticky = false,
2288
2502
  disableAnchorTracking = false,
2289
2503
  collisionAvoidance = DROPDOWN_COLLISION_AVOIDANCE,
2504
+ style: styleProp,
2290
2505
  ...elementProps
2291
2506
  } = componentProps;
2292
2507
  const store = useComboboxRootContext();
@@ -2299,6 +2514,7 @@ const ComboboxPositioner = /*#__PURE__*/React.forwardRef(function ComboboxPositi
2299
2514
  const open = useStore(store, selectors.open);
2300
2515
  const mounted = useStore(store, selectors.mounted);
2301
2516
  const openMethod = useStore(store, selectors.openMethod);
2517
+ const positionerElement = useStore(store, selectors.positionerElement);
2302
2518
  const triggerElement = useStore(store, selectors.triggerElement);
2303
2519
  const inputElement = useStore(store, selectors.inputElement);
2304
2520
  const inputGroupElement = useStore(store, selectors.inputGroupElement);
@@ -2324,20 +2540,7 @@ const ComboboxPositioner = /*#__PURE__*/React.forwardRef(function ComboboxPositi
2324
2540
  collisionAvoidance,
2325
2541
  lazyFlip: true
2326
2542
  });
2327
- useScrollLock(open && modal && openMethod !== 'touch', triggerElement);
2328
- const defaultProps = React.useMemo(() => {
2329
- const style = {
2330
- ...positioning.positionerStyles
2331
- };
2332
- if (!open) {
2333
- style.pointerEvents = 'none';
2334
- }
2335
- return {
2336
- role: 'presentation',
2337
- hidden: !mounted,
2338
- style
2339
- };
2340
- }, [open, mounted, positioning.positionerStyles]);
2543
+ useAnchoredPopupScrollLock(open && modal, openMethod === 'touch', positionerElement, triggerElement);
2341
2544
  const state = {
2342
2545
  open,
2343
2546
  side: positioning.side,
@@ -2348,26 +2551,19 @@ const ComboboxPositioner = /*#__PURE__*/React.forwardRef(function ComboboxPositi
2348
2551
  useIsoLayoutEffect(() => {
2349
2552
  store.set('popupSide', positioning.side);
2350
2553
  }, [store, positioning.side]);
2351
- const contextValue = React.useMemo(() => ({
2352
- side: positioning.side,
2353
- align: positioning.align,
2354
- arrowRef: positioning.arrowRef,
2355
- arrowUncentered: positioning.arrowUncentered,
2356
- arrowStyles: positioning.arrowStyles,
2357
- anchorHidden: positioning.anchorHidden,
2358
- isPositioned: positioning.isPositioned
2359
- }), [positioning.side, positioning.align, positioning.arrowRef, positioning.arrowUncentered, positioning.arrowStyles, positioning.anchorHidden, positioning.isPositioned]);
2360
2554
  const setPositionerElement = useStableCallback(element => {
2361
2555
  store.set('positionerElement', element);
2362
2556
  });
2363
- const element = useRenderElement('div', componentProps, {
2364
- state,
2365
- ref: [forwardedRef, setPositionerElement],
2366
- props: [defaultProps, getDisabledMountTransitionStyles(transitionStatus), elementProps],
2367
- stateAttributesMapping: popupStateMapping
2557
+ const element = usePositioner(componentProps, state, {
2558
+ styles: positioning.positionerStyles,
2559
+ transitionStatus,
2560
+ props: elementProps,
2561
+ refs: [forwardedRef, setPositionerElement],
2562
+ hidden: !mounted,
2563
+ inert: !open
2368
2564
  });
2369
2565
  return /*#__PURE__*/jsxs(ComboboxPositionerContext.Provider, {
2370
- value: contextValue,
2566
+ value: positioning,
2371
2567
  children: [mounted && modal && /*#__PURE__*/jsx(InternalBackdrop, {
2372
2568
  inert: inertValue(!open),
2373
2569
  cutout: inputGroupElement ?? inputElement ?? triggerElement
@@ -2384,11 +2580,14 @@ const stateAttributesMapping = {
2384
2580
  /**
2385
2581
  * A container for the list.
2386
2582
  * Renders a `<div>` element.
2583
+ *
2584
+ * Documentation: [Base UI Combobox](https://base-ui.com/react/components/combobox)
2387
2585
  */
2388
2586
  const ComboboxPopup = /*#__PURE__*/React.forwardRef(function ComboboxPopup(componentProps, forwardedRef) {
2389
2587
  const {
2390
2588
  render,
2391
2589
  className,
2590
+ style,
2392
2591
  initialFocus,
2393
2592
  finalFocus,
2394
2593
  ...elementProps
@@ -2406,7 +2605,16 @@ const ComboboxPopup = /*#__PURE__*/React.forwardRef(function ComboboxPopup(compo
2406
2605
  const inputInsidePopup = useStore(store, selectors.inputInsidePopup);
2407
2606
  const inputElement = useStore(store, selectors.inputElement);
2408
2607
  const modal = useStore(store, selectors.modal);
2608
+ const rootId = useStore(store, selectors.id);
2409
2609
  const empty = filteredItems.length === 0;
2610
+ const popupId = elementProps.id ?? (inputInsidePopup ? getComboboxPopupId(rootId) : undefined);
2611
+ useIsoLayoutEffect(() => {
2612
+ // Prefer the rendered DOM id, which a `render` prop element or function may override.
2613
+ store.set('popupId', store.state.popupRef.current?.id || popupId);
2614
+ return () => {
2615
+ store.set('popupId', undefined);
2616
+ };
2617
+ }, [store, popupId]);
2410
2618
  useOpenChangeComplete({
2411
2619
  open,
2412
2620
  ref: store.state.popupRef,
@@ -2428,6 +2636,7 @@ const ComboboxPopup = /*#__PURE__*/React.forwardRef(function ComboboxPopup(compo
2428
2636
  state,
2429
2637
  ref: [forwardedRef, store.state.popupRef],
2430
2638
  props: [{
2639
+ id: popupId,
2431
2640
  role: inputInsidePopup ? 'dialog' : 'presentation',
2432
2641
  tabIndex: -1,
2433
2642
  onFocus(event) {
@@ -2472,11 +2681,14 @@ if (process.env.NODE_ENV !== "production") ComboboxPopup.displayName = "Combobox
2472
2681
  /**
2473
2682
  * Displays an element positioned against the anchor.
2474
2683
  * Renders a `<div>` element.
2684
+ *
2685
+ * Documentation: [Base UI Combobox](https://base-ui.com/react/components/combobox)
2475
2686
  */
2476
2687
  const ComboboxArrow = /*#__PURE__*/React.forwardRef(function ComboboxArrow(componentProps, forwardedRef) {
2477
2688
  const {
2478
- className,
2479
2689
  render,
2690
+ className,
2691
+ style,
2480
2692
  ...elementProps
2481
2693
  } = componentProps;
2482
2694
  const store = useComboboxRootContext();
@@ -2521,6 +2733,7 @@ const ComboboxGroup = /*#__PURE__*/React.forwardRef(function ComboboxGroup(compo
2521
2733
  const {
2522
2734
  render,
2523
2735
  className,
2736
+ style,
2524
2737
  items,
2525
2738
  ...elementProps
2526
2739
  } = componentProps;
@@ -2554,11 +2767,14 @@ if (process.env.NODE_ENV !== "production") ComboboxGroup.displayName = "Combobox
2554
2767
  /**
2555
2768
  * An accessible label that is automatically associated with its parent group.
2556
2769
  * Renders a `<div>` element.
2770
+ *
2771
+ * Documentation: [Base UI Combobox](https://base-ui.com/react/components/combobox)
2557
2772
  */
2558
2773
  const ComboboxGroupLabel = /*#__PURE__*/React.forwardRef(function ComboboxGroupLabel(componentProps, forwardedRef) {
2559
2774
  const {
2560
2775
  render,
2561
2776
  className,
2777
+ style,
2562
2778
  id: idProp,
2563
2779
  ...elementProps
2564
2780
  } = componentProps;
@@ -2598,10 +2814,17 @@ function useComboboxRowContext() {
2598
2814
  return React.useContext(ComboboxRowContext);
2599
2815
  }
2600
2816
 
2601
- const ComboboxItem = /*#__PURE__*/React.memo(/*#__PURE__*/React.forwardRef(function ComboboxItem(componentProps, forwardedRef) {
2817
+ function ComboboxItemInner(props) {
2818
+ const {
2819
+ componentProps,
2820
+ forwardedRef,
2821
+ virtualized,
2822
+ indexFromFilter
2823
+ } = props;
2602
2824
  const {
2603
2825
  render,
2604
2826
  className,
2827
+ style,
2605
2828
  value: itemValue = null,
2606
2829
  index: indexProp,
2607
2830
  disabled = false,
@@ -2617,22 +2840,18 @@ const ComboboxItem = /*#__PURE__*/React.memo(/*#__PURE__*/React.forwardRef(funct
2617
2840
  });
2618
2841
  const store = useComboboxRootContext();
2619
2842
  const isRow = useComboboxRowContext();
2620
- const {
2621
- flatFilteredItems,
2622
- hasItems
2623
- } = useComboboxDerivedItemsContext();
2843
+ const hasItems = useComboboxHasItemsContext();
2624
2844
  const open = useStore(store, selectors.open);
2625
2845
  const selectionMode = useStore(store, selectors.selectionMode);
2626
2846
  const readOnly = useStore(store, selectors.readOnly);
2627
- const virtualized = useStore(store, selectors.virtualized);
2628
2847
  const isItemEqualToValue = useStore(store, selectors.isItemEqualToValue);
2629
2848
  const selectable = selectionMode !== 'none';
2630
- const index = indexProp ?? (virtualized ? findItemIndex(flatFilteredItems, itemValue, isItemEqualToValue) : listItem.index);
2849
+ const index = indexProp ?? (virtualized ? indexFromFilter ?? -1 : listItem.index);
2631
2850
  const hasRegistered = listItem.index !== -1;
2632
2851
  const rootId = useStore(store, selectors.id);
2633
2852
  const highlighted = useStore(store, selectors.isActive, index);
2634
2853
  const matchesSelectedValue = useStore(store, selectors.isSelected, itemValue);
2635
- const getItemProps = useStore(store, selectors.getItemProps);
2854
+ const itemProps = useStore(store, selectors.itemProps);
2636
2855
  const itemRef = React.useRef(null);
2637
2856
  const id = rootId != null && hasRegistered ? `${rootId}-${index}` : undefined;
2638
2857
  const selected = matchesSelectedValue && selectable;
@@ -2678,17 +2897,6 @@ const ComboboxItem = /*#__PURE__*/React.memo(/*#__PURE__*/React.forwardRef(funct
2678
2897
  store.set('selectedIndex', index);
2679
2898
  }
2680
2899
  }, [hasRegistered, hasItems, open, store, index, itemValue, isItemEqualToValue]);
2681
- const state = {
2682
- disabled,
2683
- selected,
2684
- highlighted
2685
- };
2686
- const rootProps = getItemProps({
2687
- active: highlighted,
2688
- selected
2689
- });
2690
- rootProps.id = undefined;
2691
- rootProps.onFocus = undefined;
2692
2900
  const {
2693
2901
  getButtonProps,
2694
2902
  buttonRef
@@ -2698,6 +2906,11 @@ const ComboboxItem = /*#__PURE__*/React.memo(/*#__PURE__*/React.forwardRef(funct
2698
2906
  native: nativeButton,
2699
2907
  composite: true
2700
2908
  });
2909
+ const state = {
2910
+ disabled,
2911
+ selected,
2912
+ highlighted
2913
+ };
2701
2914
  function commitSelection(nativeEvent) {
2702
2915
  function selectItem() {
2703
2916
  store.state.handleSelection(nativeEvent, itemValue);
@@ -2721,6 +2934,11 @@ const ComboboxItem = /*#__PURE__*/React.memo(/*#__PURE__*/React.forwardRef(funct
2721
2934
  didPointerDownRef.current = true;
2722
2935
  event.preventDefault();
2723
2936
  },
2937
+ onMouseDown(event) {
2938
+ // iOS Safari can emit a synthetic mousedown for touch taps without a preceding
2939
+ // pointerdown. Prevent default here too so tapping an item does not blur the input.
2940
+ event.preventDefault();
2941
+ },
2724
2942
  onClick(event) {
2725
2943
  if (disabled || readOnly) {
2726
2944
  return;
@@ -2739,7 +2957,7 @@ const ComboboxItem = /*#__PURE__*/React.memo(/*#__PURE__*/React.forwardRef(funct
2739
2957
  const element = useRenderElement('div', componentProps, {
2740
2958
  ref: [buttonRef, forwardedRef, listItem.ref, itemRef],
2741
2959
  state,
2742
- props: [rootProps, defaultProps, elementProps, getButtonProps]
2960
+ props: [itemProps, defaultProps, elementProps, getButtonProps]
2743
2961
  });
2744
2962
  const contextValue = React.useMemo(() => ({
2745
2963
  selected,
@@ -2749,6 +2967,61 @@ const ComboboxItem = /*#__PURE__*/React.memo(/*#__PURE__*/React.forwardRef(funct
2749
2967
  value: contextValue,
2750
2968
  children: element
2751
2969
  });
2970
+ }
2971
+
2972
+ /**
2973
+ * Resolves the index from the filtered items for the virtualized fallback (no `index` prop).
2974
+ * Isolated here so that this per-keystroke subscription to the derived-items context is only
2975
+ * paid by virtualized items. Those re-render on every input change anyway — the parent
2976
+ * virtualizer re-windows the list as the filtered set changes — so the extra subscription costs
2977
+ * them nothing, while it keeps every non-virtualized item off that context.
2978
+ */
2979
+ function ComboboxItemVirtualizedIndex(props) {
2980
+ const {
2981
+ componentProps,
2982
+ forwardedRef
2983
+ } = props;
2984
+ const store = useComboboxRootContext();
2985
+ const isItemEqualToValue = useStore(store, selectors.isItemEqualToValue);
2986
+ const {
2987
+ flatFilteredItems
2988
+ } = useComboboxDerivedItemsContext();
2989
+ const indexFromFilter = findItemIndex(flatFilteredItems, componentProps.value ?? null, isItemEqualToValue);
2990
+
2991
+ // Only reached when `virtualized` is true (see the wrapper below).
2992
+ return /*#__PURE__*/jsx(ComboboxItemInner, {
2993
+ componentProps: componentProps,
2994
+ forwardedRef: forwardedRef,
2995
+ virtualized: true,
2996
+ indexFromFilter: indexFromFilter
2997
+ });
2998
+ }
2999
+
3000
+ /**
3001
+ * An individual item in the list.
3002
+ * Renders a `<div>` element.
3003
+ *
3004
+ * Documentation: [Base UI Combobox](https://base-ui.com/react/components/combobox)
3005
+ */
3006
+ const ComboboxItem = /*#__PURE__*/React.memo(/*#__PURE__*/React.forwardRef(function ComboboxItem(componentProps, forwardedRef) {
3007
+ const store = useComboboxRootContext();
3008
+ const virtualized = useStore(store, selectors.virtualized);
3009
+
3010
+ // `virtualized` (and whether an item provides an explicit `index`) must be stable for an
3011
+ // item's lifetime: the two branches return different component types, so flipping it at
3012
+ // runtime remounts the item and resets its refs and effects.
3013
+ if (virtualized && componentProps.index == null) {
3014
+ return /*#__PURE__*/jsx(ComboboxItemVirtualizedIndex, {
3015
+ componentProps: componentProps,
3016
+ forwardedRef: forwardedRef
3017
+ });
3018
+ }
3019
+ return /*#__PURE__*/jsx(ComboboxItemInner, {
3020
+ componentProps: componentProps,
3021
+ forwardedRef: forwardedRef,
3022
+ virtualized: virtualized,
3023
+ indexFromFilter: undefined
3024
+ });
2752
3025
  }));
2753
3026
  if (process.env.NODE_ENV !== "production") ComboboxItem.displayName = "ComboboxItem";
2754
3027
 
@@ -2756,6 +3029,7 @@ const ComboboxRow = /*#__PURE__*/React.forwardRef(function ComboboxRow(component
2756
3029
  const {
2757
3030
  render,
2758
3031
  className,
3032
+ style,
2759
3033
  ...elementProps
2760
3034
  } = componentProps;
2761
3035
  const element = useRenderElement('div', componentProps, {
@@ -2775,12 +3049,19 @@ if (process.env.NODE_ENV !== "production") ComboboxRow.displayName = "ComboboxRo
2775
3049
  * Renders its children only when the list is empty.
2776
3050
  * Requires the `items` prop on the root component.
2777
3051
  * Announces changes politely to screen readers.
3052
+ * This component's root element must remain mounted in the DOM to announce
3053
+ * changes consistently across screen readers. Avoid hiding or removing the
3054
+ * component itself with `display: none`, `hidden`, `aria-hidden`, or conditional
3055
+ * rendering. Prefer updating or conditionally rendering its children instead.
2778
3056
  * Renders a `<div>` element.
3057
+ *
3058
+ * Documentation: [Base UI Combobox](https://base-ui.com/react/components/combobox)
2779
3059
  */
2780
3060
  const ComboboxEmpty = /*#__PURE__*/React.forwardRef(function ComboboxEmpty(componentProps, forwardedRef) {
2781
3061
  const {
2782
3062
  render,
2783
3063
  className,
3064
+ style,
2784
3065
  children: childrenProp,
2785
3066
  ...elementProps
2786
3067
  } = componentProps;
@@ -2788,9 +3069,10 @@ const ComboboxEmpty = /*#__PURE__*/React.forwardRef(function ComboboxEmpty(compo
2788
3069
  filteredItems
2789
3070
  } = useComboboxDerivedItemsContext();
2790
3071
  const store = useComboboxRootContext();
3072
+ const emptyRef = useInitialLiveRegionTextMutation();
2791
3073
  const children = filteredItems.length === 0 ? childrenProp : null;
2792
3074
  return useRenderElement('div', componentProps, {
2793
- ref: [forwardedRef, store.state.emptyRef],
3075
+ ref: [forwardedRef, store.state.emptyRef, emptyRef],
2794
3076
  props: [{
2795
3077
  children,
2796
3078
  role: 'status',
@@ -2838,6 +3120,7 @@ const ComboboxLabel = /*#__PURE__*/React.forwardRef(function ComboboxLabel(compo
2838
3120
  const {
2839
3121
  render,
2840
3122
  className,
3123
+ style,
2841
3124
  ...elementProps
2842
3125
  } = componentProps;
2843
3126
  // Keep label id derived from the root and ignore runtime `id` overrides from untyped consumers.
@@ -2925,13 +3208,13 @@ const ComboboxItemIndicator = /*#__PURE__*/React.forwardRef(function ComboboxIte
2925
3208
  });
2926
3209
  });
2927
3210
 
2928
- /** The core implementation of the indicator is split here to avoid paying the hooks
2929
- * costs unless the element needs to be mounted. */
3211
+ // Split the core implementation to avoid paying the hook costs unless the element needs to mount.
2930
3212
  if (process.env.NODE_ENV !== "production") ComboboxItemIndicator.displayName = "ComboboxItemIndicator";
2931
3213
  const Inner = /*#__PURE__*/React.memo(/*#__PURE__*/React.forwardRef((componentProps, forwardedRef) => {
2932
3214
  const {
2933
3215
  render,
2934
3216
  className,
3217
+ style,
2935
3218
  keepMounted,
2936
3219
  ...elementProps
2937
3220
  } = componentProps;
@@ -2973,6 +3256,7 @@ const ComboboxChips = /*#__PURE__*/React.forwardRef(function ComboboxChips(compo
2973
3256
  const {
2974
3257
  render,
2975
3258
  className,
3259
+ style,
2976
3260
  ...elementProps
2977
3261
  } = componentProps;
2978
3262
  const store = useComboboxRootContext();
@@ -2989,7 +3273,11 @@ const ComboboxChips = /*#__PURE__*/React.forwardRef(function ComboboxChips(compo
2989
3273
  // arrow keys inside a container unless it has a toolbar role.
2990
3274
  props: [hasSelectionChips ? {
2991
3275
  role: 'toolbar'
2992
- } : EMPTY_OBJECT, elementProps]
3276
+ } : EMPTY_OBJECT, {
3277
+ onMouseDown(event) {
3278
+ handleInputPress(event, store, store.state.disabled, store.state.readOnly);
3279
+ }
3280
+ }, elementProps]
2993
3281
  });
2994
3282
  const contextValue = React.useMemo(() => ({
2995
3283
  highlightedChipIndex,
@@ -3011,7 +3299,7 @@ if (process.env.NODE_ENV !== "production") ComboboxChipContext.displayName = "Co
3011
3299
  function useComboboxChipContext() {
3012
3300
  const context = React.useContext(ComboboxChipContext);
3013
3301
  if (!context) {
3014
- throw new Error(process.env.NODE_ENV !== "production" ? 'useComboboxChipContext must be used within a ComboboxChip' : formatErrorMessage(17));
3302
+ throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: ComboboxChipContext is missing. ComboboxChip parts must be placed within <Combobox.Chip>.' : formatErrorMessage(17));
3015
3303
  }
3016
3304
  return context;
3017
3305
  }
@@ -3020,6 +3308,7 @@ const ComboboxChip = /*#__PURE__*/React.forwardRef(function ComboboxChip(compone
3020
3308
  const {
3021
3309
  render,
3022
3310
  className,
3311
+ style,
3023
3312
  ...elementProps
3024
3313
  } = componentProps;
3025
3314
  const store = useComboboxRootContext();
@@ -3027,6 +3316,7 @@ const ComboboxChip = /*#__PURE__*/React.forwardRef(function ComboboxChip(compone
3027
3316
  setHighlightedChipIndex,
3028
3317
  chipsRef
3029
3318
  } = useComboboxChipsContext();
3319
+ const direction = useDirection();
3030
3320
  const disabled = useStore(store, selectors.disabled);
3031
3321
  const readOnly = useStore(store, selectors.readOnly);
3032
3322
  const selectedValue = useStore(store, selectors.selectedValue);
@@ -3036,16 +3326,19 @@ const ComboboxChip = /*#__PURE__*/React.forwardRef(function ComboboxChip(compone
3036
3326
  } = useCompositeListItem();
3037
3327
  function handleKeyDown(event) {
3038
3328
  let nextIndex = index;
3039
- if (event.key === 'ArrowLeft') {
3329
+ const isRtl = direction === 'rtl';
3330
+ const previousChipKey = isRtl ? 'ArrowRight' : 'ArrowLeft';
3331
+ const nextChipKey = isRtl ? 'ArrowLeft' : 'ArrowRight';
3332
+ if (event.key === previousChipKey) {
3040
3333
  event.preventDefault();
3041
3334
  if (index > 0) {
3042
3335
  nextIndex = index - 1;
3043
3336
  } else {
3044
3337
  nextIndex = undefined;
3045
3338
  }
3046
- } else if (event.key === 'ArrowRight') {
3339
+ } else if (event.key === nextChipKey) {
3047
3340
  event.preventDefault();
3048
- if (index < selectedValue.length - 1) {
3341
+ if (index < chipsRef.current.length - 1) {
3049
3342
  nextIndex = index + 1;
3050
3343
  } else {
3051
3344
  nextIndex = undefined;
@@ -3097,16 +3390,6 @@ const ComboboxChip = /*#__PURE__*/React.forwardRef(function ComboboxChip(compone
3097
3390
  } else {
3098
3391
  chipsRef.current[nextIndex]?.focus();
3099
3392
  }
3100
- },
3101
- onMouseDown(event) {
3102
- if (readOnly) {
3103
- return;
3104
- }
3105
- event.preventDefault();
3106
- if (disabled) {
3107
- return;
3108
- }
3109
- store.state.inputRef.current?.focus();
3110
3393
  }
3111
3394
  }, elementProps]
3112
3395
  });
@@ -3123,6 +3406,8 @@ if (process.env.NODE_ENV !== "production") ComboboxChip.displayName = "ComboboxC
3123
3406
  /**
3124
3407
  * A button to remove a chip.
3125
3408
  * Renders a `<button>` element.
3409
+ *
3410
+ * Documentation: [Base UI Combobox](https://base-ui.com/react/components/combobox)
3126
3411
  */
3127
3412
  const ComboboxChipRemove = /*#__PURE__*/React.forwardRef(function ComboboxChipRemove(componentProps, forwardedRef) {
3128
3413
  const {
@@ -3130,6 +3415,7 @@ const ComboboxChipRemove = /*#__PURE__*/React.forwardRef(function ComboboxChipRe
3130
3415
  className,
3131
3416
  disabled: disabledProp = false,
3132
3417
  nativeButton = true,
3418
+ style,
3133
3419
  ...elementProps
3134
3420
  } = componentProps;
3135
3421
  const store = useComboboxRootContext();
@@ -3181,6 +3467,9 @@ const ComboboxChipRemove = /*#__PURE__*/React.forwardRef(function ComboboxChipRe
3181
3467
  state,
3182
3468
  props: [{
3183
3469
  tabIndex: -1,
3470
+ onMouseDown(event) {
3471
+ event.preventDefault();
3472
+ },
3184
3473
  onClick(event) {
3185
3474
  if (disabled || readOnly) {
3186
3475
  return;