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