@tamagui/select 3.0.0-beta.922.1 → 3.0.0
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.
- package/dist/cjs/Select.cjs +23 -25
- package/dist/cjs/Select.native.cjs +27 -26
- package/dist/cjs/Select.native.js +27 -26
- package/dist/cjs/Select.native.js.map +1 -1
- package/dist/cjs/SelectImpl.cjs +56 -50
- package/dist/cjs/SelectItem.cjs +49 -60
- package/dist/cjs/SelectItem.native.cjs +51 -79
- package/dist/cjs/SelectItem.native.js +51 -79
- package/dist/cjs/SelectItem.native.js.map +1 -1
- package/dist/cjs/SelectScrollButton.cjs +2 -2
- package/dist/cjs/SelectTrigger.cjs +22 -5
- package/dist/cjs/SelectTrigger.native.cjs +15 -5
- package/dist/cjs/SelectTrigger.native.js +15 -5
- package/dist/cjs/SelectTrigger.native.js.map +1 -1
- package/dist/cjs/SelectViewport.cjs +22 -30
- package/dist/cjs/SelectViewport.native.cjs +2 -2
- package/dist/cjs/SelectViewport.native.js +2 -2
- package/dist/cjs/SelectViewport.native.js.map +1 -1
- package/dist/cjs/selectionController.cjs +15 -5
- package/dist/cjs/selectionController.native.cjs +20 -15
- package/dist/cjs/selectionController.native.js +20 -15
- package/dist/cjs/selectionController.native.js.map +1 -1
- package/dist/esm/Select.mjs +27 -28
- package/dist/esm/Select.mjs.map +1 -1
- package/dist/esm/Select.native.js +31 -29
- package/dist/esm/Select.native.js.map +1 -1
- package/dist/esm/SelectImpl.mjs +56 -50
- package/dist/esm/SelectImpl.mjs.map +1 -1
- package/dist/esm/SelectItem.mjs +50 -61
- package/dist/esm/SelectItem.mjs.map +1 -1
- package/dist/esm/SelectItem.native.js +52 -80
- package/dist/esm/SelectItem.native.js.map +1 -1
- package/dist/esm/SelectScrollButton.mjs +2 -2
- package/dist/esm/SelectScrollButton.mjs.map +1 -1
- package/dist/esm/SelectTrigger.mjs +16 -5
- package/dist/esm/SelectTrigger.mjs.map +1 -1
- package/dist/esm/SelectTrigger.native.js +15 -4
- package/dist/esm/SelectTrigger.native.js.map +1 -1
- package/dist/esm/SelectViewport.mjs +22 -30
- package/dist/esm/SelectViewport.mjs.map +1 -1
- package/dist/esm/SelectViewport.native.js +3 -3
- package/dist/esm/SelectViewport.native.js.map +1 -1
- package/dist/esm/selectionController.mjs +15 -5
- package/dist/esm/selectionController.mjs.map +1 -1
- package/dist/esm/selectionController.native.js +20 -15
- package/dist/esm/selectionController.native.js.map +1 -1
- package/dist/jsx/Select.mjs +27 -28
- package/dist/jsx/Select.mjs.map +1 -1
- package/dist/jsx/Select.native.cjs +27 -26
- package/dist/jsx/Select.native.js +27 -26
- package/dist/jsx/Select.native.js.map +1 -1
- package/dist/jsx/SelectImpl.mjs +56 -50
- package/dist/jsx/SelectImpl.mjs.map +1 -1
- package/dist/jsx/SelectItem.mjs +50 -61
- package/dist/jsx/SelectItem.mjs.map +1 -1
- package/dist/jsx/SelectItem.native.cjs +51 -79
- package/dist/jsx/SelectItem.native.js +51 -79
- package/dist/jsx/SelectItem.native.js.map +1 -1
- package/dist/jsx/SelectScrollButton.mjs +2 -2
- package/dist/jsx/SelectScrollButton.mjs.map +1 -1
- package/dist/jsx/SelectTrigger.mjs +16 -5
- package/dist/jsx/SelectTrigger.mjs.map +1 -1
- package/dist/jsx/SelectTrigger.native.cjs +15 -5
- package/dist/jsx/SelectTrigger.native.js +15 -5
- package/dist/jsx/SelectTrigger.native.js.map +1 -1
- package/dist/jsx/SelectViewport.mjs +22 -30
- package/dist/jsx/SelectViewport.mjs.map +1 -1
- package/dist/jsx/SelectViewport.native.cjs +2 -2
- package/dist/jsx/SelectViewport.native.js +2 -2
- package/dist/jsx/SelectViewport.native.js.map +1 -1
- package/dist/jsx/selectionController.mjs +15 -5
- package/dist/jsx/selectionController.mjs.map +1 -1
- package/dist/jsx/selectionController.native.cjs +20 -15
- package/dist/jsx/selectionController.native.js +20 -15
- package/dist/jsx/selectionController.native.js.map +1 -1
- package/package.json +30 -29
- package/src/Select.tsx +41 -41
- package/src/SelectImpl.tsx +83 -81
- package/src/SelectItem.tsx +66 -91
- package/src/SelectScrollButton.tsx +2 -2
- package/src/SelectTrigger.tsx +19 -2
- package/src/SelectViewport.native.tsx +4 -4
- package/src/SelectViewport.tsx +21 -27
- package/src/selectionController.ts +20 -5
- package/src/types.tsx +26 -21
- package/types/Select.d.ts +42 -81
- package/types/Select.d.ts.map +1 -1
- package/types/SelectImpl.d.ts.map +1 -1
- package/types/SelectItem.d.ts +3 -5
- package/types/SelectItem.d.ts.map +1 -1
- package/types/SelectItemText.d.ts +3 -5
- package/types/SelectItemText.d.ts.map +1 -1
- package/types/SelectScrollButton.d.ts +5 -9
- package/types/SelectScrollButton.d.ts.map +1 -1
- package/types/SelectScrollButton.native.d.ts +5 -9
- package/types/SelectScrollButton.native.d.ts.map +1 -1
- package/types/SelectTrigger.d.ts +3 -5
- package/types/SelectTrigger.d.ts.map +1 -1
- package/types/SelectViewport.d.ts +4 -9
- package/types/SelectViewport.d.ts.map +1 -1
- package/types/SelectViewport.native.d.ts +4 -9
- package/types/SelectViewport.native.d.ts.map +1 -1
- package/types/selectionController.d.ts +2 -0
- package/types/selectionController.d.ts.map +1 -1
- package/types/types.d.ts +30 -21
- package/types/types.d.ts.map +1 -1
package/src/SelectItem.tsx
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import type { GetProps } from '@tamagui/core'
|
|
12
12
|
import { composeEventHandlers } from '@tamagui/helpers'
|
|
13
13
|
import * as React from 'react'
|
|
14
|
-
import {
|
|
14
|
+
import { useSelectItemParentContext } from './context'
|
|
15
15
|
import { getSelectOptionProps } from './selectionController'
|
|
16
16
|
import type {
|
|
17
17
|
SelectActiveChangeDetails,
|
|
@@ -67,12 +67,12 @@ export const SelectItem = createStyledHOC(
|
|
|
67
67
|
} = props
|
|
68
68
|
const disabled = disabledProp ?? ariaDisabled === true
|
|
69
69
|
|
|
70
|
+
// items read only the item parent context: the select context carries
|
|
71
|
+
// viewport state (position, scroll arrows) that must not re-render the list
|
|
70
72
|
const context = useSelectItemParentContext(scope)
|
|
71
|
-
const
|
|
72
|
-
const isAdapted = useAdaptIsActive(selectContext.adaptScope)
|
|
73
|
+
const isAdapted = useAdaptIsActive(context.adaptScope)
|
|
73
74
|
|
|
74
75
|
const {
|
|
75
|
-
listRef,
|
|
76
76
|
registry,
|
|
77
77
|
mode,
|
|
78
78
|
selectedValues,
|
|
@@ -82,9 +82,10 @@ export const SelectItem = createStyledHOC(
|
|
|
82
82
|
allowMouseUpRef,
|
|
83
83
|
allowSelectRef,
|
|
84
84
|
selectTimeoutRef,
|
|
85
|
-
|
|
85
|
+
getItemProps,
|
|
86
86
|
shouldRenderWebNative,
|
|
87
|
-
|
|
87
|
+
setActiveIndex,
|
|
88
|
+
lastPointerRef,
|
|
88
89
|
moveActive,
|
|
89
90
|
search,
|
|
90
91
|
} = context
|
|
@@ -92,7 +93,6 @@ export const SelectItem = createStyledHOC(
|
|
|
92
93
|
const isSelected = selectedValues.includes(value)
|
|
93
94
|
const pendingMouseUpSelectionRef = React.useRef(false)
|
|
94
95
|
const itemNodeRef = React.useRef<any>(null)
|
|
95
|
-
const [, rerenderRegistry] = React.useReducer((version) => version + 1, 0)
|
|
96
96
|
const registrationRef = React.useRef<ReturnType<typeof registry.registerItem> | null>(
|
|
97
97
|
null
|
|
98
98
|
)
|
|
@@ -102,38 +102,13 @@ export const SelectItem = createStyledHOC(
|
|
|
102
102
|
textValue: textValueProp,
|
|
103
103
|
})
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
[registry]
|
|
108
|
-
)
|
|
109
|
-
|
|
105
|
+
// the item never subscribes to the registry: its index is read at event
|
|
106
|
+
// time, and the owner mirrors the registry's nodes into the list ref
|
|
110
107
|
useIsomorphicLayoutEffect(() => {
|
|
111
108
|
const registration = registry.registerItem(initialRegistration.current)
|
|
112
109
|
registrationRef.current = registration
|
|
113
|
-
|
|
114
|
-
registration.setNode(registeredNode)
|
|
115
|
-
const registeredIndex = registry
|
|
116
|
-
.getItems()
|
|
117
|
-
.findIndex((item) => item.id === registration.id)
|
|
118
|
-
if (
|
|
119
|
-
isWeb &&
|
|
120
|
-
listRef &&
|
|
121
|
-
registeredIndex >= 0 &&
|
|
122
|
-
registeredNode instanceof HTMLElement
|
|
123
|
-
) {
|
|
124
|
-
listRef.current[registeredIndex] = registeredNode
|
|
125
|
-
}
|
|
110
|
+
registration.setNode(itemNodeRef.current)
|
|
126
111
|
return () => {
|
|
127
|
-
const currentIndex = registry
|
|
128
|
-
.getItems()
|
|
129
|
-
.findIndex((item) => item.id === registration.id)
|
|
130
|
-
if (
|
|
131
|
-
listRef &&
|
|
132
|
-
currentIndex >= 0 &&
|
|
133
|
-
listRef.current[currentIndex] === registeredNode
|
|
134
|
-
) {
|
|
135
|
-
listRef.current[currentIndex] = null
|
|
136
|
-
}
|
|
137
112
|
registrationRef.current = null
|
|
138
113
|
registration.unregister()
|
|
139
114
|
}
|
|
@@ -147,43 +122,41 @@ export const SelectItem = createStyledHOC(
|
|
|
147
122
|
registrationRef.current?.setNode(itemNodeRef.current)
|
|
148
123
|
})
|
|
149
124
|
|
|
150
|
-
|
|
125
|
+
// the only per-item state: this item is the active one. the active index
|
|
126
|
+
// itself is a ref plus an emitter, so a hover re-renders two items, not the list
|
|
127
|
+
const [isActive, setIsActive] = React.useState(false)
|
|
128
|
+
// every item hears every change; only the two whose state flips may set
|
|
129
|
+
// it, since a same-value set still renders once when updates are queued
|
|
130
|
+
const isActiveRef = React.useRef(false)
|
|
151
131
|
|
|
152
132
|
React.useEffect(() => {
|
|
153
|
-
const handleActiveIndex = (i: number) => {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
133
|
+
const handleActiveIndex = (i: number | null) => {
|
|
134
|
+
const next = i != null && registry.getIndex(value) === i
|
|
135
|
+
if (next !== isActiveRef.current) {
|
|
136
|
+
isActiveRef.current = next
|
|
137
|
+
setIsActive(next)
|
|
138
|
+
}
|
|
139
|
+
if (next && isWeb) {
|
|
140
|
+
// use rAF to focus after browser's click handling completes
|
|
141
|
+
// this prevents the trigger from stealing focus after we set it
|
|
142
|
+
requestAnimationFrame(() => {
|
|
143
|
+
itemNodeRef.current?.focus?.()
|
|
144
|
+
})
|
|
162
145
|
}
|
|
163
146
|
}
|
|
164
147
|
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
if (currentActiveIndex !== null && currentActiveIndex !== undefined) {
|
|
168
|
-
handleActiveIndex(currentActiveIndex)
|
|
169
|
-
}
|
|
148
|
+
// the parent effect may have set it before we subscribed
|
|
149
|
+
handleActiveIndex(activeIndexRef?.current ?? null)
|
|
170
150
|
|
|
171
151
|
return activeIndexSubscribe(handleActiveIndex)
|
|
172
|
-
}, [activeIndexRef, activeIndexSubscribe,
|
|
152
|
+
}, [activeIndexRef, activeIndexSubscribe, registry, value])
|
|
173
153
|
|
|
174
154
|
const textId = React.useId()
|
|
175
155
|
|
|
176
|
-
const refCallback = React.useCallback(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (!isWeb) return
|
|
181
|
-
if (listRef && index >= 0) {
|
|
182
|
-
listRef.current[index] = node instanceof HTMLElement ? node : null
|
|
183
|
-
}
|
|
184
|
-
},
|
|
185
|
-
[index, listRef]
|
|
186
|
-
)
|
|
156
|
+
const refCallback = React.useCallback((node) => {
|
|
157
|
+
itemNodeRef.current = node
|
|
158
|
+
registrationRef.current?.setNode(node)
|
|
159
|
+
}, [])
|
|
187
160
|
|
|
188
161
|
const composedRefs = useComposedRefs(forwardedRef, refCallback)
|
|
189
162
|
|
|
@@ -203,6 +176,25 @@ export const SelectItem = createStyledHOC(
|
|
|
203
176
|
[disabled, selectValue, value]
|
|
204
177
|
)
|
|
205
178
|
|
|
179
|
+
const handleMouseMove = React.useCallback(
|
|
180
|
+
(event: any) => {
|
|
181
|
+
if (disabled) return
|
|
182
|
+
const index = registry.getIndex(value)
|
|
183
|
+
if (index < 0) return
|
|
184
|
+
const last = lastPointerRef.current
|
|
185
|
+
if (event.clientX === last.x && event.clientY === last.y) return
|
|
186
|
+
last.x = event.clientX
|
|
187
|
+
last.y = event.clientY
|
|
188
|
+
setActiveIndex(index, {
|
|
189
|
+
reason: 'item-hover',
|
|
190
|
+
event: event.nativeEvent || event,
|
|
191
|
+
trigger: event.currentTarget,
|
|
192
|
+
index,
|
|
193
|
+
} as SelectActiveChangeDetails)
|
|
194
|
+
},
|
|
195
|
+
[disabled, lastPointerRef, registry, setActiveIndex, value]
|
|
196
|
+
)
|
|
197
|
+
|
|
206
198
|
const handleKeyDown = React.useCallback(
|
|
207
199
|
(event: any) => {
|
|
208
200
|
if (disabled) return
|
|
@@ -218,7 +210,7 @@ export const SelectItem = createStyledHOC(
|
|
|
218
210
|
return
|
|
219
211
|
}
|
|
220
212
|
if (
|
|
221
|
-
!
|
|
213
|
+
!getItemProps &&
|
|
222
214
|
event.key?.length === 1 &&
|
|
223
215
|
!event.metaKey &&
|
|
224
216
|
!event.ctrlKey
|
|
@@ -229,11 +221,11 @@ export const SelectItem = createStyledHOC(
|
|
|
229
221
|
allowSelectRef.current = true
|
|
230
222
|
}
|
|
231
223
|
},
|
|
232
|
-
[allowSelectRef, disabled, handleSelect,
|
|
224
|
+
[allowSelectRef, disabled, handleSelect, getItemProps, moveActive, search]
|
|
233
225
|
)
|
|
234
226
|
|
|
235
227
|
const selectItemProps = React.useMemo(() => {
|
|
236
|
-
if (
|
|
228
|
+
if (getItemProps) {
|
|
237
229
|
const {
|
|
238
230
|
onTouchMove,
|
|
239
231
|
onTouchEnd,
|
|
@@ -244,7 +236,7 @@ export const SelectItem = createStyledHOC(
|
|
|
244
236
|
onPress,
|
|
245
237
|
...itemProps
|
|
246
238
|
} = restProps
|
|
247
|
-
const interactionProps =
|
|
239
|
+
const interactionProps = getItemProps({
|
|
248
240
|
...itemProps,
|
|
249
241
|
onTouchMove() {
|
|
250
242
|
allowSelectRef!.current = true
|
|
@@ -254,15 +246,7 @@ export const SelectItem = createStyledHOC(
|
|
|
254
246
|
allowSelectRef!.current = false
|
|
255
247
|
allowMouseUpRef!.current = true
|
|
256
248
|
},
|
|
257
|
-
onMouseMove
|
|
258
|
-
if (disabled || index < 0) return
|
|
259
|
-
setActiveIndexFast?.(index, {
|
|
260
|
-
reason: 'item-hover',
|
|
261
|
-
event: event.nativeEvent || event,
|
|
262
|
-
trigger: event.currentTarget,
|
|
263
|
-
index,
|
|
264
|
-
} as SelectActiveChangeDetails)
|
|
265
|
-
},
|
|
249
|
+
onMouseMove: handleMouseMove,
|
|
266
250
|
onKeyDown: handleKeyDown,
|
|
267
251
|
onClick(event) {
|
|
268
252
|
if (disabled) return
|
|
@@ -332,15 +316,7 @@ export const SelectItem = createStyledHOC(
|
|
|
332
316
|
return {
|
|
333
317
|
...restProps,
|
|
334
318
|
onKeyDown: composeEventHandlers(restProps.onKeyDown as any, handleKeyDown),
|
|
335
|
-
onMouseMove: composeEventHandlers(restProps.onMouseMove as any,
|
|
336
|
-
if (disabled || index < 0) return
|
|
337
|
-
setActiveIndexFast?.(index, {
|
|
338
|
-
reason: 'item-hover',
|
|
339
|
-
event: event.nativeEvent || event,
|
|
340
|
-
trigger: event.currentTarget,
|
|
341
|
-
index,
|
|
342
|
-
} as SelectActiveChangeDetails)
|
|
343
|
-
}),
|
|
319
|
+
onMouseMove: composeEventHandlers(restProps.onMouseMove as any, handleMouseMove),
|
|
344
320
|
onPress: composeEventHandlers(restProps.onPress as any, handleSelect),
|
|
345
321
|
}
|
|
346
322
|
}, [
|
|
@@ -348,12 +324,11 @@ export const SelectItem = createStyledHOC(
|
|
|
348
324
|
allowSelectRef,
|
|
349
325
|
disabled,
|
|
350
326
|
handleKeyDown,
|
|
327
|
+
handleMouseMove,
|
|
351
328
|
handleSelect,
|
|
352
|
-
|
|
353
|
-
interactions,
|
|
329
|
+
getItemProps,
|
|
354
330
|
restProps,
|
|
355
331
|
selectTimeoutRef,
|
|
356
|
-
setActiveIndexFast,
|
|
357
332
|
])
|
|
358
333
|
|
|
359
334
|
const accessibilityProps = getSelectOptionProps(
|
|
@@ -385,10 +360,10 @@ export const SelectItem = createStyledHOC(
|
|
|
385
360
|
tabIndex={
|
|
386
361
|
disabled
|
|
387
362
|
? undefined
|
|
388
|
-
: isWeb &&
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
363
|
+
: isWeb &&
|
|
364
|
+
isAdapted &&
|
|
365
|
+
(isActive || (activeIndexRef?.current == null && isSelected))
|
|
366
|
+
? 0
|
|
392
367
|
: -1
|
|
393
368
|
}
|
|
394
369
|
zIndex={100}
|
|
@@ -68,7 +68,7 @@ const SelectScrollButtonImpl = React.memo(
|
|
|
68
68
|
const { scope, dir, partClassName, ...scrollIndicatorProps } = props
|
|
69
69
|
const { className, onPointerEnter, onPointerLeave, ...frameProps } =
|
|
70
70
|
scrollIndicatorProps
|
|
71
|
-
const { open, fallback,
|
|
71
|
+
const { open, fallback, updateScrollArrows, setInnerOffset, ...context } =
|
|
72
72
|
useSelectContext(scope)
|
|
73
73
|
const floatingRef = context.floatingContext?.refs.floating
|
|
74
74
|
|
|
@@ -109,7 +109,7 @@ const SelectScrollButtonImpl = React.memo(
|
|
|
109
109
|
if (fallback) {
|
|
110
110
|
if (floating.current) {
|
|
111
111
|
floating.current.scrollTop -= amount
|
|
112
|
-
|
|
112
|
+
updateScrollArrows!()
|
|
113
113
|
}
|
|
114
114
|
} else {
|
|
115
115
|
flushSync(() => setInnerOffset!((value) => value - amount))
|
package/src/SelectTrigger.tsx
CHANGED
|
@@ -34,6 +34,8 @@ export const SelectTrigger = createStyledHOC(
|
|
|
34
34
|
forwardedRef,
|
|
35
35
|
context.floatingContext?.refs.setReference as any
|
|
36
36
|
)
|
|
37
|
+
// whether the press in flight began as a touch, so its click opens
|
|
38
|
+
const pressIsTouch = React.useRef(false)
|
|
37
39
|
|
|
38
40
|
if (itemParentContext.shouldRenderWebNative) {
|
|
39
41
|
return null
|
|
@@ -55,8 +57,8 @@ export const SelectTrigger = createStyledHOC(
|
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
59
|
const interactionProps =
|
|
58
|
-
process.env.TAMAGUI_TARGET === 'web' &&
|
|
59
|
-
?
|
|
60
|
+
process.env.TAMAGUI_TARGET === 'web' && context.interactions
|
|
61
|
+
? context.interactions.getReferenceProps({
|
|
60
62
|
...triggerProps,
|
|
61
63
|
onMouseDown: composeEventHandlers(triggerProps.onMouseDown as any, () =>
|
|
62
64
|
context.floatingContext?.update?.()
|
|
@@ -66,13 +68,28 @@ export const SelectTrigger = createStyledHOC(
|
|
|
66
68
|
...triggerProps,
|
|
67
69
|
...(isWeb
|
|
68
70
|
? {
|
|
71
|
+
// a mouse opens on press so a press, drag and release picks
|
|
72
|
+
// in one gesture. a touch opens on release, as a native
|
|
73
|
+
// select does: opening on touchstart mounts the sheet
|
|
74
|
+
// overlay under the finger, and the same tap's click then
|
|
75
|
+
// lands on it and closes the sheet again.
|
|
69
76
|
onMouseDown: composeEventHandlers(
|
|
70
77
|
triggerProps.onMouseDown as any,
|
|
71
78
|
(event: any) => {
|
|
79
|
+
pressIsTouch.current = event.type === 'touchstart'
|
|
80
|
+
if (pressIsTouch.current) return
|
|
72
81
|
event.preventDefault()
|
|
73
82
|
toggleOpen(event)
|
|
74
83
|
}
|
|
75
84
|
),
|
|
85
|
+
onPress: composeEventHandlers(
|
|
86
|
+
triggerProps.onPress as any,
|
|
87
|
+
(event: any) => {
|
|
88
|
+
if (!pressIsTouch.current) return
|
|
89
|
+
pressIsTouch.current = false
|
|
90
|
+
toggleOpen(event)
|
|
91
|
+
}
|
|
92
|
+
),
|
|
76
93
|
}
|
|
77
94
|
: {
|
|
78
95
|
onPress: composeEventHandlers(triggerProps.onPress as any, toggleOpen),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
AdaptContext,
|
|
3
2
|
AdaptPortalContents,
|
|
3
|
+
ProvideAdaptContext,
|
|
4
4
|
useAdaptContext,
|
|
5
5
|
useAdaptIsActive,
|
|
6
6
|
} from '@tamagui/adapt'
|
|
@@ -26,17 +26,17 @@ export const SelectViewport = createStyledHOC(
|
|
|
26
26
|
const context = useSelectContext(scope)
|
|
27
27
|
const itemParentContext = useSelectItemParentContext(scope)
|
|
28
28
|
const themeName = useThemeName()
|
|
29
|
-
const adaptContext = useAdaptContext()
|
|
29
|
+
const adaptContext = useAdaptContext(context.adaptScope)
|
|
30
30
|
const isAdapted = useAdaptIsActive(context.adaptScope)
|
|
31
31
|
|
|
32
32
|
const contents = (
|
|
33
33
|
<Theme name={themeName}>
|
|
34
34
|
<ForwardSelectContext itemContext={itemParentContext} context={context}>
|
|
35
|
-
<
|
|
35
|
+
<ProvideAdaptContext {...adaptContext}>
|
|
36
36
|
<SelectViewportFrame {...viewportProps} ref={forwardedRef}>
|
|
37
37
|
{children}
|
|
38
38
|
</SelectViewportFrame>
|
|
39
|
-
</
|
|
39
|
+
</ProvideAdaptContext>
|
|
40
40
|
</ForwardSelectContext>
|
|
41
41
|
</Theme>
|
|
42
42
|
)
|
package/src/SelectViewport.tsx
CHANGED
|
@@ -5,7 +5,6 @@ import { isWeb, useIsomorphicLayoutEffect } from '@tamagui/constants'
|
|
|
5
5
|
import { createStyledHOC, styled, View } from '@tamagui/core'
|
|
6
6
|
import { needsPortalRepropagation } from '@tamagui/portal'
|
|
7
7
|
import { YStack } from '@tamagui/stacks'
|
|
8
|
-
import { startTransition } from '@tamagui/start-transition'
|
|
9
8
|
import * as React from 'react'
|
|
10
9
|
import { VIEWPORT_NAME } from './constants'
|
|
11
10
|
import {
|
|
@@ -37,17 +36,13 @@ export const SelectViewport = createStyledHOC(
|
|
|
37
36
|
const viewportRef = React.useRef<any>(null)
|
|
38
37
|
const registeredItemCount = itemContext.registry.getItems().length
|
|
39
38
|
|
|
40
|
-
// lazy mount
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
startTransition(() => {
|
|
48
|
-
setLazyMounted(true)
|
|
49
|
-
})
|
|
50
|
-
}, [context.lazyMount, context.open, lazyMounted])
|
|
39
|
+
// lazy mount keeps the items out of the tree until the first open, but it
|
|
40
|
+
// has to happen in the render that opens, not in an effect after it: mounting
|
|
41
|
+
// a frame later paints an empty popover, then floating-ui positions that empty
|
|
42
|
+
// box and has to jump once the real height arrives. once mounted it stays.
|
|
43
|
+
const hasOpenedRef = React.useRef(!context.lazyMount)
|
|
44
|
+
if (context.open) hasOpenedRef.current = true
|
|
45
|
+
const lazyMounted = hasOpenedRef.current
|
|
51
46
|
|
|
52
47
|
React.useEffect(() => {
|
|
53
48
|
if (!isWeb || !isAdapted || !context.open) return
|
|
@@ -62,7 +57,7 @@ export const SelectViewport = createStyledHOC(
|
|
|
62
57
|
activeItem?.focus()
|
|
63
58
|
})
|
|
64
59
|
return () => cancelAnimationFrame(frame)
|
|
65
|
-
}, [context.
|
|
60
|
+
}, [context.open, isAdapted, registeredItemCount])
|
|
66
61
|
|
|
67
62
|
const composedRefs = useComposedRefs(
|
|
68
63
|
// @ts-ignore react 19 ref type mismatch
|
|
@@ -77,14 +72,14 @@ export const SelectViewport = createStyledHOC(
|
|
|
77
72
|
}
|
|
78
73
|
}, [isAdapted])
|
|
79
74
|
|
|
80
|
-
//
|
|
81
|
-
//
|
|
75
|
+
// items register in layout effects, so the inner middleware can only position
|
|
76
|
+
// against the selected one once the registry has settled
|
|
82
77
|
React.useEffect(() => {
|
|
83
|
-
if (context.lazyMount &&
|
|
78
|
+
if (context.lazyMount && context.open && context.update) {
|
|
84
79
|
const frame = requestAnimationFrame(context.update)
|
|
85
80
|
return () => cancelAnimationFrame(frame)
|
|
86
81
|
}
|
|
87
|
-
}, [
|
|
82
|
+
}, [context.open, registeredItemCount])
|
|
88
83
|
|
|
89
84
|
if (itemContext.shouldRenderWebNative) {
|
|
90
85
|
return <YStack position="relative">{children}</YStack>
|
|
@@ -115,16 +110,11 @@ export const SelectViewport = createStyledHOC(
|
|
|
115
110
|
)
|
|
116
111
|
}
|
|
117
112
|
|
|
118
|
-
if (!
|
|
113
|
+
if (!context.interactions) {
|
|
119
114
|
return null
|
|
120
115
|
}
|
|
121
116
|
|
|
122
|
-
const
|
|
123
|
-
style,
|
|
124
|
-
// remove this, it was set to "Select" always
|
|
125
|
-
className,
|
|
126
|
-
...floatingProps
|
|
127
|
-
} = itemContext.interactions.getFloatingProps()
|
|
117
|
+
const floatingProps = context.interactions.getFloatingProps()
|
|
128
118
|
|
|
129
119
|
// FloatingFocusManager removed — SelectContent already wraps with FocusScope
|
|
130
120
|
// that handles focus trapping and auto-focus
|
|
@@ -137,7 +127,10 @@ export const SelectViewport = createStyledHOC(
|
|
|
137
127
|
}}
|
|
138
128
|
/>
|
|
139
129
|
)}
|
|
140
|
-
|
|
130
|
+
{/* every styled item reads the presence context, and the default
|
|
131
|
+
(layout-affecting) presence hands out a new context per render, so
|
|
132
|
+
each viewport render would re-render the whole list */}
|
|
133
|
+
<AnimatePresence presenceAffectsLayout={false}>
|
|
141
134
|
{context.open ? (
|
|
142
135
|
<SelectViewportFrame
|
|
143
136
|
key="select-viewport"
|
|
@@ -146,10 +139,11 @@ export const SelectViewport = createStyledHOC(
|
|
|
146
139
|
'data-state': context.open ? 'open' : 'closed',
|
|
147
140
|
})}
|
|
148
141
|
{...viewportProps}
|
|
149
|
-
{...
|
|
142
|
+
{...context.floatingPosition}
|
|
143
|
+
outlineWidth={0}
|
|
150
144
|
{...floatingProps}
|
|
151
145
|
{...getSelectListboxProps(itemContext.mode)}
|
|
152
|
-
overflowY={disableScroll ? undefined :
|
|
146
|
+
overflowY={disableScroll ? undefined : 'auto'}
|
|
153
147
|
ref={composedRefs}
|
|
154
148
|
>
|
|
155
149
|
{lazyMounted ? children : null}
|
|
@@ -43,7 +43,12 @@ export function createSelectItemRegistry(onChange?: () => void) {
|
|
|
43
43
|
const itemNodes = new Map<symbol, any>()
|
|
44
44
|
const listeners = new Set<() => void>()
|
|
45
45
|
|
|
46
|
+
// value -> first index, rebuilt after any change: items look themselves up
|
|
47
|
+
// on every active-index emit, so the lookup must not walk the list
|
|
48
|
+
let indexByValue: Map<string, number> | null = null
|
|
49
|
+
|
|
46
50
|
const notify = () => {
|
|
51
|
+
indexByValue = null
|
|
47
52
|
onChange?.()
|
|
48
53
|
listeners.forEach((listener) => listener())
|
|
49
54
|
}
|
|
@@ -52,7 +57,18 @@ export function createSelectItemRegistry(onChange?: () => void) {
|
|
|
52
57
|
|
|
53
58
|
const getItem = (value: string) => items.find((item) => item.value === value)
|
|
54
59
|
|
|
55
|
-
const getIndex = (value: string) =>
|
|
60
|
+
const getIndex = (value: string) => {
|
|
61
|
+
if (!indexByValue) {
|
|
62
|
+
indexByValue = new Map()
|
|
63
|
+
items.forEach((item, index) => {
|
|
64
|
+
if (!indexByValue!.has(item.value)) indexByValue!.set(item.value, index)
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
return indexByValue.get(value) ?? -1
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** the mounted nodes in item order; the owner mirrors this into its list ref */
|
|
71
|
+
const getNodes = () => items.map((item) => itemNodes.get(item.id) ?? null)
|
|
56
72
|
|
|
57
73
|
const getRegisteredLabel = (value: string) => {
|
|
58
74
|
const registrations = labelRegistrations.get(value)
|
|
@@ -78,7 +94,6 @@ export function createSelectItemRegistry(onChange?: () => void) {
|
|
|
78
94
|
}
|
|
79
95
|
|
|
80
96
|
const syncNodeOrder = () => {
|
|
81
|
-
const previousOrder = items.map((item) => item.id)
|
|
82
97
|
items.sort((a, b) => {
|
|
83
98
|
const aNode = itemNodes.get(a.id)
|
|
84
99
|
const bNode = itemNodes.get(b.id)
|
|
@@ -90,9 +105,6 @@ export function createSelectItemRegistry(onChange?: () => void) {
|
|
|
90
105
|
if (position & 2) return 1
|
|
91
106
|
return 0
|
|
92
107
|
})
|
|
93
|
-
if (items.some((item, index) => item.id !== previousOrder[index])) {
|
|
94
|
-
notify()
|
|
95
|
-
}
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
const registerItem = (
|
|
@@ -138,12 +150,14 @@ export function createSelectItemRegistry(onChange?: () => void) {
|
|
|
138
150
|
notify()
|
|
139
151
|
},
|
|
140
152
|
setNode(node) {
|
|
153
|
+
if ((itemNodes.get(id) ?? null) === (node ?? null)) return
|
|
141
154
|
if (node) {
|
|
142
155
|
itemNodes.set(id, node)
|
|
143
156
|
syncNodeOrder()
|
|
144
157
|
} else {
|
|
145
158
|
itemNodes.delete(id)
|
|
146
159
|
}
|
|
160
|
+
notify()
|
|
147
161
|
},
|
|
148
162
|
unregister() {
|
|
149
163
|
const index = items.findIndex((current) => current.id === id)
|
|
@@ -223,6 +237,7 @@ export function createSelectItemRegistry(onChange?: () => void) {
|
|
|
223
237
|
getItems,
|
|
224
238
|
getItem,
|
|
225
239
|
getIndex,
|
|
240
|
+
getNodes,
|
|
226
241
|
firstEnabledIndex,
|
|
227
242
|
nextEnabledIndex,
|
|
228
243
|
findTypeaheadIndex,
|
package/src/types.tsx
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
NativeValue,
|
|
3
|
-
SizeTokens,
|
|
4
3
|
TamaguiChangeEventDetails,
|
|
5
4
|
TamaguiEventDetails,
|
|
6
5
|
ViewProps,
|
|
@@ -62,8 +61,6 @@ export type SelectImplProps = SelectScopedProps<SelectProps<string, boolean>> &
|
|
|
62
61
|
activeIndexRef: any
|
|
63
62
|
selectedIndexRef: any
|
|
64
63
|
listContentRef: any
|
|
65
|
-
/** fast setter: updates ref + emits to subscribers (no re-render) - use for hover/navigation */
|
|
66
|
-
setActiveIndexFast: (index: number | null) => void
|
|
67
64
|
}
|
|
68
65
|
|
|
69
66
|
export interface SelectProps<
|
|
@@ -83,7 +80,8 @@ export interface SelectProps<
|
|
|
83
80
|
defaultOpen?: boolean
|
|
84
81
|
onOpenChange?(open: boolean, details: SelectOpenChangeDetails): void
|
|
85
82
|
dir?: SelectDirection
|
|
86
|
-
size
|
|
83
|
+
/** a skin-owned size name, threaded opaquely to the skin parts */
|
|
84
|
+
size?: string | boolean
|
|
87
85
|
/**
|
|
88
86
|
* If passed, will render a native component instead of the custom one. Currently only `web` is supported.
|
|
89
87
|
*/
|
|
@@ -154,40 +152,39 @@ export interface SelectItemParentContextValue {
|
|
|
154
152
|
requestOpenChange: (open: boolean, details: SelectOpenChangeDetails) => void
|
|
155
153
|
selectValue: (value: string, details: SelectValueChangeDetails) => void
|
|
156
154
|
changeNativeValue: (value: SelectSelection, event: unknown) => void
|
|
157
|
-
activeIndexSubscribe: EmitterSubscriber<number>
|
|
155
|
+
activeIndexSubscribe: EmitterSubscriber<number | null>
|
|
158
156
|
activeIndexRef?: MutableRefObject<number | null>
|
|
159
157
|
allowSelectRef?: MutableRefObject<boolean>
|
|
160
158
|
allowMouseUpRef?: MutableRefObject<boolean>
|
|
161
159
|
selectTimeoutRef?: MutableRefObject<any>
|
|
162
160
|
dataRef?: MutableRefObject<ContextData>
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
getFloatingProps: (userProps?: HTMLProps<HTMLElement> | undefined) => any
|
|
166
|
-
getItemProps: (userProps?: HTMLProps<HTMLElement> | undefined) => any
|
|
167
|
-
}
|
|
161
|
+
/** web only: stable once the list mounts, so it can live beside the items */
|
|
162
|
+
getItemProps?: (userProps?: HTMLProps<HTMLElement> | undefined) => any
|
|
168
163
|
shouldRenderWebNative?: boolean
|
|
169
|
-
size
|
|
170
|
-
|
|
171
|
-
|
|
164
|
+
/** a skin-owned size name, threaded opaquely to the skin parts */
|
|
165
|
+
size?: string | boolean
|
|
166
|
+
setActiveIndex: (index: number | null, details?: SelectActiveChangeDetails) => void
|
|
167
|
+
selectedIndex: number
|
|
168
|
+
lastPointerRef: MutableRefObject<{ x: number; y: number }>
|
|
172
169
|
moveActive: (direction: 1 | -1, event?: unknown) => void
|
|
173
170
|
search: (text: string, event?: unknown) => void
|
|
174
171
|
}
|
|
175
172
|
|
|
176
173
|
export interface SelectContextValue {
|
|
174
|
+
/** web only: the trigger and viewport prop getters; change with the floating element */
|
|
175
|
+
interactions?: {
|
|
176
|
+
getReferenceProps: (userProps?: HTMLProps<Element> | undefined) => any
|
|
177
|
+
getFloatingProps: (userProps?: HTMLProps<HTMLElement> | undefined) => any
|
|
178
|
+
}
|
|
177
179
|
dir?: SelectDirection
|
|
178
180
|
scopeName: string
|
|
179
181
|
adaptScope: string
|
|
180
182
|
value: any
|
|
181
183
|
mode: SelectMode
|
|
182
184
|
selectedValues: string[]
|
|
183
|
-
activeItem?: string
|
|
184
|
-
selectionAnchor?: string
|
|
185
185
|
selectedIndex: number
|
|
186
|
-
/**
|
|
187
|
-
activeIndex: number | null
|
|
188
|
-
/** ref to current active index - always up to date, use for reads */
|
|
186
|
+
/** the active index is never state: read the ref, or subscribe through the item parent context */
|
|
189
187
|
activeIndexRef: MutableRefObject<number | null>
|
|
190
|
-
/** slow setter: updates ref + emits + triggers re-render */
|
|
191
188
|
setActiveIndex: (index: number | null, details?: SelectActiveChangeDetails) => void
|
|
192
189
|
open: boolean
|
|
193
190
|
valueNode: Element | null
|
|
@@ -197,12 +194,19 @@ export interface SelectContextValue {
|
|
|
197
194
|
blockSelection: boolean
|
|
198
195
|
upArrowRef?: MutableRefObject<HTMLDivElement | null>
|
|
199
196
|
downArrowRef?: MutableRefObject<HTMLDivElement | null>
|
|
200
|
-
|
|
197
|
+
/** re-reads the viewport and flips canScrollUp/Down when they change */
|
|
198
|
+
updateScrollArrows?: () => void
|
|
201
199
|
setInnerOffset?: Function
|
|
202
200
|
controlledScrolling?: boolean
|
|
203
201
|
canScrollUp?: boolean
|
|
204
202
|
canScrollDown?: boolean
|
|
205
203
|
floatingContext?: FloatingContext<ReferenceType>
|
|
204
|
+
/** InlineImpl only: where floating-ui put the viewport */
|
|
205
|
+
floatingPosition?: {
|
|
206
|
+
position: 'absolute' | 'fixed'
|
|
207
|
+
top: number | ''
|
|
208
|
+
left: number | ''
|
|
209
|
+
}
|
|
206
210
|
native?: NativeValue
|
|
207
211
|
disablePreventBodyScroll?: boolean
|
|
208
212
|
/** update floating-ui to recalculate */
|
|
@@ -214,7 +218,8 @@ export interface SelectContextValue {
|
|
|
214
218
|
}
|
|
215
219
|
|
|
216
220
|
export type SelectViewportExtraProps = SelectScopedProps<{
|
|
217
|
-
size
|
|
221
|
+
/** a skin-owned size name, threaded opaquely to the skin parts */
|
|
222
|
+
size?: string | boolean
|
|
218
223
|
disableScroll?: boolean
|
|
219
224
|
}>
|
|
220
225
|
|