@tamagui/select 3.0.0-beta.924.1 → 3.0.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 (106) hide show
  1. package/dist/cjs/Select.cjs +23 -25
  2. package/dist/cjs/Select.native.cjs +27 -26
  3. package/dist/cjs/Select.native.js +27 -26
  4. package/dist/cjs/Select.native.js.map +1 -1
  5. package/dist/cjs/SelectImpl.cjs +67 -63
  6. package/dist/cjs/SelectItem.cjs +49 -60
  7. package/dist/cjs/SelectItem.native.cjs +51 -79
  8. package/dist/cjs/SelectItem.native.js +51 -79
  9. package/dist/cjs/SelectItem.native.js.map +1 -1
  10. package/dist/cjs/SelectScrollButton.cjs +9 -25
  11. package/dist/cjs/SelectTrigger.cjs +22 -5
  12. package/dist/cjs/SelectTrigger.native.cjs +15 -5
  13. package/dist/cjs/SelectTrigger.native.js +15 -5
  14. package/dist/cjs/SelectTrigger.native.js.map +1 -1
  15. package/dist/cjs/SelectViewport.cjs +29 -32
  16. package/dist/cjs/SelectViewport.native.cjs +2 -2
  17. package/dist/cjs/SelectViewport.native.js +2 -2
  18. package/dist/cjs/SelectViewport.native.js.map +1 -1
  19. package/dist/cjs/selectionController.cjs +15 -5
  20. package/dist/cjs/selectionController.native.cjs +20 -15
  21. package/dist/cjs/selectionController.native.js +20 -15
  22. package/dist/cjs/selectionController.native.js.map +1 -1
  23. package/dist/esm/Select.mjs +27 -28
  24. package/dist/esm/Select.mjs.map +1 -1
  25. package/dist/esm/Select.native.js +31 -29
  26. package/dist/esm/Select.native.js.map +1 -1
  27. package/dist/esm/SelectImpl.mjs +68 -64
  28. package/dist/esm/SelectImpl.mjs.map +1 -1
  29. package/dist/esm/SelectItem.mjs +50 -61
  30. package/dist/esm/SelectItem.mjs.map +1 -1
  31. package/dist/esm/SelectItem.native.js +52 -80
  32. package/dist/esm/SelectItem.native.js.map +1 -1
  33. package/dist/esm/SelectScrollButton.mjs +10 -26
  34. package/dist/esm/SelectScrollButton.mjs.map +1 -1
  35. package/dist/esm/SelectTrigger.mjs +16 -5
  36. package/dist/esm/SelectTrigger.mjs.map +1 -1
  37. package/dist/esm/SelectTrigger.native.js +15 -4
  38. package/dist/esm/SelectTrigger.native.js.map +1 -1
  39. package/dist/esm/SelectViewport.mjs +30 -33
  40. package/dist/esm/SelectViewport.mjs.map +1 -1
  41. package/dist/esm/SelectViewport.native.js +3 -3
  42. package/dist/esm/SelectViewport.native.js.map +1 -1
  43. package/dist/esm/selectionController.mjs +15 -5
  44. package/dist/esm/selectionController.mjs.map +1 -1
  45. package/dist/esm/selectionController.native.js +20 -15
  46. package/dist/esm/selectionController.native.js.map +1 -1
  47. package/dist/jsx/Select.mjs +27 -28
  48. package/dist/jsx/Select.mjs.map +1 -1
  49. package/dist/jsx/Select.native.cjs +27 -26
  50. package/dist/jsx/Select.native.js +27 -26
  51. package/dist/jsx/Select.native.js.map +1 -1
  52. package/dist/jsx/SelectImpl.mjs +68 -64
  53. package/dist/jsx/SelectImpl.mjs.map +1 -1
  54. package/dist/jsx/SelectItem.mjs +50 -61
  55. package/dist/jsx/SelectItem.mjs.map +1 -1
  56. package/dist/jsx/SelectItem.native.cjs +51 -79
  57. package/dist/jsx/SelectItem.native.js +51 -79
  58. package/dist/jsx/SelectItem.native.js.map +1 -1
  59. package/dist/jsx/SelectScrollButton.mjs +10 -26
  60. package/dist/jsx/SelectScrollButton.mjs.map +1 -1
  61. package/dist/jsx/SelectTrigger.mjs +16 -5
  62. package/dist/jsx/SelectTrigger.mjs.map +1 -1
  63. package/dist/jsx/SelectTrigger.native.cjs +15 -5
  64. package/dist/jsx/SelectTrigger.native.js +15 -5
  65. package/dist/jsx/SelectTrigger.native.js.map +1 -1
  66. package/dist/jsx/SelectViewport.mjs +30 -33
  67. package/dist/jsx/SelectViewport.mjs.map +1 -1
  68. package/dist/jsx/SelectViewport.native.cjs +2 -2
  69. package/dist/jsx/SelectViewport.native.js +2 -2
  70. package/dist/jsx/SelectViewport.native.js.map +1 -1
  71. package/dist/jsx/selectionController.mjs +15 -5
  72. package/dist/jsx/selectionController.mjs.map +1 -1
  73. package/dist/jsx/selectionController.native.cjs +20 -15
  74. package/dist/jsx/selectionController.native.js +20 -15
  75. package/dist/jsx/selectionController.native.js.map +1 -1
  76. package/package.json +30 -29
  77. package/src/Select.tsx +41 -41
  78. package/src/SelectImpl.tsx +107 -95
  79. package/src/SelectItem.tsx +66 -91
  80. package/src/SelectScrollButton.tsx +14 -32
  81. package/src/SelectTrigger.tsx +19 -2
  82. package/src/SelectViewport.native.tsx +4 -4
  83. package/src/SelectViewport.tsx +34 -30
  84. package/src/selectionController.ts +20 -5
  85. package/src/types.tsx +26 -22
  86. package/types/Select.d.ts +42 -81
  87. package/types/Select.d.ts.map +1 -1
  88. package/types/SelectImpl.d.ts.map +1 -1
  89. package/types/SelectItem.d.ts +3 -5
  90. package/types/SelectItem.d.ts.map +1 -1
  91. package/types/SelectItemText.d.ts +3 -5
  92. package/types/SelectItemText.d.ts.map +1 -1
  93. package/types/SelectScrollButton.d.ts +5 -9
  94. package/types/SelectScrollButton.d.ts.map +1 -1
  95. package/types/SelectScrollButton.native.d.ts +5 -9
  96. package/types/SelectScrollButton.native.d.ts.map +1 -1
  97. package/types/SelectTrigger.d.ts +3 -5
  98. package/types/SelectTrigger.d.ts.map +1 -1
  99. package/types/SelectViewport.d.ts +4 -9
  100. package/types/SelectViewport.d.ts.map +1 -1
  101. package/types/SelectViewport.native.d.ts +4 -9
  102. package/types/SelectViewport.native.d.ts.map +1 -1
  103. package/types/selectionController.d.ts +2 -0
  104. package/types/selectionController.d.ts.map +1 -1
  105. package/types/types.d.ts +30 -22
  106. package/types/types.d.ts.map +1 -1
@@ -1,12 +1,10 @@
1
1
  import {
2
- autoUpdate,
3
2
  inner,
4
3
  offset,
5
4
  size,
6
5
  useClick,
7
6
  useFloatingRaw as useFloatingDom,
8
7
  useInteractions,
9
- useInnerOffset,
10
8
  useListNavigation,
11
9
  useRole,
12
10
  useTypeahead,
@@ -17,7 +15,6 @@ import { useIsomorphicLayoutEffect } from '@tamagui/constants'
17
15
  import { createChangeEventDetails, useEvent, useIsTouchDevice } from '@tamagui/core'
18
16
  import { composeEventHandlers } from '@tamagui/helpers'
19
17
  import * as React from 'react'
20
- import { flushSync } from 'react-dom'
21
18
  import { SCROLL_ARROW_THRESHOLD } from './constants'
22
19
  import {
23
20
  SelectItemParentProvider,
@@ -38,9 +35,8 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
38
35
  const selectItemParentContext = useSelectItemParentContext(scope)
39
36
  const { setActiveIndex, selectedIndex, activeIndexRef } = selectContext
40
37
 
41
- const { requestOpenChange, registry } = selectItemParentContext
38
+ const { requestOpenChange, registry, lastPointerRef } = selectItemParentContext
42
39
 
43
- const [scrollTop, setScrollTop] = React.useState(0)
44
40
  const touch = useIsTouchDevice()
45
41
 
46
42
  const listItemsRef = selectItemParentContext.listRef!
@@ -58,14 +54,12 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
58
54
 
59
55
  const [controlledScrolling, setControlledScrolling] = React.useState(false)
60
56
  const [fallback, setFallback] = React.useState(false)
61
- const [innerOffset, setInnerOffset] = React.useState(0)
62
57
  const [blockSelection, setBlockSelection] = React.useState(false)
63
- const floatingStyle = React.useRef({})
64
58
 
65
- // sync activeIndex on open/close
66
59
  React.useEffect(() => {
67
60
  if (!open) {
68
- setScrollTop(0)
61
+ setCanScrollUp(false)
62
+ setCanScrollDown(false)
69
63
  setFallback(false)
70
64
  setControlledScrolling(false)
71
65
  }
@@ -100,58 +94,62 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
100
94
  } = useFloatingDom({
101
95
  open,
102
96
  placement: 'bottom-start',
103
- whileElementsMounted: autoUpdate,
104
97
  // The removed alternate stack was the logic from floating-ui,
105
98
  // but it causes issues (open, drag select, close, then re-open its not positioned "over")
106
99
  // https://github.com/floating-ui/floating-ui/blob/master/packages/react/test/visual/components/MacSelect.tsx
107
100
  middleware: [
108
101
  size({
109
- apply({
110
- rects: {
111
- reference: { width },
112
- },
113
- }) {
114
- Object.assign(floatingStyle.current, {
115
- minWidth: width + 8,
116
- })
117
- if (refs.floating.current) {
118
- Object.assign(refs.floating.current.style, floatingStyle.current)
119
- }
102
+ apply({ rects, elements }) {
103
+ // the list overhangs the trigger by 4px a side (with the -4 cross offset)
104
+ elements.floating.style.minWidth = `${rects.reference.width + 8}px`
120
105
  },
121
106
  }),
122
107
  inner({
123
108
  listRef: listItemsRef,
124
109
  overflowRef,
125
110
  index: selectedIndex,
126
- offset: innerOffset,
127
111
  onFallbackChange: setFallback,
128
- padding: 10,
112
+ // arrows sit outside the scrollport, with room reserved at both screen edges
113
+ get padding() {
114
+ return (
115
+ 10 +
116
+ Math.max(
117
+ upArrowRef.current?.offsetHeight ?? 0,
118
+ downArrowRef.current?.offsetHeight ?? 0
119
+ )
120
+ )
121
+ },
129
122
  minItemsVisible: touch ? 10 : 4,
130
123
  referenceOverflowThreshold: 20,
131
124
  }),
132
- offset({ crossAxis: -5 }),
125
+ offset({ crossAxis: -4 }),
133
126
  ],
134
127
  } as any)
135
128
 
136
- const floatingRef = refs.floating
129
+ // the arrows are booleans that flip at a threshold, never the scroll offset
130
+ // itself, so a scroll event costs no render unless an arrow appears or goes
131
+ const [canScrollUp, setCanScrollUp] = React.useState(false)
132
+ const [canScrollDown, setCanScrollDown] = React.useState(false)
133
+ const updateScrollArrows = React.useCallback(() => {
134
+ const el = refs.floating.current
135
+ if (!el) return
136
+ setCanScrollUp(el.scrollTop > SCROLL_ARROW_THRESHOLD)
137
+ setCanScrollDown(
138
+ el.scrollTop < el.scrollHeight - el.clientHeight - SCROLL_ARROW_THRESHOLD
139
+ )
140
+ }, [refs])
137
141
 
138
- const showUpArrow = open && scrollTop > SCROLL_ARROW_THRESHOLD
139
- const showDownArrow =
140
- open &&
141
- floatingRef.current &&
142
- scrollTop <
143
- floatingRef.current.scrollHeight -
144
- floatingRef.current.clientHeight -
145
- SCROLL_ARROW_THRESHOLD
146
-
147
- const isScrollable = showDownArrow || showUpArrow
142
+ const showUpArrow = open && canScrollUp
143
+ const showDownArrow = open && canScrollDown
148
144
 
145
+ // the list is placed once per open, not kept in sync: the inner middleware
146
+ // scrolls the list to align the selected item, so recomputing on a page
147
+ // scroll would yank the list out from under the user's own scroll. the list
148
+ // is absolutely positioned, so it rides along with page scroll on its own.
149
149
  useIsomorphicLayoutEffect(() => {
150
- if (typeof window === 'undefined') return
150
+ if (!open) return
151
+ update()
151
152
  window.addEventListener('resize', update)
152
- if (open) {
153
- update()
154
- }
155
153
  return () => window.removeEventListener('resize', update)
156
154
  }, [update, open])
157
155
 
@@ -165,47 +163,51 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
165
163
  } as SelectActiveChangeDetails)
166
164
  })
167
165
 
168
- // construct interaction context for our custom hooks
166
+ // the interaction context for the floating hooks. every hook memoizes its
167
+ // props on this object, and those props sit in the item context, so a new
168
+ // identity here re-renders every item
169
169
  const dataRef = React.useRef<{ openEvent?: Event; placement?: string }>({})
170
170
  dataRef.current.placement = computedPlacement
171
- const interactionContext: FloatingInteractionContext = {
172
- open,
173
- onOpenChange: (nextOpen, event, reason) => {
174
- requestOpenChange(
175
- nextOpen,
176
- createChangeEventDetails(
177
- reason === 'list-navigation' ? 'keyboard' : 'trigger-press',
178
- event,
179
- refs.reference.current
180
- ) as SelectOpenChangeDetails
181
- )
182
- },
183
- refs: {
184
- reference: refs.reference as any,
185
- floating: refs.floating,
186
- domReference: refs.reference as any,
187
- },
188
- elements: {
189
- reference: (refs.reference?.current as Element) || null,
190
- floating: refs.floating?.current || null,
191
- domReference: (refs.reference?.current as Element) || null,
192
- },
193
- dataRef,
194
- }
171
+ const onOpenChange = useEvent((nextOpen: boolean, event?: Event, reason?: string) => {
172
+ requestOpenChange(
173
+ nextOpen,
174
+ createChangeEventDetails(
175
+ reason === 'list-navigation' ? 'keyboard' : 'trigger-press',
176
+ event,
177
+ refs.reference.current
178
+ ) as SelectOpenChangeDetails
179
+ )
180
+ })
181
+ const referenceElement = (refs.reference.current as Element | null) || null
182
+ const floatingElement = refs.floating.current || null
183
+ const interactionContext: FloatingInteractionContext = React.useMemo(
184
+ () => ({
185
+ open,
186
+ onOpenChange,
187
+ refs: {
188
+ reference: refs.reference as any,
189
+ floating: refs.floating,
190
+ domReference: refs.reference as any,
191
+ },
192
+ elements: {
193
+ reference: referenceElement,
194
+ floating: floatingElement,
195
+ domReference: referenceElement,
196
+ },
197
+ dataRef,
198
+ }),
199
+ [open, onOpenChange, refs, referenceElement, floatingElement]
200
+ )
195
201
 
196
202
  const interactionsProps = [
197
203
  useClick(interactionContext, { event: 'mousedown', keyboardHandlers: false }),
198
204
  // useDismiss removed - already handled by Dismissable in SelectContent
199
205
  useRole(interactionContext, { role: 'listbox' }),
200
- useInnerOffset(interactionContext, {
201
- enabled: !fallback && isScrollable,
202
- onChange: setInnerOffset,
203
- overflowRef,
204
- scrollRef: refs.floating,
205
- }),
206
206
  useListNavigation(interactionContext, {
207
207
  listRef: listItemsRef,
208
- activeIndex: selectContext.activeIndex ?? 0,
208
+ // items focus themselves from the active-index emitter, and the hook's
209
+ // own index syncs from item focus, so it never needs the value
210
+ activeIndex: null,
209
211
  selectedIndex,
210
212
  onNavigate: (index) => {
211
213
  if (index !== null) {
@@ -219,12 +221,15 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
219
221
  },
220
222
  disabledIndices: registry.getDisabledIndices(),
221
223
  scrollItemIntoView: false,
224
+ // items own hover: the hook's leave handler would focus the viewport when
225
+ // a scroll moves the list under a still pointer, losing the keyboard position
226
+ focusItemOnHover: false,
222
227
  }),
223
228
  useTypeahead(interactionContext, {
224
229
  listRef: listContentRef,
225
230
  onMatch,
226
231
  selectedIndex,
227
- activeIndex: selectContext.activeIndex,
232
+ activeIndex: null,
228
233
  onTypingChange: (e) => {
229
234
  state.current.isTyping = e
230
235
  },
@@ -237,12 +242,23 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
237
242
  }, interactionsProps)
238
243
  )
239
244
 
245
+ // the trigger and viewport getters. items get getItemProps alone, which is
246
+ // stable once the list mounts, so the floating element mounting (a new
247
+ // identity here) never re-renders the list
240
248
  const interactionsContext = React.useMemo(() => {
241
249
  return {
242
- ...interactions,
243
250
  getReferenceProps(props: Record<string, any> = {}) {
244
251
  return interactions.getReferenceProps({
245
252
  ...props,
253
+ // the press that opens is the pointer's last real position: WebKit
254
+ // replays a mousemove there whenever the list scrolls under it (say a
255
+ // keyboard move scrolling the next item into view), and that replay
256
+ // must not read as a hover
257
+ onMouseDown: composeEventHandlers(props.onMouseDown, (event: any) => {
258
+ const last = lastPointerRef.current
259
+ last.x = event.clientX
260
+ last.y = event.clientY
261
+ }),
246
262
  onKeyDown: composeEventHandlers(props.onKeyDown, (event: any) => {
247
263
  latestKeyboardEventRef.current = event.nativeEvent || event
248
264
  if (
@@ -266,17 +282,7 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
266
282
  getFloatingProps(props) {
267
283
  return interactions.getFloatingProps({
268
284
  ref: refs.floating,
269
- className: 'Select',
270
285
  ...props,
271
- style: {
272
- position: strategy,
273
- top: y ?? '',
274
- left: x ?? '',
275
- outline: 0,
276
- scrollbarWidth: 'none',
277
- ...floatingStyle.current,
278
- ...props?.style,
279
- },
280
286
  onPointerEnter() {
281
287
  setControlledScrolling(false)
282
288
  state.current.isMouseOutside = false
@@ -295,15 +301,16 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
295
301
  onContextMenu(e) {
296
302
  e.preventDefault()
297
303
  },
298
- onScroll(event) {
299
- flushSync(() => {
300
- setScrollTop(event.currentTarget.scrollTop)
301
- })
302
- },
304
+ onScroll: updateScrollArrows,
303
305
  })
304
306
  },
305
307
  }
306
- }, [refs.reference.current, x, y, refs.floating.current, interactions])
308
+ }, [refs, interactions, requestOpenChange, updateScrollArrows])
309
+
310
+ const floatingPosition = React.useMemo(
311
+ () => ({ position: strategy, top: y ?? '', left: x ?? '' }),
312
+ [strategy, x, y]
313
+ )
307
314
 
308
315
  // effects
309
316
 
@@ -322,7 +329,6 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
322
329
  }
323
330
  allowSelectRef.current = false
324
331
  allowMouseUpRef.current = true
325
- setInnerOffset(0)
326
332
  setFallback(false)
327
333
  setBlockSelection(false)
328
334
  }, [open])
@@ -341,13 +347,18 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
341
347
  if (controlledScrolling && index != null) {
342
348
  listItemsRef.current[index]?.scrollIntoView({ block: 'nearest' })
343
349
  }
344
- setScrollTop(refs.floating.current?.scrollTop ?? 0)
350
+ updateScrollArrows()
345
351
  }
346
352
 
347
353
  scrollActiveIntoView(activeIndexRef.current)
348
354
 
349
355
  return selectItemParentContext.activeIndexSubscribe(scrollActiveIntoView)
350
- }, [open, refs, controlledScrolling, selectItemParentContext.activeIndexSubscribe])
356
+ }, [
357
+ open,
358
+ controlledScrolling,
359
+ selectItemParentContext.activeIndexSubscribe,
360
+ updateScrollArrows,
361
+ ])
351
362
 
352
363
  React.useEffect(() => {
353
364
  if (open && fallback) {
@@ -376,10 +387,11 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
376
387
  <SelectProvider
377
388
  scope={scope}
378
389
  {...(selectContext as Required<typeof selectContext>)}
379
- setScrollTop={setScrollTop}
380
- setInnerOffset={setInnerOffset}
390
+ updateScrollArrows={updateScrollArrows}
381
391
  fallback={fallback}
382
392
  floatingContext={floatingContext as any}
393
+ interactions={interactionsContext}
394
+ floatingPosition={floatingPosition}
383
395
  canScrollDown={!!showDownArrow}
384
396
  canScrollUp={!!showUpArrow}
385
397
  controlledScrolling={controlledScrolling}
@@ -394,7 +406,7 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
394
406
  allowMouseUpRef={allowMouseUpRef}
395
407
  allowSelectRef={allowSelectRef}
396
408
  dataRef={dataRef as any}
397
- interactions={interactionsContext}
409
+ getItemProps={interactions.getItemProps}
398
410
  listRef={listItemsRef}
399
411
  selectTimeoutRef={selectTimeoutRef}
400
412
  >
@@ -11,7 +11,7 @@ import {
11
11
  import type { GetProps } from '@tamagui/core'
12
12
  import { composeEventHandlers } from '@tamagui/helpers'
13
13
  import * as React from 'react'
14
- import { useSelectContext, useSelectItemParentContext } from './context'
14
+ import { useSelectItemParentContext } from './context'
15
15
  import { getSelectOptionProps } from './selectionController'
16
16
  import type {
17
17
  SelectActiveChangeDetails,
@@ -67,12 +67,12 @@ export const SelectItem = createStyledHOC(
67
67
  } = props
68
68
  const disabled = disabledProp ?? ariaDisabled === true
69
69
 
70
+ // items read only the item parent context: the select context carries
71
+ // viewport state (position, scroll arrows) that must not re-render the list
70
72
  const context = useSelectItemParentContext(scope)
71
- const selectContext = useSelectContext(scope)
72
- const isAdapted = useAdaptIsActive(selectContext.adaptScope)
73
+ const isAdapted = useAdaptIsActive(context.adaptScope)
73
74
 
74
75
  const {
75
- listRef,
76
76
  registry,
77
77
  mode,
78
78
  selectedValues,
@@ -82,9 +82,10 @@ export const SelectItem = createStyledHOC(
82
82
  allowMouseUpRef,
83
83
  allowSelectRef,
84
84
  selectTimeoutRef,
85
- interactions,
85
+ getItemProps,
86
86
  shouldRenderWebNative,
87
- setActiveIndexFast,
87
+ setActiveIndex,
88
+ lastPointerRef,
88
89
  moveActive,
89
90
  search,
90
91
  } = context
@@ -92,7 +93,6 @@ export const SelectItem = createStyledHOC(
92
93
  const isSelected = selectedValues.includes(value)
93
94
  const pendingMouseUpSelectionRef = React.useRef(false)
94
95
  const itemNodeRef = React.useRef<any>(null)
95
- const [, rerenderRegistry] = React.useReducer((version) => version + 1, 0)
96
96
  const registrationRef = React.useRef<ReturnType<typeof registry.registerItem> | null>(
97
97
  null
98
98
  )
@@ -102,38 +102,13 @@ export const SelectItem = createStyledHOC(
102
102
  textValue: textValueProp,
103
103
  })
104
104
 
105
- useIsomorphicLayoutEffect(
106
- () => registry.subscribe(() => rerenderRegistry()),
107
- [registry]
108
- )
109
-
105
+ // the item never subscribes to the registry: its index is read at event
106
+ // time, and the owner mirrors the registry's nodes into the list ref
110
107
  useIsomorphicLayoutEffect(() => {
111
108
  const registration = registry.registerItem(initialRegistration.current)
112
109
  registrationRef.current = registration
113
- const registeredNode = itemNodeRef.current
114
- registration.setNode(registeredNode)
115
- const registeredIndex = registry
116
- .getItems()
117
- .findIndex((item) => item.id === registration.id)
118
- if (
119
- isWeb &&
120
- listRef &&
121
- registeredIndex >= 0 &&
122
- registeredNode instanceof HTMLElement
123
- ) {
124
- listRef.current[registeredIndex] = registeredNode
125
- }
110
+ registration.setNode(itemNodeRef.current)
126
111
  return () => {
127
- const currentIndex = registry
128
- .getItems()
129
- .findIndex((item) => item.id === registration.id)
130
- if (
131
- listRef &&
132
- currentIndex >= 0 &&
133
- listRef.current[currentIndex] === registeredNode
134
- ) {
135
- listRef.current[currentIndex] = null
136
- }
137
112
  registrationRef.current = null
138
113
  registration.unregister()
139
114
  }
@@ -147,43 +122,41 @@ export const SelectItem = createStyledHOC(
147
122
  registrationRef.current?.setNode(itemNodeRef.current)
148
123
  })
149
124
 
150
- const index = registry.getIndex(value)
125
+ // the only per-item state: this item is the active one. the active index
126
+ // itself is a ref plus an emitter, so a hover re-renders two items, not the list
127
+ const [isActive, setIsActive] = React.useState(false)
128
+ // every item hears every change; only the two whose state flips may set
129
+ // it, since a same-value set still renders once when updates are queued
130
+ const isActiveRef = React.useRef(false)
151
131
 
152
132
  React.useEffect(() => {
153
- const handleActiveIndex = (i: number) => {
154
- if (index === i) {
155
- if (isWeb) {
156
- // use rAF to focus after browser's click handling completes
157
- // this prevents the trigger from stealing focus after we set it
158
- requestAnimationFrame(() => {
159
- listRef?.current[index]?.focus()
160
- })
161
- }
133
+ const handleActiveIndex = (i: number | null) => {
134
+ const next = i != null && registry.getIndex(value) === i
135
+ if (next !== isActiveRef.current) {
136
+ isActiveRef.current = next
137
+ setIsActive(next)
138
+ }
139
+ if (next && isWeb) {
140
+ // use rAF to focus after browser's click handling completes
141
+ // this prevents the trigger from stealing focus after we set it
142
+ requestAnimationFrame(() => {
143
+ itemNodeRef.current?.focus?.()
144
+ })
162
145
  }
163
146
  }
164
147
 
165
- // check initial value (parent effect may have set it before we subscribed)
166
- const currentActiveIndex = activeIndexRef?.current
167
- if (currentActiveIndex !== null && currentActiveIndex !== undefined) {
168
- handleActiveIndex(currentActiveIndex)
169
- }
148
+ // the parent effect may have set it before we subscribed
149
+ handleActiveIndex(activeIndexRef?.current ?? null)
170
150
 
171
151
  return activeIndexSubscribe(handleActiveIndex)
172
- }, [activeIndexRef, activeIndexSubscribe, index, listRef])
152
+ }, [activeIndexRef, activeIndexSubscribe, registry, value])
173
153
 
174
154
  const textId = React.useId()
175
155
 
176
- const refCallback = React.useCallback(
177
- (node) => {
178
- itemNodeRef.current = node
179
- registrationRef.current?.setNode(node)
180
- if (!isWeb) return
181
- if (listRef && index >= 0) {
182
- listRef.current[index] = node instanceof HTMLElement ? node : null
183
- }
184
- },
185
- [index, listRef]
186
- )
156
+ const refCallback = React.useCallback((node) => {
157
+ itemNodeRef.current = node
158
+ registrationRef.current?.setNode(node)
159
+ }, [])
187
160
 
188
161
  const composedRefs = useComposedRefs(forwardedRef, refCallback)
189
162
 
@@ -203,6 +176,25 @@ export const SelectItem = createStyledHOC(
203
176
  [disabled, selectValue, value]
204
177
  )
205
178
 
179
+ const handleMouseMove = React.useCallback(
180
+ (event: any) => {
181
+ if (disabled) return
182
+ const index = registry.getIndex(value)
183
+ if (index < 0) return
184
+ const last = lastPointerRef.current
185
+ if (event.clientX === last.x && event.clientY === last.y) return
186
+ last.x = event.clientX
187
+ last.y = event.clientY
188
+ setActiveIndex(index, {
189
+ reason: 'item-hover',
190
+ event: event.nativeEvent || event,
191
+ trigger: event.currentTarget,
192
+ index,
193
+ } as SelectActiveChangeDetails)
194
+ },
195
+ [disabled, lastPointerRef, registry, setActiveIndex, value]
196
+ )
197
+
206
198
  const handleKeyDown = React.useCallback(
207
199
  (event: any) => {
208
200
  if (disabled) return
@@ -218,7 +210,7 @@ export const SelectItem = createStyledHOC(
218
210
  return
219
211
  }
220
212
  if (
221
- !interactions &&
213
+ !getItemProps &&
222
214
  event.key?.length === 1 &&
223
215
  !event.metaKey &&
224
216
  !event.ctrlKey
@@ -229,11 +221,11 @@ export const SelectItem = createStyledHOC(
229
221
  allowSelectRef.current = true
230
222
  }
231
223
  },
232
- [allowSelectRef, disabled, handleSelect, interactions, moveActive, search]
224
+ [allowSelectRef, disabled, handleSelect, getItemProps, moveActive, search]
233
225
  )
234
226
 
235
227
  const selectItemProps = React.useMemo(() => {
236
- if (interactions) {
228
+ if (getItemProps) {
237
229
  const {
238
230
  onTouchMove,
239
231
  onTouchEnd,
@@ -244,7 +236,7 @@ export const SelectItem = createStyledHOC(
244
236
  onPress,
245
237
  ...itemProps
246
238
  } = restProps
247
- const interactionProps = interactions.getItemProps({
239
+ const interactionProps = getItemProps({
248
240
  ...itemProps,
249
241
  onTouchMove() {
250
242
  allowSelectRef!.current = true
@@ -254,15 +246,7 @@ export const SelectItem = createStyledHOC(
254
246
  allowSelectRef!.current = false
255
247
  allowMouseUpRef!.current = true
256
248
  },
257
- onMouseMove(event) {
258
- if (disabled || index < 0) return
259
- setActiveIndexFast?.(index, {
260
- reason: 'item-hover',
261
- event: event.nativeEvent || event,
262
- trigger: event.currentTarget,
263
- index,
264
- } as SelectActiveChangeDetails)
265
- },
249
+ onMouseMove: handleMouseMove,
266
250
  onKeyDown: handleKeyDown,
267
251
  onClick(event) {
268
252
  if (disabled) return
@@ -332,15 +316,7 @@ export const SelectItem = createStyledHOC(
332
316
  return {
333
317
  ...restProps,
334
318
  onKeyDown: composeEventHandlers(restProps.onKeyDown as any, handleKeyDown),
335
- onMouseMove: composeEventHandlers(restProps.onMouseMove as any, (event: any) => {
336
- if (disabled || index < 0) return
337
- setActiveIndexFast?.(index, {
338
- reason: 'item-hover',
339
- event: event.nativeEvent || event,
340
- trigger: event.currentTarget,
341
- index,
342
- } as SelectActiveChangeDetails)
343
- }),
319
+ onMouseMove: composeEventHandlers(restProps.onMouseMove as any, handleMouseMove),
344
320
  onPress: composeEventHandlers(restProps.onPress as any, handleSelect),
345
321
  }
346
322
  }, [
@@ -348,12 +324,11 @@ export const SelectItem = createStyledHOC(
348
324
  allowSelectRef,
349
325
  disabled,
350
326
  handleKeyDown,
327
+ handleMouseMove,
351
328
  handleSelect,
352
- index,
353
- interactions,
329
+ getItemProps,
354
330
  restProps,
355
331
  selectTimeoutRef,
356
- setActiveIndexFast,
357
332
  ])
358
333
 
359
334
  const accessibilityProps = getSelectOptionProps(
@@ -385,10 +360,10 @@ export const SelectItem = createStyledHOC(
385
360
  tabIndex={
386
361
  disabled
387
362
  ? undefined
388
- : isWeb && isAdapted
389
- ? index === (selectContext.activeIndex ?? selectContext.selectedIndex)
390
- ? 0
391
- : -1
363
+ : isWeb &&
364
+ isAdapted &&
365
+ (isActive || (activeIndexRef?.current == null && isSelected))
366
+ ? 0
392
367
  : -1
393
368
  }
394
369
  zIndex={100}