@tamagui/select 2.7.6 → 3.0.0-beta.637.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 (248) hide show
  1. package/dist/cjs/Select.cjs +409 -467
  2. package/dist/cjs/Select.native.cjs +495 -0
  3. package/dist/cjs/Select.native.js +445 -492
  4. package/dist/cjs/Select.native.js.map +1 -1
  5. package/dist/cjs/SelectContent.cjs +80 -79
  6. package/dist/cjs/SelectContent.native.cjs +30 -0
  7. package/dist/cjs/SelectContent.native.js +18 -22
  8. package/dist/cjs/SelectContent.native.js.map +1 -1
  9. package/dist/cjs/SelectImpl.cjs +299 -319
  10. package/dist/cjs/SelectImpl.native.cjs +30 -0
  11. package/dist/cjs/SelectImpl.native.js +17 -24
  12. package/dist/cjs/SelectImpl.native.js.map +1 -1
  13. package/dist/cjs/SelectItem.cjs +268 -205
  14. package/dist/cjs/SelectItem.native.cjs +317 -0
  15. package/dist/cjs/SelectItem.native.js +291 -211
  16. package/dist/cjs/SelectItem.native.js.map +1 -1
  17. package/dist/cjs/SelectItemText.cjs +49 -79
  18. package/dist/cjs/SelectItemText.native.cjs +74 -0
  19. package/dist/cjs/SelectItemText.native.js +50 -79
  20. package/dist/cjs/SelectItemText.native.js.map +1 -1
  21. package/dist/cjs/SelectScrollButton.cjs +111 -130
  22. package/dist/cjs/SelectScrollButton.native.cjs +38 -0
  23. package/dist/cjs/SelectScrollButton.native.js +25 -22
  24. package/dist/cjs/SelectScrollButton.native.js.map +1 -1
  25. package/dist/cjs/SelectTrigger.cjs +69 -84
  26. package/dist/cjs/SelectTrigger.native.cjs +91 -0
  27. package/dist/cjs/SelectTrigger.native.js +71 -66
  28. package/dist/cjs/SelectTrigger.native.js.map +1 -1
  29. package/dist/cjs/SelectViewport.cjs +119 -142
  30. package/dist/cjs/SelectViewport.native.cjs +73 -0
  31. package/dist/cjs/SelectViewport.native.js +54 -50
  32. package/dist/cjs/SelectViewport.native.js.map +1 -1
  33. package/dist/cjs/constants.cjs +21 -22
  34. package/dist/cjs/constants.native.cjs +41 -0
  35. package/dist/cjs/constants.native.js +21 -21
  36. package/dist/cjs/constants.native.js.map +1 -1
  37. package/dist/cjs/context.cjs +38 -48
  38. package/dist/cjs/context.native.cjs +58 -0
  39. package/dist/cjs/context.native.js +40 -49
  40. package/dist/cjs/context.native.js.map +1 -1
  41. package/dist/cjs/index.cjs +15 -11
  42. package/dist/cjs/index.native.cjs +27 -0
  43. package/dist/cjs/index.native.js +15 -10
  44. package/dist/cjs/index.native.js.map +1 -1
  45. package/dist/cjs/selectionController.cjs +334 -0
  46. package/dist/cjs/selectionController.native.cjs +403 -0
  47. package/dist/cjs/selectionController.native.js +405 -0
  48. package/dist/cjs/selectionController.native.js.map +1 -0
  49. package/dist/cjs/types.cjs +10 -11
  50. package/dist/cjs/types.native.cjs +19 -0
  51. package/dist/cjs/types.native.js +10 -10
  52. package/dist/cjs/types.native.js.map +1 -1
  53. package/dist/esm/Select.mjs +382 -443
  54. package/dist/esm/Select.mjs.map +1 -1
  55. package/dist/esm/Select.native.js +418 -469
  56. package/dist/esm/Select.native.js.map +1 -1
  57. package/dist/esm/SelectContent.mjs +67 -61
  58. package/dist/esm/SelectContent.mjs.map +1 -1
  59. package/dist/esm/SelectContent.native.js +4 -5
  60. package/dist/esm/SelectContent.native.js.map +1 -1
  61. package/dist/esm/SelectImpl.mjs +284 -294
  62. package/dist/esm/SelectImpl.mjs.map +1 -1
  63. package/dist/esm/SelectImpl.native.js +5 -8
  64. package/dist/esm/SelectImpl.native.js.map +1 -1
  65. package/dist/esm/SelectItem.mjs +252 -182
  66. package/dist/esm/SelectItem.mjs.map +1 -1
  67. package/dist/esm/SelectItem.native.js +276 -190
  68. package/dist/esm/SelectItem.native.js.map +1 -1
  69. package/dist/esm/SelectItemText.mjs +33 -55
  70. package/dist/esm/SelectItemText.mjs.map +1 -1
  71. package/dist/esm/SelectItemText.native.js +34 -56
  72. package/dist/esm/SelectItemText.native.js.map +1 -1
  73. package/dist/esm/SelectScrollButton.mjs +95 -107
  74. package/dist/esm/SelectScrollButton.mjs.map +1 -1
  75. package/dist/esm/SelectScrollButton.native.js +11 -7
  76. package/dist/esm/SelectScrollButton.native.js.map +1 -1
  77. package/dist/esm/SelectTrigger.mjs +56 -58
  78. package/dist/esm/SelectTrigger.mjs.map +1 -1
  79. package/dist/esm/SelectTrigger.native.js +54 -44
  80. package/dist/esm/SelectTrigger.native.js.map +1 -1
  81. package/dist/esm/SelectViewport.mjs +104 -119
  82. package/dist/esm/SelectViewport.mjs.map +1 -1
  83. package/dist/esm/SelectViewport.native.js +44 -39
  84. package/dist/esm/SelectViewport.native.js.map +1 -1
  85. package/dist/esm/constants.mjs +1 -0
  86. package/dist/esm/constants.mjs.map +1 -1
  87. package/dist/esm/constants.native.js +1 -0
  88. package/dist/esm/constants.native.js.map +1 -1
  89. package/dist/esm/context.mjs +20 -28
  90. package/dist/esm/context.mjs.map +1 -1
  91. package/dist/esm/context.native.js +23 -31
  92. package/dist/esm/context.native.js.map +1 -1
  93. package/dist/esm/index.js +13 -3
  94. package/dist/esm/index.mjs +13 -3
  95. package/dist/esm/index.native.js +13 -3
  96. package/dist/esm/selectionController.mjs +305 -0
  97. package/dist/esm/selectionController.mjs.map +1 -0
  98. package/dist/esm/selectionController.native.js +372 -0
  99. package/dist/esm/selectionController.native.js.map +1 -0
  100. package/dist/esm/types.mjs +0 -2
  101. package/dist/esm/types.native.js +0 -2
  102. package/dist/jsx/Select.mjs +382 -443
  103. package/dist/jsx/Select.mjs.map +1 -1
  104. package/dist/jsx/Select.native.cjs +495 -0
  105. package/dist/jsx/Select.native.js +445 -492
  106. package/dist/jsx/Select.native.js.map +1 -1
  107. package/dist/jsx/SelectContent.mjs +67 -61
  108. package/dist/jsx/SelectContent.mjs.map +1 -1
  109. package/dist/jsx/SelectContent.native.cjs +30 -0
  110. package/dist/jsx/SelectContent.native.js +18 -22
  111. package/dist/jsx/SelectContent.native.js.map +1 -1
  112. package/dist/jsx/SelectImpl.mjs +284 -294
  113. package/dist/jsx/SelectImpl.mjs.map +1 -1
  114. package/dist/jsx/SelectImpl.native.cjs +30 -0
  115. package/dist/jsx/SelectImpl.native.js +17 -24
  116. package/dist/jsx/SelectImpl.native.js.map +1 -1
  117. package/dist/jsx/SelectItem.mjs +252 -182
  118. package/dist/jsx/SelectItem.mjs.map +1 -1
  119. package/dist/jsx/SelectItem.native.cjs +317 -0
  120. package/dist/jsx/SelectItem.native.js +291 -211
  121. package/dist/jsx/SelectItem.native.js.map +1 -1
  122. package/dist/jsx/SelectItemText.mjs +33 -55
  123. package/dist/jsx/SelectItemText.mjs.map +1 -1
  124. package/dist/jsx/SelectItemText.native.cjs +74 -0
  125. package/dist/jsx/SelectItemText.native.js +50 -79
  126. package/dist/jsx/SelectItemText.native.js.map +1 -1
  127. package/dist/jsx/SelectScrollButton.mjs +95 -107
  128. package/dist/jsx/SelectScrollButton.mjs.map +1 -1
  129. package/dist/jsx/SelectScrollButton.native.cjs +38 -0
  130. package/dist/jsx/SelectScrollButton.native.js +25 -22
  131. package/dist/jsx/SelectScrollButton.native.js.map +1 -1
  132. package/dist/jsx/SelectTrigger.mjs +56 -58
  133. package/dist/jsx/SelectTrigger.mjs.map +1 -1
  134. package/dist/jsx/SelectTrigger.native.cjs +91 -0
  135. package/dist/jsx/SelectTrigger.native.js +71 -66
  136. package/dist/jsx/SelectTrigger.native.js.map +1 -1
  137. package/dist/jsx/SelectViewport.mjs +104 -119
  138. package/dist/jsx/SelectViewport.mjs.map +1 -1
  139. package/dist/jsx/SelectViewport.native.cjs +73 -0
  140. package/dist/jsx/SelectViewport.native.js +54 -50
  141. package/dist/jsx/SelectViewport.native.js.map +1 -1
  142. package/dist/jsx/constants.mjs +1 -0
  143. package/dist/jsx/constants.mjs.map +1 -1
  144. package/dist/jsx/constants.native.cjs +41 -0
  145. package/dist/jsx/constants.native.js +21 -21
  146. package/dist/jsx/constants.native.js.map +1 -1
  147. package/dist/jsx/context.mjs +20 -28
  148. package/dist/jsx/context.mjs.map +1 -1
  149. package/dist/jsx/context.native.cjs +58 -0
  150. package/dist/jsx/context.native.js +40 -49
  151. package/dist/jsx/context.native.js.map +1 -1
  152. package/dist/jsx/index.js +13 -3
  153. package/dist/jsx/index.mjs +13 -3
  154. package/dist/jsx/index.native.cjs +27 -0
  155. package/dist/jsx/index.native.js +15 -10
  156. package/dist/jsx/index.native.js.map +1 -1
  157. package/dist/jsx/selectionController.mjs +305 -0
  158. package/dist/jsx/selectionController.mjs.map +1 -0
  159. package/dist/jsx/selectionController.native.cjs +403 -0
  160. package/dist/jsx/selectionController.native.js +405 -0
  161. package/dist/jsx/selectionController.native.js.map +1 -0
  162. package/dist/jsx/types.mjs +0 -2
  163. package/dist/jsx/types.native.cjs +19 -0
  164. package/dist/jsx/types.native.js +10 -10
  165. package/dist/jsx/types.native.js.map +1 -1
  166. package/package.json +28 -28
  167. package/src/Select.tsx +457 -357
  168. package/src/SelectContent.tsx +46 -16
  169. package/src/SelectImpl.native.tsx +0 -8
  170. package/src/SelectImpl.tsx +87 -100
  171. package/src/SelectItem.tsx +282 -136
  172. package/src/SelectItemText.tsx +16 -33
  173. package/src/SelectScrollButton.native.tsx +13 -4
  174. package/src/SelectScrollButton.tsx +56 -41
  175. package/src/SelectTrigger.tsx +77 -53
  176. package/src/SelectViewport.native.tsx +42 -25
  177. package/src/SelectViewport.tsx +52 -51
  178. package/src/context.tsx +6 -2
  179. package/src/index.tsx +5 -2
  180. package/src/selectionController.ts +389 -0
  181. package/src/types.tsx +88 -45
  182. package/types/Select.d.ts +139 -201
  183. package/types/Select.d.ts.map +1 -1
  184. package/types/SelectContent.d.ts +1 -1
  185. package/types/SelectContent.d.ts.map +1 -1
  186. package/types/SelectContent.native.d.ts.map +1 -1
  187. package/types/SelectImpl.d.ts +2 -1
  188. package/types/SelectImpl.d.ts.map +1 -1
  189. package/types/SelectImpl.native.d.ts +1 -5
  190. package/types/SelectImpl.native.d.ts.map +1 -1
  191. package/types/SelectItem.d.ts +13 -16
  192. package/types/SelectItem.d.ts.map +1 -1
  193. package/types/SelectItemText.d.ts +9 -13
  194. package/types/SelectItemText.d.ts.map +1 -1
  195. package/types/SelectScrollButton.d.ts +15 -2
  196. package/types/SelectScrollButton.d.ts.map +1 -1
  197. package/types/SelectScrollButton.native.d.ts +16 -3
  198. package/types/SelectScrollButton.native.d.ts.map +1 -1
  199. package/types/SelectTrigger.d.ts +11 -23
  200. package/types/SelectTrigger.d.ts.map +1 -1
  201. package/types/SelectViewport.d.ts +11 -38
  202. package/types/SelectViewport.d.ts.map +1 -1
  203. package/types/SelectViewport.native.d.ts +15 -4
  204. package/types/SelectViewport.native.d.ts.map +1 -1
  205. package/types/context.d.ts +2 -0
  206. package/types/context.d.ts.map +1 -1
  207. package/types/index.d.ts +5 -2
  208. package/types/index.d.ts.map +1 -1
  209. package/types/selectionController.d.ts +88 -0
  210. package/types/selectionController.d.ts.map +1 -0
  211. package/types/types.d.ts +47 -37
  212. package/types/types.d.ts.map +1 -1
  213. package/dist/cjs/BubbleSelect.cjs +0 -50
  214. package/dist/cjs/BubbleSelect.native.js +0 -30
  215. package/dist/cjs/BubbleSelect.native.js.map +0 -1
  216. package/dist/cjs/useSelectBreakpointActive.cjs +0 -32
  217. package/dist/cjs/useSelectBreakpointActive.native.js +0 -35
  218. package/dist/cjs/useSelectBreakpointActive.native.js.map +0 -1
  219. package/dist/esm/BubbleSelect.mjs +0 -27
  220. package/dist/esm/BubbleSelect.mjs.map +0 -1
  221. package/dist/esm/BubbleSelect.native.js +0 -4
  222. package/dist/esm/BubbleSelect.native.js.map +0 -1
  223. package/dist/esm/index.js.map +0 -1
  224. package/dist/esm/index.mjs.map +0 -1
  225. package/dist/esm/index.native.js.map +0 -1
  226. package/dist/esm/types.mjs.map +0 -1
  227. package/dist/esm/types.native.js.map +0 -1
  228. package/dist/esm/useSelectBreakpointActive.mjs +0 -7
  229. package/dist/esm/useSelectBreakpointActive.mjs.map +0 -1
  230. package/dist/esm/useSelectBreakpointActive.native.js +0 -7
  231. package/dist/esm/useSelectBreakpointActive.native.js.map +0 -1
  232. package/dist/jsx/BubbleSelect.mjs +0 -27
  233. package/dist/jsx/BubbleSelect.mjs.map +0 -1
  234. package/dist/jsx/BubbleSelect.native.js +0 -30
  235. package/dist/jsx/BubbleSelect.native.js.map +0 -1
  236. package/dist/jsx/index.js.map +0 -1
  237. package/dist/jsx/index.mjs.map +0 -1
  238. package/dist/jsx/types.mjs.map +0 -1
  239. package/dist/jsx/useSelectBreakpointActive.mjs +0 -7
  240. package/dist/jsx/useSelectBreakpointActive.mjs.map +0 -1
  241. package/dist/jsx/useSelectBreakpointActive.native.js +0 -35
  242. package/dist/jsx/useSelectBreakpointActive.native.js.map +0 -1
  243. package/src/BubbleSelect.tsx +0 -50
  244. package/src/useSelectBreakpointActive.tsx +0 -7
  245. package/types/BubbleSelect.d.ts +0 -2
  246. package/types/BubbleSelect.d.ts.map +0 -1
  247. package/types/useSelectBreakpointActive.d.ts +0 -3
  248. package/types/useSelectBreakpointActive.d.ts.map +0 -1
package/src/Select.tsx CHANGED
@@ -1,26 +1,24 @@
1
1
  import { Adapt, AdaptParent, useAdaptIsActive } from '@tamagui/adapt'
2
2
  import { useComposedRefs } from '@tamagui/compose-refs'
3
3
  import { isWeb, useIsomorphicLayoutEffect } from '@tamagui/constants'
4
- import type { FontSizeTokens, GetProps, SizeTokens, TamaguiElement } from '@tamagui/core'
4
+ import type { GetProps } from '@tamagui/core'
5
5
  import {
6
+ createChangeEventDetails,
7
+ createStyledHOC,
6
8
  createStyledContext,
7
- getVariableValue,
8
9
  styled,
9
- useEvent,
10
- useGet,
10
+ Text,
11
+ View,
11
12
  } from '@tamagui/core'
12
13
  import { FocusScopeController } from '@tamagui/focus-scope'
13
14
  import { registerFocusable } from '@tamagui/focusable'
14
- import { getSpace } from '@tamagui/get-token'
15
15
  import { withStaticProperties } from '@tamagui/helpers'
16
- import { Separator } from '@tamagui/separator'
17
- import { SheetController } from '@tamagui/sheet/controller'
18
- import { XStack, YStack } from '@tamagui/stacks'
19
- import { Paragraph, SizableText } from '@tamagui/text'
20
16
  import { useControllableState } from '@tamagui/use-controllable-state'
17
+ import type { ControllableStateSetter } from '@tamagui/use-controllable-state'
21
18
  import * as React from 'react'
22
19
  import {
23
20
  SelectItemParentProvider,
21
+ SelectNativeComponentContext,
24
22
  SelectProvider,
25
23
  SelectZIndexContext,
26
24
  useSelectContext,
@@ -29,12 +27,31 @@ import {
29
27
  import { SelectContent } from './SelectContent'
30
28
  import { SelectInlineImpl } from './SelectImpl'
31
29
  import { SelectItem, useSelectItemContext } from './SelectItem'
32
- import { ITEM_TEXT_NAME, SelectItemText } from './SelectItemText'
30
+ import { SelectItemText } from './SelectItemText'
33
31
  import { SelectScrollDownButton, SelectScrollUpButton } from './SelectScrollButton'
34
32
  import { SelectTrigger } from './SelectTrigger'
35
33
  import { SelectViewport } from './SelectViewport'
36
- import type { SelectImplProps, SelectProps, SelectScopedProps } from './types'
37
- import { useShowSelectSheet } from './useSelectBreakpointActive'
34
+ import {
35
+ createSelectItemRegistry,
36
+ createSelectSelectionController,
37
+ selectedValuesFromSelection,
38
+ type SelectMode,
39
+ type SelectSelection,
40
+ } from './selectionController'
41
+ import type {
42
+ SelectActiveChangeDetails,
43
+ SelectImplProps,
44
+ SelectOpenChangeDetails,
45
+ SelectProps,
46
+ SelectScopedProps,
47
+ SelectValueForMode,
48
+ SelectValueChangeDetails,
49
+ } from './types'
50
+
51
+ export type SelectValue<
52
+ Value extends string = string,
53
+ Multiple extends boolean | undefined = false,
54
+ > = SelectValueForMode<Value, Multiple>
38
55
 
39
56
  /* -------------------------------------------------------------------------------------------------
40
57
  * SelectValue
@@ -42,9 +59,8 @@ import { useShowSelectSheet } from './useSelectBreakpointActive'
42
59
 
43
60
  const VALUE_NAME = 'SelectValue'
44
61
 
45
- const SelectValueFrame = styled(SizableText, {
46
- name: VALUE_NAME,
47
- userSelect: 'none',
62
+ export const SelectValueFrame = styled(Text, {
63
+ displayName: VALUE_NAME,
48
64
  })
49
65
 
50
66
  export type SelectValueExtraProps = SelectScopedProps<{
@@ -53,9 +69,10 @@ export type SelectValueExtraProps = SelectScopedProps<{
53
69
 
54
70
  export type SelectValueProps = GetProps<typeof SelectValueFrame> & SelectValueExtraProps
55
71
 
56
- const SelectValue = SelectValueFrame.styleable<SelectValueExtraProps>(
72
+ export const SelectValue = createStyledHOC(
73
+ SelectValueFrame,
57
74
  function SelectValue(
58
- { scope, children: childrenProp, placeholder, ...props },
75
+ { scope, children: childrenProp, placeholder, ...props }: SelectValueProps,
59
76
  forwardedRef
60
77
  ) {
61
78
  // We ignore `className` and `style` as this part shouldn't be styled.
@@ -63,76 +80,69 @@ const SelectValue = SelectValueFrame.styleable<SelectValueExtraProps>(
63
80
  const itemParentContext = useSelectItemParentContext(scope)
64
81
 
65
82
  const composedRefs = useComposedRefs(
66
- // @ts-ignore TODO react 19 type needs fix
83
+ // @ts-ignore react 19 ref type mismatch
67
84
  forwardedRef,
68
85
  context.onValueNodeChange as any
69
86
  )
70
- const isEmptyValue = context.value == null || context.value === ''
71
-
72
- // Use renderValue for SSR support - called synchronously during render
73
- // Falls back to the portal-based selectedItem, then to the raw value for SSR
74
- const renderedValue = context.renderValue?.(context.value)
87
+ const isEmptyValue = context.selectedValues.length === 0
88
+
89
+ // renderValue is synchronous for ssr and lazy item mounting
90
+ const registeredValue =
91
+ context.mode === 'multiple'
92
+ ? context.selectedValues.map((value, index) => (
93
+ <React.Fragment key={value}>
94
+ {index > 0 ? ', ' : null}
95
+ {itemParentContext.registry.getItem(value)?.label ?? value}
96
+ </React.Fragment>
97
+ ))
98
+ : (itemParentContext.registry.getItem(context.selectedValues[0])?.label ??
99
+ context.value)
75
100
  const children =
76
- childrenProp ?? renderedValue ?? itemParentContext.selectedItem ?? context.value
101
+ childrenProp !== undefined
102
+ ? childrenProp
103
+ : context.renderValue
104
+ ? context.renderValue(context.value)
105
+ : registeredValue
77
106
  const selectValueChildren = isEmptyValue ? (placeholder ?? children) : children
78
107
 
79
108
  return (
80
- <SelectValueFrame
81
- {...(!props.unstyled && {
82
- size: itemParentContext.size as any,
83
- ellipsis: true,
84
- // we don't want events from the portalled `SelectValue` children to bubble
85
- // through the item they came from
86
- pointerEvents: 'none',
87
- })}
88
- ref={composedRefs}
89
- {...props}
90
- >
91
- {unwrapSelectItem(selectValueChildren)}
109
+ <SelectValueFrame pointerEvents="none" ref={composedRefs} {...props}>
110
+ {selectValueChildren}
92
111
  </SelectValueFrame>
93
112
  )
94
113
  }
95
114
  )
96
115
 
97
- function unwrapSelectItem(selectValueChildren: any) {
98
- return React.Children.map(selectValueChildren, (child) => {
99
- if (child) {
100
- if (child.type?.staticConfig?.componentName === ITEM_TEXT_NAME) {
101
- return child.props.children
102
- }
103
- if (child.props?.children) {
104
- return unwrapSelectItem(child.props.children)
105
- }
106
- }
107
- return child
108
- })
109
- }
110
-
111
116
  /* -------------------------------------------------------------------------------------------------
112
117
  * SelectIcon
113
118
  * -----------------------------------------------------------------------------------------------*/
114
119
 
115
- export const SelectIcon = styled(XStack, {
116
- name: 'SelectIcon',
120
+ export const SelectIcon = styled(View, {
121
+ displayName: 'SelectIcon',
117
122
  // @ts-ignore
118
123
  'aria-hidden': true,
119
- children: <Paragraph>▼</Paragraph>,
120
124
  })
121
125
 
122
126
  /* -------------------------------------------------------------------------------------------------
123
127
  * SelectItemIndicator
124
128
  * -----------------------------------------------------------------------------------------------*/
125
129
 
126
- const SelectItemIndicatorFrame = styled(XStack, {
127
- name: 'SelectItemIndicator',
130
+ export const SelectItemIndicatorFrame = styled(View, {
131
+ displayName: 'SelectItemIndicator',
128
132
  })
129
133
 
130
- type SelectItemIndicatorProps = SelectScopedProps<
134
+ export type SelectItemIndicatorProps = SelectScopedProps<
131
135
  GetProps<typeof SelectItemIndicatorFrame>
132
136
  >
133
137
 
134
- const SelectItemIndicator = React.forwardRef<TamaguiElement, SelectItemIndicatorProps>(
135
- function SelectItemIndicator(props, forwardedRef) {
138
+ export const SelectItemIndicator = createStyledHOC(
139
+ SelectItemIndicatorFrame,
140
+ function SelectItemIndicator(
141
+ props: {
142
+ scope?: string
143
+ },
144
+ forwardedRef
145
+ ) {
136
146
  const { scope, ...itemIndicatorProps } = props
137
147
  const context = useSelectItemParentContext(scope)
138
148
  const itemContext = useSelectItemContext(scope)
@@ -151,77 +161,68 @@ const SelectItemIndicator = React.forwardRef<TamaguiElement, SelectItemIndicator
151
161
  * SelectIndicator
152
162
  * -----------------------------------------------------------------------------------------------*/
153
163
 
154
- const SelectIndicatorFrame = styled(YStack, {
155
- name: 'SelectIndicator',
156
-
157
- variants: {
158
- unstyled: {
159
- false: {
160
- position: 'absolute',
161
- top: 0,
162
- left: 0,
163
- pointerEvents: 'none',
164
- zIndex: 10,
165
- backgroundColor: '$background',
166
- borderRadius: 0,
167
- },
168
- },
169
- } as const,
170
-
171
- defaultVariants: {
172
- unstyled: process.env.TAMAGUI_HEADLESS === '1',
173
- },
164
+ export const SelectIndicatorFrame = styled(View, {
165
+ displayName: 'SelectIndicator',
166
+ position: 'absolute',
167
+ top: 0,
168
+ left: 0,
169
+ pointerEvents: 'none',
170
+ zIndex: 10,
174
171
  })
175
172
 
176
173
  export type SelectIndicatorProps = GetProps<typeof SelectIndicatorFrame>
177
174
 
178
- const SelectIndicator = SelectIndicatorFrame.styleable<
179
- SelectScopedProps<SelectIndicatorProps>
180
- >(function SelectIndicator({ scope, ...props }, forwardedRef) {
181
- const itemContext = useSelectItemParentContext(scope)
182
- const context = useSelectContext(scope)
183
- const [layout, setLayout] = React.useState<any>(null)
184
-
185
- const rafRef = React.useRef<any>(0)
186
-
187
- React.useLayoutEffect(() => {
188
- const update = (index: number | null) => {
189
- if (typeof index !== 'number') return
190
- cancelAnimationFrame(rafRef.current)
191
- rafRef.current = requestAnimationFrame(() => {
192
- const node = itemContext.listRef?.current?.[index]
193
- if (node) {
194
- setLayout({
195
- width: Math.round(node.offsetWidth),
196
- height: Math.round(node.offsetHeight),
197
- x: Math.round(node.offsetLeft),
198
- y: Math.round(node.offsetTop),
199
- })
200
- }
201
- })
202
- }
175
+ export const SelectIndicator = createStyledHOC(
176
+ SelectIndicatorFrame,
177
+ function SelectIndicator(
178
+ { scope, ...props }: SelectScopedProps<SelectIndicatorProps>,
179
+ forwardedRef
180
+ ) {
181
+ const itemContext = useSelectItemParentContext(scope)
182
+ const context = useSelectContext(scope)
183
+ const [layout, setLayout] = React.useState<any>(null)
184
+
185
+ const rafRef = React.useRef<any>(0)
186
+
187
+ React.useLayoutEffect(() => {
188
+ const update = (index: number | null) => {
189
+ if (typeof index !== 'number') return
190
+ cancelAnimationFrame(rafRef.current)
191
+ rafRef.current = requestAnimationFrame(() => {
192
+ const node = itemContext.listRef?.current?.[index]
193
+ if (node) {
194
+ setLayout({
195
+ width: Math.round(node.offsetWidth),
196
+ height: Math.round(node.offsetHeight),
197
+ x: Math.round(node.offsetLeft),
198
+ y: Math.round(node.offsetTop),
199
+ })
200
+ }
201
+ })
202
+ }
203
203
 
204
- // use ref for initial read to avoid depending on state
205
- if (context.open && context.activeIndexRef.current !== null) {
206
- update(context.activeIndexRef.current)
207
- }
204
+ // use ref for initial read to avoid depending on state
205
+ if (context.open && context.activeIndexRef.current !== null) {
206
+ update(context.activeIndexRef.current)
207
+ }
208
208
 
209
- return itemContext.activeIndexSubscribe(update)
210
- }, [context.open, itemContext.listRef])
209
+ return itemContext.activeIndexSubscribe(update)
210
+ }, [context.open, itemContext.listRef])
211
211
 
212
- if (!layout) return null
212
+ if (!layout) return null
213
213
 
214
- return (
215
- <SelectIndicatorFrame
216
- ref={forwardedRef}
217
- {...props}
218
- width={layout.width}
219
- height={layout.height}
220
- x={layout.x}
221
- y={layout.y}
222
- />
223
- )
224
- })
214
+ return (
215
+ <SelectIndicatorFrame
216
+ ref={forwardedRef}
217
+ {...props}
218
+ width={layout.width}
219
+ height={layout.height}
220
+ x={layout.x}
221
+ y={layout.y}
222
+ />
223
+ )
224
+ }
225
+ )
225
226
 
226
227
  /* -------------------------------------------------------------------------------------------------
227
228
  * SelectGroup
@@ -234,96 +235,61 @@ type SelectGroupContextValue = { id: string }
234
235
  const { Provider: SelectGroupContextProvider, useStyledContext: useSelectGroupContext } =
235
236
  createStyledContext<SelectGroupContextValue>({ id: '' }, 'SelectGroup')
236
237
 
237
- export const SelectGroupFrame = styled(YStack, {
238
- name: GROUP_NAME,
239
- width: '100%',
238
+ export const SelectGroupFrame = styled(View, {
239
+ displayName: GROUP_NAME,
240
240
  })
241
241
 
242
- const NativeSelectTextFrame = styled(SizableText, {
242
+ const NativeSelectFrame = styled(Text, {
243
+ displayName: 'NativeSelect',
243
244
  render: 'select',
244
- backgroundColor: '$background',
245
- borderColor: '$borderColor',
246
- hoverStyle: {
247
- backgroundColor: '$backgroundHover',
248
- },
249
- })
250
-
251
- const NativeSelectFrame = styled(YStack, {
252
- name: 'NativeSelect',
253
-
254
- variants: {
255
- size: {
256
- '...size': (val, extras) => {
257
- const { tokens } = extras
258
- const paddingHorizontal = getVariableValue(tokens.space[val])
259
-
260
- return {
261
- borderRadius: tokens.radius[val] ?? val,
262
- minHeight: tokens.size[val],
263
- paddingRight: paddingHorizontal + 20,
264
- paddingLeft: paddingHorizontal,
265
- paddingVertical: getSpace(val, {
266
- shift: -3,
267
- }),
268
- }
269
- },
270
- },
271
-
272
- unstyled: {
273
- false: {
274
- borderWidth: 1,
275
- borderColor: '$borderColor',
276
- userSelect: 'none',
277
- outlineWidth: 0,
278
- paddingRight: 10,
279
- },
280
- },
281
- } as const,
282
-
283
- defaultVariants: {
284
- size: '$2',
285
- unstyled: process.env.TAMAGUI_HEADLESS === '1' ? true : false,
286
- },
287
245
  })
246
+ const NativeSelect = NativeSelectFrame as any
288
247
 
289
248
  type SelectGroupProps = SelectScopedProps<GetProps<typeof SelectGroupFrame>>
290
249
 
291
- const SelectGroup = React.forwardRef<TamaguiElement, SelectGroupProps>(
292
- (props, forwardedRef) => {
250
+ export const SelectGroup = createStyledHOC(
251
+ SelectGroupFrame,
252
+ (props: SelectGroupProps, forwardedRef) => {
293
253
  const { scope, ...groupProps } = props
294
254
  const groupId = React.useId()
295
255
 
296
256
  const context = useSelectContext(scope)
297
257
  const itemParentContext = useSelectItemParentContext(scope)
298
- const size = itemParentContext.size ?? '$true'
258
+ const StyledNativeSelect = React.useContext(SelectNativeComponentContext)
299
259
  const nativeSelectRef = React.useRef<HTMLSelectElement>(null)
300
260
 
301
261
  const content = (() => {
302
262
  if (itemParentContext.shouldRenderWebNative) {
303
263
  return (
304
- <NativeSelectFrame
305
- asChild
306
- size={size}
307
- // @ts-expect-error until we support typing based on tag
264
+ <NativeSelect
265
+ {...(StyledNativeSelect && {
266
+ render: StyledNativeSelect,
267
+ size: itemParentContext.size,
268
+ style: { appearance: 'none' },
269
+ })}
270
+ {...groupProps}
271
+ // @ts-ignore it's ok since render="select"
308
272
  value={context.value}
273
+ multiple={context.mode === 'multiple'}
274
+ name={itemParentContext.name}
275
+ form={itemParentContext.form}
309
276
  id={itemParentContext.id}
277
+ onChange={
278
+ ((event: React.ChangeEvent<HTMLSelectElement>) => {
279
+ const value =
280
+ context.mode === 'multiple'
281
+ ? Array.from(
282
+ event.currentTarget.selectedOptions,
283
+ (option) => option.value
284
+ )
285
+ : event.currentTarget.value
286
+ itemParentContext.changeNativeValue(value, event.nativeEvent)
287
+ }) as any
288
+ }
289
+ ref={nativeSelectRef as any}
310
290
  >
311
- <NativeSelectTextFrame
312
- // @ts-ignore it's ok since render="select"
313
- onChange={(event: React.ChangeEvent<HTMLSelectElement>) => {
314
- itemParentContext.onChange(event.currentTarget.value)
315
- }}
316
- size={size as FontSizeTokens}
317
- ref={nativeSelectRef as any}
318
- style={{
319
- color: 'var(--color)',
320
- // @ts-ignore
321
- appearance: 'none',
322
- }}
323
- >
324
- {props.children}
325
- </NativeSelectTextFrame>
326
- </NativeSelectFrame>
291
+ {props.children}
292
+ </NativeSelect>
327
293
  )
328
294
  }
329
295
  return (
@@ -353,39 +319,14 @@ SelectGroup.displayName = GROUP_NAME
353
319
 
354
320
  const LABEL_NAME = 'SelectLabel'
355
321
 
356
- const SelectLabelFrame = styled(SizableText, {
357
- name: LABEL_NAME,
358
-
359
- variants: {
360
- unstyled: {
361
- false: {
362
- size: '$true',
363
- ellipsis: true,
364
- maxWidth: '100%',
365
- cursor: 'default',
366
- },
367
- },
368
-
369
- size: {
370
- '...size': (val: SizeTokens, { tokens }) => {
371
- return {
372
- paddingHorizontal: tokens.space[val],
373
- paddingVertical: getSpace(tokens.space[val], {
374
- shift: -4,
375
- }),
376
- }
377
- },
378
- },
379
- } as const,
380
-
381
- defaultVariants: {
382
- unstyled: process.env.TAMAGUI_HEADLESS === '1',
383
- },
322
+ export const SelectLabelFrame = styled(Text, {
323
+ displayName: LABEL_NAME,
384
324
  })
385
325
 
386
326
  export type SelectLabelProps = SelectScopedProps<GetProps<typeof SelectLabelFrame>>
387
327
 
388
- const SelectLabel = SelectLabelFrame.styleable<{ scope?: any }>(
328
+ export const SelectLabel = createStyledHOC(
329
+ SelectLabelFrame,
389
330
  (props: SelectLabelProps, forwardedRef) => {
390
331
  const { scope, ...labelProps } = props
391
332
  const context = useSelectItemParentContext(scope)
@@ -399,7 +340,6 @@ const SelectLabel = SelectLabelFrame.styleable<{ scope?: any }>(
399
340
  <SelectLabelFrame
400
341
  render="div"
401
342
  id={groupContext.id}
402
- size={context.size}
403
343
  {...labelProps}
404
344
  ref={forwardedRef}
405
345
  />
@@ -411,36 +351,10 @@ const SelectLabel = SelectLabelFrame.styleable<{ scope?: any }>(
411
351
  * SelectSeparator
412
352
  * -----------------------------------------------------------------------------------------------*/
413
353
 
414
- export const SelectSeparator = styled(Separator, {
415
- name: 'SelectSeparator',
354
+ export const SelectSeparator = styled(View, {
355
+ displayName: 'SelectSeparator',
416
356
  })
417
357
 
418
- const SelectSheetController = (
419
- props: SelectScopedProps<{
420
- children: React.ReactNode
421
- onOpenChange: React.Dispatch<React.SetStateAction<boolean>>
422
- }>
423
- ) => {
424
- const context = useSelectContext(props.scope)
425
- const showSheet = useShowSelectSheet(context)
426
- const isAdapted = useAdaptIsActive(context.adaptScope)
427
- const getShowSheet = useGet(showSheet)
428
-
429
- return (
430
- <SheetController
431
- onOpenChange={(val) => {
432
- if (getShowSheet()) {
433
- props.onOpenChange(val)
434
- }
435
- }}
436
- open={context.open}
437
- hidden={!isAdapted}
438
- >
439
- {props.children}
440
- </SheetController>
441
- )
442
- }
443
-
444
358
  const SelectSheetImpl = (props: SelectImplProps) => {
445
359
  return <>{props.children}</>
446
360
  }
@@ -449,36 +363,67 @@ const SelectSheetImpl = (props: SelectImplProps) => {
449
363
  * Select
450
364
  * -----------------------------------------------------------------------------------------------*/
451
365
 
452
- export const Select = withStaticProperties(
453
- function Select<Value extends string = string>(
454
- props: SelectScopedProps<SelectProps<Value>>
455
- ) {
456
- const adaptScope = `AdaptSelect${props.scope || ''}`
366
+ export function SelectRoot<
367
+ Value extends string = string,
368
+ Multiple extends boolean | undefined = false,
369
+ >(props: SelectScopedProps<SelectProps<Value, Multiple>>) {
370
+ const adaptScope = `AdaptSelect${props.scope || ''}`
371
+
372
+ // open state lives here (above AdaptParent) so the Adapt handoff can drive
373
+ // the adapted Sheet's open/close and unmount timing directly, mirroring Dialog
374
+ const [open, requestOpenChange] = useControllableState<
375
+ boolean,
376
+ SelectOpenChangeDetails
377
+ >({
378
+ prop: props.open,
379
+ defaultProp: props.defaultOpen || false,
380
+ onChange: props.onOpenChange as any,
381
+ })
457
382
 
458
- return (
459
- <AdaptParent scope={adaptScope} portal>
460
- <SelectInner scope={props.scope} adaptScope={adaptScope} {...props} />
461
- </AdaptParent>
462
- )
463
- },
464
- {
465
- Adapt: Adapt,
466
- Content: SelectContent,
467
- Group: SelectGroup,
468
- Icon: SelectIcon,
469
- Item: SelectItem,
470
- ItemIndicator: SelectItemIndicator,
471
- ItemText: SelectItemText,
472
- Label: SelectLabel,
473
- ScrollDownButton: SelectScrollDownButton,
474
- ScrollUpButton: SelectScrollUpButton,
475
- Trigger: SelectTrigger,
476
- Value: SelectValue,
477
- Viewport: SelectViewport,
478
- Indicator: SelectIndicator,
479
- FocusScope: FocusScopeController,
480
- }
481
- )
383
+ const handleAdaptOpenChange = React.useCallback(
384
+ (nextOpen: boolean) => {
385
+ requestOpenChange(
386
+ nextOpen,
387
+ createChangeEventDetails(
388
+ nextOpen ? 'trigger-press' : 'outside-press'
389
+ ) as SelectOpenChangeDetails
390
+ )
391
+ },
392
+ [requestOpenChange]
393
+ )
394
+
395
+ return (
396
+ <AdaptParent scope={adaptScope} open={open} onOpenChange={handleAdaptOpenChange}>
397
+ <SelectInner
398
+ {...(props as SelectScopedProps<SelectProps<string, boolean>>)}
399
+ scope={props.scope}
400
+ adaptScope={adaptScope}
401
+ open={open}
402
+ requestOpenChange={requestOpenChange}
403
+ />
404
+ </AdaptParent>
405
+ )
406
+ }
407
+
408
+ export const Select = withStaticProperties(SelectRoot, {
409
+ Root: SelectRoot,
410
+ Adapt: Adapt,
411
+ Content: SelectContent,
412
+ Group: SelectGroup,
413
+ Icon: SelectIcon,
414
+ Item: SelectItem,
415
+ ItemIndicator: SelectItemIndicator,
416
+ ItemText: SelectItemText,
417
+ Label: SelectLabel,
418
+ ScrollDownButton: SelectScrollDownButton,
419
+ ScrollUpButton: SelectScrollUpButton,
420
+ Separator: SelectSeparator,
421
+ Trigger: SelectTrigger,
422
+ Value: SelectValue,
423
+ Viewport: SelectViewport,
424
+ Indicator: SelectIndicator,
425
+ FocusScope: FocusScopeController,
426
+ })
482
427
 
483
428
  function useEmitter<A>() {
484
429
  const listenersRef = React.useRef<Set<Function>>(new Set())
@@ -494,20 +439,30 @@ function useEmitter<A>() {
494
439
  return [emit, subscribe] as const
495
440
  }
496
441
 
497
- function SelectInner(props: SelectScopedProps<SelectProps> & { adaptScope: string }) {
442
+ function SelectInner(
443
+ props: SelectScopedProps<SelectProps<string, boolean>> & {
444
+ adaptScope: string
445
+ open: boolean
446
+ requestOpenChange: ControllableStateSetter<boolean, SelectOpenChangeDetails>
447
+ }
448
+ ) {
498
449
  const {
499
450
  scope = '',
500
451
  adaptScope,
501
452
  native,
502
453
  children,
503
- open: openProp,
504
- defaultOpen,
505
- onOpenChange,
454
+ open,
455
+ requestOpenChange,
456
+ defaultOpen: _defaultOpen,
457
+ onOpenChange: _onOpenChange,
506
458
  value: valueProp,
507
459
  defaultValue,
508
460
  onValueChange,
461
+ multiple = false,
462
+ name,
463
+ form,
509
464
  disablePreventBodyScroll,
510
- size: sizeProp = '$true',
465
+ size: sizeProp = true,
511
466
  onActiveChange,
512
467
  dir,
513
468
  id,
@@ -517,45 +472,69 @@ function SelectInner(props: SelectScopedProps<SelectProps> & { adaptScope: strin
517
472
  } = props
518
473
 
519
474
  const isAdapted = useAdaptIsActive(adaptScope)
520
- const SelectImpl = isAdapted || !isWeb ? SelectSheetImpl : SelectInlineImpl
521
- const forceUpdate = React.useReducer(() => ({}), {})[1]
522
- const [selectedItem, setSelectedItem] = React.useState<React.ReactNode>(null)
523
-
524
- const [open, setOpen] = useControllableState({
525
- prop: openProp,
526
- defaultProp: defaultOpen || false,
527
- onChange: onOpenChange,
528
- })
529
-
530
- const [value, setValue] = useControllableState({
531
- prop: valueProp,
532
- defaultProp: defaultValue || '',
533
- onChange: onValueChange,
475
+ const SelectImpl = isAdapted ? SelectSheetImpl : SelectInlineImpl
476
+ const mode: SelectMode = multiple ? 'multiple' : 'single'
477
+
478
+ const [valueState, requestValueChange] = useControllableState<
479
+ SelectSelection,
480
+ SelectValueChangeDetails
481
+ >({
482
+ prop: valueProp as SelectSelection | undefined,
483
+ defaultProp: (defaultValue ?? (mode === 'multiple' ? [] : '')) as SelectSelection,
484
+ onChange: onValueChange as any,
534
485
  transition: true,
535
486
  })
536
487
 
537
- React.useEffect(() => {
538
- if (open) {
539
- emitValue(value)
540
- }
541
- }, [open])
488
+ const value = React.useMemo(
489
+ () =>
490
+ mode === 'multiple'
491
+ ? selectedValuesFromSelection(mode, valueState)
492
+ : typeof valueState === 'string'
493
+ ? valueState
494
+ : '',
495
+ [mode, valueState]
496
+ )
497
+ const selectedValues = React.useMemo(
498
+ () => selectedValuesFromSelection(mode, value),
499
+ [mode, value]
500
+ )
542
501
 
543
- React.useEffect(() => {
544
- emitValue(value)
545
- }, [value])
546
-
547
- if (process.env.TAMAGUI_TARGET === 'native') {
548
- React.useEffect(() => {
549
- if (!props.id) return
550
-
551
- return registerFocusable(props.id, {
552
- focusAndSelect: () => {
553
- setOpen?.((value) => !value)
554
- },
555
- focus: () => {},
556
- })
557
- }, [props.id])
502
+ const [, rerenderRegistry] = React.useReducer((version) => version + 1, 0)
503
+ const registryRef = React.useRef<ReturnType<typeof createSelectItemRegistry> | null>(
504
+ null
505
+ )
506
+ if (!registryRef.current) {
507
+ registryRef.current = createSelectItemRegistry(rerenderRegistry)
558
508
  }
509
+ const registry = registryRef.current
510
+
511
+ const controllerRef = React.useRef<ReturnType<
512
+ typeof createSelectSelectionController
513
+ > | null>(null)
514
+ if (!controllerRef.current) {
515
+ controllerRef.current = createSelectSelectionController({
516
+ mode,
517
+ value,
518
+ registry,
519
+ })
520
+ }
521
+ const controller = controllerRef.current
522
+ controller.setMode(mode)
523
+ controller.setValue(value)
524
+
525
+ React.useEffect(() => {
526
+ if (process.env.TAMAGUI_TARGET !== 'native' || !props.id) return
527
+
528
+ return registerFocusable(props.id, {
529
+ focusAndSelect: () => {
530
+ requestOpenChange(
531
+ (current) => !current,
532
+ createChangeEventDetails('trigger-press') as SelectOpenChangeDetails
533
+ )
534
+ },
535
+ focus: () => {},
536
+ })
537
+ }, [props.id, requestOpenChange])
559
538
 
560
539
  // activeIndex is stored in a ref to avoid re-renders on every hover
561
540
  // we have two setters:
@@ -565,14 +544,17 @@ function SelectInner(props: SelectScopedProps<SelectProps> & { adaptScope: strin
565
544
  const activeIndexRef = React.useRef<number | null>(null)
566
545
  const [activeIndex, setActiveIndexState] = React.useState<number | null>(null)
567
546
 
568
- const [emitValue, valueSubscribe] = useEmitter<any>()
569
547
  const [emitActiveIndex, activeIndexSubscribe] = useEmitter<number>()
570
548
 
571
549
  const selectedIndexRef = React.useRef<number | null>(null)
572
550
  const listContentRef = React.useRef<string[]>([])
573
- const [selectedIndex, setSelectedIndex] = React.useState(0)
551
+ const listRef = React.useRef<Array<HTMLElement | null>>([])
574
552
  const [valueNode, setValueNode] = React.useState<HTMLElement | null>(null)
553
+ const selectedIndex = Math.max(0, controller.selectionAnchorIndex())
554
+ listContentRef.current = registry.getTypeaheadLabels()
555
+ listRef.current.length = registry.getItems().length
575
556
 
557
+ // Intentionally dependency-less: selectedIndexRef mirrors every render for non-reactive reads.
576
558
  useIsomorphicLayoutEffect(() => {
577
559
  selectedIndexRef.current = selectedIndex
578
560
  })
@@ -586,54 +568,158 @@ function SelectInner(props: SelectScopedProps<SelectProps> & { adaptScope: strin
586
568
  // fast setter: updates ref + emits to subscribers without causing re-renders
587
569
  // use this for mouse hover / keyboard navigation where we don't need parent re-renders
588
570
  const setActiveIndexFast = React.useCallback(
589
- (index: number | null) => {
571
+ (index: number | null, details?: SelectActiveChangeDetails) => {
590
572
  if (activeIndexRef.current !== index) {
591
573
  activeIndexRef.current = index
574
+ controller.setActiveIndex(index)
592
575
  if (typeof index === 'number') {
593
576
  emitActiveIndex(index)
577
+ const item = registry.getItems()[index]
578
+ if (item && details) {
579
+ onActiveChange?.(item.value, details)
580
+ }
594
581
  }
595
582
  }
596
583
  },
597
- [emitActiveIndex]
584
+ [controller, emitActiveIndex, onActiveChange, registry]
598
585
  )
599
586
 
600
587
  // slow setter: also triggers a re-render for components that need the state value
601
588
  // use this sparingly, e.g., when controlled scrolling needs to scroll item into view
602
589
  const setActiveIndex = React.useCallback(
603
- (index: number | null) => {
604
- setActiveIndexFast(index)
590
+ (index: number | null, details?: SelectActiveChangeDetails) => {
591
+ setActiveIndexFast(index, details)
605
592
  setActiveIndexState(index)
606
593
  },
607
594
  [setActiveIndexFast]
608
595
  )
609
596
 
597
+ const activeDetails = React.useCallback(
598
+ (
599
+ reason: SelectActiveChangeDetails['reason'],
600
+ index: number,
601
+ event?: unknown
602
+ ): SelectActiveChangeDetails => ({
603
+ reason,
604
+ event,
605
+ trigger: undefined,
606
+ index,
607
+ }),
608
+ []
609
+ )
610
+
611
+ const moveActive = React.useCallback(
612
+ (direction: 1 | -1, event?: unknown) => {
613
+ const nextIndex = registry.nextEnabledIndex(activeIndexRef.current, direction)
614
+ if (nextIndex >= 0) {
615
+ setActiveIndex(nextIndex, activeDetails('keyboard', nextIndex, event))
616
+ }
617
+ },
618
+ [activeDetails, registry, setActiveIndex]
619
+ )
620
+
621
+ const typeaheadRef = React.useRef({ text: '', timeout: null as any })
622
+ const search = React.useCallback(
623
+ (text: string, event?: unknown) => {
624
+ clearTimeout(typeaheadRef.current.timeout)
625
+ typeaheadRef.current.text += text
626
+ let nextIndex = registry.findTypeaheadIndex(
627
+ typeaheadRef.current.text,
628
+ activeIndexRef.current
629
+ )
630
+ if (nextIndex < 0 && typeaheadRef.current.text.length > 1) {
631
+ typeaheadRef.current.text = text
632
+ nextIndex = registry.findTypeaheadIndex(text, activeIndexRef.current)
633
+ }
634
+ typeaheadRef.current.timeout = setTimeout(() => {
635
+ typeaheadRef.current.text = ''
636
+ }, 750)
637
+ if (nextIndex >= 0) {
638
+ setActiveIndex(nextIndex, activeDetails('keyboard', nextIndex, event))
639
+ }
640
+ },
641
+ [activeDetails, registry, setActiveIndex]
642
+ )
643
+
644
+ React.useEffect(() => {
645
+ return () => clearTimeout(typeaheadRef.current.timeout)
646
+ }, [])
647
+
648
+ React.useEffect(() => {
649
+ if (open) {
650
+ const nextIndex = controller.initialActiveIndex()
651
+ if (nextIndex >= 0) {
652
+ setActiveIndex(nextIndex, activeDetails('list-navigation', nextIndex))
653
+ }
654
+ } else {
655
+ setActiveIndexFast(null)
656
+ setActiveIndexState(null)
657
+ }
658
+ }, [open, selectedIndex, registry.getItems().length])
659
+
660
+ const selectValue = React.useCallback(
661
+ (nextItemValue: string, details: SelectValueChangeDetails) => {
662
+ const item = registry.getItem(nextItemValue)
663
+ if (!item || item.disabled) return
664
+ controller.setMode(mode)
665
+ controller.setValue(value)
666
+ const previousValue = controller.value
667
+ const nextValue = controller.toggle(nextItemValue)
668
+ requestValueChange(nextValue, details)
669
+ if (details.isCanceled) {
670
+ controller.setValue(previousValue)
671
+ return
672
+ }
673
+ if (controller.shouldCloseOnSelect) {
674
+ requestOpenChange(
675
+ false,
676
+ createChangeEventDetails(
677
+ details.reason === 'keyboard' ? 'keyboard' : 'item-press',
678
+ details.event,
679
+ details.trigger
680
+ ) as SelectOpenChangeDetails
681
+ )
682
+ }
683
+ },
684
+ [controller, mode, registry, requestOpenChange, requestValueChange, value]
685
+ )
686
+
687
+ const changeNativeValue = React.useCallback(
688
+ (nextValue: SelectSelection, event: unknown) => {
689
+ const details = createChangeEventDetails(
690
+ 'native-change',
691
+ event
692
+ ) as SelectValueChangeDetails
693
+ requestValueChange(nextValue, details)
694
+ if (!details.isCanceled) {
695
+ controller.setValue(nextValue)
696
+ }
697
+ },
698
+ [controller, requestValueChange]
699
+ )
700
+
610
701
  const content = (
611
702
  <SelectItemParentProvider
612
703
  scopeName={scope}
613
704
  scope={scope}
614
705
  adaptScope={adaptScope}
615
- initialValue={React.useMemo(() => value, [open])}
706
+ mode={mode}
707
+ selectedValues={selectedValues}
708
+ registry={registry}
616
709
  size={sizeProp}
617
710
  activeIndexSubscribe={activeIndexSubscribe}
618
711
  activeIndexRef={activeIndexRef}
619
- valueSubscribe={valueSubscribe}
620
- setOpen={setOpen}
712
+ requestOpenChange={requestOpenChange}
621
713
  id={id}
622
- onChange={React.useCallback((val) => {
623
- setValue(val)
624
- emitValue(val)
625
- }, [])}
626
- onActiveChange={useEvent((value, index) => {
627
- onActiveChange?.(value, index)
628
- })}
629
- setSelectedIndex={setSelectedIndex}
630
- setValueAtIndex={React.useCallback((index, value) => {
631
- listContentRef.current[index] = value
632
- }, [])}
714
+ name={name}
715
+ form={form}
716
+ selectValue={selectValue}
717
+ changeNativeValue={changeNativeValue}
633
718
  shouldRenderWebNative={shouldRenderWebNative}
634
719
  setActiveIndexFast={setActiveIndexFast}
635
- selectedItem={selectedItem}
636
- setSelectedItem={setSelectedItem}
720
+ listRef={listRef}
721
+ moveActive={moveActive}
722
+ search={search}
637
723
  >
638
724
  <SelectProvider
639
725
  scope={scope}
@@ -643,7 +729,6 @@ function SelectInner(props: SelectScopedProps<SelectProps> & { adaptScope: strin
643
729
  dir={dir}
644
730
  blockSelection={false}
645
731
  fallback={false}
646
- forceUpdate={forceUpdate}
647
732
  valueNode={valueNode}
648
733
  onValueNodeChange={setValueNode}
649
734
  activeIndex={activeIndex}
@@ -651,29 +736,44 @@ function SelectInner(props: SelectScopedProps<SelectProps> & { adaptScope: strin
651
736
  selectedIndex={selectedIndex}
652
737
  setActiveIndex={setActiveIndex}
653
738
  value={value}
739
+ mode={mode}
740
+ selectedValues={selectedValues}
741
+ activeItem={
742
+ activeIndex == null ? undefined : registry.getItems()[activeIndex]?.value
743
+ }
744
+ selectionAnchor={controller.selectionAnchor()?.value}
654
745
  open={open}
655
746
  native={native}
656
747
  renderValue={renderValue}
657
748
  lazyMount={lazyMount}
658
749
  >
659
- <SelectSheetController onOpenChange={setOpen} scope={scope}>
660
- {shouldRenderWebNative ? (
661
- children
662
- ) : (
663
- <SelectImpl
664
- activeIndexRef={activeIndexRef}
665
- listContentRef={listContentRef}
666
- selectedIndexRef={selectedIndexRef}
667
- setActiveIndexFast={setActiveIndexFast}
668
- {...props}
669
- open={open}
670
- value={value}
671
- >
672
- {children}
673
- </SelectImpl>
674
- )}
675
- </SelectSheetController>
750
+ {shouldRenderWebNative ? (
751
+ children
752
+ ) : (
753
+ <SelectImpl
754
+ activeIndexRef={activeIndexRef}
755
+ listContentRef={listContentRef}
756
+ selectedIndexRef={selectedIndexRef}
757
+ setActiveIndexFast={setActiveIndexFast}
758
+ {...props}
759
+ open={open}
760
+ value={value}
761
+ >
762
+ {children}
763
+ </SelectImpl>
764
+ )}
676
765
  </SelectProvider>
766
+ {isWeb && !shouldRenderWebNative && name
767
+ ? (mode === 'multiple' ? selectedValues : [value as string]).map((inputValue) => (
768
+ <input
769
+ key={inputValue}
770
+ type="hidden"
771
+ name={name}
772
+ form={form}
773
+ value={inputValue}
774
+ />
775
+ ))
776
+ : null}
677
777
  </SelectItemParentProvider>
678
778
  )
679
779