@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,10 +1,9 @@
1
1
  import { createStyledHOC, styled, View } from '@tamagui/core'
2
- import { autoUpdate, offset, useFloatingRaw as useFloating } from '@tamagui/floating'
2
+ import { autoUpdate, useFloatingRaw as useFloating } from '@tamagui/floating'
3
3
  import { useComposedRefs } from '@tamagui/compose-refs'
4
4
  import type { TamaguiElement } from '@tamagui/core'
5
5
  import { composeEventHandlers } from '@tamagui/helpers'
6
6
  import * as React from 'react'
7
- import { flushSync } from 'react-dom'
8
7
 
9
8
  import { useSelectContext } from './context'
10
9
  import type {
@@ -68,13 +67,12 @@ const SelectScrollButtonImpl = React.memo(
68
67
  const { scope, dir, partClassName, ...scrollIndicatorProps } = props
69
68
  const { className, onPointerEnter, onPointerLeave, ...frameProps } =
70
69
  scrollIndicatorProps
71
- const { open, fallback, setScrollTop, setInnerOffset, ...context } =
72
- useSelectContext(scope)
70
+ const { open, ...context } = useSelectContext(scope)
73
71
  const floatingRef = context.floatingContext?.refs.floating
74
72
 
75
- const statusRef = React.useRef<'idle' | 'active'>('idle')
76
73
  const isVisible = context[dir === 'down' ? 'canScrollDown' : 'canScrollUp']
77
74
  const frameRef = React.useRef<any>(null)
75
+ const arrowRef = dir === 'up' ? context.upArrowRef : context.downArrowRef
78
76
 
79
77
  const { x, y, refs, strategy } = useFloating({
80
78
  open: open && isVisible,
@@ -83,38 +81,21 @@ const SelectScrollButtonImpl = React.memo(
83
81
  reference: floatingRef?.current,
84
82
  },
85
83
  placement: dir === 'up' ? 'top' : 'bottom',
86
- middleware: [offset(({ rects }) => -rects.floating.height)],
87
- whileElementsMounted: (...args) => autoUpdate(...args, { animationFrame: true }),
84
+ whileElementsMounted: autoUpdate,
88
85
  })
89
86
 
90
87
  const setFloatingRef = React.useCallback(
91
88
  (node: TamaguiElement | null) => {
92
89
  refs.setFloating(node as HTMLElement | null)
90
+ if (arrowRef) arrowRef.current = node as HTMLDivElement | null
93
91
  },
94
- [refs.setFloating]
92
+ [refs.setFloating, arrowRef]
95
93
  )
96
94
  const composedRef = useComposedRefs(forwardedRef, setFloatingRef)
97
95
 
98
96
  React.useEffect(() => {
99
97
  return () => cancelAnimationFrame(frameRef.current)
100
- }, [])
101
-
102
- if (!isVisible) {
103
- return null
104
- }
105
-
106
- const onScroll = (amount: number) => {
107
- const floating = floatingRef
108
- if (!floating) return
109
- if (fallback) {
110
- if (floating.current) {
111
- floating.current.scrollTop -= amount
112
- flushSync(() => setScrollTop!(floating.current?.scrollTop ?? 0))
113
- }
114
- } else {
115
- flushSync(() => setInnerOffset!((value) => value - amount))
116
- }
117
- }
98
+ }, [isVisible])
118
99
 
119
100
  return (
120
101
  <SelectScrollButtonFrame
@@ -122,13 +103,15 @@ const SelectScrollButtonImpl = React.memo(
122
103
  className={`${partClassName} ${className || ''}`.trim()}
123
104
  aria-hidden
124
105
  {...frameProps}
106
+ // keep hidden buttons measurable so placement reserves their actual height
107
+ visibility={isVisible ? 'visible' : 'hidden'}
108
+ pointerEvents={isVisible ? 'auto' : 'none'}
125
109
  zIndex={1000}
126
110
  position={strategy}
127
111
  left={x || 0}
128
112
  top={y || 0}
129
113
  width={`calc(${(floatingRef?.current?.offsetWidth ?? 0) - 2}px)`}
130
114
  onPointerEnter={composeEventHandlers(onPointerEnter as any, () => {
131
- statusRef.current = 'active'
132
115
  let prevNow = Date.now()
133
116
 
134
117
  function frame() {
@@ -151,11 +134,11 @@ const SelectScrollButtonImpl = React.memo(
151
134
  : element.scrollTop + pixelsToScroll <
152
135
  element.scrollHeight - element.clientHeight
153
136
 
154
- onScroll(
137
+ // the viewport's own scroll handler flips the arrows
138
+ element.scrollTop +=
155
139
  dir === 'up'
156
- ? Math.min(pixelsToScroll, remainingPixels)
157
- : Math.max(-pixelsToScroll, -remainingPixels)
158
- )
140
+ ? -Math.min(pixelsToScroll, remainingPixels)
141
+ : Math.min(pixelsToScroll, remainingPixels)
159
142
 
160
143
  if (scrollRemaining) {
161
144
  frameRef.current = requestAnimationFrame(frame)
@@ -167,7 +150,6 @@ const SelectScrollButtonImpl = React.memo(
167
150
  frameRef.current = requestAnimationFrame(frame)
168
151
  })}
169
152
  onPointerLeave={composeEventHandlers(onPointerLeave as any, () => {
170
- statusRef.current = 'idle'
171
153
  cancelAnimationFrame(frameRef.current)
172
154
  })}
173
155
  />
@@ -34,6 +34,8 @@ export const SelectTrigger = createStyledHOC(
34
34
  forwardedRef,
35
35
  context.floatingContext?.refs.setReference as any
36
36
  )
37
+ // whether the press in flight began as a touch, so its click opens
38
+ const pressIsTouch = React.useRef(false)
37
39
 
38
40
  if (itemParentContext.shouldRenderWebNative) {
39
41
  return null
@@ -55,8 +57,8 @@ export const SelectTrigger = createStyledHOC(
55
57
  }
56
58
  }
57
59
  const interactionProps =
58
- process.env.TAMAGUI_TARGET === 'web' && itemParentContext.interactions
59
- ? itemParentContext.interactions.getReferenceProps({
60
+ process.env.TAMAGUI_TARGET === 'web' && context.interactions
61
+ ? context.interactions.getReferenceProps({
60
62
  ...triggerProps,
61
63
  onMouseDown: composeEventHandlers(triggerProps.onMouseDown as any, () =>
62
64
  context.floatingContext?.update?.()
@@ -66,13 +68,28 @@ export const SelectTrigger = createStyledHOC(
66
68
  ...triggerProps,
67
69
  ...(isWeb
68
70
  ? {
71
+ // a mouse opens on press so a press, drag and release picks
72
+ // in one gesture. a touch opens on release, as a native
73
+ // select does: opening on touchstart mounts the sheet
74
+ // overlay under the finger, and the same tap's click then
75
+ // lands on it and closes the sheet again.
69
76
  onMouseDown: composeEventHandlers(
70
77
  triggerProps.onMouseDown as any,
71
78
  (event: any) => {
79
+ pressIsTouch.current = event.type === 'touchstart'
80
+ if (pressIsTouch.current) return
72
81
  event.preventDefault()
73
82
  toggleOpen(event)
74
83
  }
75
84
  ),
85
+ onPress: composeEventHandlers(
86
+ triggerProps.onPress as any,
87
+ (event: any) => {
88
+ if (!pressIsTouch.current) return
89
+ pressIsTouch.current = false
90
+ toggleOpen(event)
91
+ }
92
+ ),
76
93
  }
77
94
  : {
78
95
  onPress: composeEventHandlers(triggerProps.onPress as any, toggleOpen),
@@ -1,6 +1,6 @@
1
1
  import {
2
- AdaptContext,
3
2
  AdaptPortalContents,
3
+ ProvideAdaptContext,
4
4
  useAdaptContext,
5
5
  useAdaptIsActive,
6
6
  } from '@tamagui/adapt'
@@ -26,17 +26,17 @@ export const SelectViewport = createStyledHOC(
26
26
  const context = useSelectContext(scope)
27
27
  const itemParentContext = useSelectItemParentContext(scope)
28
28
  const themeName = useThemeName()
29
- const adaptContext = useAdaptContext()
29
+ const adaptContext = useAdaptContext(context.adaptScope)
30
30
  const isAdapted = useAdaptIsActive(context.adaptScope)
31
31
 
32
32
  const contents = (
33
33
  <Theme name={themeName}>
34
34
  <ForwardSelectContext itemContext={itemParentContext} context={context}>
35
- <AdaptContext.Provider {...adaptContext}>
35
+ <ProvideAdaptContext {...adaptContext}>
36
36
  <SelectViewportFrame {...viewportProps} ref={forwardedRef}>
37
37
  {children}
38
38
  </SelectViewportFrame>
39
- </AdaptContext.Provider>
39
+ </ProvideAdaptContext>
40
40
  </ForwardSelectContext>
41
41
  </Theme>
42
42
  )
@@ -2,10 +2,9 @@ import { AdaptPortalContents, useAdaptIsActive } from '@tamagui/adapt'
2
2
  import { AnimatePresence } from '@tamagui/animate-presence'
3
3
  import { useComposedRefs } from '@tamagui/compose-refs'
4
4
  import { isWeb, useIsomorphicLayoutEffect } from '@tamagui/constants'
5
- import { createStyledHOC, styled, View } from '@tamagui/core'
5
+ import { createStyledHOC, splitStyleProps, styled, View } from '@tamagui/core'
6
6
  import { needsPortalRepropagation } from '@tamagui/portal'
7
7
  import { YStack } from '@tamagui/stacks'
8
- import { startTransition } from '@tamagui/start-transition'
9
8
  import * as React from 'react'
10
9
  import { VIEWPORT_NAME } from './constants'
11
10
  import {
@@ -27,6 +26,15 @@ export const SelectViewportFrame = styled(View, {
27
26
 
28
27
  const needsRepropagation = needsPortalRepropagation()
29
28
 
29
+ // the viewport's styles dress the floating list (a capped height, a card
30
+ // background, border and shadow). in a sheet the sheet is the surface, so only
31
+ // the element props carry over and the items fill the sheet. a styled() skin
32
+ // hands its styles to this HOC as class names (web) or a style (native)
33
+ const getSheetFrameProps = (props: Record<string, any>) => {
34
+ const { className, style, 'data-tamagui-hoc-class': _hocClassNames, ...rest } = props
35
+ return splitStyleProps(rest)[1]
36
+ }
37
+
30
38
  export const SelectViewport = createStyledHOC(
31
39
  SelectViewportFrame,
32
40
  function SelectViewport(props: SelectViewportProps, forwardedRef) {
@@ -37,17 +45,13 @@ export const SelectViewport = createStyledHOC(
37
45
  const viewportRef = React.useRef<any>(null)
38
46
  const registeredItemCount = itemContext.registry.getItems().length
39
47
 
40
- // lazy mount: defer mounting children until first open using startTransition
41
- const [lazyMounted, setLazyMounted] = React.useState(context.lazyMount ? false : true)
42
-
43
- React.useEffect(() => {
44
- if (!context.lazyMount) return
45
- if (!context.open) return
46
- if (lazyMounted) return
47
- startTransition(() => {
48
- setLazyMounted(true)
49
- })
50
- }, [context.lazyMount, context.open, lazyMounted])
48
+ // lazy mount keeps the items out of the tree until the first open, but it
49
+ // has to happen in the render that opens, not in an effect after it: mounting
50
+ // a frame later paints an empty popover, then floating-ui positions that empty
51
+ // box and has to jump once the real height arrives. once mounted it stays.
52
+ const hasOpenedRef = React.useRef(!context.lazyMount)
53
+ if (context.open) hasOpenedRef.current = true
54
+ const lazyMounted = hasOpenedRef.current
51
55
 
52
56
  React.useEffect(() => {
53
57
  if (!isWeb || !isAdapted || !context.open) return
@@ -62,7 +66,7 @@ export const SelectViewport = createStyledHOC(
62
66
  activeItem?.focus()
63
67
  })
64
68
  return () => cancelAnimationFrame(frame)
65
- }, [context.activeIndex, context.open, isAdapted, registeredItemCount])
69
+ }, [context.open, isAdapted, registeredItemCount])
66
70
 
67
71
  const composedRefs = useComposedRefs(
68
72
  // @ts-ignore react 19 ref type mismatch
@@ -77,23 +81,24 @@ export const SelectViewport = createStyledHOC(
77
81
  }
78
82
  }, [isAdapted])
79
83
 
80
- // after lazy children mount, force floating-ui to recompute so inner middleware
81
- // can position using the now-present list items
84
+ // items register in layout effects, so the inner middleware can only position
85
+ // against the selected one once the registry has settled
82
86
  React.useEffect(() => {
83
- if (context.lazyMount && lazyMounted && context.open && context.update) {
87
+ if (context.lazyMount && context.open && context.update) {
84
88
  const frame = requestAnimationFrame(context.update)
85
89
  return () => cancelAnimationFrame(frame)
86
90
  }
87
- }, [lazyMounted, registeredItemCount])
91
+ }, [context.open, registeredItemCount])
88
92
 
89
93
  if (itemContext.shouldRenderWebNative) {
90
94
  return <YStack position="relative">{children}</YStack>
91
95
  }
92
96
 
93
97
  if (isAdapted || !isWeb) {
98
+ const frameProps = isAdapted ? getSheetFrameProps(viewportProps) : viewportProps
94
99
  let content = (
95
100
  <SelectViewportFrame
96
- {...viewportProps}
101
+ {...frameProps}
97
102
  {...(isWeb ? (getSelectListboxProps(itemContext.mode) as any) : {})}
98
103
  data-select-viewport=""
99
104
  ref={composedRefs}
@@ -115,16 +120,11 @@ export const SelectViewport = createStyledHOC(
115
120
  )
116
121
  }
117
122
 
118
- if (!itemContext.interactions) {
123
+ if (!context.interactions) {
119
124
  return null
120
125
  }
121
126
 
122
- const {
123
- style,
124
- // remove this, it was set to "Select" always
125
- className,
126
- ...floatingProps
127
- } = itemContext.interactions.getFloatingProps()
127
+ const floatingProps = context.interactions.getFloatingProps()
128
128
 
129
129
  // FloatingFocusManager removed — SelectContent already wraps with FocusScope
130
130
  // that handles focus trapping and auto-focus
@@ -137,7 +137,10 @@ export const SelectViewport = createStyledHOC(
137
137
  }}
138
138
  />
139
139
  )}
140
- <AnimatePresence>
140
+ {/* every styled item reads the presence context, and the default
141
+ (layout-affecting) presence hands out a new context per render, so
142
+ each viewport render would re-render the whole list */}
143
+ <AnimatePresence presenceAffectsLayout={false}>
141
144
  {context.open ? (
142
145
  <SelectViewportFrame
143
146
  key="select-viewport"
@@ -146,10 +149,11 @@ export const SelectViewport = createStyledHOC(
146
149
  'data-state': context.open ? 'open' : 'closed',
147
150
  })}
148
151
  {...viewportProps}
149
- {...style}
152
+ {...context.floatingPosition}
153
+ outlineWidth={0}
150
154
  {...floatingProps}
151
155
  {...getSelectListboxProps(itemContext.mode)}
152
- overflowY={disableScroll ? undefined : (style.overflow ?? 'auto')}
156
+ overflowY={disableScroll ? undefined : 'auto'}
153
157
  ref={composedRefs}
154
158
  >
155
159
  {lazyMounted ? children : null}
@@ -171,7 +175,7 @@ const selectViewportCSS = `
171
175
  [data-select-viewport] {
172
176
  scrollbar-width: none;
173
177
  -webkit-overflow-scrolling: touch;
174
- overscroll-behavior: contain;
178
+ overscroll-behavior: none;
175
179
  }
176
180
 
177
181
  [data-select-viewport]::-webkit-scrollbar{
@@ -43,7 +43,12 @@ export function createSelectItemRegistry(onChange?: () => void) {
43
43
  const itemNodes = new Map<symbol, any>()
44
44
  const listeners = new Set<() => void>()
45
45
 
46
+ // value -> first index, rebuilt after any change: items look themselves up
47
+ // on every active-index emit, so the lookup must not walk the list
48
+ let indexByValue: Map<string, number> | null = null
49
+
46
50
  const notify = () => {
51
+ indexByValue = null
47
52
  onChange?.()
48
53
  listeners.forEach((listener) => listener())
49
54
  }
@@ -52,7 +57,18 @@ export function createSelectItemRegistry(onChange?: () => void) {
52
57
 
53
58
  const getItem = (value: string) => items.find((item) => item.value === value)
54
59
 
55
- const getIndex = (value: string) => items.findIndex((item) => item.value === value)
60
+ const getIndex = (value: string) => {
61
+ if (!indexByValue) {
62
+ indexByValue = new Map()
63
+ items.forEach((item, index) => {
64
+ if (!indexByValue!.has(item.value)) indexByValue!.set(item.value, index)
65
+ })
66
+ }
67
+ return indexByValue.get(value) ?? -1
68
+ }
69
+
70
+ /** the mounted nodes in item order; the owner mirrors this into its list ref */
71
+ const getNodes = () => items.map((item) => itemNodes.get(item.id) ?? null)
56
72
 
57
73
  const getRegisteredLabel = (value: string) => {
58
74
  const registrations = labelRegistrations.get(value)
@@ -78,7 +94,6 @@ export function createSelectItemRegistry(onChange?: () => void) {
78
94
  }
79
95
 
80
96
  const syncNodeOrder = () => {
81
- const previousOrder = items.map((item) => item.id)
82
97
  items.sort((a, b) => {
83
98
  const aNode = itemNodes.get(a.id)
84
99
  const bNode = itemNodes.get(b.id)
@@ -90,9 +105,6 @@ export function createSelectItemRegistry(onChange?: () => void) {
90
105
  if (position & 2) return 1
91
106
  return 0
92
107
  })
93
- if (items.some((item, index) => item.id !== previousOrder[index])) {
94
- notify()
95
- }
96
108
  }
97
109
 
98
110
  const registerItem = (
@@ -138,12 +150,14 @@ export function createSelectItemRegistry(onChange?: () => void) {
138
150
  notify()
139
151
  },
140
152
  setNode(node) {
153
+ if ((itemNodes.get(id) ?? null) === (node ?? null)) return
141
154
  if (node) {
142
155
  itemNodes.set(id, node)
143
156
  syncNodeOrder()
144
157
  } else {
145
158
  itemNodes.delete(id)
146
159
  }
160
+ notify()
147
161
  },
148
162
  unregister() {
149
163
  const index = items.findIndex((current) => current.id === id)
@@ -223,6 +237,7 @@ export function createSelectItemRegistry(onChange?: () => void) {
223
237
  getItems,
224
238
  getItem,
225
239
  getIndex,
240
+ getNodes,
226
241
  firstEnabledIndex,
227
242
  nextEnabledIndex,
228
243
  findTypeaheadIndex,
package/src/types.tsx CHANGED
@@ -1,6 +1,5 @@
1
1
  import type {
2
2
  NativeValue,
3
- SizeTokens,
4
3
  TamaguiChangeEventDetails,
5
4
  TamaguiEventDetails,
6
5
  ViewProps,
@@ -62,8 +61,6 @@ export type SelectImplProps = SelectScopedProps<SelectProps<string, boolean>> &
62
61
  activeIndexRef: any
63
62
  selectedIndexRef: any
64
63
  listContentRef: any
65
- /** fast setter: updates ref + emits to subscribers (no re-render) - use for hover/navigation */
66
- setActiveIndexFast: (index: number | null) => void
67
64
  }
68
65
 
69
66
  export interface SelectProps<
@@ -83,7 +80,8 @@ export interface SelectProps<
83
80
  defaultOpen?: boolean
84
81
  onOpenChange?(open: boolean, details: SelectOpenChangeDetails): void
85
82
  dir?: SelectDirection
86
- size?: SizeTokens | true
83
+ /** a skin-owned size name, threaded opaquely to the skin parts */
84
+ size?: string | boolean
87
85
  /**
88
86
  * If passed, will render a native component instead of the custom one. Currently only `web` is supported.
89
87
  */
@@ -154,40 +152,39 @@ export interface SelectItemParentContextValue {
154
152
  requestOpenChange: (open: boolean, details: SelectOpenChangeDetails) => void
155
153
  selectValue: (value: string, details: SelectValueChangeDetails) => void
156
154
  changeNativeValue: (value: SelectSelection, event: unknown) => void
157
- activeIndexSubscribe: EmitterSubscriber<number>
155
+ activeIndexSubscribe: EmitterSubscriber<number | null>
158
156
  activeIndexRef?: MutableRefObject<number | null>
159
157
  allowSelectRef?: MutableRefObject<boolean>
160
158
  allowMouseUpRef?: MutableRefObject<boolean>
161
159
  selectTimeoutRef?: MutableRefObject<any>
162
160
  dataRef?: MutableRefObject<ContextData>
163
- interactions?: {
164
- getReferenceProps: (userProps?: HTMLProps<Element> | undefined) => any
165
- getFloatingProps: (userProps?: HTMLProps<HTMLElement> | undefined) => any
166
- getItemProps: (userProps?: HTMLProps<HTMLElement> | undefined) => any
167
- }
161
+ /** web only: stable once the list mounts, so it can live beside the items */
162
+ getItemProps?: (userProps?: HTMLProps<HTMLElement> | undefined) => any
168
163
  shouldRenderWebNative?: boolean
169
- size?: SizeTokens | true
170
- /** fast setter: updates ref + emits to subscribers (no re-render) - use for keyboard navigation */
171
- setActiveIndexFast?: (index: number | null, details?: SelectActiveChangeDetails) => void
164
+ /** a skin-owned size name, threaded opaquely to the skin parts */
165
+ size?: string | boolean
166
+ setActiveIndex: (index: number | null, details?: SelectActiveChangeDetails) => void
167
+ selectedIndex: number
168
+ lastPointerRef: MutableRefObject<{ x: number; y: number }>
172
169
  moveActive: (direction: 1 | -1, event?: unknown) => void
173
170
  search: (text: string, event?: unknown) => void
174
171
  }
175
172
 
176
173
  export interface SelectContextValue {
174
+ /** web only: the trigger and viewport prop getters; change with the floating element */
175
+ interactions?: {
176
+ getReferenceProps: (userProps?: HTMLProps<Element> | undefined) => any
177
+ getFloatingProps: (userProps?: HTMLProps<HTMLElement> | undefined) => any
178
+ }
177
179
  dir?: SelectDirection
178
180
  scopeName: string
179
181
  adaptScope: string
180
182
  value: any
181
183
  mode: SelectMode
182
184
  selectedValues: string[]
183
- activeItem?: string
184
- selectionAnchor?: string
185
185
  selectedIndex: number
186
- /** current active index state - use for rendering, may lag behind ref */
187
- activeIndex: number | null
188
- /** ref to current active index - always up to date, use for reads */
186
+ /** the active index is never state: read the ref, or subscribe through the item parent context */
189
187
  activeIndexRef: MutableRefObject<number | null>
190
- /** slow setter: updates ref + emits + triggers re-render */
191
188
  setActiveIndex: (index: number | null, details?: SelectActiveChangeDetails) => void
192
189
  open: boolean
193
190
  valueNode: Element | null
@@ -197,12 +194,18 @@ export interface SelectContextValue {
197
194
  blockSelection: boolean
198
195
  upArrowRef?: MutableRefObject<HTMLDivElement | null>
199
196
  downArrowRef?: MutableRefObject<HTMLDivElement | null>
200
- setScrollTop?: Function
201
- setInnerOffset?: Function
197
+ /** re-reads the viewport and flips canScrollUp/Down when they change */
198
+ updateScrollArrows?: () => void
202
199
  controlledScrolling?: boolean
203
200
  canScrollUp?: boolean
204
201
  canScrollDown?: boolean
205
202
  floatingContext?: FloatingContext<ReferenceType>
203
+ /** InlineImpl only: where floating-ui put the viewport */
204
+ floatingPosition?: {
205
+ position: 'absolute' | 'fixed'
206
+ top: number | ''
207
+ left: number | ''
208
+ }
206
209
  native?: NativeValue
207
210
  disablePreventBodyScroll?: boolean
208
211
  /** update floating-ui to recalculate */
@@ -214,7 +217,8 @@ export interface SelectContextValue {
214
217
  }
215
218
 
216
219
  export type SelectViewportExtraProps = SelectScopedProps<{
217
- size?: SizeTokens | true
220
+ /** a skin-owned size name, threaded opaquely to the skin parts */
221
+ size?: string | boolean
218
222
  disableScroll?: boolean
219
223
  }>
220
224