@tamagui/select 2.7.7 → 3.0.0-beta.1091.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 +406 -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 +79 -79
  6. package/dist/cjs/SelectContent.native.cjs +29 -0
  7. package/dist/cjs/SelectContent.native.js +17 -22
  8. package/dist/cjs/SelectContent.native.js.map +1 -1
  9. package/dist/cjs/SelectImpl.cjs +305 -320
  10. package/dist/cjs/SelectImpl.native.cjs +29 -0
  11. package/dist/cjs/SelectImpl.native.js +16 -24
  12. package/dist/cjs/SelectImpl.native.js.map +1 -1
  13. package/dist/cjs/SelectItem.cjs +256 -205
  14. package/dist/cjs/SelectItem.native.cjs +288 -0
  15. package/dist/cjs/SelectItem.native.js +264 -213
  16. package/dist/cjs/SelectItem.native.js.map +1 -1
  17. package/dist/cjs/SelectItemText.cjs +48 -79
  18. package/dist/cjs/SelectItemText.native.cjs +73 -0
  19. package/dist/cjs/SelectItemText.native.js +49 -79
  20. package/dist/cjs/SelectItemText.native.js.map +1 -1
  21. package/dist/cjs/SelectScrollButton.cjs +110 -130
  22. package/dist/cjs/SelectScrollButton.native.cjs +37 -0
  23. package/dist/cjs/SelectScrollButton.native.js +24 -22
  24. package/dist/cjs/SelectScrollButton.native.js.map +1 -1
  25. package/dist/cjs/SelectTrigger.cjs +68 -84
  26. package/dist/cjs/SelectTrigger.native.cjs +90 -0
  27. package/dist/cjs/SelectTrigger.native.js +70 -66
  28. package/dist/cjs/SelectTrigger.native.js.map +1 -1
  29. package/dist/cjs/SelectViewport.cjs +121 -142
  30. package/dist/cjs/SelectViewport.native.cjs +72 -0
  31. package/dist/cjs/SelectViewport.native.js +53 -50
  32. package/dist/cjs/SelectViewport.native.js.map +1 -1
  33. package/dist/cjs/constants.cjs +20 -22
  34. package/dist/cjs/constants.native.cjs +40 -0
  35. package/dist/cjs/constants.native.js +20 -21
  36. package/dist/cjs/constants.native.js.map +1 -1
  37. package/dist/cjs/context.cjs +37 -48
  38. package/dist/cjs/context.native.cjs +57 -0
  39. package/dist/cjs/context.native.js +39 -49
  40. package/dist/cjs/context.native.js.map +1 -1
  41. package/dist/cjs/index.cjs +14 -11
  42. package/dist/cjs/index.native.cjs +26 -0
  43. package/dist/cjs/index.native.js +14 -10
  44. package/dist/cjs/index.native.js.map +1 -1
  45. package/dist/cjs/selectionController.cjs +343 -0
  46. package/dist/cjs/selectionController.native.cjs +407 -0
  47. package/dist/cjs/selectionController.native.js +409 -0
  48. package/dist/cjs/selectionController.native.js.map +1 -0
  49. package/dist/cjs/types.cjs +9 -11
  50. package/dist/cjs/types.native.cjs +18 -0
  51. package/dist/cjs/types.native.js +9 -10
  52. package/dist/cjs/types.native.js.map +1 -1
  53. package/dist/esm/Select.mjs +381 -443
  54. package/dist/esm/Select.mjs.map +1 -1
  55. package/dist/esm/Select.native.js +420 -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 +291 -295
  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 +240 -181
  66. package/dist/esm/SelectItem.mjs.map +1 -1
  67. package/dist/esm/SelectItem.native.js +249 -191
  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 +107 -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 +315 -0
  97. package/dist/esm/selectionController.mjs.map +1 -0
  98. package/dist/esm/selectionController.native.js +377 -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 +381 -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 +29 -0
  110. package/dist/jsx/SelectContent.native.js +17 -22
  111. package/dist/jsx/SelectContent.native.js.map +1 -1
  112. package/dist/jsx/SelectImpl.mjs +291 -295
  113. package/dist/jsx/SelectImpl.mjs.map +1 -1
  114. package/dist/jsx/SelectImpl.native.cjs +29 -0
  115. package/dist/jsx/SelectImpl.native.js +16 -24
  116. package/dist/jsx/SelectImpl.native.js.map +1 -1
  117. package/dist/jsx/SelectItem.mjs +240 -181
  118. package/dist/jsx/SelectItem.mjs.map +1 -1
  119. package/dist/jsx/SelectItem.native.cjs +288 -0
  120. package/dist/jsx/SelectItem.native.js +264 -213
  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 +73 -0
  125. package/dist/jsx/SelectItemText.native.js +49 -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 +37 -0
  130. package/dist/jsx/SelectScrollButton.native.js +24 -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 +90 -0
  135. package/dist/jsx/SelectTrigger.native.js +70 -66
  136. package/dist/jsx/SelectTrigger.native.js.map +1 -1
  137. package/dist/jsx/SelectViewport.mjs +107 -119
  138. package/dist/jsx/SelectViewport.mjs.map +1 -1
  139. package/dist/jsx/SelectViewport.native.cjs +72 -0
  140. package/dist/jsx/SelectViewport.native.js +53 -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 +40 -0
  145. package/dist/jsx/constants.native.js +20 -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 +57 -0
  150. package/dist/jsx/context.native.js +39 -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 +26 -0
  155. package/dist/jsx/index.native.js +14 -10
  156. package/dist/jsx/index.native.js.map +1 -1
  157. package/dist/jsx/selectionController.mjs +315 -0
  158. package/dist/jsx/selectionController.mjs.map +1 -0
  159. package/dist/jsx/selectionController.native.cjs +407 -0
  160. package/dist/jsx/selectionController.native.js +409 -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 +18 -0
  164. package/dist/jsx/types.native.js +9 -10
  165. package/dist/jsx/types.native.js.map +1 -1
  166. package/package.json +31 -30
  167. package/src/Select.tsx +478 -378
  168. package/src/SelectContent.tsx +46 -16
  169. package/src/SelectImpl.native.tsx +0 -8
  170. package/src/SelectImpl.tsx +149 -160
  171. package/src/SelectItem.tsx +280 -159
  172. package/src/SelectItemText.tsx +16 -33
  173. package/src/SelectScrollButton.native.tsx +13 -4
  174. package/src/SelectScrollButton.tsx +57 -42
  175. package/src/SelectTrigger.tsx +77 -53
  176. package/src/SelectViewport.native.tsx +42 -25
  177. package/src/SelectViewport.tsx +60 -60
  178. package/src/context.tsx +6 -2
  179. package/src/index.tsx +5 -2
  180. package/src/selectionController.ts +404 -0
  181. package/src/types.tsx +105 -59
  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 +90 -0
  210. package/types/selectionController.d.ts.map +1 -0
  211. package/types/types.d.ts +66 -50
  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
@@ -0,0 +1,404 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ export type SelectMode = 'single' | 'multiple'
4
+ export type SelectSelection = string | string[]
5
+
6
+ export type SelectRegisteredItem = {
7
+ id: symbol
8
+ value: string
9
+ disabled: boolean
10
+ textValue?: string
11
+ label?: ReactNode
12
+ }
13
+
14
+ export type SelectItemRegistration = {
15
+ id: symbol
16
+ update(item: Partial<Omit<SelectRegisteredItem, 'id'>>): void
17
+ setNode(node: unknown): void
18
+ unregister(): void
19
+ }
20
+
21
+ export type SelectItemRegistry = ReturnType<typeof createSelectItemRegistry>
22
+
23
+ export function getSelectLabelText(label: ReactNode): string {
24
+ if (typeof label === 'string' || typeof label === 'number') {
25
+ return String(label)
26
+ }
27
+ if (Array.isArray(label)) {
28
+ return label.map(getSelectLabelText).join('')
29
+ }
30
+ if (label && typeof label === 'object' && 'props' in label) {
31
+ return getSelectLabelText((label as any).props?.children)
32
+ }
33
+ return ''
34
+ }
35
+
36
+ export function createSelectItemRegistry(onChange?: () => void) {
37
+ const items: SelectRegisteredItem[] = []
38
+ const labelRegistrations = new Map<
39
+ string,
40
+ Map<symbol, { label: ReactNode; textValue?: string }>
41
+ >()
42
+ const explicitTextValues = new Map<symbol, string | undefined>()
43
+ const itemNodes = new Map<symbol, any>()
44
+ const listeners = new Set<() => void>()
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
+
50
+ const notify = () => {
51
+ indexByValue = null
52
+ onChange?.()
53
+ listeners.forEach((listener) => listener())
54
+ }
55
+
56
+ const getItems = () => items
57
+
58
+ const getItem = (value: string) => items.find((item) => item.value === value)
59
+
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)
72
+
73
+ const getRegisteredLabel = (value: string) => {
74
+ const registrations = labelRegistrations.get(value)
75
+ if (!registrations?.size) return undefined
76
+ const values = Array.from(registrations.values())
77
+ return values[values.length - 1]
78
+ }
79
+
80
+ const applyRegisteredLabel = (value: string) => {
81
+ const registeredLabel = getRegisteredLabel(value)
82
+ let changed = false
83
+ for (const item of items) {
84
+ if (item.value !== value) continue
85
+ const nextLabel = registeredLabel?.label
86
+ const nextTextValue = explicitTextValues.get(item.id) || registeredLabel?.textValue
87
+ if (item.label !== nextLabel || item.textValue !== nextTextValue) {
88
+ item.label = nextLabel
89
+ item.textValue = nextTextValue
90
+ changed = true
91
+ }
92
+ }
93
+ if (changed) notify()
94
+ }
95
+
96
+ const syncNodeOrder = () => {
97
+ items.sort((a, b) => {
98
+ const aNode = itemNodes.get(a.id)
99
+ const bNode = itemNodes.get(b.id)
100
+ if (!aNode || !bNode || typeof aNode.compareDocumentPosition !== 'function') {
101
+ return 0
102
+ }
103
+ const position = aNode.compareDocumentPosition(bNode)
104
+ if (position & 4) return -1
105
+ if (position & 2) return 1
106
+ return 0
107
+ })
108
+ }
109
+
110
+ const registerItem = (
111
+ item: Omit<SelectRegisteredItem, 'id'>
112
+ ): SelectItemRegistration => {
113
+ const id = Symbol(item.value)
114
+ const registeredLabel = getRegisteredLabel(item.value)
115
+ const registeredItem: SelectRegisteredItem = {
116
+ ...item,
117
+ ...registeredLabel,
118
+ textValue: item.textValue ?? registeredLabel?.textValue,
119
+ id,
120
+ }
121
+ explicitTextValues.set(id, item.textValue)
122
+ items.push(registeredItem)
123
+ notify()
124
+
125
+ return {
126
+ id,
127
+ update(next) {
128
+ const index = items.findIndex((current) => current.id === id)
129
+ if (index < 0) return
130
+ const current = items[index]
131
+ if ('textValue' in next) {
132
+ explicitTextValues.set(id, next.textValue)
133
+ }
134
+ const nextItem = { ...current, ...next }
135
+ if (next.value !== undefined && next.value !== current.value) {
136
+ const nextLabel = getRegisteredLabel(next.value)
137
+ nextItem.label = nextLabel?.label
138
+ }
139
+ const explicitTextValue = explicitTextValues.get(id)
140
+ nextItem.textValue =
141
+ explicitTextValue ||
142
+ getSelectLabelText(nextItem.label) ||
143
+ (nextItem.label ? nextItem.value : undefined)
144
+ const changed = Object.entries(next).some(
145
+ ([key, value]) => current[key as keyof SelectRegisteredItem] !== value
146
+ )
147
+ const textValueChanged = current.textValue !== nextItem.textValue
148
+ if (!changed && !textValueChanged) return
149
+ items[index] = nextItem
150
+ notify()
151
+ },
152
+ setNode(node) {
153
+ if ((itemNodes.get(id) ?? null) === (node ?? null)) return
154
+ if (node) {
155
+ itemNodes.set(id, node)
156
+ syncNodeOrder()
157
+ } else {
158
+ itemNodes.delete(id)
159
+ }
160
+ notify()
161
+ },
162
+ unregister() {
163
+ const index = items.findIndex((current) => current.id === id)
164
+ if (index < 0) return
165
+ items.splice(index, 1)
166
+ explicitTextValues.delete(id)
167
+ itemNodes.delete(id)
168
+ notify()
169
+ },
170
+ }
171
+ }
172
+
173
+ const registerLabel = (value: string, label: ReactNode, textValue?: string) => {
174
+ const id = Symbol(value)
175
+ const normalizedTextValue = textValue || getSelectLabelText(label) || value
176
+ const registrations =
177
+ labelRegistrations.get(value) ??
178
+ (() => {
179
+ const next = new Map<symbol, { label: ReactNode; textValue?: string }>()
180
+ labelRegistrations.set(value, next)
181
+ return next
182
+ })()
183
+ registrations.set(id, { label, textValue: normalizedTextValue })
184
+ applyRegisteredLabel(value)
185
+
186
+ return () => {
187
+ registrations.delete(id)
188
+ if (!registrations.size) {
189
+ labelRegistrations.delete(value)
190
+ }
191
+ applyRegisteredLabel(value)
192
+ }
193
+ }
194
+
195
+ const firstEnabledIndex = () => items.findIndex((item) => !item.disabled)
196
+
197
+ const nextEnabledIndex = (fromIndex: number | null, direction: 1 | -1) => {
198
+ if (!items.length) return -1
199
+ if (fromIndex == null || fromIndex < 0 || fromIndex >= items.length) {
200
+ if (direction === 1) return firstEnabledIndex()
201
+ for (let index = items.length - 1; index >= 0; index--) {
202
+ if (!items[index].disabled) return index
203
+ }
204
+ return -1
205
+ }
206
+ for (let offset = 1; offset <= items.length; offset++) {
207
+ const index = (fromIndex + direction * offset + items.length) % items.length
208
+ if (!items[index]?.disabled) return index
209
+ }
210
+ return -1
211
+ }
212
+
213
+ const findTypeaheadIndex = (search: string, fromIndex: number | null) => {
214
+ const normalizedSearch = search.trim().toLocaleLowerCase()
215
+ if (!normalizedSearch) return -1
216
+ const start = fromIndex == null || fromIndex < 0 ? -1 : fromIndex
217
+ for (let offset = 1; offset <= items.length; offset++) {
218
+ const index = (start + offset) % items.length
219
+ const item = items[index]
220
+ const label = item.textValue || getSelectLabelText(item.label) || item.value
221
+ if (!item.disabled && label.toLocaleLowerCase().startsWith(normalizedSearch)) {
222
+ return index
223
+ }
224
+ }
225
+ return -1
226
+ }
227
+
228
+ return {
229
+ subscribe(listener: () => void) {
230
+ listeners.add(listener)
231
+ return () => {
232
+ listeners.delete(listener)
233
+ }
234
+ },
235
+ registerItem,
236
+ registerLabel,
237
+ getItems,
238
+ getItem,
239
+ getIndex,
240
+ getNodes,
241
+ firstEnabledIndex,
242
+ nextEnabledIndex,
243
+ findTypeaheadIndex,
244
+ getDisabledIndices: () =>
245
+ items.flatMap((item, index) => (item.disabled ? [index] : [])),
246
+ getTypeaheadLabels: () =>
247
+ items.map((item) => item.textValue || getSelectLabelText(item.label) || item.value),
248
+ }
249
+ }
250
+
251
+ export function normalizeSelectSelection(
252
+ mode: SelectMode,
253
+ value: SelectSelection | undefined
254
+ ): SelectSelection {
255
+ if (mode === 'single') {
256
+ return typeof value === 'string' ? value : ''
257
+ }
258
+ const values = Array.isArray(value) ? value : []
259
+ return [...new Set(values)]
260
+ }
261
+
262
+ export function selectedValuesFromSelection(
263
+ mode: SelectMode,
264
+ value: SelectSelection | undefined
265
+ ): string[] {
266
+ const normalized = normalizeSelectSelection(mode, value)
267
+ return mode === 'multiple'
268
+ ? (normalized as string[])
269
+ : normalized
270
+ ? [normalized as string]
271
+ : []
272
+ }
273
+
274
+ export function createSelectSelectionController({
275
+ mode: initialMode,
276
+ value: initialValue,
277
+ registry,
278
+ }: {
279
+ mode: SelectMode
280
+ value: SelectSelection
281
+ registry: SelectItemRegistry
282
+ }) {
283
+ let mode = initialMode
284
+ let value = normalizeSelectSelection(mode, initialValue)
285
+ let activeIndex: number | null = null
286
+
287
+ const selectedValues = () => selectedValuesFromSelection(mode, value)
288
+
289
+ const setMode = (nextMode: SelectMode) => {
290
+ if (mode === nextMode) return value
291
+ mode = nextMode
292
+ value = normalizeSelectSelection(mode, value)
293
+ return value
294
+ }
295
+
296
+ const setValue = (nextValue: SelectSelection) => {
297
+ value = normalizeSelectSelection(mode, nextValue)
298
+ return value
299
+ }
300
+
301
+ const isSelected = (itemValue: string) => selectedValues().includes(itemValue)
302
+
303
+ const toggle = (itemValue: string) => {
304
+ const item = registry.getItem(itemValue)
305
+ if (!item || item.disabled) return value
306
+ if (mode === 'single') {
307
+ value = itemValue
308
+ return value
309
+ }
310
+ const current = selectedValues()
311
+ value = current.includes(itemValue)
312
+ ? current.filter((currentValue) => currentValue !== itemValue)
313
+ : [...current, itemValue]
314
+ return value
315
+ }
316
+
317
+ const selectionAnchor = () => {
318
+ const values = selectedValues()
319
+ for (let index = values.length - 1; index >= 0; index--) {
320
+ const item = registry.getItem(values[index])
321
+ if (item) return item
322
+ }
323
+ const firstEnabledIndex = registry.firstEnabledIndex()
324
+ return firstEnabledIndex < 0 ? undefined : registry.getItems()[firstEnabledIndex]
325
+ }
326
+
327
+ const selectionAnchorIndex = () => {
328
+ const anchor = selectionAnchor()
329
+ return anchor ? registry.getIndex(anchor.value) : -1
330
+ }
331
+
332
+ const initialActiveIndex = () => {
333
+ const anchorIndex = selectionAnchorIndex()
334
+ const anchor = anchorIndex < 0 ? undefined : registry.getItems()[anchorIndex]
335
+ return anchor && !anchor.disabled ? anchorIndex : registry.firstEnabledIndex()
336
+ }
337
+
338
+ const moveActive = (direction: 1 | -1) => {
339
+ const nextIndex = registry.nextEnabledIndex(activeIndex, direction)
340
+ activeIndex = nextIndex < 0 ? null : nextIndex
341
+ return activeIndex == null ? undefined : registry.getItems()[activeIndex]
342
+ }
343
+
344
+ return {
345
+ registry,
346
+ get mode() {
347
+ return mode
348
+ },
349
+ get value() {
350
+ return value
351
+ },
352
+ get activeIndex() {
353
+ return activeIndex
354
+ },
355
+ get activeItem() {
356
+ return activeIndex == null ? undefined : registry.getItems()[activeIndex]
357
+ },
358
+ get shouldCloseOnSelect() {
359
+ return mode === 'single'
360
+ },
361
+ setMode,
362
+ setValue,
363
+ isSelected,
364
+ toggle,
365
+ selectionAnchor,
366
+ selectionAnchorIndex,
367
+ initialActiveIndex,
368
+ setActiveIndex(index: number | null) {
369
+ activeIndex = index
370
+ },
371
+ moveActive,
372
+ }
373
+ }
374
+
375
+ export function getSelectListboxProps(mode: SelectMode) {
376
+ return {
377
+ role: 'listbox' as const,
378
+ 'aria-multiselectable': mode === 'multiple' ? true : undefined,
379
+ }
380
+ }
381
+
382
+ export function getSelectOptionProps(
383
+ mode: SelectMode,
384
+ selected: boolean,
385
+ disabled: boolean,
386
+ platform: 'web' | 'native'
387
+ ) {
388
+ if (platform === 'native') {
389
+ return {
390
+ accessibilityRole:
391
+ mode === 'multiple' ? ('checkbox' as const) : ('button' as const),
392
+ accessibilityState: {
393
+ selected: mode === 'single' ? selected : undefined,
394
+ checked: mode === 'multiple' ? selected : undefined,
395
+ disabled,
396
+ },
397
+ }
398
+ }
399
+ return {
400
+ role: 'option' as const,
401
+ 'aria-selected': selected,
402
+ 'aria-disabled': disabled || undefined,
403
+ }
404
+ }
package/src/types.tsx CHANGED
@@ -1,12 +1,17 @@
1
- import type { NativeValue, SizeTokens } from '@tamagui/core'
2
- import type { YStackProps } from '@tamagui/stacks'
3
1
  import type {
4
- DispatchWithoutAction,
5
- HTMLProps,
6
- MutableRefObject,
7
- ReactNode,
8
- RefObject,
9
- } from 'react'
2
+ NativeValue,
3
+ SizeTokens,
4
+ TamaguiChangeEventDetails,
5
+ TamaguiEventDetails,
6
+ ViewProps,
7
+ } from '@tamagui/core'
8
+ import type { DismissableProps } from '@tamagui/dismissable'
9
+ import type { HTMLProps, MutableRefObject, ReactNode, RefObject } from 'react'
10
+ import type {
11
+ SelectItemRegistry,
12
+ SelectMode,
13
+ SelectSelection,
14
+ } from './selectionController'
10
15
 
11
16
  // minimal types replacing @floating-ui/react imports
12
17
  type ContextData = Record<string, any>
@@ -30,27 +35,53 @@ export type SelectScopes = string
30
35
 
31
36
  export type SelectScopedProps<P> = P & { scope?: SelectScopes }
32
37
 
33
- export type SelectImplProps = SelectScopedProps<SelectProps> & {
38
+ export type SelectValueForMode<
39
+ Value extends string = string,
40
+ Multiple extends boolean | undefined = false,
41
+ > = Multiple extends true
42
+ ? Value[]
43
+ : Multiple extends false | undefined
44
+ ? Value
45
+ : Value | Value[]
46
+
47
+ export type SelectValueChangeDetails = TamaguiChangeEventDetails<
48
+ 'item-press' | 'keyboard' | 'native-change'
49
+ >
50
+
51
+ export type SelectOpenChangeDetails = TamaguiChangeEventDetails<
52
+ 'trigger-press' | 'keyboard' | 'outside-press' | 'escape-key' | 'item-press'
53
+ >
54
+
55
+ export type SelectActiveChangeDetails = TamaguiEventDetails<
56
+ 'item-hover' | 'list-navigation' | 'keyboard',
57
+ unknown,
58
+ { index: number }
59
+ >
60
+
61
+ export type SelectImplProps = SelectScopedProps<SelectProps<string, boolean>> & {
34
62
  activeIndexRef: any
35
63
  selectedIndexRef: any
36
64
  listContentRef: any
37
- /** fast setter: updates ref + emits to subscribers (no re-render) - use for hover/navigation */
38
- setActiveIndexFast: (index: number | null) => void
39
65
  }
40
66
 
41
- export interface SelectProps<Value extends string = string> {
67
+ export interface SelectProps<
68
+ Value extends string = string,
69
+ Multiple extends boolean | undefined = false,
70
+ > {
42
71
  id?: string
43
72
  children?: ReactNode
44
- value?: Value
45
- defaultValue?: Value
46
- onValueChange?(value: Value): void
73
+ multiple?: Multiple
74
+ value?: SelectValueForMode<Value, Multiple>
75
+ defaultValue?: SelectValueForMode<Value, Multiple>
76
+ onValueChange?(
77
+ value: SelectValueForMode<Value, Multiple>,
78
+ details: SelectValueChangeDetails
79
+ ): void
47
80
  open?: boolean
48
81
  defaultOpen?: boolean
49
- onOpenChange?(open: boolean): void
82
+ onOpenChange?(open: boolean, details: SelectOpenChangeDetails): void
50
83
  dir?: SelectDirection
51
- name?: string
52
- autoComplete?: string
53
- size?: SizeTokens
84
+ size?: SizeTokens | true
54
85
  /**
55
86
  * If passed, will render a native component instead of the custom one. Currently only `web` is supported.
56
87
  */
@@ -64,7 +95,7 @@ export interface SelectProps<Value extends string = string> {
64
95
  /**
65
96
  * Called when an item is hovered by mouse or navigated to by keyboard.
66
97
  */
67
- onActiveChange?(value: string, index: number): void
98
+ onActiveChange?(value: string, details: SelectActiveChangeDetails): void
68
99
 
69
100
  /**
70
101
  * Render function for the selected value. Use this for SSR support.
@@ -79,7 +110,13 @@ export interface SelectProps<Value extends string = string> {
79
110
  * >
80
111
  * ```
81
112
  */
82
- renderValue?(value: Value): ReactNode
113
+ renderValue?(value: SelectValueForMode<Value, Multiple>): ReactNode
114
+
115
+ /** web form field name. inert on react native. */
116
+ name?: string
117
+
118
+ /** associates the web form control with an external form. inert on react native. */
119
+ form?: string
83
120
 
84
121
  /**
85
122
  * When true, defers mounting Select items until opened using startTransition.
@@ -106,64 +143,69 @@ export interface SelectItemParentContextValue {
106
143
  adaptScope: string
107
144
  scopeName: string
108
145
  id?: string
109
- initialValue?: any
110
- setSelectedIndex: (index: number) => void
146
+ name?: string
147
+ form?: string
148
+ mode: SelectMode
149
+ selectedValues: string[]
150
+ registry: SelectItemRegistry
111
151
  listRef?: MutableRefObject<Array<HTMLElement | null>>
112
- setOpen: (open: boolean) => void
113
- onChange: (value: string) => void
114
- onActiveChange: (value: string, index: number) => void
115
- activeIndexSubscribe: EmitterSubscriber<number>
152
+ requestOpenChange: (open: boolean, details: SelectOpenChangeDetails) => void
153
+ selectValue: (value: string, details: SelectValueChangeDetails) => void
154
+ changeNativeValue: (value: SelectSelection, event: unknown) => void
155
+ activeIndexSubscribe: EmitterSubscriber<number | null>
116
156
  activeIndexRef?: MutableRefObject<number | null>
117
- valueSubscribe: EmitterSubscriber<any>
118
157
  allowSelectRef?: MutableRefObject<boolean>
119
158
  allowMouseUpRef?: MutableRefObject<boolean>
120
- setValueAtIndex: (index: number, value: string) => void
121
159
  selectTimeoutRef?: MutableRefObject<any>
122
160
  dataRef?: MutableRefObject<ContextData>
123
- interactions?: {
124
- getReferenceProps: (userProps?: HTMLProps<Element> | undefined) => any
125
- getFloatingProps: (userProps?: HTMLProps<HTMLElement> | undefined) => any
126
- getItemProps: (userProps?: HTMLProps<HTMLElement> | undefined) => any
127
- }
161
+ /** web only: stable once the list mounts, so it can live beside the items */
162
+ getItemProps?: (userProps?: HTMLProps<HTMLElement> | undefined) => any
128
163
  shouldRenderWebNative?: boolean
129
- size?: SizeTokens
130
- /** fast setter: updates ref + emits to subscribers (no re-render) - use for keyboard navigation */
131
- setActiveIndexFast?: (index: number | null) => void
132
- /** the rendered content of the currently selected item (for portaling to SelectValue) */
133
- selectedItem: ReactNode
134
- /** sets the selected item content */
135
- setSelectedItem: (item: ReactNode) => void
164
+ size?: SizeTokens | true
165
+ setActiveIndex: (index: number | null, details?: SelectActiveChangeDetails) => void
166
+ selectedIndex: number
167
+ lastPointerRef: MutableRefObject<{ x: number; y: number }>
168
+ moveActive: (direction: 1 | -1, event?: unknown) => void
169
+ search: (text: string, event?: unknown) => void
136
170
  }
137
171
 
138
172
  export interface SelectContextValue {
173
+ /** web only: the trigger and viewport prop getters; change with the floating element */
174
+ interactions?: {
175
+ getReferenceProps: (userProps?: HTMLProps<Element> | undefined) => any
176
+ getFloatingProps: (userProps?: HTMLProps<HTMLElement> | undefined) => any
177
+ }
139
178
  dir?: SelectDirection
140
179
  scopeName: string
141
180
  adaptScope: string
142
181
  value: any
182
+ mode: SelectMode
183
+ selectedValues: string[]
143
184
  selectedIndex: number
144
- /** current active index state - use for rendering, may lag behind ref */
145
- activeIndex: number | null
146
- /** ref to current active index - always up to date, use for reads */
185
+ /** the active index is never state: read the ref, or subscribe through the item parent context */
147
186
  activeIndexRef: MutableRefObject<number | null>
148
- /** slow setter: updates ref + emits + triggers re-render */
149
- setActiveIndex: (index: number | null) => void
187
+ setActiveIndex: (index: number | null, details?: SelectActiveChangeDetails) => void
150
188
  open: boolean
151
189
  valueNode: Element | null
152
190
  onValueNodeChange(node: HTMLElement): void
153
- forceUpdate: DispatchWithoutAction
154
- // SheetImpl only:
155
- isInSheet?: boolean
156
191
  // InlineImpl only:
157
192
  fallback: boolean
158
193
  blockSelection: boolean
159
194
  upArrowRef?: MutableRefObject<HTMLDivElement | null>
160
195
  downArrowRef?: MutableRefObject<HTMLDivElement | null>
161
- setScrollTop?: Function
196
+ /** re-reads the viewport and flips canScrollUp/Down when they change */
197
+ updateScrollArrows?: () => void
162
198
  setInnerOffset?: Function
163
199
  controlledScrolling?: boolean
164
200
  canScrollUp?: boolean
165
201
  canScrollDown?: boolean
166
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
+ }
167
209
  native?: NativeValue
168
210
  disablePreventBodyScroll?: boolean
169
211
  /** update floating-ui to recalculate */
@@ -175,24 +217,28 @@ export interface SelectContextValue {
175
217
  }
176
218
 
177
219
  export type SelectViewportExtraProps = SelectScopedProps<{
178
- size?: SizeTokens
220
+ size?: SizeTokens | true
179
221
  disableScroll?: boolean
180
- unstyled?: boolean
181
222
  }>
182
223
 
183
- export type SelectViewportProps = YStackProps & SelectViewportExtraProps
224
+ export type SelectViewportProps = ViewProps & SelectViewportExtraProps
184
225
 
185
- export type SelectContentProps = SelectScopedProps<{
186
- children?: React.ReactNode
187
- }>
226
+ export type SelectContentProps = SelectScopedProps<
227
+ {
228
+ children?: React.ReactNode
229
+ } & Pick<
230
+ DismissableProps,
231
+ 'onEscapeKeyDown' | 'onPointerDownOutside' | 'onFocusOutside' | 'onInteractOutside'
232
+ >
233
+ >
188
234
 
189
- export type SelectScrollButtonImplProps = YStackProps &
235
+ export type SelectScrollButtonImplProps = ViewProps &
190
236
  SelectScopedProps<{
191
237
  dir: 'up' | 'down'
192
- componentName: string
238
+ partClassName: string
193
239
  }>
194
240
 
195
241
  export interface SelectScrollButtonProps extends Omit<
196
242
  SelectScrollButtonImplProps,
197
- 'dir' | 'componentName'
243
+ 'dir' | 'partClassName'
198
244
  > {}