@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
@@ -3,15 +3,14 @@
3
3
  var React = require('react');
4
4
  var useButton = require('./useButton-cjs.js');
5
5
  var useRenderElement = require('./useRenderElement-cjs.js');
6
- var InternalBackdrop = require('./InternalBackdrop-cjs.js');
6
+ var useScrollLock = require('./useScrollLock-cjs.js');
7
+ var isElementDisabled = require('./isElementDisabled-cjs.js');
7
8
  var useLabelableId = require('./useLabelableId-cjs.js');
8
9
  var resolveAriaLabelledBy = require('./resolveAriaLabelledBy-cjs.js');
10
+ var scrollEdges = require('./scrollEdges-cjs.js');
9
11
  var jsxRuntime = require('react/jsx-runtime');
10
12
  var useCompositeListItem = require('./useCompositeListItem-cjs.js');
11
- var ReactDOM = require('react-dom');
12
- var index_esm = require('./index.esm-cjs.js');
13
- var CSPContext = require('./CSPContext-cjs.js');
14
- var ToolbarRootContext = require('./ToolbarRootContext-cjs.js');
13
+ var floatingUi_utils = require('./floating-ui.utils-cjs.js');
15
14
  var clamp = require('./clamp-cjs.js');
16
15
  var floatingUi_utils_dom = require('./floating-ui.utils.dom-cjs.js');
17
16
  var floatingUi_reactDom = require('./floating-ui.react-dom-cjs.js');
@@ -34,7 +33,25 @@ function _interopNamespaceDefault(e) {
34
33
  }
35
34
 
36
35
  var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
37
- var ReactDOM__namespace = /*#__PURE__*/_interopNamespaceDefault(ReactDOM);
36
+
37
+ /**
38
+ * Returns a previous value of its argument.
39
+ * @param value Current value.
40
+ * @returns Previous value, or null if there is no previous value.
41
+ */
42
+ function usePreviousValue(value) {
43
+ const [state, setState] = React__namespace.useState({
44
+ current: value,
45
+ previous: null
46
+ });
47
+ if (value !== state.current) {
48
+ setState({
49
+ current: value,
50
+ previous: state.current
51
+ });
52
+ }
53
+ return state.previous;
54
+ }
38
55
 
39
56
  const SelectRootContext = /*#__PURE__*/React__namespace.createContext(null);
40
57
  if (process.env.NODE_ENV !== "production") SelectRootContext.displayName = "SelectRootContext";
@@ -56,16 +73,16 @@ function useSelectFloatingContext() {
56
73
  }
57
74
 
58
75
  const selectors = {
59
- id: InternalBackdrop.createSelector(state => state.id),
60
- labelId: InternalBackdrop.createSelector(state => state.labelId),
61
- modal: InternalBackdrop.createSelector(state => state.modal),
62
- multiple: InternalBackdrop.createSelector(state => state.multiple),
63
- items: InternalBackdrop.createSelector(state => state.items),
64
- itemToStringLabel: InternalBackdrop.createSelector(state => state.itemToStringLabel),
65
- itemToStringValue: InternalBackdrop.createSelector(state => state.itemToStringValue),
66
- isItemEqualToValue: InternalBackdrop.createSelector(state => state.isItemEqualToValue),
67
- value: InternalBackdrop.createSelector(state => state.value),
68
- hasSelectedValue: InternalBackdrop.createSelector(state => {
76
+ id: useScrollLock.createSelector(state => state.id),
77
+ labelId: useScrollLock.createSelector(state => state.labelId),
78
+ modal: useScrollLock.createSelector(state => state.modal),
79
+ multiple: useScrollLock.createSelector(state => state.multiple),
80
+ items: useScrollLock.createSelector(state => state.items),
81
+ itemToStringLabel: useScrollLock.createSelector(state => state.itemToStringLabel),
82
+ itemToStringValue: useScrollLock.createSelector(state => state.itemToStringValue),
83
+ isItemEqualToValue: useScrollLock.createSelector(state => state.isItemEqualToValue),
84
+ value: useScrollLock.createSelector(state => state.value),
85
+ hasSelectedValue: useScrollLock.createSelector(state => {
69
86
  const {
70
87
  value,
71
88
  multiple,
@@ -79,42 +96,41 @@ const selectors = {
79
96
  }
80
97
  return resolveAriaLabelledBy.stringifyAsValue(value, itemToStringValue) !== '';
81
98
  }),
82
- hasNullItemLabel: InternalBackdrop.createSelector((state, enabled) => {
99
+ hasNullItemLabel: useScrollLock.createSelector((state, enabled) => {
83
100
  return enabled ? resolveAriaLabelledBy.hasNullItemLabel(state.items) : false;
84
101
  }),
85
- open: InternalBackdrop.createSelector(state => state.open),
86
- mounted: InternalBackdrop.createSelector(state => state.mounted),
87
- forceMount: InternalBackdrop.createSelector(state => state.forceMount),
88
- transitionStatus: InternalBackdrop.createSelector(state => state.transitionStatus),
89
- openMethod: InternalBackdrop.createSelector(state => state.openMethod),
90
- activeIndex: InternalBackdrop.createSelector(state => state.activeIndex),
91
- selectedIndex: InternalBackdrop.createSelector(state => state.selectedIndex),
92
- isActive: InternalBackdrop.createSelector((state, index) => state.activeIndex === index),
93
- isSelected: InternalBackdrop.createSelector((state, index, itemValue) => {
102
+ open: useScrollLock.createSelector(state => state.open),
103
+ mounted: useScrollLock.createSelector(state => state.mounted),
104
+ forceMount: useScrollLock.createSelector(state => state.forceMount),
105
+ transitionStatus: useScrollLock.createSelector(state => state.transitionStatus),
106
+ openMethod: useScrollLock.createSelector(state => state.openMethod),
107
+ activeIndex: useScrollLock.createSelector(state => state.activeIndex),
108
+ selectedIndex: useScrollLock.createSelector(state => state.selectedIndex),
109
+ isActive: useScrollLock.createSelector((state, index) => state.activeIndex === index),
110
+ isSelected: useScrollLock.createSelector((state, itemValue) => {
94
111
  const comparer = state.isItemEqualToValue;
95
112
  const storeValue = state.value;
96
113
  if (state.multiple) {
97
114
  return Array.isArray(storeValue) && storeValue.some(selectedItem => resolveAriaLabelledBy.compareItemEquality(itemValue, selectedItem, comparer));
98
115
  }
99
116
 
100
- // `selectedIndex` is only updated after the items mount for the first time,
101
- // the value check avoids a re-render for the initially selected item.
102
- if (state.selectedIndex === index && state.selectedIndex !== null) {
103
- return true;
104
- }
117
+ // The value is the source of truth: a stale `selectedIndex` (e.g. the controlled
118
+ // value changes while the popup is open, where the index sync is deferred) must not
119
+ // keep a previously selected item marked as selected.
105
120
  return resolveAriaLabelledBy.compareItemEquality(itemValue, storeValue, comparer);
106
121
  }),
107
- isSelectedByFocus: InternalBackdrop.createSelector((state, index) => {
122
+ isSelectedByFocus: useScrollLock.createSelector((state, index) => {
108
123
  return state.selectedIndex === index;
109
124
  }),
110
- popupProps: InternalBackdrop.createSelector(state => state.popupProps),
111
- triggerProps: InternalBackdrop.createSelector(state => state.triggerProps),
112
- triggerElement: InternalBackdrop.createSelector(state => state.triggerElement),
113
- positionerElement: InternalBackdrop.createSelector(state => state.positionerElement),
114
- listElement: InternalBackdrop.createSelector(state => state.listElement),
115
- scrollUpArrowVisible: InternalBackdrop.createSelector(state => state.scrollUpArrowVisible),
116
- scrollDownArrowVisible: InternalBackdrop.createSelector(state => state.scrollDownArrowVisible),
117
- hasScrollArrows: InternalBackdrop.createSelector(state => state.hasScrollArrows)
125
+ popupProps: useScrollLock.createSelector(state => state.popupProps),
126
+ triggerProps: useScrollLock.createSelector(state => state.triggerProps),
127
+ triggerElement: useScrollLock.createSelector(state => state.triggerElement),
128
+ positionerElement: useScrollLock.createSelector(state => state.positionerElement),
129
+ listElement: useScrollLock.createSelector(state => state.listElement),
130
+ popupSide: useScrollLock.createSelector(state => state.popupSide),
131
+ scrollUpArrowVisible: useScrollLock.createSelector(state => state.scrollUpArrowVisible),
132
+ scrollDownArrowVisible: useScrollLock.createSelector(state => state.scrollDownArrowVisible),
133
+ hasScrollArrows: useScrollLock.createSelector(state => state.hasScrollArrows)
118
134
  };
119
135
 
120
136
  function SelectRoot(props) {
@@ -127,6 +143,7 @@ function SelectRoot(props) {
127
143
  defaultOpen = false,
128
144
  onOpenChange,
129
145
  name: nameProp,
146
+ form,
130
147
  autoComplete,
131
148
  disabled: disabledProp = false,
132
149
  readOnly = false,
@@ -150,7 +167,6 @@ function SelectRoot(props) {
150
167
  setDirty,
151
168
  setTouched,
152
169
  setFocused,
153
- shouldValidateOnChange,
154
170
  validityData,
155
171
  setFilled,
156
172
  name: fieldName,
@@ -183,11 +199,12 @@ function SelectRoot(props) {
183
199
  const valueRef = React__namespace.useRef(null);
184
200
  const valuesRef = React__namespace.useRef([]);
185
201
  const typingRef = React__namespace.useRef(false);
186
- const keyboardActiveRef = React__namespace.useRef(false);
202
+ const firstItemTextRef = React__namespace.useRef(null);
187
203
  const selectedItemTextRef = React__namespace.useRef(null);
188
204
  const selectionRef = React__namespace.useRef({
189
205
  allowSelectedMouseUp: false,
190
- allowUnselectedMouseUp: false
206
+ allowUnselectedMouseUp: false,
207
+ dragY: 0
191
208
  });
192
209
  const alignItemWithTriggerActiveRef = React__namespace.useRef(false);
193
210
  const {
@@ -198,8 +215,8 @@ function SelectRoot(props) {
198
215
  const {
199
216
  openMethod,
200
217
  triggerProps: interactionTypeProps
201
- } = InternalBackdrop.useOpenInteractionType(open);
202
- const store = useRenderElement.useRefWithInit(() => new InternalBackdrop.Store({
218
+ } = useScrollLock.useOpenInteractionType(open);
219
+ const store = useRenderElement.useRefWithInit(() => new useScrollLock.Store({
203
220
  id: generatedId,
204
221
  labelId: undefined,
205
222
  modal,
@@ -221,16 +238,22 @@ function SelectRoot(props) {
221
238
  triggerElement: null,
222
239
  positionerElement: null,
223
240
  listElement: null,
241
+ popupSide: null,
224
242
  scrollUpArrowVisible: false,
225
243
  scrollDownArrowVisible: false,
226
244
  hasScrollArrows: false
227
245
  })).current;
228
- const activeIndex = InternalBackdrop.useStore(store, selectors.activeIndex);
229
- const selectedIndex = InternalBackdrop.useStore(store, selectors.selectedIndex);
230
- const triggerElement = InternalBackdrop.useStore(store, selectors.triggerElement);
231
- const positionerElement = InternalBackdrop.useStore(store, selectors.positionerElement);
246
+ const activeIndex = useScrollLock.useStore(store, selectors.activeIndex);
247
+ const selectedIndex = useScrollLock.useStore(store, selectors.selectedIndex);
248
+ const triggerElement = useScrollLock.useStore(store, selectors.triggerElement);
249
+ const positionerElement = useScrollLock.useStore(store, selectors.positionerElement);
250
+ const previousOpenMethod = usePreviousValue(openMethod);
251
+ const renderedOpenMethod = openMethod ?? previousOpenMethod ?? null;
232
252
  const serializedValue = React__namespace.useMemo(() => {
233
- if (multiple && Array.isArray(value) && value.length === 0) {
253
+ // In multiple mode the shared input is nameless; per-value entries are submitted via
254
+ // `hiddenInputs`. Its value is therefore irrelevant, and passing the whole array to
255
+ // `stringifyAsValue` would invoke a user `itemToStringValue` with an array it doesn't expect.
256
+ if (multiple) {
234
257
  return '';
235
258
  }
236
259
  return resolveAriaLabelledBy.stringifyAsValue(value, itemToStringValue);
@@ -242,15 +265,12 @@ function SelectRoot(props) {
242
265
  return resolveAriaLabelledBy.stringifyAsValue(value, itemToStringValue);
243
266
  }, [multiple, value, itemToStringValue]);
244
267
  const controlRef = useButton.useValueAsRef(store.state.triggerElement);
245
- useLabelableId.useField({
246
- id: generatedId,
247
- commit: validation.commit,
248
- value,
249
- controlRef,
250
- name,
251
- getValue: () => fieldStringValue
252
- });
268
+ const getStringifiedValueForForm = useButton.useStableCallback(() => fieldStringValue);
269
+ useLabelableId.useRegisterFieldControl(controlRef, generatedId, value, getStringifiedValueForForm, !disabled, nameProp);
253
270
  const initialValueRef = React__namespace.useRef(value);
271
+ // Mirror the `hasSelectedValue` store selector so the Field's filled state agrees with the
272
+ // trigger/value placeholder semantics (a value serializing to `''` counts as empty).
273
+ const hasSelectedValue = multiple ? Array.isArray(value) && value.length > 0 : value != null && resolveAriaLabelledBy.stringifyAsValue(value, itemToStringValue) !== '';
254
274
  useButton.useIsoLayoutEffect(() => {
255
275
  // Ensure the values and labels are registered for programmatic value changes.
256
276
  if (value !== initialValueRef.current) {
@@ -258,35 +278,43 @@ function SelectRoot(props) {
258
278
  }
259
279
  }, [store, value]);
260
280
  useButton.useIsoLayoutEffect(() => {
261
- setFilled(multiple ? Array.isArray(value) && value.length > 0 : value != null);
262
- }, [multiple, value, setFilled]);
281
+ setFilled(hasSelectedValue);
282
+ }, [hasSelectedValue, setFilled]);
263
283
  useButton.useIsoLayoutEffect(function syncSelectedIndex() {
264
- if (open) {
265
- return;
266
- }
267
284
  const registry = valuesRef.current;
285
+ let nextIndex;
268
286
  if (multiple) {
269
287
  const currentValue = Array.isArray(value) ? value : [];
270
288
  if (currentValue.length === 0) {
271
- store.set('selectedIndex', null);
272
- return;
289
+ nextIndex = null;
290
+ } else {
291
+ const lastValue = currentValue[currentValue.length - 1];
292
+ const lastIndex = resolveAriaLabelledBy.findItemIndex(registry, lastValue, isItemEqualToValue);
293
+ nextIndex = lastIndex === -1 ? null : lastIndex;
273
294
  }
274
- const lastValue = currentValue[currentValue.length - 1];
275
- const lastIndex = resolveAriaLabelledBy.findItemIndex(registry, lastValue, isItemEqualToValue);
276
- store.set('selectedIndex', lastIndex === -1 ? null : lastIndex);
295
+ } else {
296
+ const index = resolveAriaLabelledBy.findItemIndex(registry, value, isItemEqualToValue);
297
+ nextIndex = index === -1 ? null : index;
298
+ }
299
+ if (nextIndex === null) {
300
+ selectedItemTextRef.current = null;
301
+ }
302
+ if (open) {
277
303
  return;
278
304
  }
279
- const index = resolveAriaLabelledBy.findItemIndex(registry, value, isItemEqualToValue);
280
- store.set('selectedIndex', index === -1 ? null : index);
281
- }, [multiple, open, value, valuesRef, isItemEqualToValue, store]);
305
+ store.set('selectedIndex', nextIndex);
306
+ }, [hasSelectedValue, multiple, open, value, valuesRef, isItemEqualToValue, store, selectedItemTextRef]);
307
+ function isSelectedValueDirty(currentValue) {
308
+ const initialValue = validityData.initialValue;
309
+ if (Array.isArray(currentValue) && Array.isArray(initialValue)) {
310
+ return !resolveAriaLabelledBy.areArraysEqual(currentValue, initialValue, (itemValue, initialItemValue) => resolveAriaLabelledBy.compareItemEquality(itemValue, initialItemValue, isItemEqualToValue));
311
+ }
312
+ return currentValue !== initialValue;
313
+ }
282
314
  useButton.useValueChanged(value, () => {
283
315
  clearErrors(name);
284
- setDirty(value !== validityData.initialValue);
285
- if (shouldValidateOnChange()) {
286
- validation.commit(value);
287
- } else {
288
- validation.commit(value, true);
289
- }
316
+ setDirty(isSelectedValueDirty(value));
317
+ validation.change(value);
290
318
  });
291
319
  const setOpen = useButton.useStableCallback((nextOpen, eventDetails) => {
292
320
  onOpenChange?.(nextOpen, eventDetails);
@@ -301,24 +329,13 @@ function SelectRoot(props) {
301
329
  validation.commit(value);
302
330
  }
303
331
  }
304
-
305
- // The active index will sync to the last selected index on the next open.
306
- // Workaround `enableFocusInside` in Floating UI setting `tabindex=0` of a non-highlighted
307
- // option upon close when tabbing out due to `keepMounted=true`:
308
- // https://github.com/floating-ui/floating-ui/pull/3004/files#diff-962a7439cdeb09ea98d4b622a45d517bce07ad8c3f866e089bda05f4b0bbd875R194-R199
309
- // This otherwise causes options to retain `tabindex=0` incorrectly when the popup is closed
310
- // when tabbing outside.
311
- if (!nextOpen && store.state.activeIndex !== null) {
312
- const activeOption = listRef.current[store.state.activeIndex];
313
- // Wait for Floating UI's focus effect to have fired
314
- queueMicrotask(() => {
315
- activeOption?.setAttribute('tabindex', '-1');
316
- });
317
- }
318
332
  });
319
333
  const handleUnmount = useButton.useStableCallback(() => {
320
334
  setMounted(false);
321
- store.set('activeIndex', null);
335
+ store.update({
336
+ activeIndex: null,
337
+ openMethod: null
338
+ });
322
339
  onOpenChangeComplete?.(false);
323
340
  });
324
341
  useButton.useOpenChangeComplete({
@@ -346,10 +363,10 @@ function SelectRoot(props) {
346
363
  if (!scroller) {
347
364
  return;
348
365
  }
349
- const viewportTop = scroller.scrollTop;
350
- const viewportBottom = scroller.scrollTop + scroller.clientHeight;
351
- const shouldShowUp = viewportTop > 1;
352
- const shouldShowDown = viewportBottom < scroller.scrollHeight - 1;
366
+ const maxScrollTop = scrollEdges.getMaxScrollOffset(scroller.scrollHeight, scroller.clientHeight);
367
+ const scrollTop = scrollEdges.normalizeScrollOffset(scroller.scrollTop, maxScrollTop);
368
+ const shouldShowUp = scrollTop > 0;
369
+ const shouldShowDown = scrollTop < maxScrollTop;
353
370
  if (store.state.scrollUpArrowVisible !== shouldShowUp) {
354
371
  store.set('scrollUpArrowVisible', shouldShowUp);
355
372
  }
@@ -365,13 +382,11 @@ function SelectRoot(props) {
365
382
  floating: positionerElement
366
383
  }
367
384
  });
368
- const click = InternalBackdrop.useClick(floatingContext, {
385
+ const click = useScrollLock.useClick(floatingContext, {
369
386
  enabled: !readOnly && !disabled,
370
387
  event: 'mousedown'
371
388
  });
372
- const dismiss = InternalBackdrop.useDismiss(floatingContext, {
373
- bubbles: false
374
- });
389
+ const dismiss = useScrollLock.useDismiss(floatingContext);
375
390
  const listNavigation = useCompositeListItem.useListNavigation(floatingContext, {
376
391
  enabled: !readOnly && !disabled,
377
392
  listRef,
@@ -385,15 +400,19 @@ function SelectRoot(props) {
385
400
  }
386
401
  store.set('activeIndex', nextActiveIndex);
387
402
  },
388
- // Implement our own listeners since `onPointerLeave` on each option fires while scrolling with
389
- // the `alignItemWithTrigger=true`, causing a performance issue on Chrome.
390
- focusItemOnHover: false
403
+ focusItemOnHover: highlightItemOnHover
391
404
  });
392
405
  const typeahead = useCompositeListItem.useTypeahead(floatingContext, {
393
406
  enabled: !readOnly && !disabled && (open || !multiple),
394
407
  listRef: labelsRef,
395
408
  activeIndex,
396
409
  selectedIndex,
410
+ // Skip disabled items while matching so typeahead advances to the next selectable item
411
+ // (a click can never select a disabled item and native `<select>` skips them too). Resolve
412
+ // the disabled state from the element via the attribute-only `isElementDisabled` so the
413
+ // hidden, force-mounted items used for closed-trigger typeahead aren't dropped by the
414
+ // `elementsRef`/visibility filter that `disabledIndices` deliberately sidesteps.
415
+ disabledIndices: index => isElementDisabled.isElementDisabled(listRef.current[index]),
397
416
  onMatch(index) {
398
417
  if (open) {
399
418
  store.set('activeIndex', index);
@@ -401,23 +420,22 @@ function SelectRoot(props) {
401
420
  setValue(valuesRef.current[index], useButton.createChangeEventDetails('none'));
402
421
  }
403
422
  },
404
- onTypingChange(typing) {
423
+ onTyping(typing) {
405
424
  typingRef.current = typing;
406
425
  }
407
426
  });
408
- const {
409
- getReferenceProps,
410
- getFloatingProps,
411
- getItemProps
412
- } = InternalBackdrop.useInteractions([click, dismiss, listNavigation, typeahead]);
413
427
  const mergedTriggerProps = React__namespace.useMemo(() => {
414
- return useRenderElement.mergeProps(getReferenceProps(), interactionTypeProps, generatedId ? {
415
- id: generatedId
416
- } : useRenderElement.EMPTY_OBJECT);
417
- }, [getReferenceProps, interactionTypeProps, generatedId]);
418
- InternalBackdrop.useOnFirstRender(() => {
428
+ const triggerInteractionProps = useRenderElement.mergeProps(typeahead.reference, listNavigation.reference, dismiss.reference, click.reference, interactionTypeProps);
429
+ if (generatedId) {
430
+ triggerInteractionProps.id = generatedId;
431
+ }
432
+ return triggerInteractionProps;
433
+ }, [click.reference, typeahead.reference, listNavigation.reference, dismiss.reference, interactionTypeProps, generatedId]);
434
+ const popupProps = React__namespace.useMemo(() => useRenderElement.mergeProps(useScrollLock.FOCUSABLE_POPUP_PROPS, typeahead.floating, listNavigation.floating, dismiss.floating), [typeahead.floating, listNavigation.floating, dismiss.floating]);
435
+ const itemProps = listNavigation.item ?? useRenderElement.EMPTY_OBJECT;
436
+ useScrollLock.useOnFirstRender(() => {
419
437
  store.update({
420
- popupProps: getFloatingProps(),
438
+ popupProps,
421
439
  triggerProps: mergedTriggerProps
422
440
  });
423
441
  });
@@ -430,15 +448,15 @@ function SelectRoot(props) {
430
448
  open,
431
449
  mounted,
432
450
  transitionStatus,
433
- popupProps: getFloatingProps(),
451
+ popupProps,
434
452
  triggerProps: mergedTriggerProps,
435
453
  items,
436
454
  itemToStringLabel,
437
455
  itemToStringValue,
438
456
  isItemEqualToValue,
439
- openMethod
457
+ openMethod: renderedOpenMethod
440
458
  });
441
- }, [store, generatedId, modal, multiple, value, open, mounted, transitionStatus, getFloatingProps, mergedTriggerProps, items, itemToStringLabel, itemToStringValue, isItemEqualToValue, openMethod]);
459
+ }, [store, generatedId, modal, multiple, value, open, mounted, transitionStatus, popupProps, mergedTriggerProps, items, itemToStringLabel, itemToStringValue, isItemEqualToValue, renderedOpenMethod]);
442
460
  const contextValue = React__namespace.useMemo(() => ({
443
461
  store,
444
462
  name,
@@ -446,8 +464,6 @@ function SelectRoot(props) {
446
464
  disabled,
447
465
  readOnly,
448
466
  multiple,
449
- itemToStringLabel,
450
- itemToStringValue,
451
467
  highlightItemOnHover,
452
468
  setValue,
453
469
  setOpen,
@@ -456,20 +472,19 @@ function SelectRoot(props) {
456
472
  scrollHandlerRef,
457
473
  handleScrollArrowVisibility,
458
474
  scrollArrowsMountedCountRef,
459
- getItemProps,
460
- events: floatingContext.context.events,
475
+ itemProps,
461
476
  valueRef,
462
477
  valuesRef,
463
478
  labelsRef,
464
479
  typingRef,
465
480
  selectionRef,
481
+ firstItemTextRef,
466
482
  selectedItemTextRef,
467
483
  validation,
468
484
  onOpenChangeComplete,
469
- keyboardActiveRef,
470
485
  alignItemWithTriggerActiveRef,
471
486
  initialValueRef
472
- }), [store, name, required, disabled, readOnly, multiple, itemToStringLabel, itemToStringValue, highlightItemOnHover, setValue, setOpen, getItemProps, floatingContext.context.events, validation, onOpenChangeComplete, handleScrollArrowVisibility]);
487
+ }), [store, name, required, disabled, readOnly, multiple, highlightItemOnHover, setValue, setOpen, itemProps, validation, onOpenChangeComplete, handleScrollArrowVisibility]);
473
488
  const ref = useRenderElement.useMergedRefs(inputRef, validation.inputRef);
474
489
  const hasMultipleSelection = multiple && Array.isArray(value) && value.length > 0;
475
490
  const hiddenInputName = multiple ? undefined : name;
@@ -481,32 +496,33 @@ function SelectRoot(props) {
481
496
  const currentSerializedValue = resolveAriaLabelledBy.stringifyAsValue(v, itemToStringValue);
482
497
  return /*#__PURE__*/jsxRuntime.jsx("input", {
483
498
  type: "hidden",
499
+ form: form,
484
500
  name: name,
485
- value: currentSerializedValue
501
+ value: currentSerializedValue,
502
+ disabled: disabled
486
503
  }, currentSerializedValue);
487
504
  });
488
- }, [multiple, value, name, itemToStringValue]);
505
+ }, [multiple, value, form, name, itemToStringValue, disabled]);
489
506
  return /*#__PURE__*/jsxRuntime.jsx(SelectRootContext.Provider, {
490
507
  value: contextValue,
491
508
  children: /*#__PURE__*/jsxRuntime.jsxs(SelectFloatingContext.Provider, {
492
509
  value: floatingContext,
493
510
  children: [children, /*#__PURE__*/jsxRuntime.jsx("input", {
494
- ...validation.getInputValidationProps({
511
+ ...validation.getValidationProps(disabled, {
495
512
  onFocus() {
496
513
  // Move focus to the trigger element when the hidden input is focused.
497
514
  store.state.triggerElement?.focus({
498
515
  // Supported in Chrome from 144 (January 2026)
499
- // @ts-expect-error - focusVisible is not yet in the lib.dom.d.ts
500
516
  focusVisible: true
501
517
  });
502
518
  },
503
519
  // Handle browser autofill.
504
520
  onChange(event) {
505
521
  // Workaround for https://github.com/facebook/react/issues/9023
506
- if (event.nativeEvent.defaultPrevented) {
522
+ if (event.nativeEvent.defaultPrevented || disabled || readOnly) {
507
523
  return;
508
524
  }
509
- const nextValue = event.target.value;
525
+ const nextValue = event.currentTarget.value;
510
526
  const details = useButton.createChangeEventDetails(useButton.none, event.nativeEvent);
511
527
  function handleChange() {
512
528
  if (multiple) {
@@ -514,20 +530,21 @@ function SelectRoot(props) {
514
530
  return;
515
531
  }
516
532
 
517
- // Handle single selection: match against registered values using serialization
518
- const matchingValue = valuesRef.current.find(v => {
519
- const candidate = resolveAriaLabelledBy.stringifyAsValue(v, itemToStringValue);
520
- if (candidate.toLowerCase() === nextValue.toLowerCase()) {
521
- return true;
522
- }
523
- return false;
524
- });
533
+ // Preserve the original serialized matching, then fall back to rendered text,
534
+ // which browsers can autofill for primitive values like `value="US">United States`.
535
+ const nextValueLower = nextValue.toLowerCase();
536
+ let matchingIndex = valuesRef.current.findIndex(candidate => resolveAriaLabelledBy.stringifyAsValue(candidate, itemToStringValue).toLowerCase() === nextValueLower || resolveAriaLabelledBy.stringifyAsLabel(candidate, itemToStringLabel).toLowerCase() === nextValueLower);
537
+ if (matchingIndex === -1) {
538
+ matchingIndex = valuesRef.current.findIndex((_, index) => {
539
+ const renderedLabel = labelsRef.current[index];
540
+ return renderedLabel != null && renderedLabel.toLowerCase() === nextValueLower;
541
+ });
542
+ }
543
+ const matchingValue = matchingIndex === -1 ? undefined : valuesRef.current[matchingIndex];
525
544
  if (matchingValue != null) {
526
- setDirty(matchingValue !== validityData.initialValue);
545
+ // `setValue` may be canceled by `onValueChange`; rely on `useValueChanged` to
546
+ // mark the field dirty and run validation only when the value actually changes.
527
547
  setValue(matchingValue, details);
528
- if (shouldValidateOnChange()) {
529
- validation.commit(matchingValue);
530
- }
531
548
  }
532
549
  }
533
550
  store.set('forceMount', true);
@@ -535,6 +552,7 @@ function SelectRoot(props) {
535
552
  }
536
553
  }),
537
554
  id: generatedId && hiddenInputName == null ? `${generatedId}-hidden-input` : undefined,
555
+ form: form,
538
556
  name: hiddenInputName,
539
557
  autoComplete: autoComplete,
540
558
  value: serializedValue,
@@ -544,7 +562,8 @@ function SelectRoot(props) {
544
562
  ref: ref,
545
563
  style: name ? useButton.visuallyHiddenInput : useButton.visuallyHidden,
546
564
  tabIndex: -1,
547
- "aria-hidden": true
565
+ "aria-hidden": true,
566
+ suppressHydrationWarning: true
548
567
  }), hiddenInputs]
549
568
  })
550
569
  });
@@ -552,10 +571,12 @@ function SelectRoot(props) {
552
571
 
553
572
  const BOUNDARY_OFFSET = 2;
554
573
  const SELECTED_DELAY = 400;
555
- const UNSELECTED_DELAY = 200;
556
574
  const stateAttributesMapping$2 = {
557
- ...InternalBackdrop.pressableTriggerOpenStateMapping,
575
+ ...useScrollLock.pressableTriggerOpenStateMapping,
558
576
  ...useLabelableId.fieldValidityMapping,
577
+ popupSide: side => side ? {
578
+ 'data-popup-side': side
579
+ } : null,
559
580
  value: () => null
560
581
  };
561
582
 
@@ -572,6 +593,7 @@ const SelectTrigger = /*#__PURE__*/React__namespace.forwardRef(function SelectTr
572
593
  id: idProp,
573
594
  disabled: disabledProp = false,
574
595
  nativeButton = true,
596
+ style,
575
597
  ...elementProps
576
598
  } = componentProps;
577
599
  const {
@@ -592,20 +614,20 @@ const SelectTrigger = /*#__PURE__*/React__namespace.forwardRef(function SelectTr
592
614
  readOnly,
593
615
  required,
594
616
  alignItemWithTriggerActiveRef,
595
- disabled: selectDisabled,
596
- keyboardActiveRef
617
+ disabled: selectDisabled
597
618
  } = useSelectRootContext();
598
619
  const disabled = fieldDisabled || selectDisabled || disabledProp;
599
- const open = InternalBackdrop.useStore(store, selectors.open);
600
- const value = InternalBackdrop.useStore(store, selectors.value);
601
- const triggerProps = InternalBackdrop.useStore(store, selectors.triggerProps);
602
- const positionerElement = InternalBackdrop.useStore(store, selectors.positionerElement);
603
- const listElement = InternalBackdrop.useStore(store, selectors.listElement);
604
- const rootId = InternalBackdrop.useStore(store, selectors.id);
605
- const selectLabelId = InternalBackdrop.useStore(store, selectors.labelId);
606
- const hasSelectedValue = InternalBackdrop.useStore(store, selectors.hasSelectedValue);
607
- const shouldCheckNullItemLabel = !hasSelectedValue && open;
608
- const hasNullItemLabel = InternalBackdrop.useStore(store, selectors.hasNullItemLabel, shouldCheckNullItemLabel);
620
+ const open = useScrollLock.useStore(store, selectors.open);
621
+ const mounted = useScrollLock.useStore(store, selectors.mounted);
622
+ const value = useScrollLock.useStore(store, selectors.value);
623
+ const triggerProps = useScrollLock.useStore(store, selectors.triggerProps);
624
+ const positionerElement = useScrollLock.useStore(store, selectors.positionerElement);
625
+ const listElement = useScrollLock.useStore(store, selectors.listElement);
626
+ const popupSideValue = useScrollLock.useStore(store, selectors.popupSide);
627
+ const rootId = useScrollLock.useStore(store, selectors.id);
628
+ const selectLabelId = useScrollLock.useStore(store, selectors.labelId);
629
+ const hasSelectedValue = useScrollLock.useStore(store, selectors.hasSelectedValue);
630
+ const popupSide = mounted && positionerElement ? popupSideValue : null;
609
631
  const id = idProp ?? rootId;
610
632
  const ariaLabelledBy = resolveAriaLabelledBy.resolveAriaLabelledBy(fieldLabelId, selectLabelId);
611
633
  useLabelableId.useLabelableId({
@@ -623,61 +645,41 @@ const SelectTrigger = /*#__PURE__*/React__namespace.forwardRef(function SelectTr
623
645
  const setTriggerElement = useButton.useStableCallback(element => {
624
646
  store.set('triggerElement', element);
625
647
  });
626
- const mergedRef = useRenderElement.useMergedRefs(forwardedRef, triggerRef, buttonRef, setTriggerElement);
627
648
  const timeoutFocus = useButton.useTimeout();
628
649
  const timeoutMouseDown = useButton.useTimeout();
629
650
  const selectedDelayTimeout = useButton.useTimeout();
630
- const unselectedDelayTimeout = useButton.useTimeout();
631
651
  React__namespace.useEffect(() => {
632
652
  if (open) {
633
- const hasSelectedItemInList = hasSelectedValue || hasNullItemLabel;
634
- const shouldDelayUnselectedMouseUpLonger = !hasSelectedItemInList;
635
-
636
- // When there is no selected item in the list (placeholder-only selects), a mousedown
637
- // on the trigger followed by a quick mouseup over the first option can accidentally select
638
- // within 200ms. Delay unselected mouseup to match the safer 400ms window.
639
- if (shouldDelayUnselectedMouseUpLonger) {
640
- selectedDelayTimeout.start(SELECTED_DELAY, () => {
641
- selectionRef.current.allowUnselectedMouseUp = true;
642
- selectionRef.current.allowSelectedMouseUp = true;
643
- });
644
- } else {
645
- // mousedown -> move to unselected item -> mouseup should not select within 200ms.
646
- unselectedDelayTimeout.start(UNSELECTED_DELAY, () => {
647
- selectionRef.current.allowUnselectedMouseUp = true;
648
-
649
- // mousedown -> mouseup on selected item should not select within 400ms.
650
- selectedDelayTimeout.start(UNSELECTED_DELAY, () => {
651
- selectionRef.current.allowSelectedMouseUp = true;
652
- });
653
- });
654
- }
653
+ // A mousedown on the trigger can open the popup under the cursor. Keep mouseup selection
654
+ // disabled briefly so releasing over either the selected item or a neighboring item doesn't
655
+ // commit an accidental selection. SelectItem can still opt into unselected mouseup sooner
656
+ // after a real drag over the item.
657
+ selectedDelayTimeout.start(SELECTED_DELAY, () => {
658
+ selectionRef.current.allowUnselectedMouseUp = true;
659
+ selectionRef.current.allowSelectedMouseUp = true;
660
+ });
655
661
  return () => {
656
662
  selectedDelayTimeout.clear();
657
- unselectedDelayTimeout.clear();
658
663
  };
659
664
  }
660
665
  selectionRef.current = {
661
666
  allowSelectedMouseUp: false,
662
- allowUnselectedMouseUp: false
667
+ allowUnselectedMouseUp: false,
668
+ dragY: 0
663
669
  };
664
670
  timeoutMouseDown.clear();
665
671
  return undefined;
666
- }, [open, hasSelectedValue, hasNullItemLabel, selectionRef, timeoutMouseDown, selectedDelayTimeout, unselectedDelayTimeout]);
667
- const ariaControlsId = React__namespace.useMemo(() => {
668
- return listElement?.id ?? useButton.getFloatingFocusElement(positionerElement)?.id;
669
- }, [listElement, positionerElement]);
670
- const props = useRenderElement.mergeProps(triggerProps, {
672
+ }, [open, selectionRef, timeoutMouseDown, selectedDelayTimeout]);
673
+ const mergedProps = useRenderElement.mergeProps(triggerProps, {
671
674
  id,
672
675
  role: 'combobox',
673
676
  'aria-expanded': open ? 'true' : 'false',
674
677
  'aria-haspopup': 'listbox',
675
- 'aria-controls': open ? ariaControlsId : undefined,
678
+ 'aria-controls': open ? listElement?.id ?? useScrollLock.getFloatingFocusElement(positionerElement)?.id : undefined,
676
679
  'aria-labelledby': ariaLabelledBy,
677
680
  'aria-readonly': readOnly || undefined,
678
681
  'aria-required': required || undefined,
679
682
  tabIndex: disabled ? -1 : 0,
680
- ref: mergedRef,
681
683
  onFocus(event) {
682
684
  setFocused(true);
683
685
 
@@ -689,8 +691,6 @@ const SelectTrigger = /*#__PURE__*/React__namespace.forwardRef(function SelectTr
689
691
  // Saves a re-render on initial click: `forceMount === true` mounts
690
692
  // the items before `open === true`. We could sync those cycles better
691
693
  // without a timeout, but this is enough for now.
692
- //
693
- // XXX: might be causing `act()` warnings.
694
694
  timeoutFocus.start(0, () => {
695
695
  store.set('forceMount', true);
696
696
  });
@@ -706,12 +706,6 @@ const SelectTrigger = /*#__PURE__*/React__namespace.forwardRef(function SelectTr
706
706
  validation.commit(value);
707
707
  }
708
708
  },
709
- onPointerMove() {
710
- keyboardActiveRef.current = false;
711
- },
712
- onKeyDown() {
713
- keyboardActiveRef.current = true;
714
- },
715
709
  onMouseDown(event) {
716
710
  if (open) {
717
711
  return;
@@ -723,8 +717,9 @@ const SelectTrigger = /*#__PURE__*/React__namespace.forwardRef(function SelectTr
723
717
  }
724
718
  const mouseUpTarget = mouseEvent.target;
725
719
 
726
- // Early return if clicked on trigger element or its children
727
- if (useButton.contains(triggerRef.current, mouseUpTarget) || useButton.contains(positionerRef.current, mouseUpTarget) || mouseUpTarget === triggerRef.current) {
720
+ // Don't treat the release as an outside press when it lands on the trigger or inside
721
+ // the popup positioner (or their children).
722
+ if (useButton.contains(triggerRef.current, mouseUpTarget) || useButton.contains(positionerRef.current, mouseUpTarget)) {
728
723
  return;
729
724
  }
730
725
  const bounds = useCompositeListItem.getPseudoElementBounds(triggerRef.current);
@@ -741,7 +736,8 @@ const SelectTrigger = /*#__PURE__*/React__namespace.forwardRef(function SelectTr
741
736
  });
742
737
  });
743
738
  }
744
- }, validation.getValidationProps, elementProps, getButtonProps);
739
+ }, elementProps, getButtonProps);
740
+ const props = validation.getValidationProps(disabled, mergedProps);
745
741
 
746
742
  // ensure nested useButton does not overwrite the combobox role:
747
743
  // <Toolbar.Button render={<Select.Trigger />} />
@@ -752,10 +748,11 @@ const SelectTrigger = /*#__PURE__*/React__namespace.forwardRef(function SelectTr
752
748
  disabled,
753
749
  value,
754
750
  readOnly,
751
+ popupSide,
755
752
  placeholder: !hasSelectedValue
756
753
  };
757
754
  return useRenderElement.useRenderElement('button', componentProps, {
758
- ref: [forwardedRef, triggerRef],
755
+ ref: [forwardedRef, triggerRef, buttonRef, setTriggerElement],
759
756
  state,
760
757
  stateAttributesMapping: stateAttributesMapping$2,
761
758
  props
@@ -779,18 +776,19 @@ const SelectValue = /*#__PURE__*/React__namespace.forwardRef(function SelectValu
779
776
  render,
780
777
  children: childrenProp,
781
778
  placeholder,
779
+ style,
782
780
  ...elementProps
783
781
  } = componentProps;
784
782
  const {
785
783
  store,
786
784
  valueRef
787
785
  } = useSelectRootContext();
788
- const value = InternalBackdrop.useStore(store, selectors.value);
789
- const items = InternalBackdrop.useStore(store, selectors.items);
790
- const itemToStringLabel = InternalBackdrop.useStore(store, selectors.itemToStringLabel);
791
- const hasSelectedValue = InternalBackdrop.useStore(store, selectors.hasSelectedValue);
786
+ const value = useScrollLock.useStore(store, selectors.value);
787
+ const items = useScrollLock.useStore(store, selectors.items);
788
+ const itemToStringLabel = useScrollLock.useStore(store, selectors.itemToStringLabel);
789
+ const hasSelectedValue = useScrollLock.useStore(store, selectors.hasSelectedValue);
792
790
  const shouldCheckNullItemLabel = !hasSelectedValue && placeholder != null && childrenProp == null;
793
- const hasNullLabel = InternalBackdrop.useStore(store, selectors.hasNullItemLabel, shouldCheckNullItemLabel);
791
+ const hasNullLabel = useScrollLock.useStore(store, selectors.hasNullItemLabel, shouldCheckNullItemLabel);
794
792
  const state = {
795
793
  value,
796
794
  placeholder: !hasSelectedValue
@@ -826,15 +824,15 @@ const SelectPortal = /*#__PURE__*/React__namespace.forwardRef(function SelectPor
826
824
  const {
827
825
  store
828
826
  } = useSelectRootContext();
829
- const mounted = InternalBackdrop.useStore(store, selectors.mounted);
830
- const forceMount = InternalBackdrop.useStore(store, selectors.forceMount);
827
+ const mounted = useScrollLock.useStore(store, selectors.mounted);
828
+ const forceMount = useScrollLock.useStore(store, selectors.forceMount);
831
829
  const shouldRender = mounted || forceMount;
832
830
  if (!shouldRender) {
833
831
  return null;
834
832
  }
835
833
  return /*#__PURE__*/jsxRuntime.jsx(SelectPortalContext.Provider, {
836
834
  value: true,
837
- children: /*#__PURE__*/jsxRuntime.jsx(InternalBackdrop.FloatingPortal, {
835
+ children: /*#__PURE__*/jsxRuntime.jsx(useScrollLock.FloatingPortal, {
838
836
  ref: forwardedRef,
839
837
  ...portalProps
840
838
  })
@@ -890,7 +888,8 @@ const SelectPositioner = /*#__PURE__*/React__namespace.forwardRef(function Selec
890
888
  sticky = false,
891
889
  disableAnchorTracking,
892
890
  alignItemWithTrigger = true,
893
- collisionAvoidance = InternalBackdrop.DROPDOWN_COLLISION_AVOIDANCE,
891
+ collisionAvoidance = useScrollLock.DROPDOWN_COLLISION_AVOIDANCE,
892
+ style,
894
893
  ...elementProps
895
894
  } = componentProps;
896
895
  const {
@@ -905,15 +904,15 @@ const SelectPositioner = /*#__PURE__*/React__namespace.forwardRef(function Selec
905
904
  setValue
906
905
  } = useSelectRootContext();
907
906
  const floatingRootContext = useSelectFloatingContext();
908
- const open = InternalBackdrop.useStore(store, selectors.open);
909
- const mounted = InternalBackdrop.useStore(store, selectors.mounted);
910
- const modal = InternalBackdrop.useStore(store, selectors.modal);
911
- const value = InternalBackdrop.useStore(store, selectors.value);
912
- const openMethod = InternalBackdrop.useStore(store, selectors.openMethod);
913
- const positionerElement = InternalBackdrop.useStore(store, selectors.positionerElement);
914
- const triggerElement = InternalBackdrop.useStore(store, selectors.triggerElement);
915
- const isItemEqualToValue = InternalBackdrop.useStore(store, selectors.isItemEqualToValue);
916
- const transitionStatus = InternalBackdrop.useStore(store, selectors.transitionStatus);
907
+ const open = useScrollLock.useStore(store, selectors.open);
908
+ const mounted = useScrollLock.useStore(store, selectors.mounted);
909
+ const modal = useScrollLock.useStore(store, selectors.modal);
910
+ const value = useScrollLock.useStore(store, selectors.value);
911
+ const openMethod = useScrollLock.useStore(store, selectors.openMethod);
912
+ const positionerElement = useScrollLock.useStore(store, selectors.positionerElement);
913
+ const triggerElement = useScrollLock.useStore(store, selectors.triggerElement);
914
+ const isItemEqualToValue = useScrollLock.useStore(store, selectors.isItemEqualToValue);
915
+ const transitionStatus = useScrollLock.useStore(store, selectors.transitionStatus);
917
916
  const scrollUpArrowRef = React__namespace.useRef(null);
918
917
  const scrollDownArrowRef = React__namespace.useRef(null);
919
918
  const [controlledAlignItemWithTrigger, setControlledAlignItemWithTrigger] = React__namespace.useState(alignItemWithTrigger);
@@ -932,7 +931,7 @@ const SelectPositioner = /*#__PURE__*/React__namespace.forwardRef(function Selec
932
931
  }
933
932
  }, [store, mounted]);
934
933
  React__namespace.useImperativeHandle(alignItemWithTriggerActiveRef, () => alignItemWithTriggerActive);
935
- InternalBackdrop.useScrollLock((alignItemWithTriggerActive || modal) && open && openMethod !== 'touch', triggerElement);
934
+ useCompositeListItem.useAnchoredPopupScrollLock((alignItemWithTriggerActive || modal) && open, openMethod === 'touch', positionerElement, triggerElement);
936
935
  const positioning = useCompositeListItem.useAnchorPositioning({
937
936
  anchor,
938
937
  floatingRootContext,
@@ -952,34 +951,25 @@ const SelectPositioner = /*#__PURE__*/React__namespace.forwardRef(function Selec
952
951
  });
953
952
  const renderedSide = alignItemWithTriggerActive ? 'none' : positioning.side;
954
953
  const positionerStyles = alignItemWithTriggerActive ? FIXED : positioning.positionerStyles;
955
- const defaultProps = React__namespace.useMemo(() => {
956
- const hiddenStyles = {};
957
- if (!open) {
958
- hiddenStyles.pointerEvents = 'none';
959
- }
960
- return {
961
- role: 'presentation',
962
- hidden: !mounted,
963
- style: {
964
- ...positionerStyles,
965
- ...hiddenStyles
966
- }
967
- };
968
- }, [open, mounted, positionerStyles]);
969
954
  const state = {
970
955
  open,
971
956
  side: renderedSide,
972
957
  align: positioning.align,
973
958
  anchorHidden: positioning.anchorHidden
974
959
  };
960
+ useButton.useIsoLayoutEffect(() => {
961
+ store.set('popupSide', positioning.side);
962
+ }, [store, positioning.side]);
975
963
  const setPositionerElement = useButton.useStableCallback(element => {
976
964
  store.set('positionerElement', element);
977
965
  });
978
- const element = useRenderElement.useRenderElement('div', componentProps, {
979
- ref: [forwardedRef, setPositionerElement],
980
- state,
981
- stateAttributesMapping: InternalBackdrop.popupStateMapping,
982
- props: [defaultProps, useCompositeListItem.getDisabledMountTransitionStyles(transitionStatus), elementProps]
966
+ const element = useCompositeListItem.usePositioner(componentProps, state, {
967
+ styles: positionerStyles,
968
+ transitionStatus,
969
+ props: elementProps,
970
+ refs: [forwardedRef, setPositionerElement],
971
+ hidden: !mounted,
972
+ inert: !open
983
973
  });
984
974
  const prevMapSizeRef = React__namespace.useRef(0);
985
975
  const onMapChange = useButton.useStableCallback(map => {
@@ -1045,8 +1035,8 @@ const SelectPositioner = /*#__PURE__*/React__namespace.forwardRef(function Selec
1045
1035
  onMapChange: onMapChange,
1046
1036
  children: /*#__PURE__*/jsxRuntime.jsxs(SelectPositionerContext.Provider, {
1047
1037
  value: contextValue,
1048
- children: [mounted && modal && /*#__PURE__*/jsxRuntime.jsx(InternalBackdrop.InternalBackdrop, {
1049
- inert: InternalBackdrop.inertValue(!open),
1038
+ children: [mounted && modal && /*#__PURE__*/jsxRuntime.jsx(useScrollLock.InternalBackdrop, {
1039
+ inert: useScrollLock.inertValue(!open),
1050
1040
  cutout: triggerElement
1051
1041
  }), element]
1052
1042
  })
@@ -1054,20 +1044,8 @@ const SelectPositioner = /*#__PURE__*/React__namespace.forwardRef(function Selec
1054
1044
  });
1055
1045
  if (process.env.NODE_ENV !== "production") SelectPositioner.displayName = "SelectPositioner";
1056
1046
 
1057
- function isMouseWithinBounds(event) {
1058
- const targetRect = event.currentTarget.getBoundingClientRect();
1059
-
1060
- // Safari randomly fires `mouseleave` incorrectly when the item is
1061
- // aligned to the trigger. This is a workaround to prevent the highlight
1062
- // from being removed while the cursor is still within the bounds of the item.
1063
- // https://github.com/mui/base-ui/issues/869
1064
- const isWithinBounds = targetRect.top + 1 <= event.clientY && event.clientY <= targetRect.bottom - 1 && targetRect.left + 1 <= event.clientX && event.clientX <= targetRect.right - 1;
1065
- return isWithinBounds;
1066
- }
1067
-
1068
- const SCROLL_EPS_PX = 1;
1069
1047
  const stateAttributesMapping = {
1070
- ...InternalBackdrop.popupStateMapping,
1048
+ ...useScrollLock.popupStateMapping,
1071
1049
  ...useButton.transitionStatusMapping
1072
1050
  };
1073
1051
 
@@ -1081,6 +1059,7 @@ const SelectPopup = /*#__PURE__*/React__namespace.forwardRef(function SelectPopu
1081
1059
  const {
1082
1060
  render,
1083
1061
  className,
1062
+ style,
1084
1063
  finalFocus,
1085
1064
  ...elementProps
1086
1065
  } = componentProps;
@@ -1090,39 +1069,38 @@ const SelectPopup = /*#__PURE__*/React__namespace.forwardRef(function SelectPopu
1090
1069
  onOpenChangeComplete,
1091
1070
  setOpen,
1092
1071
  valueRef,
1072
+ firstItemTextRef,
1093
1073
  selectedItemTextRef,
1094
- keyboardActiveRef,
1095
1074
  multiple,
1096
1075
  handleScrollArrowVisibility,
1097
1076
  scrollHandlerRef,
1077
+ listRef,
1098
1078
  highlightItemOnHover
1099
1079
  } = useSelectRootContext();
1100
1080
  const {
1101
1081
  side,
1102
1082
  align,
1103
1083
  alignItemWithTriggerActive,
1104
- setControlledAlignItemWithTrigger,
1105
- scrollDownArrowRef,
1106
- scrollUpArrowRef
1084
+ isPositioned,
1085
+ setControlledAlignItemWithTrigger
1107
1086
  } = useSelectPositionerContext();
1108
- const insideToolbar = ToolbarRootContext.useToolbarRootContext(true) != null;
1087
+ const insideToolbar = isElementDisabled.useToolbarRootContext(true) != null;
1109
1088
  const floatingRootContext = useSelectFloatingContext();
1089
+ const direction = useScrollLock.useDirection();
1110
1090
  const {
1111
1091
  nonce,
1112
1092
  disableStyleElements
1113
- } = CSPContext.useCSPContext();
1114
- const highlightTimeout = useButton.useTimeout();
1115
- const id = InternalBackdrop.useStore(store, selectors.id);
1116
- const open = InternalBackdrop.useStore(store, selectors.open);
1117
- const mounted = InternalBackdrop.useStore(store, selectors.mounted);
1118
- const popupProps = InternalBackdrop.useStore(store, selectors.popupProps);
1119
- const transitionStatus = InternalBackdrop.useStore(store, selectors.transitionStatus);
1120
- const triggerElement = InternalBackdrop.useStore(store, selectors.triggerElement);
1121
- const positionerElement = InternalBackdrop.useStore(store, selectors.positionerElement);
1122
- const listElement = InternalBackdrop.useStore(store, selectors.listElement);
1123
- const initialHeightRef = React__namespace.useRef(0);
1093
+ } = scrollEdges.useCSPContext();
1094
+ const id = useScrollLock.useStore(store, selectors.id);
1095
+ const open = useScrollLock.useStore(store, selectors.open);
1096
+ const openMethod = useScrollLock.useStore(store, selectors.openMethod);
1097
+ const mounted = useScrollLock.useStore(store, selectors.mounted);
1098
+ const popupProps = useScrollLock.useStore(store, selectors.popupProps);
1099
+ const transitionStatus = useScrollLock.useStore(store, selectors.transitionStatus);
1100
+ const triggerElement = useScrollLock.useStore(store, selectors.triggerElement);
1101
+ const positionerElement = useScrollLock.useStore(store, selectors.positionerElement);
1102
+ const listElement = useScrollLock.useStore(store, selectors.listElement);
1124
1103
  const reachedMaxHeightRef = React__namespace.useRef(false);
1125
- const maxHeightRef = React__namespace.useRef(0);
1126
1104
  const initialPlacedRef = React__namespace.useRef(false);
1127
1105
  const originalPositionerStylesRef = React__namespace.useRef({});
1128
1106
  const scrollArrowFrame = useButton.useAnimationFrame();
@@ -1136,12 +1114,18 @@ const SelectPopup = /*#__PURE__*/React__namespace.forwardRef(function SelectPopu
1136
1114
  }
1137
1115
  const isTopPositioned = positionerElement.style.top === '0px';
1138
1116
  const isBottomPositioned = positionerElement.style.bottom === '0px';
1139
- const currentHeight = positionerElement.getBoundingClientRect().height;
1117
+ if (!isTopPositioned && !isBottomPositioned) {
1118
+ handleScrollArrowVisibility();
1119
+ return;
1120
+ }
1121
+ const scale = getScale(positionerElement);
1122
+ const currentHeight = normalizeSize(positionerElement.getBoundingClientRect().height, 'y', scale);
1140
1123
  const doc = useButton.ownerDocument(positionerElement);
1141
- const positionerStyles = getComputedStyle(positionerElement);
1124
+ const win = floatingUi_utils_dom.getWindow(positionerElement);
1125
+ const positionerStyles = win.getComputedStyle(positionerElement);
1142
1126
  const marginTop = parseFloat(positionerStyles.marginTop);
1143
1127
  const marginBottom = parseFloat(positionerStyles.marginBottom);
1144
- const maxPopupHeight = getMaxPopupHeight(getComputedStyle(popupRef.current));
1128
+ const maxPopupHeight = getMaxPopupHeight(win.getComputedStyle(popupRef.current));
1145
1129
  const maxAvailableHeight = Math.min(doc.documentElement.clientHeight - marginTop - marginBottom, maxPopupHeight);
1146
1130
  const scrollTop = scroller.scrollTop;
1147
1131
  const maxScrollTop = getMaxScrollTop(scroller);
@@ -1159,42 +1143,29 @@ const SelectPopup = /*#__PURE__*/React__namespace.forwardRef(function SelectPopu
1159
1143
  setHeight(currentHeight + heightDelta);
1160
1144
  }
1161
1145
  scroller.scrollTop = targetScrollTop;
1162
- if (maxAvailableHeight - (currentHeight + heightDelta) <= SCROLL_EPS_PX) {
1146
+ if (maxAvailableHeight - (currentHeight + heightDelta) <= scrollEdges.SCROLL_EDGE_TOLERANCE_PX) {
1163
1147
  reachedMaxHeightRef.current = true;
1164
1148
  }
1165
1149
  handleScrollArrowVisibility();
1166
1150
  };
1167
- if (isTopPositioned) {
1168
- const diff = maxScrollTop - scrollTop;
1169
- const idealHeight = currentHeight + diff;
1170
- const nextHeight = Math.min(idealHeight, maxAvailableHeight);
1171
- nextPositionerHeight = nextHeight;
1172
- if (diff <= SCROLL_EPS_PX) {
1173
- handleSmallDiff(diff, maxScrollTop);
1174
- return;
1175
- }
1176
- if (maxAvailableHeight - nextHeight > SCROLL_EPS_PX) {
1151
+ const diff = isTopPositioned ? maxScrollTop - scrollTop : scrollTop;
1152
+ const nextHeight = Math.min(currentHeight + diff, maxAvailableHeight);
1153
+ nextPositionerHeight = nextHeight;
1154
+ if (diff <= scrollEdges.SCROLL_EDGE_TOLERANCE_PX) {
1155
+ handleSmallDiff(diff, isTopPositioned ? maxScrollTop : 0);
1156
+ return;
1157
+ }
1158
+ if (maxAvailableHeight - nextHeight > scrollEdges.SCROLL_EDGE_TOLERANCE_PX) {
1159
+ if (isTopPositioned) {
1177
1160
  scrollToMax = true;
1178
1161
  } else {
1179
- setReachedMax = true;
1180
- }
1181
- } else if (isBottomPositioned) {
1182
- const diff = scrollTop;
1183
- const idealHeight = currentHeight + diff;
1184
- const nextHeight = Math.min(idealHeight, maxAvailableHeight);
1185
- const overshoot = idealHeight - maxAvailableHeight;
1186
- nextPositionerHeight = nextHeight;
1187
- if (diff <= SCROLL_EPS_PX) {
1188
- handleSmallDiff(diff, 0);
1189
- return;
1190
- }
1191
- if (maxAvailableHeight - nextHeight > SCROLL_EPS_PX) {
1192
1162
  nextScrollTop = 0;
1193
- } else {
1194
- setReachedMax = true;
1195
- if (scrollTop < maxScrollTop) {
1196
- nextScrollTop = scrollTop - (diff - overshoot);
1197
- }
1163
+ }
1164
+ } else {
1165
+ setReachedMax = true;
1166
+ if (isBottomPositioned && scrollTop < maxScrollTop) {
1167
+ const overshoot = currentHeight + diff - maxAvailableHeight;
1168
+ nextScrollTop = scrollTop - (diff - overshoot);
1198
1169
  }
1199
1170
  }
1200
1171
  nextPositionerHeight = Math.ceil(nextPositionerHeight);
@@ -1207,11 +1178,11 @@ const SelectPopup = /*#__PURE__*/React__namespace.forwardRef(function SelectPopu
1207
1178
  const target = scrollToMax ? nextMaxScrollTop : clamp.clamp(nextScrollTop, 0, nextMaxScrollTop);
1208
1179
 
1209
1180
  // Avoid adjustments that re-trigger scroll events forever.
1210
- if (Math.abs(scroller.scrollTop - target) > SCROLL_EPS_PX) {
1181
+ if (Math.abs(scroller.scrollTop - target) > scrollEdges.SCROLL_EDGE_TOLERANCE_PX) {
1211
1182
  scroller.scrollTop = target;
1212
1183
  }
1213
1184
  }
1214
- if (setReachedMax || nextPositionerHeight >= maxAvailableHeight - SCROLL_EPS_PX) {
1185
+ if (setReachedMax || nextPositionerHeight >= maxAvailableHeight - scrollEdges.SCROLL_EDGE_TOLERANCE_PX) {
1215
1186
  reachedMaxHeightRef.current = true;
1216
1187
  }
1217
1188
  handleScrollArrowVisibility();
@@ -1254,13 +1225,15 @@ const SelectPopup = /*#__PURE__*/React__namespace.forwardRef(function SelectPopu
1254
1225
  }
1255
1226
  initialPlacedRef.current = false;
1256
1227
  reachedMaxHeightRef.current = false;
1257
- initialHeightRef.current = 0;
1258
- maxHeightRef.current = 0;
1259
1228
  clearStyles(positionerElement, originalPositionerStylesRef.current);
1260
1229
  }, [open, alignItemWithTriggerActive, positionerElement, popupRef]);
1261
1230
  useButton.useIsoLayoutEffect(() => {
1262
1231
  const popupElement = popupRef.current;
1263
- if (!open || !triggerElement || !positionerElement || !popupElement || store.state.transitionStatus === 'ending') {
1232
+
1233
+ // Wait for Floating UI's first positioning pass before reading DOM geometry.
1234
+ // We replace the final coordinates for aligned selects, but still need middleware
1235
+ // like `size()` to set CSS variables such as `--anchor-width`.
1236
+ if (!open || !triggerElement || !positionerElement || !popupElement || alignItemWithTriggerActive && !isPositioned || store.state.transitionStatus === 'ending') {
1264
1237
  return;
1265
1238
  }
1266
1239
  if (!alignItemWithTriggerActive) {
@@ -1270,114 +1243,113 @@ const SelectPopup = /*#__PURE__*/React__namespace.forwardRef(function SelectPopu
1270
1243
  return;
1271
1244
  }
1272
1245
 
1273
- // Wait for `selectedItemTextRef.current` to be set.
1274
- queueMicrotask(() => {
1275
- // Ensure we remove any transforms that can affect the location of the popup
1276
- // and therefore the calculations.
1277
- const restoreTransformStyles = unsetTransformStyles(popupElement);
1278
- popupElement.style.removeProperty('--transform-origin');
1279
- try {
1280
- const positionerStyles = getComputedStyle(positionerElement);
1281
- const popupStyles = getComputedStyle(popupElement);
1282
- const doc = useButton.ownerDocument(triggerElement);
1283
- const win = floatingUi_utils_dom.getWindow(positionerElement);
1284
- const scale = getScale(triggerElement);
1285
- const triggerRect = normalizeRect(triggerElement.getBoundingClientRect(), scale);
1286
- const positionerRect = normalizeRect(positionerElement.getBoundingClientRect(), scale);
1287
- const triggerX = triggerRect.left;
1288
- const triggerHeight = triggerRect.height;
1289
- const scroller = listElement || popupElement;
1290
- const scrollHeight = scroller.scrollHeight;
1291
- const borderBottom = parseFloat(popupStyles.borderBottomWidth);
1292
- const marginTop = parseFloat(positionerStyles.marginTop) || 10;
1293
- const marginBottom = parseFloat(positionerStyles.marginBottom) || 10;
1294
- const minHeight = parseFloat(positionerStyles.minHeight) || 100;
1295
- const maxPopupHeight = getMaxPopupHeight(popupStyles);
1296
- const paddingLeft = 5;
1297
- const paddingRight = 5;
1298
- const triggerCollisionThreshold = 20;
1299
- const viewportHeight = doc.documentElement.clientHeight - marginTop - marginBottom;
1300
- const viewportWidth = doc.documentElement.clientWidth;
1301
- const availableSpaceBeneathTrigger = viewportHeight - triggerRect.bottom + triggerHeight;
1302
- const textElement = selectedItemTextRef.current;
1303
- const valueElement = valueRef.current;
1304
- let textRect;
1305
- let offsetX = 0;
1306
- let offsetY = 0;
1307
- if (textElement && valueElement) {
1308
- const valueRect = normalizeRect(valueElement.getBoundingClientRect(), scale);
1309
- textRect = normalizeRect(textElement.getBoundingClientRect(), scale);
1310
- const valueLeftFromTriggerLeft = valueRect.left - triggerX;
1311
- const textLeftFromPositionerLeft = textRect.left - positionerRect.left;
1312
- const valueCenterFromPositionerTop = valueRect.top - triggerRect.top + valueRect.height / 2;
1313
- const textCenterFromTriggerTop = textRect.top - positionerRect.top + textRect.height / 2;
1314
- offsetX = valueLeftFromTriggerLeft - textLeftFromPositionerLeft;
1315
- offsetY = textCenterFromTriggerTop - valueCenterFromPositionerTop;
1316
- }
1317
- const idealHeight = availableSpaceBeneathTrigger + offsetY + marginBottom + borderBottom;
1318
- let height = Math.min(viewportHeight, idealHeight);
1319
- const maxHeight = viewportHeight - marginTop - marginBottom;
1320
- const scrollTop = idealHeight - height;
1321
- const left = Math.max(paddingLeft, triggerX + offsetX);
1322
- const maxRight = viewportWidth - paddingRight;
1323
- const rightOverflow = Math.max(0, left + positionerRect.width - maxRight);
1324
- positionerElement.style.left = `${left - rightOverflow}px`;
1325
- positionerElement.style.height = `${height}px`;
1326
- positionerElement.style.maxHeight = 'auto';
1327
- positionerElement.style.marginTop = `${marginTop}px`;
1328
- positionerElement.style.marginBottom = `${marginBottom}px`;
1329
- popupElement.style.height = '100%';
1330
- const maxScrollTop = getMaxScrollTop(scroller);
1331
- const isTopPositioned = scrollTop >= maxScrollTop - SCROLL_EPS_PX;
1332
- if (isTopPositioned) {
1333
- height = Math.min(viewportHeight, positionerRect.height) - (scrollTop - maxScrollTop);
1334
- }
1335
-
1336
- // When the trigger is too close to the top or bottom of the viewport, or the minHeight is
1337
- // reached, we fallback to aligning the popup to the trigger as the UX is poor otherwise.
1338
- const fallbackToAlignPopupToTrigger = triggerRect.top < triggerCollisionThreshold || triggerRect.bottom > viewportHeight - triggerCollisionThreshold || Math.ceil(height) + SCROLL_EPS_PX < Math.min(scrollHeight, minHeight);
1246
+ // Ensure we remove any transforms that can affect the location of the popup
1247
+ // and therefore the calculations.
1248
+ const restoreTransformStyles = unsetTransformStyles(popupElement);
1249
+ popupElement.style.removeProperty('--transform-origin');
1250
+ try {
1251
+ let textElement = selectedItemTextRef.current;
1252
+ if (!textElement?.isConnected) {
1253
+ const hasSelectedValue = selectors.hasSelectedValue(store.state);
1254
+ textElement = !hasSelectedValue && firstItemTextRef.current?.isConnected ? firstItemTextRef.current : null;
1255
+ }
1256
+ const valueElement = valueRef.current;
1257
+ const win = floatingUi_utils_dom.getWindow(positionerElement);
1258
+ const positionerStyles = win.getComputedStyle(positionerElement);
1259
+ const popupStyles = win.getComputedStyle(popupElement);
1260
+ const doc = useButton.ownerDocument(triggerElement);
1261
+ const scale = getScale(triggerElement);
1262
+ const triggerRect = normalizeRect(triggerElement.getBoundingClientRect(), scale);
1263
+ const positionerRect = normalizeRect(positionerElement.getBoundingClientRect(), scale);
1264
+ const triggerHeight = triggerRect.height;
1265
+ const scroller = listElement || popupElement;
1266
+ const scrollHeight = scroller.scrollHeight;
1267
+ const borderBottom = parseFloat(popupStyles.borderBottomWidth);
1268
+ // The `|| N` fallbacks cover an unset/`auto` value (parses to `NaN`). Note a literal `0`
1269
+ // also resolves to the fallback, so an explicit `margin: 0` or `min-height: 0` still takes
1270
+ // the default below.
1271
+ const marginTop = parseFloat(positionerStyles.marginTop) || 10;
1272
+ const marginBottom = parseFloat(positionerStyles.marginBottom) || 10;
1273
+ const minHeight = parseFloat(positionerStyles.minHeight) || 100;
1274
+ const maxPopupHeight = getMaxPopupHeight(popupStyles);
1275
+ const paddingLeft = 5;
1276
+ const paddingRight = 5;
1277
+ const triggerCollisionThreshold = 20;
1278
+ const viewportHeight = doc.documentElement.clientHeight - marginTop - marginBottom;
1279
+ const viewportWidth = doc.documentElement.clientWidth;
1280
+ const availableSpaceBeneathTrigger = viewportHeight - triggerRect.bottom + triggerHeight;
1281
+ let textRect;
1282
+ let alignedLeft = direction === 'rtl' ? triggerRect.right - positionerRect.width : triggerRect.left;
1283
+ let offsetY = 0;
1284
+ if (textElement && valueElement) {
1285
+ const valueRect = normalizeRect(valueElement.getBoundingClientRect(), scale);
1286
+ textRect = normalizeRect(textElement.getBoundingClientRect(), scale);
1287
+ alignedLeft = positionerRect.left + (direction === 'rtl' ? valueRect.right - textRect.right : valueRect.left - textRect.left);
1288
+ const valueCenterFromTriggerTop = valueRect.top - triggerRect.top + valueRect.height / 2;
1289
+ const textCenterFromPositionerTop = textRect.top - positionerRect.top + textRect.height / 2;
1290
+ offsetY = textCenterFromPositionerTop - valueCenterFromTriggerTop;
1291
+ }
1292
+ const idealHeight = availableSpaceBeneathTrigger + offsetY + marginBottom + borderBottom;
1293
+ let height = Math.min(viewportHeight, idealHeight);
1294
+ const maxHeight = viewportHeight - marginTop - marginBottom;
1295
+ const scrollTop = idealHeight - height;
1296
+ const maxRight = viewportWidth - paddingRight;
1297
+ positionerElement.style.left = `${clamp.clamp(alignedLeft, paddingLeft, maxRight - positionerRect.width)}px`;
1298
+ positionerElement.style.height = `${height}px`;
1299
+ // `none` (not the invalid `auto`) so the explicit height governs in align mode and isn't
1300
+ // clamped by a `max-height` from user CSS.
1301
+ positionerElement.style.maxHeight = 'none';
1302
+ positionerElement.style.marginTop = `${marginTop}px`;
1303
+ positionerElement.style.marginBottom = `${marginBottom}px`;
1304
+ popupElement.style.height = '100%';
1305
+ const maxScrollTop = getMaxScrollTop(scroller);
1306
+ const isTopPositioned = scrollTop >= maxScrollTop - scrollEdges.SCROLL_EDGE_TOLERANCE_PX;
1307
+ if (isTopPositioned) {
1308
+ height = Math.min(viewportHeight, positionerRect.height) - (scrollTop - maxScrollTop);
1309
+ }
1339
1310
 
1340
- // Safari doesn't position the popup correctly when pinch-zoomed.
1341
- const isPinchZoomed = (win.visualViewport?.scale ?? 1) !== 1 && useButton.isWebKit;
1342
- if (fallbackToAlignPopupToTrigger || isPinchZoomed) {
1343
- initialPlacedRef.current = true;
1344
- clearStyles(positionerElement, originalPositionerStylesRef.current);
1345
- ReactDOM__namespace.flushSync(() => setControlledAlignItemWithTrigger(false));
1346
- return;
1347
- }
1348
- if (isTopPositioned) {
1349
- const topOffset = Math.max(0, viewportHeight - idealHeight);
1350
- positionerElement.style.top = positionerRect.height >= maxHeight ? '0' : `${topOffset}px`;
1351
- positionerElement.style.height = `${height}px`;
1352
- scroller.scrollTop = getMaxScrollTop(scroller);
1353
- initialHeightRef.current = Math.max(minHeight, height);
1354
- } else {
1355
- positionerElement.style.bottom = '0';
1356
- initialHeightRef.current = Math.max(minHeight, height);
1357
- scroller.scrollTop = scrollTop;
1358
- }
1359
- if (textRect) {
1360
- const popupTop = positionerRect.top;
1361
- const popupHeight = positionerRect.height;
1362
- const textCenterY = textRect.top + textRect.height / 2;
1363
- const transformOriginY = popupHeight > 0 ? (textCenterY - popupTop) / popupHeight * 100 : 50;
1364
- const clampedY = clamp.clamp(transformOriginY, 0, 100);
1365
- popupElement.style.setProperty('--transform-origin', `50% ${clampedY}%`);
1366
- }
1367
- if (initialHeightRef.current === viewportHeight || height >= maxPopupHeight) {
1368
- reachedMaxHeightRef.current = true;
1369
- }
1370
- handleScrollArrowVisibility();
1311
+ // When the trigger is too close to the top or bottom of the viewport, or the minHeight is
1312
+ // reached, we fallback to aligning the popup to the trigger as the UX is poor otherwise.
1313
+ const fallbackToAlignPopupToTrigger = triggerRect.top < triggerCollisionThreshold || triggerRect.bottom > viewportHeight - triggerCollisionThreshold || Math.ceil(height) + scrollEdges.SCROLL_EDGE_TOLERANCE_PX < Math.min(scrollHeight, minHeight);
1371
1314
 
1372
- // Avoid the `onScroll` event logic from triggering before the popup is placed.
1373
- setTimeout(() => {
1374
- initialPlacedRef.current = true;
1375
- });
1376
- } finally {
1377
- restoreTransformStyles();
1315
+ // Safari doesn't position the popup correctly when pinch-zoomed.
1316
+ const isPinchZoomed = (win.visualViewport?.scale ?? 1) !== 1 && useScrollLock.webkit;
1317
+ if (fallbackToAlignPopupToTrigger || isPinchZoomed) {
1318
+ initialPlacedRef.current = true;
1319
+ clearStyles(positionerElement, originalPositionerStylesRef.current);
1320
+ setControlledAlignItemWithTrigger(false);
1321
+ return;
1378
1322
  }
1379
- });
1380
- }, [store, open, positionerElement, triggerElement, valueRef, selectedItemTextRef, popupRef, handleScrollArrowVisibility, alignItemWithTriggerActive, setControlledAlignItemWithTrigger, scrollArrowFrame, scrollDownArrowRef, scrollUpArrowRef, listElement]);
1323
+ const initialHeight = Math.max(minHeight, height);
1324
+ if (isTopPositioned) {
1325
+ const topOffset = Math.max(0, viewportHeight - idealHeight);
1326
+ positionerElement.style.top = positionerRect.height >= maxHeight ? '0' : `${topOffset}px`;
1327
+ positionerElement.style.height = `${height}px`;
1328
+ scroller.scrollTop = getMaxScrollTop(scroller);
1329
+ } else {
1330
+ positionerElement.style.bottom = '0';
1331
+ scroller.scrollTop = scrollTop;
1332
+ }
1333
+ if (textRect) {
1334
+ const popupTop = positionerRect.top;
1335
+ const popupHeight = positionerRect.height;
1336
+ const textCenterY = textRect.top + textRect.height / 2;
1337
+ const transformOriginY = popupHeight > 0 ? (textCenterY - popupTop) / popupHeight * 100 : 50;
1338
+ const clampedY = clamp.clamp(transformOriginY, 0, 100);
1339
+ popupElement.style.setProperty('--transform-origin', `50% ${clampedY}%`);
1340
+ }
1341
+ if (initialHeight === viewportHeight || height >= maxPopupHeight) {
1342
+ reachedMaxHeightRef.current = true;
1343
+ }
1344
+ handleScrollArrowVisibility();
1345
+ if (highlightItemOnHover && store.state.selectedIndex === null && store.state.activeIndex === null && listRef.current[0] != null) {
1346
+ store.set('activeIndex', 0);
1347
+ }
1348
+ initialPlacedRef.current = true;
1349
+ } finally {
1350
+ restoreTransformStyles();
1351
+ }
1352
+ }, [store, open, positionerElement, triggerElement, valueRef, firstItemTextRef, selectedItemTextRef, popupRef, handleScrollArrowVisibility, alignItemWithTriggerActive, setControlledAlignItemWithTrigger, scrollArrowFrame, listElement, listRef, highlightItemOnHover, direction, isPositioned]);
1381
1353
  React__namespace.useEffect(() => {
1382
1354
  if (!alignItemWithTriggerActive || !positionerElement || !open) {
1383
1355
  return undefined;
@@ -1386,10 +1358,7 @@ const SelectPopup = /*#__PURE__*/React__namespace.forwardRef(function SelectPopu
1386
1358
  function handleResize(event) {
1387
1359
  setOpen(false, useButton.createChangeEventDetails(useButton.windowResize, event));
1388
1360
  }
1389
- win.addEventListener('resize', handleResize);
1390
- return () => {
1391
- win.removeEventListener('resize', handleResize);
1392
- };
1361
+ return useButton.addEventListener(win, 'resize', handleResize);
1393
1362
  }, [setOpen, alignItemWithTriggerActive, positionerElement, open]);
1394
1363
  const defaultProps = {
1395
1364
  ...(listElement ? {
@@ -1401,26 +1370,10 @@ const SelectPopup = /*#__PURE__*/React__namespace.forwardRef(function SelectPopu
1401
1370
  id: `${id}-list`
1402
1371
  }),
1403
1372
  onKeyDown(event) {
1404
- keyboardActiveRef.current = true;
1405
- if (insideToolbar && CSPContext.COMPOSITE_KEYS.has(event.key)) {
1373
+ if (insideToolbar && scrollEdges.COMPOSITE_KEYS.has(event.key)) {
1406
1374
  event.stopPropagation();
1407
1375
  }
1408
1376
  },
1409
- onMouseMove() {
1410
- keyboardActiveRef.current = false;
1411
- },
1412
- onPointerLeave(event) {
1413
- if (!highlightItemOnHover || isMouseWithinBounds(event) || event.pointerType === 'touch') {
1414
- return;
1415
- }
1416
- const popup = event.currentTarget;
1417
- highlightTimeout.start(0, () => {
1418
- store.set('activeIndex', null);
1419
- popup.focus({
1420
- preventScroll: true
1421
- });
1422
- });
1423
- },
1424
1377
  onScroll(event) {
1425
1378
  if (listElement) {
1426
1379
  return;
@@ -1438,14 +1391,15 @@ const SelectPopup = /*#__PURE__*/React__namespace.forwardRef(function SelectPopu
1438
1391
  state,
1439
1392
  stateAttributesMapping,
1440
1393
  props: [popupProps, defaultProps, useCompositeListItem.getDisabledMountTransitionStyles(transitionStatus), {
1441
- className: !listElement && alignItemWithTriggerActive ? CSPContext.styleDisableScrollbar.className : undefined
1394
+ className: !listElement && alignItemWithTriggerActive ? scrollEdges.styleDisableScrollbar.className : undefined
1442
1395
  }, elementProps]
1443
1396
  });
1444
1397
  return /*#__PURE__*/jsxRuntime.jsxs(React__namespace.Fragment, {
1445
- children: [!disableStyleElements && CSPContext.styleDisableScrollbar.getElement(nonce), /*#__PURE__*/jsxRuntime.jsx(InternalBackdrop.FloatingFocusManager, {
1398
+ children: [!disableStyleElements && scrollEdges.styleDisableScrollbar.getElement(nonce), /*#__PURE__*/jsxRuntime.jsx(useScrollLock.FloatingFocusManager, {
1446
1399
  context: floatingRootContext,
1447
1400
  modal: false,
1448
1401
  disabled: !mounted,
1402
+ openInteractionType: openMethod,
1449
1403
  returnFocus: finalFocus,
1450
1404
  restoreFocus: true,
1451
1405
  children: element
@@ -1458,18 +1412,21 @@ function getMaxPopupHeight(popupStyles) {
1458
1412
  return maxHeightStyle.endsWith('px') ? parseFloat(maxHeightStyle) || Infinity : Infinity;
1459
1413
  }
1460
1414
  function getMaxScrollTop(scroller) {
1461
- return Math.max(0, scroller.scrollHeight - scroller.clientHeight);
1415
+ return scrollEdges.getMaxScrollOffset(scroller.scrollHeight, scroller.clientHeight);
1462
1416
  }
1463
1417
  function getScale(element) {
1464
1418
  // The platform API is async-capable, but the DOM platform returns a plain scale object.
1465
1419
  return floatingUi_reactDom.platform.getScale(element);
1466
1420
  }
1421
+ function normalizeSize(size, axis, scale) {
1422
+ return size / scale[axis];
1423
+ }
1467
1424
  function normalizeRect(rect, scale) {
1468
- return index_esm.rectToClientRect({
1469
- x: rect.x / scale.x,
1470
- y: rect.y / scale.y,
1471
- width: rect.width / scale.x,
1472
- height: rect.height / scale.y
1425
+ return floatingUi_utils.rectToClientRect({
1426
+ x: normalizeSize(rect.x, 'x', scale),
1427
+ y: normalizeSize(rect.y, 'y', scale),
1428
+ width: normalizeSize(rect.width, 'x', scale),
1429
+ height: normalizeSize(rect.height, 'y', scale)
1473
1430
  });
1474
1431
  }
1475
1432
  const TRANSFORM_STYLE_RESETS = [['transform', 'none'], ['scale', '1'], ['translate', '0 0']];
@@ -1502,8 +1459,9 @@ function unsetTransformStyles(popupElement) {
1502
1459
  */
1503
1460
  const SelectList = /*#__PURE__*/React__namespace.forwardRef(function SelectList(componentProps, forwardedRef) {
1504
1461
  const {
1505
- className,
1506
1462
  render,
1463
+ className,
1464
+ style,
1507
1465
  ...elementProps
1508
1466
  } = componentProps;
1509
1467
  const {
@@ -1513,10 +1471,10 @@ const SelectList = /*#__PURE__*/React__namespace.forwardRef(function SelectList(
1513
1471
  const {
1514
1472
  alignItemWithTriggerActive
1515
1473
  } = useSelectPositionerContext();
1516
- const hasScrollArrows = InternalBackdrop.useStore(store, selectors.hasScrollArrows);
1517
- const openMethod = InternalBackdrop.useStore(store, selectors.openMethod);
1518
- const multiple = InternalBackdrop.useStore(store, selectors.multiple);
1519
- const id = InternalBackdrop.useStore(store, selectors.id);
1474
+ const hasScrollArrows = useScrollLock.useStore(store, selectors.hasScrollArrows);
1475
+ const openMethod = useScrollLock.useStore(store, selectors.openMethod);
1476
+ const multiple = useScrollLock.useStore(store, selectors.multiple);
1477
+ const id = useScrollLock.useStore(store, selectors.id);
1520
1478
  const defaultProps = {
1521
1479
  id: `${id}-list`,
1522
1480
  role: 'listbox',
@@ -1527,7 +1485,7 @@ const SelectList = /*#__PURE__*/React__namespace.forwardRef(function SelectList(
1527
1485
  ...(alignItemWithTriggerActive && {
1528
1486
  style: LIST_FUNCTIONAL_STYLES
1529
1487
  }),
1530
- className: hasScrollArrows && openMethod !== 'touch' ? CSPContext.styleDisableScrollbar.className : undefined
1488
+ className: hasScrollArrows && openMethod !== 'touch' ? scrollEdges.styleDisableScrollbar.className : undefined
1531
1489
  };
1532
1490
  const setListElement = useButton.useStableCallback(element => {
1533
1491
  store.set('listElement', element);
@@ -1553,6 +1511,7 @@ const SelectItem = /*#__PURE__*/React__namespace.memo(/*#__PURE__*/React__namesp
1553
1511
  const {
1554
1512
  render,
1555
1513
  className,
1514
+ style,
1556
1515
  value: itemValue = null,
1557
1516
  label,
1558
1517
  disabled = false,
@@ -1567,25 +1526,25 @@ const SelectItem = /*#__PURE__*/React__namespace.memo(/*#__PURE__*/React__namesp
1567
1526
  });
1568
1527
  const {
1569
1528
  store,
1570
- getItemProps,
1529
+ itemProps,
1571
1530
  setOpen,
1572
1531
  setValue,
1573
1532
  selectionRef,
1574
1533
  typingRef,
1575
1534
  valuesRef,
1576
- keyboardActiveRef,
1577
1535
  multiple,
1578
- highlightItemOnHover
1536
+ selectedItemTextRef,
1537
+ disabled: selectDisabled,
1538
+ readOnly
1579
1539
  } = useSelectRootContext();
1580
- const highlightTimeout = useButton.useTimeout();
1581
- const highlighted = InternalBackdrop.useStore(store, selectors.isActive, listItem.index);
1582
- const selected = InternalBackdrop.useStore(store, selectors.isSelected, listItem.index, itemValue);
1583
- const selectedByFocus = InternalBackdrop.useStore(store, selectors.isSelectedByFocus, listItem.index);
1584
- const isItemEqualToValue = InternalBackdrop.useStore(store, selectors.isItemEqualToValue);
1540
+ const highlighted = useScrollLock.useStore(store, selectors.isActive, listItem.index);
1541
+ const open = useScrollLock.useStore(store, selectors.open);
1542
+ const selected = useScrollLock.useStore(store, selectors.isSelected, itemValue);
1543
+ const selectedByFocus = useScrollLock.useStore(store, selectors.isSelectedByFocus, listItem.index);
1544
+ const isItemEqualToValue = useScrollLock.useStore(store, selectors.isItemEqualToValue);
1585
1545
  const index = listItem.index;
1586
1546
  const hasRegistered = index !== -1;
1587
1547
  const itemRef = React__namespace.useRef(null);
1588
- const indexRef = useButton.useValueAsRef(index);
1589
1548
  useButton.useIsoLayoutEffect(() => {
1590
1549
  if (!hasRegistered) {
1591
1550
  return undefined;
@@ -1598,34 +1557,27 @@ const SelectItem = /*#__PURE__*/React__namespace.memo(/*#__PURE__*/React__namesp
1598
1557
  }, [hasRegistered, index, itemValue, valuesRef]);
1599
1558
  useButton.useIsoLayoutEffect(() => {
1600
1559
  if (!hasRegistered) {
1601
- return undefined;
1560
+ return;
1602
1561
  }
1603
1562
  const selectedValue = store.state.value;
1604
1563
  let selectedCandidate = selectedValue;
1605
- if (multiple && Array.isArray(selectedValue) && selectedValue.length > 0) {
1606
- selectedCandidate = selectedValue[selectedValue.length - 1];
1564
+ if (multiple && Array.isArray(selectedValue)) {
1565
+ // Compare against the last selected item, or `undefined` when nothing is selected — never
1566
+ // the raw array, which a custom `isItemEqualToValue` isn't expected to receive.
1567
+ selectedCandidate = selectedValue.length > 0 ? selectedValue[selectedValue.length - 1] : undefined;
1607
1568
  }
1608
1569
  if (selectedCandidate !== undefined && resolveAriaLabelledBy.compareItemEquality(itemValue, selectedCandidate, isItemEqualToValue)) {
1609
1570
  store.set('selectedIndex', index);
1571
+ // Make sure SelectPopup can measure the selected item on first open.
1572
+ // SelectItemText can still update this ref later when focus moves.
1573
+ if (textRef.current) {
1574
+ selectedItemTextRef.current = textRef.current;
1575
+ }
1610
1576
  }
1611
- return undefined;
1612
- }, [hasRegistered, index, multiple, isItemEqualToValue, store, itemValue]);
1613
- const state = {
1614
- disabled,
1615
- selected,
1616
- highlighted
1617
- };
1618
- const rootProps = getItemProps({
1619
- active: highlighted,
1620
- selected
1621
- });
1622
- // With our custom `focusItemOnHover` implementation, this interferes with the logic and can
1623
- // cause the index state to be stuck when leaving the select popup.
1624
- rootProps.onFocus = undefined;
1625
- rootProps.id = undefined;
1577
+ }, [hasRegistered, index, multiple, isItemEqualToValue, store, itemValue, selectedItemTextRef]);
1626
1578
  const lastKeyRef = React__namespace.useRef(null);
1627
1579
  const pointerTypeRef = React__namespace.useRef('mouse');
1628
- const didPointerDownRef = React__namespace.useRef(false);
1580
+ const allowMouseSelectionRef = React__namespace.useRef(false);
1629
1581
  const {
1630
1582
  getButtonProps,
1631
1583
  buttonRef
@@ -1635,7 +1587,17 @@ const SelectItem = /*#__PURE__*/React__namespace.memo(/*#__PURE__*/React__namesp
1635
1587
  native: nativeButton,
1636
1588
  composite: true
1637
1589
  });
1590
+ const state = {
1591
+ disabled,
1592
+ selected,
1593
+ highlighted
1594
+ };
1638
1595
  function commitSelection(event) {
1596
+ // A forced-open select (`open`/`defaultOpen`) can still receive item activations even
1597
+ // when the root is disabled or read-only, so guard the commit here too.
1598
+ if (selectDisabled || readOnly) {
1599
+ return;
1600
+ }
1639
1601
  const selectedValue = store.state.value;
1640
1602
  if (multiple) {
1641
1603
  const currentValue = Array.isArray(selectedValue) ? selectedValue : [];
@@ -1646,39 +1608,13 @@ const SelectItem = /*#__PURE__*/React__namespace.memo(/*#__PURE__*/React__namesp
1646
1608
  setOpen(false, useButton.createChangeEventDetails(useButton.itemPress, event));
1647
1609
  }
1648
1610
  }
1611
+ function resetDragMovement() {
1612
+ selectionRef.current.dragY = 0;
1613
+ }
1649
1614
  const defaultProps = {
1650
1615
  role: 'option',
1651
1616
  'aria-selected': selected,
1652
- tabIndex: highlighted ? 0 : -1,
1653
- onFocus() {
1654
- store.set('activeIndex', index);
1655
- },
1656
- onMouseEnter() {
1657
- if (!keyboardActiveRef.current && store.state.selectedIndex === null && highlightItemOnHover) {
1658
- store.set('activeIndex', index);
1659
- }
1660
- },
1661
- onMouseMove() {
1662
- if (highlightItemOnHover) {
1663
- store.set('activeIndex', index);
1664
- }
1665
- },
1666
- onMouseLeave(event) {
1667
- if (!highlightItemOnHover || keyboardActiveRef.current || isMouseWithinBounds(event)) {
1668
- return;
1669
- }
1670
- highlightTimeout.start(0, () => {
1671
- if (store.state.activeIndex === index) {
1672
- store.set('activeIndex', null);
1673
- }
1674
- });
1675
- },
1676
- onTouchStart() {
1677
- selectionRef.current = {
1678
- allowSelectedMouseUp: false,
1679
- allowUnselectedMouseUp: false
1680
- };
1681
- },
1617
+ tabIndex: open && highlighted ? 0 : -1,
1682
1618
  onKeyDown(event) {
1683
1619
  lastKeyRef.current = event.key;
1684
1620
  store.set('activeIndex', index);
@@ -1687,13 +1623,24 @@ const SelectItem = /*#__PURE__*/React__namespace.memo(/*#__PURE__*/React__namesp
1687
1623
  }
1688
1624
  },
1689
1625
  onClick(event) {
1690
- didPointerDownRef.current = false;
1626
+ const isMouseClick = event.type === 'click' && pointerTypeRef.current !== 'touch';
1627
+ const clickPointerType = event.nativeEvent.pointerType;
1628
+ const isVirtualMouseClick = isMouseClick && useButton.isVirtualClick(event.nativeEvent) && (
1629
+ // Generic no-pointer `detail === 0` clicks stay tied to highlight state. Virtual
1630
+ // clicks that carry browser pointer data, including an empty string from assistive
1631
+ // technology, can activate unhighlighted items.
1632
+ clickPointerType !== undefined || highlighted);
1633
+ // With alignItemWithTrigger, opening can place an item under the cursor. Real mouse
1634
+ // clicks must start on the item, while virtual clicks represent explicit keyboard or
1635
+ // assistive technology activation.
1636
+ const isInvalidMouseClick = isMouseClick && !isVirtualMouseClick && !allowMouseSelectionRef.current;
1637
+ allowMouseSelectionRef.current = false;
1691
1638
 
1692
1639
  // Prevent double commit on {Enter}
1693
1640
  if (event.type === 'keydown' && lastKeyRef.current === null) {
1694
1641
  return;
1695
1642
  }
1696
- if (disabled || event.type === 'keydown' && lastKeyRef.current === ' ' && typingRef.current || pointerTypeRef.current !== 'touch' && !highlighted) {
1643
+ if (disabled || event.type === 'keydown' && lastKeyRef.current === ' ' && typingRef.current || isInvalidMouseClick) {
1697
1644
  return;
1698
1645
  }
1699
1646
  lastKeyRef.current = null;
@@ -1702,40 +1649,52 @@ const SelectItem = /*#__PURE__*/React__namespace.memo(/*#__PURE__*/React__namesp
1702
1649
  onPointerEnter(event) {
1703
1650
  pointerTypeRef.current = event.pointerType;
1704
1651
  },
1652
+ onPointerMove(event) {
1653
+ if (event.pointerType === 'mouse' && event.buttons === 1) {
1654
+ const selection = selectionRef.current;
1655
+ selection.dragY += event.movementY;
1656
+ if (selection.dragY ** 2 >= 64) {
1657
+ selection.allowUnselectedMouseUp = true;
1658
+ }
1659
+ }
1660
+ },
1705
1661
  onPointerDown(event) {
1706
1662
  pointerTypeRef.current = event.pointerType;
1707
- didPointerDownRef.current = true;
1663
+ allowMouseSelectionRef.current = true;
1664
+ resetDragMovement();
1708
1665
  },
1709
1666
  onMouseUp() {
1710
- if (disabled) {
1667
+ resetDragMovement();
1668
+ if (disabled || pointerTypeRef.current === 'touch') {
1711
1669
  return;
1712
1670
  }
1713
1671
 
1714
- // Regular click (pointerdown on this element) if didPointerDownRef is set, otherwise drag-to-select
1715
- if (didPointerDownRef.current) {
1716
- didPointerDownRef.current = false;
1672
+ // Regular clicks are committed by the click event.
1673
+ if (allowMouseSelectionRef.current) {
1717
1674
  return;
1718
1675
  }
1719
1676
  const disallowSelectedMouseUp = !selectionRef.current.allowSelectedMouseUp && selected;
1720
1677
  const disallowUnselectedMouseUp = !selectionRef.current.allowUnselectedMouseUp && !selected;
1721
- if (disallowSelectedMouseUp || disallowUnselectedMouseUp || pointerTypeRef.current !== 'touch' && !highlighted) {
1678
+ if (disallowSelectedMouseUp || disallowUnselectedMouseUp) {
1722
1679
  return;
1723
1680
  }
1681
+ allowMouseSelectionRef.current = true;
1724
1682
  itemRef.current?.click();
1683
+ allowMouseSelectionRef.current = false;
1725
1684
  }
1726
1685
  };
1727
1686
  const element = useRenderElement.useRenderElement('div', componentProps, {
1728
1687
  ref: [buttonRef, forwardedRef, listItem.ref, itemRef],
1729
1688
  state,
1730
- props: [rootProps, defaultProps, elementProps, getButtonProps]
1689
+ props: [itemProps, defaultProps, elementProps, getButtonProps]
1731
1690
  });
1732
1691
  const contextValue = React__namespace.useMemo(() => ({
1733
1692
  selected,
1734
- indexRef,
1693
+ index,
1735
1694
  textRef,
1736
1695
  selectedByFocus,
1737
1696
  hasRegistered
1738
- }), [selected, indexRef, textRef, selectedByFocus, hasRegistered]);
1697
+ }), [selected, index, textRef, selectedByFocus, hasRegistered]);
1739
1698
  return /*#__PURE__*/jsxRuntime.jsx(SelectItemContext.Provider, {
1740
1699
  value: contextValue,
1741
1700
  children: element
@@ -1760,13 +1719,13 @@ const SelectItemIndicator = /*#__PURE__*/React__namespace.forwardRef(function Se
1760
1719
  });
1761
1720
  });
1762
1721
 
1763
- /** The core implementation of the indicator is split here to avoid paying the hooks
1764
- * costs unless the element needs to be mounted. */
1722
+ // Split the core implementation to avoid paying the hook costs unless the element needs to mount.
1765
1723
  if (process.env.NODE_ENV !== "production") SelectItemIndicator.displayName = "SelectItemIndicator";
1766
1724
  const Inner = /*#__PURE__*/React__namespace.memo(/*#__PURE__*/React__namespace.forwardRef((componentProps, forwardedRef) => {
1767
1725
  const {
1768
1726
  render,
1769
1727
  className,
1728
+ style,
1770
1729
  keepMounted,
1771
1730
  ...elementProps
1772
1731
  } = componentProps;
@@ -1812,28 +1771,32 @@ if (process.env.NODE_ENV !== "production") Inner.displayName = "Inner";
1812
1771
  */
1813
1772
  const SelectItemText = /*#__PURE__*/React__namespace.memo(/*#__PURE__*/React__namespace.forwardRef(function SelectItemText(componentProps, forwardedRef) {
1814
1773
  const {
1815
- indexRef,
1774
+ index,
1816
1775
  textRef,
1817
1776
  selectedByFocus,
1818
1777
  hasRegistered
1819
1778
  } = useSelectItemContext();
1820
1779
  const {
1780
+ firstItemTextRef,
1821
1781
  selectedItemTextRef
1822
1782
  } = useSelectRootContext();
1823
1783
  const {
1824
- className,
1825
1784
  render,
1785
+ className,
1786
+ style,
1826
1787
  ...elementProps
1827
1788
  } = componentProps;
1828
1789
  const localRef = React__namespace.useCallback(node => {
1829
- if (!node || !hasRegistered) {
1790
+ if (!node) {
1830
1791
  return;
1831
1792
  }
1832
- const hasNoSelectedItemText = selectedItemTextRef.current === null || !selectedItemTextRef.current.isConnected;
1833
- if (selectedByFocus || hasNoSelectedItemText && indexRef.current === 0) {
1793
+ if (hasRegistered && index === 0) {
1794
+ firstItemTextRef.current = node;
1795
+ }
1796
+ if (hasRegistered && selectedByFocus) {
1834
1797
  selectedItemTextRef.current = node;
1835
1798
  }
1836
- }, [selectedItemTextRef, indexRef, selectedByFocus, hasRegistered]);
1799
+ }, [firstItemTextRef, selectedItemTextRef, index, selectedByFocus, hasRegistered]);
1837
1800
  const element = useRenderElement.useRenderElement('div', componentProps, {
1838
1801
  ref: [localRef, forwardedRef, textRef],
1839
1802
  props: elementProps
@@ -1854,8 +1817,9 @@ function useSelectGroupContext() {
1854
1817
 
1855
1818
  const SelectGroup = /*#__PURE__*/React__namespace.forwardRef(function SelectGroup(componentProps, forwardedRef) {
1856
1819
  const {
1857
- className,
1858
1820
  render,
1821
+ className,
1822
+ style,
1859
1823
  ...elementProps
1860
1824
  } = componentProps;
1861
1825
  const [labelId, setLabelId] = React__namespace.useState();
@@ -1885,8 +1849,9 @@ if (process.env.NODE_ENV !== "production") SelectGroup.displayName = "SelectGrou
1885
1849
  */
1886
1850
  const SelectGroupLabel = /*#__PURE__*/React__namespace.forwardRef(function SelectGroupLabel(componentProps, forwardedRef) {
1887
1851
  const {
1888
- className,
1889
1852
  render,
1853
+ className,
1854
+ style,
1890
1855
  id: idProp,
1891
1856
  ...elementProps
1892
1857
  } = componentProps;
@@ -1920,5 +1885,6 @@ exports.SelectRoot = SelectRoot;
1920
1885
  exports.SelectTrigger = SelectTrigger;
1921
1886
  exports.SelectValue = SelectValue;
1922
1887
  exports.selectors = selectors;
1888
+ exports.usePreviousValue = usePreviousValue;
1923
1889
  exports.useSelectPositionerContext = useSelectPositionerContext;
1924
1890
  exports.useSelectRootContext = useSelectRootContext;