@jobber/components 8.23.0 → 8.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/dist/Autocomplete/index.cjs +1 -1
  2. package/dist/Autocomplete/index.mjs +1 -1
  3. package/dist/Card/index.cjs +4 -4
  4. package/dist/Card/index.mjs +4 -4
  5. package/dist/Chip/index.cjs +1 -1
  6. package/dist/Chip/index.mjs +1 -1
  7. package/dist/Chips/InternalChipDismissible/hooks/index.cjs +1 -1
  8. package/dist/Chips/InternalChipDismissible/hooks/index.mjs +1 -1
  9. package/dist/Chips/InternalChipDismissible/index.cjs +1 -1
  10. package/dist/Chips/InternalChipDismissible/index.mjs +1 -1
  11. package/dist/Chips/index.cjs +1 -1
  12. package/dist/Chips/index.mjs +1 -1
  13. package/dist/ComboboxChipRemove-cjs.js +668 -379
  14. package/dist/ComboboxChipRemove-es.js +508 -219
  15. package/dist/ConfirmationModal/index.cjs +1 -1
  16. package/dist/ConfirmationModal/index.mjs +1 -1
  17. package/dist/DataDump/index.cjs +4 -4
  18. package/dist/DataDump/index.mjs +4 -4
  19. package/dist/DataList/components/DataListActions/index.cjs +1 -1
  20. package/dist/DataList/components/DataListActions/index.mjs +1 -1
  21. package/dist/DataList/components/DataListBulkActions/index.cjs +1 -1
  22. package/dist/DataList/components/DataListBulkActions/index.mjs +1 -1
  23. package/dist/DataList/components/DataListFilters/components/DataListSort/index.cjs +1 -1
  24. package/dist/DataList/components/DataListFilters/components/DataListSort/index.mjs +1 -1
  25. package/dist/DataList/components/DataListFilters/index.cjs +1 -1
  26. package/dist/DataList/components/DataListFilters/index.mjs +1 -1
  27. package/dist/DataList/components/DataListHeader/index.cjs +1 -1
  28. package/dist/DataList/components/DataListHeader/index.mjs +1 -1
  29. package/dist/DataList/components/DataListItem/index.cjs +1 -1
  30. package/dist/DataList/components/DataListItem/index.mjs +1 -1
  31. package/dist/DataList/components/DataListItemActions/index.cjs +1 -1
  32. package/dist/DataList/components/DataListItemActions/index.mjs +1 -1
  33. package/dist/DataList/components/DataListItemActionsOverflow/index.cjs +1 -1
  34. package/dist/DataList/components/DataListItemActionsOverflow/index.mjs +1 -1
  35. package/dist/DataList/components/DataListItems/index.cjs +1 -1
  36. package/dist/DataList/components/DataListItems/index.mjs +1 -1
  37. package/dist/DataList/components/DataListLayout/index.cjs +1 -1
  38. package/dist/DataList/components/DataListLayout/index.mjs +1 -1
  39. package/dist/DataList/components/DataListLayoutActions/index.cjs +1 -1
  40. package/dist/DataList/components/DataListLayoutActions/index.mjs +1 -1
  41. package/dist/DataList/index.cjs +1 -1
  42. package/dist/DataList/index.mjs +1 -1
  43. package/dist/DatePicker/index.cjs +1 -1
  44. package/dist/DatePicker/index.mjs +1 -1
  45. package/dist/Dialog/index.cjs +3 -3
  46. package/dist/Dialog/index.mjs +3 -3
  47. package/dist/DrawerDescription-cjs.js +32 -33
  48. package/dist/DrawerDescription-es.js +19 -20
  49. package/dist/DrawerTitle-cjs.js +7 -5
  50. package/dist/DrawerTitle-es.js +7 -5
  51. package/dist/FilterPicker/components/FilterPickerActivator/index.cjs +1 -1
  52. package/dist/FilterPicker/components/FilterPickerActivator/index.mjs +1 -1
  53. package/dist/FilterPicker/components/FilterPickerContent/index.cjs +1 -1
  54. package/dist/FilterPicker/components/FilterPickerContent/index.mjs +1 -1
  55. package/dist/FilterPicker/components/FilterPickerTrigger/index.cjs +1 -1
  56. package/dist/FilterPicker/components/FilterPickerTrigger/index.mjs +1 -1
  57. package/dist/FilterPicker/index.cjs +1 -1
  58. package/dist/FilterPicker/index.mjs +1 -1
  59. package/dist/FormFieldWrapper-cjs.js +2 -2
  60. package/dist/FormFieldWrapper-es.js +2 -2
  61. package/dist/FormatFile/index.cjs +1 -1
  62. package/dist/FormatFile/index.mjs +1 -1
  63. package/dist/Gallery/index.cjs +1 -1
  64. package/dist/Gallery/index.mjs +1 -1
  65. package/dist/InputDate/index.cjs +1 -1
  66. package/dist/InputDate/index.mjs +1 -1
  67. package/dist/InputNumberExperimental-cjs.js +319 -102
  68. package/dist/InputNumberExperimental-es.js +321 -104
  69. package/dist/LightBox/index.cjs +1 -1
  70. package/dist/LightBox/index.mjs +1 -1
  71. package/dist/Menu/index.cjs +4 -4
  72. package/dist/Menu/index.mjs +4 -4
  73. package/dist/MenuSubmenuTrigger-cjs.js +545 -498
  74. package/dist/MenuSubmenuTrigger-es.js +490 -443
  75. package/dist/Modal/index.cjs +1 -1
  76. package/dist/Modal/index.mjs +1 -1
  77. package/dist/NumberFieldInput-cjs.js +609 -496
  78. package/dist/NumberFieldInput-es.js +612 -498
  79. package/dist/Page/index.cjs +4 -4
  80. package/dist/Page/index.mjs +4 -4
  81. package/dist/Popover/index.cjs +1 -1
  82. package/dist/Popover/index.mjs +1 -1
  83. package/dist/ScrollAreaViewport-cjs.js +326 -601
  84. package/dist/ScrollAreaViewport-es.js +278 -548
  85. package/dist/SelectGroupLabel-cjs.js +494 -528
  86. package/dist/SelectGroupLabel-es.js +411 -445
  87. package/dist/Separator-cjs.js +1 -0
  88. package/dist/Separator-es.js +1 -0
  89. package/dist/Tooltip/index.cjs +1 -1
  90. package/dist/Tooltip/index.mjs +1 -1
  91. package/dist/docs/InputTime/InputTime.md +8 -0
  92. package/dist/docs/SelectPrimitive/SelectPrimitive.md +38 -25
  93. package/dist/floating-ui.react-cjs.js +586 -10
  94. package/dist/floating-ui.react-dom-cjs.js +92 -92
  95. package/dist/floating-ui.react-dom-es.js +1 -1
  96. package/dist/floating-ui.react-es.js +580 -4
  97. package/dist/floating-ui.utils-cjs.js +159 -0
  98. package/dist/floating-ui.utils-es.js +136 -0
  99. package/dist/index.cjs +4 -4
  100. package/dist/index.mjs +4 -4
  101. package/dist/{ToolbarRootContext-cjs.js → isElementDisabled-cjs.js} +5 -0
  102. package/dist/{ToolbarRootContext-es.js → isElementDisabled-es.js} +5 -1
  103. package/dist/primitives/BottomSheet/index.cjs +3 -3
  104. package/dist/primitives/BottomSheet/index.mjs +3 -3
  105. package/dist/primitives/ComboboxPrimitive/index.cjs +2 -2
  106. package/dist/primitives/ComboboxPrimitive/index.mjs +2 -2
  107. package/dist/primitives/InputNumberExperimental/index.cjs +1 -1
  108. package/dist/primitives/InputNumberExperimental/index.mjs +1 -1
  109. package/dist/primitives/SelectPrimitive/index.cjs +5 -5
  110. package/dist/primitives/SelectPrimitive/index.mjs +5 -5
  111. package/dist/primitives/index.cjs +4 -4
  112. package/dist/primitives/index.mjs +4 -4
  113. package/dist/resolveAriaLabelledBy-cjs.js +5 -0
  114. package/dist/resolveAriaLabelledBy-es.js +5 -1
  115. package/dist/{CSPContext-cjs.js → scrollEdges-cjs.js} +30 -2
  116. package/dist/{CSPContext-es.js → scrollEdges-es.js} +28 -3
  117. package/dist/styles.css +9 -3
  118. package/dist/unstyledPrimitives/index.cjs +1217 -639
  119. package/dist/unstyledPrimitives/index.mjs +1163 -585
  120. package/dist/useButton-cjs.js +283 -317
  121. package/dist/useButton-es.js +277 -298
  122. package/dist/useCompositeListItem-cjs.js +328 -279
  123. package/dist/useCompositeListItem-es.js +293 -246
  124. package/dist/useLabel-cjs.js +11 -1
  125. package/dist/useLabel-es.js +11 -2
  126. package/dist/useLabelableId-cjs.js +37 -91
  127. package/dist/useLabelableId-es.js +37 -90
  128. package/dist/useRenderElement-cjs.js +81 -26
  129. package/dist/useRenderElement-es.js +81 -27
  130. package/dist/{InternalBackdrop-cjs.js → useScrollLock-cjs.js} +1286 -1220
  131. package/dist/{InternalBackdrop-es.js → useScrollLock-es.js} +1250 -1207
  132. package/package.json +3 -3
  133. package/dist/index.esm-cjs.js +0 -738
  134. package/dist/index.esm-es.js +0 -712
@@ -1,19 +1,37 @@
1
1
  import * as React from 'react';
2
- import { E as useControlled, k as useTransitionStatus, n as useValueAsRef, a as useIsoLayoutEffect, Q as useValueChanged, b as useStableCallback, f as useOpenChangeComplete, d as createChangeEventDetails, G as none, H as visuallyHiddenInput, I as visuallyHidden, y as focusOut, U as outsidePress, c as useButton, h as useTimeout, m as getFloatingFocusElement, r as contains, o as ownerDocument, V as cancelOpen, t as transitionStatusMapping, W as useAnimationFrame, X as isWebKit, Y as windowResize, Z as itemPress, u as useBaseUiId } from './useButton-es.js';
3
- import { f as formatErrorMessage, a as useRefWithInit, m as mergeProps, b as useMergedRefs, c as EMPTY_ARRAY, E as EMPTY_OBJECT, u as useRenderElement } from './useRenderElement-es.js';
4
- import { o as createSelector, q as useOpenInteractionType, r as useStore, s as useClick, t as useDismiss, v as useInteractions, w as useOnFirstRender, S as Store, x as pressableTriggerOpenStateMapping, y as FloatingPortal, z as useScrollLock, I as InternalBackdrop, A as inertValue, B as DROPDOWN_COLLISION_AVOIDANCE, p as popupStateMapping, F as FloatingFocusManager } from './InternalBackdrop-es.js';
5
- import { a as useFormContext, b as useFieldRootContext, c as useLabelableId, d as useField, f as fieldValidityMapping, u as useLabelableContext } from './useLabelableId-es.js';
6
- import { s as stringifyAsValue, h as hasNullItemLabel, c as compareItemEquality, f as findItemIndex, d as defaultItemEquality, r as resolveAriaLabelledBy, a as resolveMultipleLabels, b as resolveSelectedLabel, e as selectedValueIncludes, g as removeItem } from './resolveAriaLabelledBy-es.js';
2
+ import { z as useControlled, k as useTransitionStatus, m as useValueAsRef, b as useStableCallback, a as useIsoLayoutEffect, O as useValueChanged, f as useOpenChangeComplete, d as createChangeEventDetails, C as none, D as visuallyHiddenInput, E as visuallyHidden, v as focusOut, Q as outsidePress, c as useButton, h as useTimeout, q as contains, o as ownerDocument, R as cancelOpen, t as transitionStatusMapping, n as useAnimationFrame, B as addEventListener, U as windowResize, w as isVirtualClick, V as itemPress, u as useBaseUiId } from './useButton-es.js';
3
+ import { f as formatErrorMessage, a as useRefWithInit, m as mergeProps, E as EMPTY_OBJECT, c as useMergedRefs, b as EMPTY_ARRAY, u as useRenderElement } from './useRenderElement-es.js';
4
+ import { t as createSelector, v as useOpenInteractionType, w as useStore, x as useClick, y as useDismiss, F as FOCUSABLE_POPUP_PROPS, z as useOnFirstRender, S as Store, B as pressableTriggerOpenStateMapping, g as getFloatingFocusElement, E as FloatingPortal, I as InternalBackdrop, G as inertValue, H as DROPDOWN_COLLISION_AVOIDANCE, p as popupStateMapping, r as useDirection, J as webkit, a as FloatingFocusManager } from './useScrollLock-es.js';
5
+ import { i as isElementDisabled, u as useToolbarRootContext } from './isElementDisabled-es.js';
6
+ import { a as useFormContext, b as useFieldRootContext, c as useLabelableId, d as useRegisterFieldControl, f as fieldValidityMapping, u as useLabelableContext } from './useLabelableId-es.js';
7
+ import { s as stringifyAsValue, h as hasNullItemLabel, c as compareItemEquality, f as findItemIndex, d as defaultItemEquality, a as stringifyAsLabel, b as areArraysEqual, r as resolveAriaLabelledBy, e as resolveMultipleLabels, g as resolveSelectedLabel, i as selectedValueIncludes, j as removeItem } from './resolveAriaLabelledBy-es.js';
8
+ import { g as getMaxScrollOffset, n as normalizeScrollOffset, u as useCSPContext, S as SCROLL_EDGE_TOLERANCE_PX, C as COMPOSITE_KEYS, s as styleDisableScrollbar } from './scrollEdges-es.js';
7
9
  import { jsx, jsxs } from 'react/jsx-runtime';
8
- import { u as useFloatingRootContext, a as useListNavigation, b as useTypeahead, g as getPseudoElementBounds, c as useAnchorPositioning, C as CompositeList, d as getDisabledMountTransitionStyles, e as useCompositeListItem, I as IndexGuessBehavior } from './useCompositeListItem-es.js';
9
- import * as ReactDOM from 'react-dom';
10
- import { r as rectToClientRect } from './index.esm-es.js';
11
- import { u as useCSPContext, C as COMPOSITE_KEYS, s as styleDisableScrollbar } from './CSPContext-es.js';
12
- import { u as useToolbarRootContext } from './ToolbarRootContext-es.js';
10
+ import { u as useFloatingRootContext, a as useListNavigation, b as useTypeahead, g as getPseudoElementBounds, c as useAnchoredPopupScrollLock, d as useAnchorPositioning, e as usePositioner, C as CompositeList, f as getDisabledMountTransitionStyles, h as useCompositeListItem, I as IndexGuessBehavior } from './useCompositeListItem-es.js';
11
+ import { r as rectToClientRect } from './floating-ui.utils-es.js';
13
12
  import { c as clamp } from './clamp-es.js';
14
13
  import { o as getWindow } from './floating-ui.utils.dom-es.js';
15
14
  import { p as platform } from './floating-ui.react-dom-es.js';
16
15
 
16
+ /**
17
+ * Returns a previous value of its argument.
18
+ * @param value Current value.
19
+ * @returns Previous value, or null if there is no previous value.
20
+ */
21
+ function usePreviousValue(value) {
22
+ const [state, setState] = React.useState({
23
+ current: value,
24
+ previous: null
25
+ });
26
+ if (value !== state.current) {
27
+ setState({
28
+ current: value,
29
+ previous: state.current
30
+ });
31
+ }
32
+ return state.previous;
33
+ }
34
+
17
35
  const SelectRootContext = /*#__PURE__*/React.createContext(null);
18
36
  if (process.env.NODE_ENV !== "production") SelectRootContext.displayName = "SelectRootContext";
19
37
  const SelectFloatingContext = /*#__PURE__*/React.createContext(null);
@@ -68,18 +86,16 @@ const selectors = {
68
86
  activeIndex: createSelector(state => state.activeIndex),
69
87
  selectedIndex: createSelector(state => state.selectedIndex),
70
88
  isActive: createSelector((state, index) => state.activeIndex === index),
71
- isSelected: createSelector((state, index, itemValue) => {
89
+ isSelected: createSelector((state, itemValue) => {
72
90
  const comparer = state.isItemEqualToValue;
73
91
  const storeValue = state.value;
74
92
  if (state.multiple) {
75
93
  return Array.isArray(storeValue) && storeValue.some(selectedItem => compareItemEquality(itemValue, selectedItem, comparer));
76
94
  }
77
95
 
78
- // `selectedIndex` is only updated after the items mount for the first time,
79
- // the value check avoids a re-render for the initially selected item.
80
- if (state.selectedIndex === index && state.selectedIndex !== null) {
81
- return true;
82
- }
96
+ // The value is the source of truth: a stale `selectedIndex` (e.g. the controlled
97
+ // value changes while the popup is open, where the index sync is deferred) must not
98
+ // keep a previously selected item marked as selected.
83
99
  return compareItemEquality(itemValue, storeValue, comparer);
84
100
  }),
85
101
  isSelectedByFocus: createSelector((state, index) => {
@@ -90,6 +106,7 @@ const selectors = {
90
106
  triggerElement: createSelector(state => state.triggerElement),
91
107
  positionerElement: createSelector(state => state.positionerElement),
92
108
  listElement: createSelector(state => state.listElement),
109
+ popupSide: createSelector(state => state.popupSide),
93
110
  scrollUpArrowVisible: createSelector(state => state.scrollUpArrowVisible),
94
111
  scrollDownArrowVisible: createSelector(state => state.scrollDownArrowVisible),
95
112
  hasScrollArrows: createSelector(state => state.hasScrollArrows)
@@ -105,6 +122,7 @@ function SelectRoot(props) {
105
122
  defaultOpen = false,
106
123
  onOpenChange,
107
124
  name: nameProp,
125
+ form,
108
126
  autoComplete,
109
127
  disabled: disabledProp = false,
110
128
  readOnly = false,
@@ -128,7 +146,6 @@ function SelectRoot(props) {
128
146
  setDirty,
129
147
  setTouched,
130
148
  setFocused,
131
- shouldValidateOnChange,
132
149
  validityData,
133
150
  setFilled,
134
151
  name: fieldName,
@@ -161,11 +178,12 @@ function SelectRoot(props) {
161
178
  const valueRef = React.useRef(null);
162
179
  const valuesRef = React.useRef([]);
163
180
  const typingRef = React.useRef(false);
164
- const keyboardActiveRef = React.useRef(false);
181
+ const firstItemTextRef = React.useRef(null);
165
182
  const selectedItemTextRef = React.useRef(null);
166
183
  const selectionRef = React.useRef({
167
184
  allowSelectedMouseUp: false,
168
- allowUnselectedMouseUp: false
185
+ allowUnselectedMouseUp: false,
186
+ dragY: 0
169
187
  });
170
188
  const alignItemWithTriggerActiveRef = React.useRef(false);
171
189
  const {
@@ -199,6 +217,7 @@ function SelectRoot(props) {
199
217
  triggerElement: null,
200
218
  positionerElement: null,
201
219
  listElement: null,
220
+ popupSide: null,
202
221
  scrollUpArrowVisible: false,
203
222
  scrollDownArrowVisible: false,
204
223
  hasScrollArrows: false
@@ -207,8 +226,13 @@ function SelectRoot(props) {
207
226
  const selectedIndex = useStore(store, selectors.selectedIndex);
208
227
  const triggerElement = useStore(store, selectors.triggerElement);
209
228
  const positionerElement = useStore(store, selectors.positionerElement);
229
+ const previousOpenMethod = usePreviousValue(openMethod);
230
+ const renderedOpenMethod = openMethod ?? previousOpenMethod ?? null;
210
231
  const serializedValue = React.useMemo(() => {
211
- if (multiple && Array.isArray(value) && value.length === 0) {
232
+ // In multiple mode the shared input is nameless; per-value entries are submitted via
233
+ // `hiddenInputs`. Its value is therefore irrelevant, and passing the whole array to
234
+ // `stringifyAsValue` would invoke a user `itemToStringValue` with an array it doesn't expect.
235
+ if (multiple) {
212
236
  return '';
213
237
  }
214
238
  return stringifyAsValue(value, itemToStringValue);
@@ -220,15 +244,12 @@ function SelectRoot(props) {
220
244
  return stringifyAsValue(value, itemToStringValue);
221
245
  }, [multiple, value, itemToStringValue]);
222
246
  const controlRef = useValueAsRef(store.state.triggerElement);
223
- useField({
224
- id: generatedId,
225
- commit: validation.commit,
226
- value,
227
- controlRef,
228
- name,
229
- getValue: () => fieldStringValue
230
- });
247
+ const getStringifiedValueForForm = useStableCallback(() => fieldStringValue);
248
+ useRegisterFieldControl(controlRef, generatedId, value, getStringifiedValueForForm, !disabled, nameProp);
231
249
  const initialValueRef = React.useRef(value);
250
+ // Mirror the `hasSelectedValue` store selector so the Field's filled state agrees with the
251
+ // trigger/value placeholder semantics (a value serializing to `''` counts as empty).
252
+ const hasSelectedValue = multiple ? Array.isArray(value) && value.length > 0 : value != null && stringifyAsValue(value, itemToStringValue) !== '';
232
253
  useIsoLayoutEffect(() => {
233
254
  // Ensure the values and labels are registered for programmatic value changes.
234
255
  if (value !== initialValueRef.current) {
@@ -236,35 +257,43 @@ function SelectRoot(props) {
236
257
  }
237
258
  }, [store, value]);
238
259
  useIsoLayoutEffect(() => {
239
- setFilled(multiple ? Array.isArray(value) && value.length > 0 : value != null);
240
- }, [multiple, value, setFilled]);
260
+ setFilled(hasSelectedValue);
261
+ }, [hasSelectedValue, setFilled]);
241
262
  useIsoLayoutEffect(function syncSelectedIndex() {
242
- if (open) {
243
- return;
244
- }
245
263
  const registry = valuesRef.current;
264
+ let nextIndex;
246
265
  if (multiple) {
247
266
  const currentValue = Array.isArray(value) ? value : [];
248
267
  if (currentValue.length === 0) {
249
- store.set('selectedIndex', null);
250
- return;
268
+ nextIndex = null;
269
+ } else {
270
+ const lastValue = currentValue[currentValue.length - 1];
271
+ const lastIndex = findItemIndex(registry, lastValue, isItemEqualToValue);
272
+ nextIndex = lastIndex === -1 ? null : lastIndex;
251
273
  }
252
- const lastValue = currentValue[currentValue.length - 1];
253
- const lastIndex = findItemIndex(registry, lastValue, isItemEqualToValue);
254
- store.set('selectedIndex', lastIndex === -1 ? null : lastIndex);
274
+ } else {
275
+ const index = findItemIndex(registry, value, isItemEqualToValue);
276
+ nextIndex = index === -1 ? null : index;
277
+ }
278
+ if (nextIndex === null) {
279
+ selectedItemTextRef.current = null;
280
+ }
281
+ if (open) {
255
282
  return;
256
283
  }
257
- const index = findItemIndex(registry, value, isItemEqualToValue);
258
- store.set('selectedIndex', index === -1 ? null : index);
259
- }, [multiple, open, value, valuesRef, isItemEqualToValue, store]);
284
+ store.set('selectedIndex', nextIndex);
285
+ }, [hasSelectedValue, multiple, open, value, valuesRef, isItemEqualToValue, store, selectedItemTextRef]);
286
+ function isSelectedValueDirty(currentValue) {
287
+ const initialValue = validityData.initialValue;
288
+ if (Array.isArray(currentValue) && Array.isArray(initialValue)) {
289
+ return !areArraysEqual(currentValue, initialValue, (itemValue, initialItemValue) => compareItemEquality(itemValue, initialItemValue, isItemEqualToValue));
290
+ }
291
+ return currentValue !== initialValue;
292
+ }
260
293
  useValueChanged(value, () => {
261
294
  clearErrors(name);
262
- setDirty(value !== validityData.initialValue);
263
- if (shouldValidateOnChange()) {
264
- validation.commit(value);
265
- } else {
266
- validation.commit(value, true);
267
- }
295
+ setDirty(isSelectedValueDirty(value));
296
+ validation.change(value);
268
297
  });
269
298
  const setOpen = useStableCallback((nextOpen, eventDetails) => {
270
299
  onOpenChange?.(nextOpen, eventDetails);
@@ -279,24 +308,13 @@ function SelectRoot(props) {
279
308
  validation.commit(value);
280
309
  }
281
310
  }
282
-
283
- // The active index will sync to the last selected index on the next open.
284
- // Workaround `enableFocusInside` in Floating UI setting `tabindex=0` of a non-highlighted
285
- // option upon close when tabbing out due to `keepMounted=true`:
286
- // https://github.com/floating-ui/floating-ui/pull/3004/files#diff-962a7439cdeb09ea98d4b622a45d517bce07ad8c3f866e089bda05f4b0bbd875R194-R199
287
- // This otherwise causes options to retain `tabindex=0` incorrectly when the popup is closed
288
- // when tabbing outside.
289
- if (!nextOpen && store.state.activeIndex !== null) {
290
- const activeOption = listRef.current[store.state.activeIndex];
291
- // Wait for Floating UI's focus effect to have fired
292
- queueMicrotask(() => {
293
- activeOption?.setAttribute('tabindex', '-1');
294
- });
295
- }
296
311
  });
297
312
  const handleUnmount = useStableCallback(() => {
298
313
  setMounted(false);
299
- store.set('activeIndex', null);
314
+ store.update({
315
+ activeIndex: null,
316
+ openMethod: null
317
+ });
300
318
  onOpenChangeComplete?.(false);
301
319
  });
302
320
  useOpenChangeComplete({
@@ -324,10 +342,10 @@ function SelectRoot(props) {
324
342
  if (!scroller) {
325
343
  return;
326
344
  }
327
- const viewportTop = scroller.scrollTop;
328
- const viewportBottom = scroller.scrollTop + scroller.clientHeight;
329
- const shouldShowUp = viewportTop > 1;
330
- const shouldShowDown = viewportBottom < scroller.scrollHeight - 1;
345
+ const maxScrollTop = getMaxScrollOffset(scroller.scrollHeight, scroller.clientHeight);
346
+ const scrollTop = normalizeScrollOffset(scroller.scrollTop, maxScrollTop);
347
+ const shouldShowUp = scrollTop > 0;
348
+ const shouldShowDown = scrollTop < maxScrollTop;
331
349
  if (store.state.scrollUpArrowVisible !== shouldShowUp) {
332
350
  store.set('scrollUpArrowVisible', shouldShowUp);
333
351
  }
@@ -347,9 +365,7 @@ function SelectRoot(props) {
347
365
  enabled: !readOnly && !disabled,
348
366
  event: 'mousedown'
349
367
  });
350
- const dismiss = useDismiss(floatingContext, {
351
- bubbles: false
352
- });
368
+ const dismiss = useDismiss(floatingContext);
353
369
  const listNavigation = useListNavigation(floatingContext, {
354
370
  enabled: !readOnly && !disabled,
355
371
  listRef,
@@ -363,15 +379,19 @@ function SelectRoot(props) {
363
379
  }
364
380
  store.set('activeIndex', nextActiveIndex);
365
381
  },
366
- // Implement our own listeners since `onPointerLeave` on each option fires while scrolling with
367
- // the `alignItemWithTrigger=true`, causing a performance issue on Chrome.
368
- focusItemOnHover: false
382
+ focusItemOnHover: highlightItemOnHover
369
383
  });
370
384
  const typeahead = useTypeahead(floatingContext, {
371
385
  enabled: !readOnly && !disabled && (open || !multiple),
372
386
  listRef: labelsRef,
373
387
  activeIndex,
374
388
  selectedIndex,
389
+ // Skip disabled items while matching so typeahead advances to the next selectable item
390
+ // (a click can never select a disabled item and native `<select>` skips them too). Resolve
391
+ // the disabled state from the element via the attribute-only `isElementDisabled` so the
392
+ // hidden, force-mounted items used for closed-trigger typeahead aren't dropped by the
393
+ // `elementsRef`/visibility filter that `disabledIndices` deliberately sidesteps.
394
+ disabledIndices: index => isElementDisabled(listRef.current[index]),
375
395
  onMatch(index) {
376
396
  if (open) {
377
397
  store.set('activeIndex', index);
@@ -379,23 +399,22 @@ function SelectRoot(props) {
379
399
  setValue(valuesRef.current[index], createChangeEventDetails('none'));
380
400
  }
381
401
  },
382
- onTypingChange(typing) {
402
+ onTyping(typing) {
383
403
  typingRef.current = typing;
384
404
  }
385
405
  });
386
- const {
387
- getReferenceProps,
388
- getFloatingProps,
389
- getItemProps
390
- } = useInteractions([click, dismiss, listNavigation, typeahead]);
391
406
  const mergedTriggerProps = React.useMemo(() => {
392
- return mergeProps(getReferenceProps(), interactionTypeProps, generatedId ? {
393
- id: generatedId
394
- } : EMPTY_OBJECT);
395
- }, [getReferenceProps, interactionTypeProps, generatedId]);
407
+ const triggerInteractionProps = mergeProps(typeahead.reference, listNavigation.reference, dismiss.reference, click.reference, interactionTypeProps);
408
+ if (generatedId) {
409
+ triggerInteractionProps.id = generatedId;
410
+ }
411
+ return triggerInteractionProps;
412
+ }, [click.reference, typeahead.reference, listNavigation.reference, dismiss.reference, interactionTypeProps, generatedId]);
413
+ const popupProps = React.useMemo(() => mergeProps(FOCUSABLE_POPUP_PROPS, typeahead.floating, listNavigation.floating, dismiss.floating), [typeahead.floating, listNavigation.floating, dismiss.floating]);
414
+ const itemProps = listNavigation.item ?? EMPTY_OBJECT;
396
415
  useOnFirstRender(() => {
397
416
  store.update({
398
- popupProps: getFloatingProps(),
417
+ popupProps,
399
418
  triggerProps: mergedTriggerProps
400
419
  });
401
420
  });
@@ -408,15 +427,15 @@ function SelectRoot(props) {
408
427
  open,
409
428
  mounted,
410
429
  transitionStatus,
411
- popupProps: getFloatingProps(),
430
+ popupProps,
412
431
  triggerProps: mergedTriggerProps,
413
432
  items,
414
433
  itemToStringLabel,
415
434
  itemToStringValue,
416
435
  isItemEqualToValue,
417
- openMethod
436
+ openMethod: renderedOpenMethod
418
437
  });
419
- }, [store, generatedId, modal, multiple, value, open, mounted, transitionStatus, getFloatingProps, mergedTriggerProps, items, itemToStringLabel, itemToStringValue, isItemEqualToValue, openMethod]);
438
+ }, [store, generatedId, modal, multiple, value, open, mounted, transitionStatus, popupProps, mergedTriggerProps, items, itemToStringLabel, itemToStringValue, isItemEqualToValue, renderedOpenMethod]);
420
439
  const contextValue = React.useMemo(() => ({
421
440
  store,
422
441
  name,
@@ -424,8 +443,6 @@ function SelectRoot(props) {
424
443
  disabled,
425
444
  readOnly,
426
445
  multiple,
427
- itemToStringLabel,
428
- itemToStringValue,
429
446
  highlightItemOnHover,
430
447
  setValue,
431
448
  setOpen,
@@ -434,20 +451,19 @@ function SelectRoot(props) {
434
451
  scrollHandlerRef,
435
452
  handleScrollArrowVisibility,
436
453
  scrollArrowsMountedCountRef,
437
- getItemProps,
438
- events: floatingContext.context.events,
454
+ itemProps,
439
455
  valueRef,
440
456
  valuesRef,
441
457
  labelsRef,
442
458
  typingRef,
443
459
  selectionRef,
460
+ firstItemTextRef,
444
461
  selectedItemTextRef,
445
462
  validation,
446
463
  onOpenChangeComplete,
447
- keyboardActiveRef,
448
464
  alignItemWithTriggerActiveRef,
449
465
  initialValueRef
450
- }), [store, name, required, disabled, readOnly, multiple, itemToStringLabel, itemToStringValue, highlightItemOnHover, setValue, setOpen, getItemProps, floatingContext.context.events, validation, onOpenChangeComplete, handleScrollArrowVisibility]);
466
+ }), [store, name, required, disabled, readOnly, multiple, highlightItemOnHover, setValue, setOpen, itemProps, validation, onOpenChangeComplete, handleScrollArrowVisibility]);
451
467
  const ref = useMergedRefs(inputRef, validation.inputRef);
452
468
  const hasMultipleSelection = multiple && Array.isArray(value) && value.length > 0;
453
469
  const hiddenInputName = multiple ? undefined : name;
@@ -459,32 +475,33 @@ function SelectRoot(props) {
459
475
  const currentSerializedValue = stringifyAsValue(v, itemToStringValue);
460
476
  return /*#__PURE__*/jsx("input", {
461
477
  type: "hidden",
478
+ form: form,
462
479
  name: name,
463
- value: currentSerializedValue
480
+ value: currentSerializedValue,
481
+ disabled: disabled
464
482
  }, currentSerializedValue);
465
483
  });
466
- }, [multiple, value, name, itemToStringValue]);
484
+ }, [multiple, value, form, name, itemToStringValue, disabled]);
467
485
  return /*#__PURE__*/jsx(SelectRootContext.Provider, {
468
486
  value: contextValue,
469
487
  children: /*#__PURE__*/jsxs(SelectFloatingContext.Provider, {
470
488
  value: floatingContext,
471
489
  children: [children, /*#__PURE__*/jsx("input", {
472
- ...validation.getInputValidationProps({
490
+ ...validation.getValidationProps(disabled, {
473
491
  onFocus() {
474
492
  // Move focus to the trigger element when the hidden input is focused.
475
493
  store.state.triggerElement?.focus({
476
494
  // Supported in Chrome from 144 (January 2026)
477
- // @ts-expect-error - focusVisible is not yet in the lib.dom.d.ts
478
495
  focusVisible: true
479
496
  });
480
497
  },
481
498
  // Handle browser autofill.
482
499
  onChange(event) {
483
500
  // Workaround for https://github.com/facebook/react/issues/9023
484
- if (event.nativeEvent.defaultPrevented) {
501
+ if (event.nativeEvent.defaultPrevented || disabled || readOnly) {
485
502
  return;
486
503
  }
487
- const nextValue = event.target.value;
504
+ const nextValue = event.currentTarget.value;
488
505
  const details = createChangeEventDetails(none, event.nativeEvent);
489
506
  function handleChange() {
490
507
  if (multiple) {
@@ -492,20 +509,21 @@ function SelectRoot(props) {
492
509
  return;
493
510
  }
494
511
 
495
- // Handle single selection: match against registered values using serialization
496
- const matchingValue = valuesRef.current.find(v => {
497
- const candidate = stringifyAsValue(v, itemToStringValue);
498
- if (candidate.toLowerCase() === nextValue.toLowerCase()) {
499
- return true;
500
- }
501
- return false;
502
- });
512
+ // Preserve the original serialized matching, then fall back to rendered text,
513
+ // which browsers can autofill for primitive values like `value="US">United States`.
514
+ const nextValueLower = nextValue.toLowerCase();
515
+ let matchingIndex = valuesRef.current.findIndex(candidate => stringifyAsValue(candidate, itemToStringValue).toLowerCase() === nextValueLower || stringifyAsLabel(candidate, itemToStringLabel).toLowerCase() === nextValueLower);
516
+ if (matchingIndex === -1) {
517
+ matchingIndex = valuesRef.current.findIndex((_, index) => {
518
+ const renderedLabel = labelsRef.current[index];
519
+ return renderedLabel != null && renderedLabel.toLowerCase() === nextValueLower;
520
+ });
521
+ }
522
+ const matchingValue = matchingIndex === -1 ? undefined : valuesRef.current[matchingIndex];
503
523
  if (matchingValue != null) {
504
- setDirty(matchingValue !== validityData.initialValue);
524
+ // `setValue` may be canceled by `onValueChange`; rely on `useValueChanged` to
525
+ // mark the field dirty and run validation only when the value actually changes.
505
526
  setValue(matchingValue, details);
506
- if (shouldValidateOnChange()) {
507
- validation.commit(matchingValue);
508
- }
509
527
  }
510
528
  }
511
529
  store.set('forceMount', true);
@@ -513,6 +531,7 @@ function SelectRoot(props) {
513
531
  }
514
532
  }),
515
533
  id: generatedId && hiddenInputName == null ? `${generatedId}-hidden-input` : undefined,
534
+ form: form,
516
535
  name: hiddenInputName,
517
536
  autoComplete: autoComplete,
518
537
  value: serializedValue,
@@ -522,7 +541,8 @@ function SelectRoot(props) {
522
541
  ref: ref,
523
542
  style: name ? visuallyHiddenInput : visuallyHidden,
524
543
  tabIndex: -1,
525
- "aria-hidden": true
544
+ "aria-hidden": true,
545
+ suppressHydrationWarning: true
526
546
  }), hiddenInputs]
527
547
  })
528
548
  });
@@ -530,10 +550,12 @@ function SelectRoot(props) {
530
550
 
531
551
  const BOUNDARY_OFFSET = 2;
532
552
  const SELECTED_DELAY = 400;
533
- const UNSELECTED_DELAY = 200;
534
553
  const stateAttributesMapping$2 = {
535
554
  ...pressableTriggerOpenStateMapping,
536
555
  ...fieldValidityMapping,
556
+ popupSide: side => side ? {
557
+ 'data-popup-side': side
558
+ } : null,
537
559
  value: () => null
538
560
  };
539
561
 
@@ -550,6 +572,7 @@ const SelectTrigger = /*#__PURE__*/React.forwardRef(function SelectTrigger(compo
550
572
  id: idProp,
551
573
  disabled: disabledProp = false,
552
574
  nativeButton = true,
575
+ style,
553
576
  ...elementProps
554
577
  } = componentProps;
555
578
  const {
@@ -570,20 +593,20 @@ const SelectTrigger = /*#__PURE__*/React.forwardRef(function SelectTrigger(compo
570
593
  readOnly,
571
594
  required,
572
595
  alignItemWithTriggerActiveRef,
573
- disabled: selectDisabled,
574
- keyboardActiveRef
596
+ disabled: selectDisabled
575
597
  } = useSelectRootContext();
576
598
  const disabled = fieldDisabled || selectDisabled || disabledProp;
577
599
  const open = useStore(store, selectors.open);
600
+ const mounted = useStore(store, selectors.mounted);
578
601
  const value = useStore(store, selectors.value);
579
602
  const triggerProps = useStore(store, selectors.triggerProps);
580
603
  const positionerElement = useStore(store, selectors.positionerElement);
581
604
  const listElement = useStore(store, selectors.listElement);
605
+ const popupSideValue = useStore(store, selectors.popupSide);
582
606
  const rootId = useStore(store, selectors.id);
583
607
  const selectLabelId = useStore(store, selectors.labelId);
584
608
  const hasSelectedValue = useStore(store, selectors.hasSelectedValue);
585
- const shouldCheckNullItemLabel = !hasSelectedValue && open;
586
- const hasNullItemLabel = useStore(store, selectors.hasNullItemLabel, shouldCheckNullItemLabel);
609
+ const popupSide = mounted && positionerElement ? popupSideValue : null;
587
610
  const id = idProp ?? rootId;
588
611
  const ariaLabelledBy = resolveAriaLabelledBy(fieldLabelId, selectLabelId);
589
612
  useLabelableId({
@@ -601,61 +624,41 @@ const SelectTrigger = /*#__PURE__*/React.forwardRef(function SelectTrigger(compo
601
624
  const setTriggerElement = useStableCallback(element => {
602
625
  store.set('triggerElement', element);
603
626
  });
604
- const mergedRef = useMergedRefs(forwardedRef, triggerRef, buttonRef, setTriggerElement);
605
627
  const timeoutFocus = useTimeout();
606
628
  const timeoutMouseDown = useTimeout();
607
629
  const selectedDelayTimeout = useTimeout();
608
- const unselectedDelayTimeout = useTimeout();
609
630
  React.useEffect(() => {
610
631
  if (open) {
611
- const hasSelectedItemInList = hasSelectedValue || hasNullItemLabel;
612
- const shouldDelayUnselectedMouseUpLonger = !hasSelectedItemInList;
613
-
614
- // When there is no selected item in the list (placeholder-only selects), a mousedown
615
- // on the trigger followed by a quick mouseup over the first option can accidentally select
616
- // within 200ms. Delay unselected mouseup to match the safer 400ms window.
617
- if (shouldDelayUnselectedMouseUpLonger) {
618
- selectedDelayTimeout.start(SELECTED_DELAY, () => {
619
- selectionRef.current.allowUnselectedMouseUp = true;
620
- selectionRef.current.allowSelectedMouseUp = true;
621
- });
622
- } else {
623
- // mousedown -> move to unselected item -> mouseup should not select within 200ms.
624
- unselectedDelayTimeout.start(UNSELECTED_DELAY, () => {
625
- selectionRef.current.allowUnselectedMouseUp = true;
626
-
627
- // mousedown -> mouseup on selected item should not select within 400ms.
628
- selectedDelayTimeout.start(UNSELECTED_DELAY, () => {
629
- selectionRef.current.allowSelectedMouseUp = true;
630
- });
631
- });
632
- }
632
+ // A mousedown on the trigger can open the popup under the cursor. Keep mouseup selection
633
+ // disabled briefly so releasing over either the selected item or a neighboring item doesn't
634
+ // commit an accidental selection. SelectItem can still opt into unselected mouseup sooner
635
+ // after a real drag over the item.
636
+ selectedDelayTimeout.start(SELECTED_DELAY, () => {
637
+ selectionRef.current.allowUnselectedMouseUp = true;
638
+ selectionRef.current.allowSelectedMouseUp = true;
639
+ });
633
640
  return () => {
634
641
  selectedDelayTimeout.clear();
635
- unselectedDelayTimeout.clear();
636
642
  };
637
643
  }
638
644
  selectionRef.current = {
639
645
  allowSelectedMouseUp: false,
640
- allowUnselectedMouseUp: false
646
+ allowUnselectedMouseUp: false,
647
+ dragY: 0
641
648
  };
642
649
  timeoutMouseDown.clear();
643
650
  return undefined;
644
- }, [open, hasSelectedValue, hasNullItemLabel, selectionRef, timeoutMouseDown, selectedDelayTimeout, unselectedDelayTimeout]);
645
- const ariaControlsId = React.useMemo(() => {
646
- return listElement?.id ?? getFloatingFocusElement(positionerElement)?.id;
647
- }, [listElement, positionerElement]);
648
- const props = mergeProps(triggerProps, {
651
+ }, [open, selectionRef, timeoutMouseDown, selectedDelayTimeout]);
652
+ const mergedProps = mergeProps(triggerProps, {
649
653
  id,
650
654
  role: 'combobox',
651
655
  'aria-expanded': open ? 'true' : 'false',
652
656
  'aria-haspopup': 'listbox',
653
- 'aria-controls': open ? ariaControlsId : undefined,
657
+ 'aria-controls': open ? listElement?.id ?? getFloatingFocusElement(positionerElement)?.id : undefined,
654
658
  'aria-labelledby': ariaLabelledBy,
655
659
  'aria-readonly': readOnly || undefined,
656
660
  'aria-required': required || undefined,
657
661
  tabIndex: disabled ? -1 : 0,
658
- ref: mergedRef,
659
662
  onFocus(event) {
660
663
  setFocused(true);
661
664
 
@@ -667,8 +670,6 @@ const SelectTrigger = /*#__PURE__*/React.forwardRef(function SelectTrigger(compo
667
670
  // Saves a re-render on initial click: `forceMount === true` mounts
668
671
  // the items before `open === true`. We could sync those cycles better
669
672
  // without a timeout, but this is enough for now.
670
- //
671
- // XXX: might be causing `act()` warnings.
672
673
  timeoutFocus.start(0, () => {
673
674
  store.set('forceMount', true);
674
675
  });
@@ -684,12 +685,6 @@ const SelectTrigger = /*#__PURE__*/React.forwardRef(function SelectTrigger(compo
684
685
  validation.commit(value);
685
686
  }
686
687
  },
687
- onPointerMove() {
688
- keyboardActiveRef.current = false;
689
- },
690
- onKeyDown() {
691
- keyboardActiveRef.current = true;
692
- },
693
688
  onMouseDown(event) {
694
689
  if (open) {
695
690
  return;
@@ -701,8 +696,9 @@ const SelectTrigger = /*#__PURE__*/React.forwardRef(function SelectTrigger(compo
701
696
  }
702
697
  const mouseUpTarget = mouseEvent.target;
703
698
 
704
- // Early return if clicked on trigger element or its children
705
- if (contains(triggerRef.current, mouseUpTarget) || contains(positionerRef.current, mouseUpTarget) || mouseUpTarget === triggerRef.current) {
699
+ // Don't treat the release as an outside press when it lands on the trigger or inside
700
+ // the popup positioner (or their children).
701
+ if (contains(triggerRef.current, mouseUpTarget) || contains(positionerRef.current, mouseUpTarget)) {
706
702
  return;
707
703
  }
708
704
  const bounds = getPseudoElementBounds(triggerRef.current);
@@ -719,7 +715,8 @@ const SelectTrigger = /*#__PURE__*/React.forwardRef(function SelectTrigger(compo
719
715
  });
720
716
  });
721
717
  }
722
- }, validation.getValidationProps, elementProps, getButtonProps);
718
+ }, elementProps, getButtonProps);
719
+ const props = validation.getValidationProps(disabled, mergedProps);
723
720
 
724
721
  // ensure nested useButton does not overwrite the combobox role:
725
722
  // <Toolbar.Button render={<Select.Trigger />} />
@@ -730,10 +727,11 @@ const SelectTrigger = /*#__PURE__*/React.forwardRef(function SelectTrigger(compo
730
727
  disabled,
731
728
  value,
732
729
  readOnly,
730
+ popupSide,
733
731
  placeholder: !hasSelectedValue
734
732
  };
735
733
  return useRenderElement('button', componentProps, {
736
- ref: [forwardedRef, triggerRef],
734
+ ref: [forwardedRef, triggerRef, buttonRef, setTriggerElement],
737
735
  state,
738
736
  stateAttributesMapping: stateAttributesMapping$2,
739
737
  props
@@ -757,6 +755,7 @@ const SelectValue = /*#__PURE__*/React.forwardRef(function SelectValue(component
757
755
  render,
758
756
  children: childrenProp,
759
757
  placeholder,
758
+ style,
760
759
  ...elementProps
761
760
  } = componentProps;
762
761
  const {
@@ -869,6 +868,7 @@ const SelectPositioner = /*#__PURE__*/React.forwardRef(function SelectPositioner
869
868
  disableAnchorTracking,
870
869
  alignItemWithTrigger = true,
871
870
  collisionAvoidance = DROPDOWN_COLLISION_AVOIDANCE,
871
+ style,
872
872
  ...elementProps
873
873
  } = componentProps;
874
874
  const {
@@ -910,7 +910,7 @@ const SelectPositioner = /*#__PURE__*/React.forwardRef(function SelectPositioner
910
910
  }
911
911
  }, [store, mounted]);
912
912
  React.useImperativeHandle(alignItemWithTriggerActiveRef, () => alignItemWithTriggerActive);
913
- useScrollLock((alignItemWithTriggerActive || modal) && open && openMethod !== 'touch', triggerElement);
913
+ useAnchoredPopupScrollLock((alignItemWithTriggerActive || modal) && open, openMethod === 'touch', positionerElement, triggerElement);
914
914
  const positioning = useAnchorPositioning({
915
915
  anchor,
916
916
  floatingRootContext,
@@ -930,34 +930,25 @@ const SelectPositioner = /*#__PURE__*/React.forwardRef(function SelectPositioner
930
930
  });
931
931
  const renderedSide = alignItemWithTriggerActive ? 'none' : positioning.side;
932
932
  const positionerStyles = alignItemWithTriggerActive ? FIXED : positioning.positionerStyles;
933
- const defaultProps = React.useMemo(() => {
934
- const hiddenStyles = {};
935
- if (!open) {
936
- hiddenStyles.pointerEvents = 'none';
937
- }
938
- return {
939
- role: 'presentation',
940
- hidden: !mounted,
941
- style: {
942
- ...positionerStyles,
943
- ...hiddenStyles
944
- }
945
- };
946
- }, [open, mounted, positionerStyles]);
947
933
  const state = {
948
934
  open,
949
935
  side: renderedSide,
950
936
  align: positioning.align,
951
937
  anchorHidden: positioning.anchorHidden
952
938
  };
939
+ useIsoLayoutEffect(() => {
940
+ store.set('popupSide', positioning.side);
941
+ }, [store, positioning.side]);
953
942
  const setPositionerElement = useStableCallback(element => {
954
943
  store.set('positionerElement', element);
955
944
  });
956
- const element = useRenderElement('div', componentProps, {
957
- ref: [forwardedRef, setPositionerElement],
958
- state,
959
- stateAttributesMapping: popupStateMapping,
960
- props: [defaultProps, getDisabledMountTransitionStyles(transitionStatus), elementProps]
945
+ const element = usePositioner(componentProps, state, {
946
+ styles: positionerStyles,
947
+ transitionStatus,
948
+ props: elementProps,
949
+ refs: [forwardedRef, setPositionerElement],
950
+ hidden: !mounted,
951
+ inert: !open
961
952
  });
962
953
  const prevMapSizeRef = React.useRef(0);
963
954
  const onMapChange = useStableCallback(map => {
@@ -1032,18 +1023,6 @@ const SelectPositioner = /*#__PURE__*/React.forwardRef(function SelectPositioner
1032
1023
  });
1033
1024
  if (process.env.NODE_ENV !== "production") SelectPositioner.displayName = "SelectPositioner";
1034
1025
 
1035
- function isMouseWithinBounds(event) {
1036
- const targetRect = event.currentTarget.getBoundingClientRect();
1037
-
1038
- // Safari randomly fires `mouseleave` incorrectly when the item is
1039
- // aligned to the trigger. This is a workaround to prevent the highlight
1040
- // from being removed while the cursor is still within the bounds of the item.
1041
- // https://github.com/mui/base-ui/issues/869
1042
- const isWithinBounds = targetRect.top + 1 <= event.clientY && event.clientY <= targetRect.bottom - 1 && targetRect.left + 1 <= event.clientX && event.clientX <= targetRect.right - 1;
1043
- return isWithinBounds;
1044
- }
1045
-
1046
- const SCROLL_EPS_PX = 1;
1047
1026
  const stateAttributesMapping = {
1048
1027
  ...popupStateMapping,
1049
1028
  ...transitionStatusMapping
@@ -1059,6 +1038,7 @@ const SelectPopup = /*#__PURE__*/React.forwardRef(function SelectPopup(component
1059
1038
  const {
1060
1039
  render,
1061
1040
  className,
1041
+ style,
1062
1042
  finalFocus,
1063
1043
  ...elementProps
1064
1044
  } = componentProps;
@@ -1068,39 +1048,38 @@ const SelectPopup = /*#__PURE__*/React.forwardRef(function SelectPopup(component
1068
1048
  onOpenChangeComplete,
1069
1049
  setOpen,
1070
1050
  valueRef,
1051
+ firstItemTextRef,
1071
1052
  selectedItemTextRef,
1072
- keyboardActiveRef,
1073
1053
  multiple,
1074
1054
  handleScrollArrowVisibility,
1075
1055
  scrollHandlerRef,
1056
+ listRef,
1076
1057
  highlightItemOnHover
1077
1058
  } = useSelectRootContext();
1078
1059
  const {
1079
1060
  side,
1080
1061
  align,
1081
1062
  alignItemWithTriggerActive,
1082
- setControlledAlignItemWithTrigger,
1083
- scrollDownArrowRef,
1084
- scrollUpArrowRef
1063
+ isPositioned,
1064
+ setControlledAlignItemWithTrigger
1085
1065
  } = useSelectPositionerContext();
1086
1066
  const insideToolbar = useToolbarRootContext(true) != null;
1087
1067
  const floatingRootContext = useSelectFloatingContext();
1068
+ const direction = useDirection();
1088
1069
  const {
1089
1070
  nonce,
1090
1071
  disableStyleElements
1091
1072
  } = useCSPContext();
1092
- const highlightTimeout = useTimeout();
1093
1073
  const id = useStore(store, selectors.id);
1094
1074
  const open = useStore(store, selectors.open);
1075
+ const openMethod = useStore(store, selectors.openMethod);
1095
1076
  const mounted = useStore(store, selectors.mounted);
1096
1077
  const popupProps = useStore(store, selectors.popupProps);
1097
1078
  const transitionStatus = useStore(store, selectors.transitionStatus);
1098
1079
  const triggerElement = useStore(store, selectors.triggerElement);
1099
1080
  const positionerElement = useStore(store, selectors.positionerElement);
1100
1081
  const listElement = useStore(store, selectors.listElement);
1101
- const initialHeightRef = React.useRef(0);
1102
1082
  const reachedMaxHeightRef = React.useRef(false);
1103
- const maxHeightRef = React.useRef(0);
1104
1083
  const initialPlacedRef = React.useRef(false);
1105
1084
  const originalPositionerStylesRef = React.useRef({});
1106
1085
  const scrollArrowFrame = useAnimationFrame();
@@ -1114,12 +1093,18 @@ const SelectPopup = /*#__PURE__*/React.forwardRef(function SelectPopup(component
1114
1093
  }
1115
1094
  const isTopPositioned = positionerElement.style.top === '0px';
1116
1095
  const isBottomPositioned = positionerElement.style.bottom === '0px';
1117
- const currentHeight = positionerElement.getBoundingClientRect().height;
1096
+ if (!isTopPositioned && !isBottomPositioned) {
1097
+ handleScrollArrowVisibility();
1098
+ return;
1099
+ }
1100
+ const scale = getScale(positionerElement);
1101
+ const currentHeight = normalizeSize(positionerElement.getBoundingClientRect().height, 'y', scale);
1118
1102
  const doc = ownerDocument(positionerElement);
1119
- const positionerStyles = getComputedStyle(positionerElement);
1103
+ const win = getWindow(positionerElement);
1104
+ const positionerStyles = win.getComputedStyle(positionerElement);
1120
1105
  const marginTop = parseFloat(positionerStyles.marginTop);
1121
1106
  const marginBottom = parseFloat(positionerStyles.marginBottom);
1122
- const maxPopupHeight = getMaxPopupHeight(getComputedStyle(popupRef.current));
1107
+ const maxPopupHeight = getMaxPopupHeight(win.getComputedStyle(popupRef.current));
1123
1108
  const maxAvailableHeight = Math.min(doc.documentElement.clientHeight - marginTop - marginBottom, maxPopupHeight);
1124
1109
  const scrollTop = scroller.scrollTop;
1125
1110
  const maxScrollTop = getMaxScrollTop(scroller);
@@ -1137,42 +1122,29 @@ const SelectPopup = /*#__PURE__*/React.forwardRef(function SelectPopup(component
1137
1122
  setHeight(currentHeight + heightDelta);
1138
1123
  }
1139
1124
  scroller.scrollTop = targetScrollTop;
1140
- if (maxAvailableHeight - (currentHeight + heightDelta) <= SCROLL_EPS_PX) {
1125
+ if (maxAvailableHeight - (currentHeight + heightDelta) <= SCROLL_EDGE_TOLERANCE_PX) {
1141
1126
  reachedMaxHeightRef.current = true;
1142
1127
  }
1143
1128
  handleScrollArrowVisibility();
1144
1129
  };
1145
- if (isTopPositioned) {
1146
- const diff = maxScrollTop - scrollTop;
1147
- const idealHeight = currentHeight + diff;
1148
- const nextHeight = Math.min(idealHeight, maxAvailableHeight);
1149
- nextPositionerHeight = nextHeight;
1150
- if (diff <= SCROLL_EPS_PX) {
1151
- handleSmallDiff(diff, maxScrollTop);
1152
- return;
1153
- }
1154
- if (maxAvailableHeight - nextHeight > SCROLL_EPS_PX) {
1130
+ const diff = isTopPositioned ? maxScrollTop - scrollTop : scrollTop;
1131
+ const nextHeight = Math.min(currentHeight + diff, maxAvailableHeight);
1132
+ nextPositionerHeight = nextHeight;
1133
+ if (diff <= SCROLL_EDGE_TOLERANCE_PX) {
1134
+ handleSmallDiff(diff, isTopPositioned ? maxScrollTop : 0);
1135
+ return;
1136
+ }
1137
+ if (maxAvailableHeight - nextHeight > SCROLL_EDGE_TOLERANCE_PX) {
1138
+ if (isTopPositioned) {
1155
1139
  scrollToMax = true;
1156
1140
  } else {
1157
- setReachedMax = true;
1158
- }
1159
- } else if (isBottomPositioned) {
1160
- const diff = scrollTop;
1161
- const idealHeight = currentHeight + diff;
1162
- const nextHeight = Math.min(idealHeight, maxAvailableHeight);
1163
- const overshoot = idealHeight - maxAvailableHeight;
1164
- nextPositionerHeight = nextHeight;
1165
- if (diff <= SCROLL_EPS_PX) {
1166
- handleSmallDiff(diff, 0);
1167
- return;
1168
- }
1169
- if (maxAvailableHeight - nextHeight > SCROLL_EPS_PX) {
1170
1141
  nextScrollTop = 0;
1171
- } else {
1172
- setReachedMax = true;
1173
- if (scrollTop < maxScrollTop) {
1174
- nextScrollTop = scrollTop - (diff - overshoot);
1175
- }
1142
+ }
1143
+ } else {
1144
+ setReachedMax = true;
1145
+ if (isBottomPositioned && scrollTop < maxScrollTop) {
1146
+ const overshoot = currentHeight + diff - maxAvailableHeight;
1147
+ nextScrollTop = scrollTop - (diff - overshoot);
1176
1148
  }
1177
1149
  }
1178
1150
  nextPositionerHeight = Math.ceil(nextPositionerHeight);
@@ -1185,11 +1157,11 @@ const SelectPopup = /*#__PURE__*/React.forwardRef(function SelectPopup(component
1185
1157
  const target = scrollToMax ? nextMaxScrollTop : clamp(nextScrollTop, 0, nextMaxScrollTop);
1186
1158
 
1187
1159
  // Avoid adjustments that re-trigger scroll events forever.
1188
- if (Math.abs(scroller.scrollTop - target) > SCROLL_EPS_PX) {
1160
+ if (Math.abs(scroller.scrollTop - target) > SCROLL_EDGE_TOLERANCE_PX) {
1189
1161
  scroller.scrollTop = target;
1190
1162
  }
1191
1163
  }
1192
- if (setReachedMax || nextPositionerHeight >= maxAvailableHeight - SCROLL_EPS_PX) {
1164
+ if (setReachedMax || nextPositionerHeight >= maxAvailableHeight - SCROLL_EDGE_TOLERANCE_PX) {
1193
1165
  reachedMaxHeightRef.current = true;
1194
1166
  }
1195
1167
  handleScrollArrowVisibility();
@@ -1232,13 +1204,15 @@ const SelectPopup = /*#__PURE__*/React.forwardRef(function SelectPopup(component
1232
1204
  }
1233
1205
  initialPlacedRef.current = false;
1234
1206
  reachedMaxHeightRef.current = false;
1235
- initialHeightRef.current = 0;
1236
- maxHeightRef.current = 0;
1237
1207
  clearStyles(positionerElement, originalPositionerStylesRef.current);
1238
1208
  }, [open, alignItemWithTriggerActive, positionerElement, popupRef]);
1239
1209
  useIsoLayoutEffect(() => {
1240
1210
  const popupElement = popupRef.current;
1241
- if (!open || !triggerElement || !positionerElement || !popupElement || store.state.transitionStatus === 'ending') {
1211
+
1212
+ // Wait for Floating UI's first positioning pass before reading DOM geometry.
1213
+ // We replace the final coordinates for aligned selects, but still need middleware
1214
+ // like `size()` to set CSS variables such as `--anchor-width`.
1215
+ if (!open || !triggerElement || !positionerElement || !popupElement || alignItemWithTriggerActive && !isPositioned || store.state.transitionStatus === 'ending') {
1242
1216
  return;
1243
1217
  }
1244
1218
  if (!alignItemWithTriggerActive) {
@@ -1248,114 +1222,113 @@ const SelectPopup = /*#__PURE__*/React.forwardRef(function SelectPopup(component
1248
1222
  return;
1249
1223
  }
1250
1224
 
1251
- // Wait for `selectedItemTextRef.current` to be set.
1252
- queueMicrotask(() => {
1253
- // Ensure we remove any transforms that can affect the location of the popup
1254
- // and therefore the calculations.
1255
- const restoreTransformStyles = unsetTransformStyles(popupElement);
1256
- popupElement.style.removeProperty('--transform-origin');
1257
- try {
1258
- const positionerStyles = getComputedStyle(positionerElement);
1259
- const popupStyles = getComputedStyle(popupElement);
1260
- const doc = ownerDocument(triggerElement);
1261
- const win = getWindow(positionerElement);
1262
- const scale = getScale(triggerElement);
1263
- const triggerRect = normalizeRect(triggerElement.getBoundingClientRect(), scale);
1264
- const positionerRect = normalizeRect(positionerElement.getBoundingClientRect(), scale);
1265
- const triggerX = triggerRect.left;
1266
- const triggerHeight = triggerRect.height;
1267
- const scroller = listElement || popupElement;
1268
- const scrollHeight = scroller.scrollHeight;
1269
- const borderBottom = parseFloat(popupStyles.borderBottomWidth);
1270
- const marginTop = parseFloat(positionerStyles.marginTop) || 10;
1271
- const marginBottom = parseFloat(positionerStyles.marginBottom) || 10;
1272
- const minHeight = parseFloat(positionerStyles.minHeight) || 100;
1273
- const maxPopupHeight = getMaxPopupHeight(popupStyles);
1274
- const paddingLeft = 5;
1275
- const paddingRight = 5;
1276
- const triggerCollisionThreshold = 20;
1277
- const viewportHeight = doc.documentElement.clientHeight - marginTop - marginBottom;
1278
- const viewportWidth = doc.documentElement.clientWidth;
1279
- const availableSpaceBeneathTrigger = viewportHeight - triggerRect.bottom + triggerHeight;
1280
- const textElement = selectedItemTextRef.current;
1281
- const valueElement = valueRef.current;
1282
- let textRect;
1283
- let offsetX = 0;
1284
- let offsetY = 0;
1285
- if (textElement && valueElement) {
1286
- const valueRect = normalizeRect(valueElement.getBoundingClientRect(), scale);
1287
- textRect = normalizeRect(textElement.getBoundingClientRect(), scale);
1288
- const valueLeftFromTriggerLeft = valueRect.left - triggerX;
1289
- const textLeftFromPositionerLeft = textRect.left - positionerRect.left;
1290
- const valueCenterFromPositionerTop = valueRect.top - triggerRect.top + valueRect.height / 2;
1291
- const textCenterFromTriggerTop = textRect.top - positionerRect.top + textRect.height / 2;
1292
- offsetX = valueLeftFromTriggerLeft - textLeftFromPositionerLeft;
1293
- offsetY = textCenterFromTriggerTop - valueCenterFromPositionerTop;
1294
- }
1295
- const idealHeight = availableSpaceBeneathTrigger + offsetY + marginBottom + borderBottom;
1296
- let height = Math.min(viewportHeight, idealHeight);
1297
- const maxHeight = viewportHeight - marginTop - marginBottom;
1298
- const scrollTop = idealHeight - height;
1299
- const left = Math.max(paddingLeft, triggerX + offsetX);
1300
- const maxRight = viewportWidth - paddingRight;
1301
- const rightOverflow = Math.max(0, left + positionerRect.width - maxRight);
1302
- positionerElement.style.left = `${left - rightOverflow}px`;
1303
- positionerElement.style.height = `${height}px`;
1304
- positionerElement.style.maxHeight = 'auto';
1305
- positionerElement.style.marginTop = `${marginTop}px`;
1306
- positionerElement.style.marginBottom = `${marginBottom}px`;
1307
- popupElement.style.height = '100%';
1308
- const maxScrollTop = getMaxScrollTop(scroller);
1309
- const isTopPositioned = scrollTop >= maxScrollTop - SCROLL_EPS_PX;
1310
- if (isTopPositioned) {
1311
- height = Math.min(viewportHeight, positionerRect.height) - (scrollTop - maxScrollTop);
1312
- }
1313
-
1314
- // When the trigger is too close to the top or bottom of the viewport, or the minHeight is
1315
- // reached, we fallback to aligning the popup to the trigger as the UX is poor otherwise.
1316
- const fallbackToAlignPopupToTrigger = triggerRect.top < triggerCollisionThreshold || triggerRect.bottom > viewportHeight - triggerCollisionThreshold || Math.ceil(height) + SCROLL_EPS_PX < Math.min(scrollHeight, minHeight);
1225
+ // Ensure we remove any transforms that can affect the location of the popup
1226
+ // and therefore the calculations.
1227
+ const restoreTransformStyles = unsetTransformStyles(popupElement);
1228
+ popupElement.style.removeProperty('--transform-origin');
1229
+ try {
1230
+ let textElement = selectedItemTextRef.current;
1231
+ if (!textElement?.isConnected) {
1232
+ const hasSelectedValue = selectors.hasSelectedValue(store.state);
1233
+ textElement = !hasSelectedValue && firstItemTextRef.current?.isConnected ? firstItemTextRef.current : null;
1234
+ }
1235
+ const valueElement = valueRef.current;
1236
+ const win = getWindow(positionerElement);
1237
+ const positionerStyles = win.getComputedStyle(positionerElement);
1238
+ const popupStyles = win.getComputedStyle(popupElement);
1239
+ const doc = ownerDocument(triggerElement);
1240
+ const scale = getScale(triggerElement);
1241
+ const triggerRect = normalizeRect(triggerElement.getBoundingClientRect(), scale);
1242
+ const positionerRect = normalizeRect(positionerElement.getBoundingClientRect(), scale);
1243
+ const triggerHeight = triggerRect.height;
1244
+ const scroller = listElement || popupElement;
1245
+ const scrollHeight = scroller.scrollHeight;
1246
+ const borderBottom = parseFloat(popupStyles.borderBottomWidth);
1247
+ // The `|| N` fallbacks cover an unset/`auto` value (parses to `NaN`). Note a literal `0`
1248
+ // also resolves to the fallback, so an explicit `margin: 0` or `min-height: 0` still takes
1249
+ // the default below.
1250
+ const marginTop = parseFloat(positionerStyles.marginTop) || 10;
1251
+ const marginBottom = parseFloat(positionerStyles.marginBottom) || 10;
1252
+ const minHeight = parseFloat(positionerStyles.minHeight) || 100;
1253
+ const maxPopupHeight = getMaxPopupHeight(popupStyles);
1254
+ const paddingLeft = 5;
1255
+ const paddingRight = 5;
1256
+ const triggerCollisionThreshold = 20;
1257
+ const viewportHeight = doc.documentElement.clientHeight - marginTop - marginBottom;
1258
+ const viewportWidth = doc.documentElement.clientWidth;
1259
+ const availableSpaceBeneathTrigger = viewportHeight - triggerRect.bottom + triggerHeight;
1260
+ let textRect;
1261
+ let alignedLeft = direction === 'rtl' ? triggerRect.right - positionerRect.width : triggerRect.left;
1262
+ let offsetY = 0;
1263
+ if (textElement && valueElement) {
1264
+ const valueRect = normalizeRect(valueElement.getBoundingClientRect(), scale);
1265
+ textRect = normalizeRect(textElement.getBoundingClientRect(), scale);
1266
+ alignedLeft = positionerRect.left + (direction === 'rtl' ? valueRect.right - textRect.right : valueRect.left - textRect.left);
1267
+ const valueCenterFromTriggerTop = valueRect.top - triggerRect.top + valueRect.height / 2;
1268
+ const textCenterFromPositionerTop = textRect.top - positionerRect.top + textRect.height / 2;
1269
+ offsetY = textCenterFromPositionerTop - valueCenterFromTriggerTop;
1270
+ }
1271
+ const idealHeight = availableSpaceBeneathTrigger + offsetY + marginBottom + borderBottom;
1272
+ let height = Math.min(viewportHeight, idealHeight);
1273
+ const maxHeight = viewportHeight - marginTop - marginBottom;
1274
+ const scrollTop = idealHeight - height;
1275
+ const maxRight = viewportWidth - paddingRight;
1276
+ positionerElement.style.left = `${clamp(alignedLeft, paddingLeft, maxRight - positionerRect.width)}px`;
1277
+ positionerElement.style.height = `${height}px`;
1278
+ // `none` (not the invalid `auto`) so the explicit height governs in align mode and isn't
1279
+ // clamped by a `max-height` from user CSS.
1280
+ positionerElement.style.maxHeight = 'none';
1281
+ positionerElement.style.marginTop = `${marginTop}px`;
1282
+ positionerElement.style.marginBottom = `${marginBottom}px`;
1283
+ popupElement.style.height = '100%';
1284
+ const maxScrollTop = getMaxScrollTop(scroller);
1285
+ const isTopPositioned = scrollTop >= maxScrollTop - SCROLL_EDGE_TOLERANCE_PX;
1286
+ if (isTopPositioned) {
1287
+ height = Math.min(viewportHeight, positionerRect.height) - (scrollTop - maxScrollTop);
1288
+ }
1317
1289
 
1318
- // Safari doesn't position the popup correctly when pinch-zoomed.
1319
- const isPinchZoomed = (win.visualViewport?.scale ?? 1) !== 1 && isWebKit;
1320
- if (fallbackToAlignPopupToTrigger || isPinchZoomed) {
1321
- initialPlacedRef.current = true;
1322
- clearStyles(positionerElement, originalPositionerStylesRef.current);
1323
- ReactDOM.flushSync(() => setControlledAlignItemWithTrigger(false));
1324
- return;
1325
- }
1326
- if (isTopPositioned) {
1327
- const topOffset = Math.max(0, viewportHeight - idealHeight);
1328
- positionerElement.style.top = positionerRect.height >= maxHeight ? '0' : `${topOffset}px`;
1329
- positionerElement.style.height = `${height}px`;
1330
- scroller.scrollTop = getMaxScrollTop(scroller);
1331
- initialHeightRef.current = Math.max(minHeight, height);
1332
- } else {
1333
- positionerElement.style.bottom = '0';
1334
- initialHeightRef.current = Math.max(minHeight, height);
1335
- scroller.scrollTop = scrollTop;
1336
- }
1337
- if (textRect) {
1338
- const popupTop = positionerRect.top;
1339
- const popupHeight = positionerRect.height;
1340
- const textCenterY = textRect.top + textRect.height / 2;
1341
- const transformOriginY = popupHeight > 0 ? (textCenterY - popupTop) / popupHeight * 100 : 50;
1342
- const clampedY = clamp(transformOriginY, 0, 100);
1343
- popupElement.style.setProperty('--transform-origin', `50% ${clampedY}%`);
1344
- }
1345
- if (initialHeightRef.current === viewportHeight || height >= maxPopupHeight) {
1346
- reachedMaxHeightRef.current = true;
1347
- }
1348
- handleScrollArrowVisibility();
1290
+ // When the trigger is too close to the top or bottom of the viewport, or the minHeight is
1291
+ // reached, we fallback to aligning the popup to the trigger as the UX is poor otherwise.
1292
+ const fallbackToAlignPopupToTrigger = triggerRect.top < triggerCollisionThreshold || triggerRect.bottom > viewportHeight - triggerCollisionThreshold || Math.ceil(height) + SCROLL_EDGE_TOLERANCE_PX < Math.min(scrollHeight, minHeight);
1349
1293
 
1350
- // Avoid the `onScroll` event logic from triggering before the popup is placed.
1351
- setTimeout(() => {
1352
- initialPlacedRef.current = true;
1353
- });
1354
- } finally {
1355
- restoreTransformStyles();
1294
+ // Safari doesn't position the popup correctly when pinch-zoomed.
1295
+ const isPinchZoomed = (win.visualViewport?.scale ?? 1) !== 1 && webkit;
1296
+ if (fallbackToAlignPopupToTrigger || isPinchZoomed) {
1297
+ initialPlacedRef.current = true;
1298
+ clearStyles(positionerElement, originalPositionerStylesRef.current);
1299
+ setControlledAlignItemWithTrigger(false);
1300
+ return;
1356
1301
  }
1357
- });
1358
- }, [store, open, positionerElement, triggerElement, valueRef, selectedItemTextRef, popupRef, handleScrollArrowVisibility, alignItemWithTriggerActive, setControlledAlignItemWithTrigger, scrollArrowFrame, scrollDownArrowRef, scrollUpArrowRef, listElement]);
1302
+ const initialHeight = Math.max(minHeight, height);
1303
+ if (isTopPositioned) {
1304
+ const topOffset = Math.max(0, viewportHeight - idealHeight);
1305
+ positionerElement.style.top = positionerRect.height >= maxHeight ? '0' : `${topOffset}px`;
1306
+ positionerElement.style.height = `${height}px`;
1307
+ scroller.scrollTop = getMaxScrollTop(scroller);
1308
+ } else {
1309
+ positionerElement.style.bottom = '0';
1310
+ scroller.scrollTop = scrollTop;
1311
+ }
1312
+ if (textRect) {
1313
+ const popupTop = positionerRect.top;
1314
+ const popupHeight = positionerRect.height;
1315
+ const textCenterY = textRect.top + textRect.height / 2;
1316
+ const transformOriginY = popupHeight > 0 ? (textCenterY - popupTop) / popupHeight * 100 : 50;
1317
+ const clampedY = clamp(transformOriginY, 0, 100);
1318
+ popupElement.style.setProperty('--transform-origin', `50% ${clampedY}%`);
1319
+ }
1320
+ if (initialHeight === viewportHeight || height >= maxPopupHeight) {
1321
+ reachedMaxHeightRef.current = true;
1322
+ }
1323
+ handleScrollArrowVisibility();
1324
+ if (highlightItemOnHover && store.state.selectedIndex === null && store.state.activeIndex === null && listRef.current[0] != null) {
1325
+ store.set('activeIndex', 0);
1326
+ }
1327
+ initialPlacedRef.current = true;
1328
+ } finally {
1329
+ restoreTransformStyles();
1330
+ }
1331
+ }, [store, open, positionerElement, triggerElement, valueRef, firstItemTextRef, selectedItemTextRef, popupRef, handleScrollArrowVisibility, alignItemWithTriggerActive, setControlledAlignItemWithTrigger, scrollArrowFrame, listElement, listRef, highlightItemOnHover, direction, isPositioned]);
1359
1332
  React.useEffect(() => {
1360
1333
  if (!alignItemWithTriggerActive || !positionerElement || !open) {
1361
1334
  return undefined;
@@ -1364,10 +1337,7 @@ const SelectPopup = /*#__PURE__*/React.forwardRef(function SelectPopup(component
1364
1337
  function handleResize(event) {
1365
1338
  setOpen(false, createChangeEventDetails(windowResize, event));
1366
1339
  }
1367
- win.addEventListener('resize', handleResize);
1368
- return () => {
1369
- win.removeEventListener('resize', handleResize);
1370
- };
1340
+ return addEventListener(win, 'resize', handleResize);
1371
1341
  }, [setOpen, alignItemWithTriggerActive, positionerElement, open]);
1372
1342
  const defaultProps = {
1373
1343
  ...(listElement ? {
@@ -1379,26 +1349,10 @@ const SelectPopup = /*#__PURE__*/React.forwardRef(function SelectPopup(component
1379
1349
  id: `${id}-list`
1380
1350
  }),
1381
1351
  onKeyDown(event) {
1382
- keyboardActiveRef.current = true;
1383
1352
  if (insideToolbar && COMPOSITE_KEYS.has(event.key)) {
1384
1353
  event.stopPropagation();
1385
1354
  }
1386
1355
  },
1387
- onMouseMove() {
1388
- keyboardActiveRef.current = false;
1389
- },
1390
- onPointerLeave(event) {
1391
- if (!highlightItemOnHover || isMouseWithinBounds(event) || event.pointerType === 'touch') {
1392
- return;
1393
- }
1394
- const popup = event.currentTarget;
1395
- highlightTimeout.start(0, () => {
1396
- store.set('activeIndex', null);
1397
- popup.focus({
1398
- preventScroll: true
1399
- });
1400
- });
1401
- },
1402
1356
  onScroll(event) {
1403
1357
  if (listElement) {
1404
1358
  return;
@@ -1424,6 +1378,7 @@ const SelectPopup = /*#__PURE__*/React.forwardRef(function SelectPopup(component
1424
1378
  context: floatingRootContext,
1425
1379
  modal: false,
1426
1380
  disabled: !mounted,
1381
+ openInteractionType: openMethod,
1427
1382
  returnFocus: finalFocus,
1428
1383
  restoreFocus: true,
1429
1384
  children: element
@@ -1436,18 +1391,21 @@ function getMaxPopupHeight(popupStyles) {
1436
1391
  return maxHeightStyle.endsWith('px') ? parseFloat(maxHeightStyle) || Infinity : Infinity;
1437
1392
  }
1438
1393
  function getMaxScrollTop(scroller) {
1439
- return Math.max(0, scroller.scrollHeight - scroller.clientHeight);
1394
+ return getMaxScrollOffset(scroller.scrollHeight, scroller.clientHeight);
1440
1395
  }
1441
1396
  function getScale(element) {
1442
1397
  // The platform API is async-capable, but the DOM platform returns a plain scale object.
1443
1398
  return platform.getScale(element);
1444
1399
  }
1400
+ function normalizeSize(size, axis, scale) {
1401
+ return size / scale[axis];
1402
+ }
1445
1403
  function normalizeRect(rect, scale) {
1446
1404
  return rectToClientRect({
1447
- x: rect.x / scale.x,
1448
- y: rect.y / scale.y,
1449
- width: rect.width / scale.x,
1450
- height: rect.height / scale.y
1405
+ x: normalizeSize(rect.x, 'x', scale),
1406
+ y: normalizeSize(rect.y, 'y', scale),
1407
+ width: normalizeSize(rect.width, 'x', scale),
1408
+ height: normalizeSize(rect.height, 'y', scale)
1451
1409
  });
1452
1410
  }
1453
1411
  const TRANSFORM_STYLE_RESETS = [['transform', 'none'], ['scale', '1'], ['translate', '0 0']];
@@ -1480,8 +1438,9 @@ function unsetTransformStyles(popupElement) {
1480
1438
  */
1481
1439
  const SelectList = /*#__PURE__*/React.forwardRef(function SelectList(componentProps, forwardedRef) {
1482
1440
  const {
1483
- className,
1484
1441
  render,
1442
+ className,
1443
+ style,
1485
1444
  ...elementProps
1486
1445
  } = componentProps;
1487
1446
  const {
@@ -1531,6 +1490,7 @@ const SelectItem = /*#__PURE__*/React.memo(/*#__PURE__*/React.forwardRef(functio
1531
1490
  const {
1532
1491
  render,
1533
1492
  className,
1493
+ style,
1534
1494
  value: itemValue = null,
1535
1495
  label,
1536
1496
  disabled = false,
@@ -1545,25 +1505,25 @@ const SelectItem = /*#__PURE__*/React.memo(/*#__PURE__*/React.forwardRef(functio
1545
1505
  });
1546
1506
  const {
1547
1507
  store,
1548
- getItemProps,
1508
+ itemProps,
1549
1509
  setOpen,
1550
1510
  setValue,
1551
1511
  selectionRef,
1552
1512
  typingRef,
1553
1513
  valuesRef,
1554
- keyboardActiveRef,
1555
1514
  multiple,
1556
- highlightItemOnHover
1515
+ selectedItemTextRef,
1516
+ disabled: selectDisabled,
1517
+ readOnly
1557
1518
  } = useSelectRootContext();
1558
- const highlightTimeout = useTimeout();
1559
1519
  const highlighted = useStore(store, selectors.isActive, listItem.index);
1560
- const selected = useStore(store, selectors.isSelected, listItem.index, itemValue);
1520
+ const open = useStore(store, selectors.open);
1521
+ const selected = useStore(store, selectors.isSelected, itemValue);
1561
1522
  const selectedByFocus = useStore(store, selectors.isSelectedByFocus, listItem.index);
1562
1523
  const isItemEqualToValue = useStore(store, selectors.isItemEqualToValue);
1563
1524
  const index = listItem.index;
1564
1525
  const hasRegistered = index !== -1;
1565
1526
  const itemRef = React.useRef(null);
1566
- const indexRef = useValueAsRef(index);
1567
1527
  useIsoLayoutEffect(() => {
1568
1528
  if (!hasRegistered) {
1569
1529
  return undefined;
@@ -1576,34 +1536,27 @@ const SelectItem = /*#__PURE__*/React.memo(/*#__PURE__*/React.forwardRef(functio
1576
1536
  }, [hasRegistered, index, itemValue, valuesRef]);
1577
1537
  useIsoLayoutEffect(() => {
1578
1538
  if (!hasRegistered) {
1579
- return undefined;
1539
+ return;
1580
1540
  }
1581
1541
  const selectedValue = store.state.value;
1582
1542
  let selectedCandidate = selectedValue;
1583
- if (multiple && Array.isArray(selectedValue) && selectedValue.length > 0) {
1584
- selectedCandidate = selectedValue[selectedValue.length - 1];
1543
+ if (multiple && Array.isArray(selectedValue)) {
1544
+ // Compare against the last selected item, or `undefined` when nothing is selected — never
1545
+ // the raw array, which a custom `isItemEqualToValue` isn't expected to receive.
1546
+ selectedCandidate = selectedValue.length > 0 ? selectedValue[selectedValue.length - 1] : undefined;
1585
1547
  }
1586
1548
  if (selectedCandidate !== undefined && compareItemEquality(itemValue, selectedCandidate, isItemEqualToValue)) {
1587
1549
  store.set('selectedIndex', index);
1550
+ // Make sure SelectPopup can measure the selected item on first open.
1551
+ // SelectItemText can still update this ref later when focus moves.
1552
+ if (textRef.current) {
1553
+ selectedItemTextRef.current = textRef.current;
1554
+ }
1588
1555
  }
1589
- return undefined;
1590
- }, [hasRegistered, index, multiple, isItemEqualToValue, store, itemValue]);
1591
- const state = {
1592
- disabled,
1593
- selected,
1594
- highlighted
1595
- };
1596
- const rootProps = getItemProps({
1597
- active: highlighted,
1598
- selected
1599
- });
1600
- // With our custom `focusItemOnHover` implementation, this interferes with the logic and can
1601
- // cause the index state to be stuck when leaving the select popup.
1602
- rootProps.onFocus = undefined;
1603
- rootProps.id = undefined;
1556
+ }, [hasRegistered, index, multiple, isItemEqualToValue, store, itemValue, selectedItemTextRef]);
1604
1557
  const lastKeyRef = React.useRef(null);
1605
1558
  const pointerTypeRef = React.useRef('mouse');
1606
- const didPointerDownRef = React.useRef(false);
1559
+ const allowMouseSelectionRef = React.useRef(false);
1607
1560
  const {
1608
1561
  getButtonProps,
1609
1562
  buttonRef
@@ -1613,7 +1566,17 @@ const SelectItem = /*#__PURE__*/React.memo(/*#__PURE__*/React.forwardRef(functio
1613
1566
  native: nativeButton,
1614
1567
  composite: true
1615
1568
  });
1569
+ const state = {
1570
+ disabled,
1571
+ selected,
1572
+ highlighted
1573
+ };
1616
1574
  function commitSelection(event) {
1575
+ // A forced-open select (`open`/`defaultOpen`) can still receive item activations even
1576
+ // when the root is disabled or read-only, so guard the commit here too.
1577
+ if (selectDisabled || readOnly) {
1578
+ return;
1579
+ }
1617
1580
  const selectedValue = store.state.value;
1618
1581
  if (multiple) {
1619
1582
  const currentValue = Array.isArray(selectedValue) ? selectedValue : [];
@@ -1624,39 +1587,13 @@ const SelectItem = /*#__PURE__*/React.memo(/*#__PURE__*/React.forwardRef(functio
1624
1587
  setOpen(false, createChangeEventDetails(itemPress, event));
1625
1588
  }
1626
1589
  }
1590
+ function resetDragMovement() {
1591
+ selectionRef.current.dragY = 0;
1592
+ }
1627
1593
  const defaultProps = {
1628
1594
  role: 'option',
1629
1595
  'aria-selected': selected,
1630
- tabIndex: highlighted ? 0 : -1,
1631
- onFocus() {
1632
- store.set('activeIndex', index);
1633
- },
1634
- onMouseEnter() {
1635
- if (!keyboardActiveRef.current && store.state.selectedIndex === null && highlightItemOnHover) {
1636
- store.set('activeIndex', index);
1637
- }
1638
- },
1639
- onMouseMove() {
1640
- if (highlightItemOnHover) {
1641
- store.set('activeIndex', index);
1642
- }
1643
- },
1644
- onMouseLeave(event) {
1645
- if (!highlightItemOnHover || keyboardActiveRef.current || isMouseWithinBounds(event)) {
1646
- return;
1647
- }
1648
- highlightTimeout.start(0, () => {
1649
- if (store.state.activeIndex === index) {
1650
- store.set('activeIndex', null);
1651
- }
1652
- });
1653
- },
1654
- onTouchStart() {
1655
- selectionRef.current = {
1656
- allowSelectedMouseUp: false,
1657
- allowUnselectedMouseUp: false
1658
- };
1659
- },
1596
+ tabIndex: open && highlighted ? 0 : -1,
1660
1597
  onKeyDown(event) {
1661
1598
  lastKeyRef.current = event.key;
1662
1599
  store.set('activeIndex', index);
@@ -1665,13 +1602,24 @@ const SelectItem = /*#__PURE__*/React.memo(/*#__PURE__*/React.forwardRef(functio
1665
1602
  }
1666
1603
  },
1667
1604
  onClick(event) {
1668
- didPointerDownRef.current = false;
1605
+ const isMouseClick = event.type === 'click' && pointerTypeRef.current !== 'touch';
1606
+ const clickPointerType = event.nativeEvent.pointerType;
1607
+ const isVirtualMouseClick = isMouseClick && isVirtualClick(event.nativeEvent) && (
1608
+ // Generic no-pointer `detail === 0` clicks stay tied to highlight state. Virtual
1609
+ // clicks that carry browser pointer data, including an empty string from assistive
1610
+ // technology, can activate unhighlighted items.
1611
+ clickPointerType !== undefined || highlighted);
1612
+ // With alignItemWithTrigger, opening can place an item under the cursor. Real mouse
1613
+ // clicks must start on the item, while virtual clicks represent explicit keyboard or
1614
+ // assistive technology activation.
1615
+ const isInvalidMouseClick = isMouseClick && !isVirtualMouseClick && !allowMouseSelectionRef.current;
1616
+ allowMouseSelectionRef.current = false;
1669
1617
 
1670
1618
  // Prevent double commit on {Enter}
1671
1619
  if (event.type === 'keydown' && lastKeyRef.current === null) {
1672
1620
  return;
1673
1621
  }
1674
- if (disabled || event.type === 'keydown' && lastKeyRef.current === ' ' && typingRef.current || pointerTypeRef.current !== 'touch' && !highlighted) {
1622
+ if (disabled || event.type === 'keydown' && lastKeyRef.current === ' ' && typingRef.current || isInvalidMouseClick) {
1675
1623
  return;
1676
1624
  }
1677
1625
  lastKeyRef.current = null;
@@ -1680,40 +1628,52 @@ const SelectItem = /*#__PURE__*/React.memo(/*#__PURE__*/React.forwardRef(functio
1680
1628
  onPointerEnter(event) {
1681
1629
  pointerTypeRef.current = event.pointerType;
1682
1630
  },
1631
+ onPointerMove(event) {
1632
+ if (event.pointerType === 'mouse' && event.buttons === 1) {
1633
+ const selection = selectionRef.current;
1634
+ selection.dragY += event.movementY;
1635
+ if (selection.dragY ** 2 >= 64) {
1636
+ selection.allowUnselectedMouseUp = true;
1637
+ }
1638
+ }
1639
+ },
1683
1640
  onPointerDown(event) {
1684
1641
  pointerTypeRef.current = event.pointerType;
1685
- didPointerDownRef.current = true;
1642
+ allowMouseSelectionRef.current = true;
1643
+ resetDragMovement();
1686
1644
  },
1687
1645
  onMouseUp() {
1688
- if (disabled) {
1646
+ resetDragMovement();
1647
+ if (disabled || pointerTypeRef.current === 'touch') {
1689
1648
  return;
1690
1649
  }
1691
1650
 
1692
- // Regular click (pointerdown on this element) if didPointerDownRef is set, otherwise drag-to-select
1693
- if (didPointerDownRef.current) {
1694
- didPointerDownRef.current = false;
1651
+ // Regular clicks are committed by the click event.
1652
+ if (allowMouseSelectionRef.current) {
1695
1653
  return;
1696
1654
  }
1697
1655
  const disallowSelectedMouseUp = !selectionRef.current.allowSelectedMouseUp && selected;
1698
1656
  const disallowUnselectedMouseUp = !selectionRef.current.allowUnselectedMouseUp && !selected;
1699
- if (disallowSelectedMouseUp || disallowUnselectedMouseUp || pointerTypeRef.current !== 'touch' && !highlighted) {
1657
+ if (disallowSelectedMouseUp || disallowUnselectedMouseUp) {
1700
1658
  return;
1701
1659
  }
1660
+ allowMouseSelectionRef.current = true;
1702
1661
  itemRef.current?.click();
1662
+ allowMouseSelectionRef.current = false;
1703
1663
  }
1704
1664
  };
1705
1665
  const element = useRenderElement('div', componentProps, {
1706
1666
  ref: [buttonRef, forwardedRef, listItem.ref, itemRef],
1707
1667
  state,
1708
- props: [rootProps, defaultProps, elementProps, getButtonProps]
1668
+ props: [itemProps, defaultProps, elementProps, getButtonProps]
1709
1669
  });
1710
1670
  const contextValue = React.useMemo(() => ({
1711
1671
  selected,
1712
- indexRef,
1672
+ index,
1713
1673
  textRef,
1714
1674
  selectedByFocus,
1715
1675
  hasRegistered
1716
- }), [selected, indexRef, textRef, selectedByFocus, hasRegistered]);
1676
+ }), [selected, index, textRef, selectedByFocus, hasRegistered]);
1717
1677
  return /*#__PURE__*/jsx(SelectItemContext.Provider, {
1718
1678
  value: contextValue,
1719
1679
  children: element
@@ -1738,13 +1698,13 @@ const SelectItemIndicator = /*#__PURE__*/React.forwardRef(function SelectItemInd
1738
1698
  });
1739
1699
  });
1740
1700
 
1741
- /** The core implementation of the indicator is split here to avoid paying the hooks
1742
- * costs unless the element needs to be mounted. */
1701
+ // Split the core implementation to avoid paying the hook costs unless the element needs to mount.
1743
1702
  if (process.env.NODE_ENV !== "production") SelectItemIndicator.displayName = "SelectItemIndicator";
1744
1703
  const Inner = /*#__PURE__*/React.memo(/*#__PURE__*/React.forwardRef((componentProps, forwardedRef) => {
1745
1704
  const {
1746
1705
  render,
1747
1706
  className,
1707
+ style,
1748
1708
  keepMounted,
1749
1709
  ...elementProps
1750
1710
  } = componentProps;
@@ -1790,28 +1750,32 @@ if (process.env.NODE_ENV !== "production") Inner.displayName = "Inner";
1790
1750
  */
1791
1751
  const SelectItemText = /*#__PURE__*/React.memo(/*#__PURE__*/React.forwardRef(function SelectItemText(componentProps, forwardedRef) {
1792
1752
  const {
1793
- indexRef,
1753
+ index,
1794
1754
  textRef,
1795
1755
  selectedByFocus,
1796
1756
  hasRegistered
1797
1757
  } = useSelectItemContext();
1798
1758
  const {
1759
+ firstItemTextRef,
1799
1760
  selectedItemTextRef
1800
1761
  } = useSelectRootContext();
1801
1762
  const {
1802
- className,
1803
1763
  render,
1764
+ className,
1765
+ style,
1804
1766
  ...elementProps
1805
1767
  } = componentProps;
1806
1768
  const localRef = React.useCallback(node => {
1807
- if (!node || !hasRegistered) {
1769
+ if (!node) {
1808
1770
  return;
1809
1771
  }
1810
- const hasNoSelectedItemText = selectedItemTextRef.current === null || !selectedItemTextRef.current.isConnected;
1811
- if (selectedByFocus || hasNoSelectedItemText && indexRef.current === 0) {
1772
+ if (hasRegistered && index === 0) {
1773
+ firstItemTextRef.current = node;
1774
+ }
1775
+ if (hasRegistered && selectedByFocus) {
1812
1776
  selectedItemTextRef.current = node;
1813
1777
  }
1814
- }, [selectedItemTextRef, indexRef, selectedByFocus, hasRegistered]);
1778
+ }, [firstItemTextRef, selectedItemTextRef, index, selectedByFocus, hasRegistered]);
1815
1779
  const element = useRenderElement('div', componentProps, {
1816
1780
  ref: [localRef, forwardedRef, textRef],
1817
1781
  props: elementProps
@@ -1832,8 +1796,9 @@ function useSelectGroupContext() {
1832
1796
 
1833
1797
  const SelectGroup = /*#__PURE__*/React.forwardRef(function SelectGroup(componentProps, forwardedRef) {
1834
1798
  const {
1835
- className,
1836
1799
  render,
1800
+ className,
1801
+ style,
1837
1802
  ...elementProps
1838
1803
  } = componentProps;
1839
1804
  const [labelId, setLabelId] = React.useState();
@@ -1863,8 +1828,9 @@ if (process.env.NODE_ENV !== "production") SelectGroup.displayName = "SelectGrou
1863
1828
  */
1864
1829
  const SelectGroupLabel = /*#__PURE__*/React.forwardRef(function SelectGroupLabel(componentProps, forwardedRef) {
1865
1830
  const {
1866
- className,
1867
1831
  render,
1832
+ className,
1833
+ style,
1868
1834
  id: idProp,
1869
1835
  ...elementProps
1870
1836
  } = componentProps;
@@ -1885,4 +1851,4 @@ const SelectGroupLabel = /*#__PURE__*/React.forwardRef(function SelectGroupLabel
1885
1851
  });
1886
1852
  if (process.env.NODE_ENV !== "production") SelectGroupLabel.displayName = "SelectGroupLabel";
1887
1853
 
1888
- export { SelectRoot as S, SelectPortal as a, SelectTrigger as b, SelectValue as c, SelectPositioner as d, SelectPopup as e, SelectList as f, SelectItem as g, SelectItemText as h, SelectItemIndicator as i, SelectGroup as j, SelectGroupLabel as k, useSelectPositionerContext as l, selectors as s, useSelectRootContext as u };
1854
+ export { SelectRoot as S, SelectPortal as a, SelectTrigger as b, SelectValue as c, SelectPositioner as d, SelectPopup as e, SelectList as f, SelectItem as g, SelectItemText as h, SelectItemIndicator as i, SelectGroup as j, SelectGroupLabel as k, useSelectRootContext as l, useSelectPositionerContext as m, selectors as s, usePreviousValue as u };