@tamagui/select 3.0.0-beta.924.1 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +67 -63
- 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 +9 -25
- 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 +29 -32
- 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 +68 -64
- 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 +10 -26
- 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 +30 -33
- 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 +68 -64
- 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 +10 -26
- 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 +30 -33
- 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 +107 -95
- package/src/SelectItem.tsx +66 -91
- package/src/SelectScrollButton.tsx +14 -32
- package/src/SelectTrigger.tsx +19 -2
- package/src/SelectViewport.native.tsx +4 -4
- package/src/SelectViewport.tsx +34 -30
- package/src/selectionController.ts +20 -5
- package/src/types.tsx +26 -22
- 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 -22
- package/types/types.d.ts.map +1 -1
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { createStyledHOC, styled, View } from '@tamagui/core'
|
|
2
|
-
import { autoUpdate,
|
|
2
|
+
import { autoUpdate, useFloatingRaw as useFloating } from '@tamagui/floating'
|
|
3
3
|
import { useComposedRefs } from '@tamagui/compose-refs'
|
|
4
4
|
import type { TamaguiElement } from '@tamagui/core'
|
|
5
5
|
import { composeEventHandlers } from '@tamagui/helpers'
|
|
6
6
|
import * as React from 'react'
|
|
7
|
-
import { flushSync } from 'react-dom'
|
|
8
7
|
|
|
9
8
|
import { useSelectContext } from './context'
|
|
10
9
|
import type {
|
|
@@ -68,13 +67,12 @@ const SelectScrollButtonImpl = React.memo(
|
|
|
68
67
|
const { scope, dir, partClassName, ...scrollIndicatorProps } = props
|
|
69
68
|
const { className, onPointerEnter, onPointerLeave, ...frameProps } =
|
|
70
69
|
scrollIndicatorProps
|
|
71
|
-
const { open,
|
|
72
|
-
useSelectContext(scope)
|
|
70
|
+
const { open, ...context } = useSelectContext(scope)
|
|
73
71
|
const floatingRef = context.floatingContext?.refs.floating
|
|
74
72
|
|
|
75
|
-
const statusRef = React.useRef<'idle' | 'active'>('idle')
|
|
76
73
|
const isVisible = context[dir === 'down' ? 'canScrollDown' : 'canScrollUp']
|
|
77
74
|
const frameRef = React.useRef<any>(null)
|
|
75
|
+
const arrowRef = dir === 'up' ? context.upArrowRef : context.downArrowRef
|
|
78
76
|
|
|
79
77
|
const { x, y, refs, strategy } = useFloating({
|
|
80
78
|
open: open && isVisible,
|
|
@@ -83,38 +81,21 @@ const SelectScrollButtonImpl = React.memo(
|
|
|
83
81
|
reference: floatingRef?.current,
|
|
84
82
|
},
|
|
85
83
|
placement: dir === 'up' ? 'top' : 'bottom',
|
|
86
|
-
|
|
87
|
-
whileElementsMounted: (...args) => autoUpdate(...args, { animationFrame: true }),
|
|
84
|
+
whileElementsMounted: autoUpdate,
|
|
88
85
|
})
|
|
89
86
|
|
|
90
87
|
const setFloatingRef = React.useCallback(
|
|
91
88
|
(node: TamaguiElement | null) => {
|
|
92
89
|
refs.setFloating(node as HTMLElement | null)
|
|
90
|
+
if (arrowRef) arrowRef.current = node as HTMLDivElement | null
|
|
93
91
|
},
|
|
94
|
-
[refs.setFloating]
|
|
92
|
+
[refs.setFloating, arrowRef]
|
|
95
93
|
)
|
|
96
94
|
const composedRef = useComposedRefs(forwardedRef, setFloatingRef)
|
|
97
95
|
|
|
98
96
|
React.useEffect(() => {
|
|
99
97
|
return () => cancelAnimationFrame(frameRef.current)
|
|
100
|
-
}, [])
|
|
101
|
-
|
|
102
|
-
if (!isVisible) {
|
|
103
|
-
return null
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const onScroll = (amount: number) => {
|
|
107
|
-
const floating = floatingRef
|
|
108
|
-
if (!floating) return
|
|
109
|
-
if (fallback) {
|
|
110
|
-
if (floating.current) {
|
|
111
|
-
floating.current.scrollTop -= amount
|
|
112
|
-
flushSync(() => setScrollTop!(floating.current?.scrollTop ?? 0))
|
|
113
|
-
}
|
|
114
|
-
} else {
|
|
115
|
-
flushSync(() => setInnerOffset!((value) => value - amount))
|
|
116
|
-
}
|
|
117
|
-
}
|
|
98
|
+
}, [isVisible])
|
|
118
99
|
|
|
119
100
|
return (
|
|
120
101
|
<SelectScrollButtonFrame
|
|
@@ -122,13 +103,15 @@ const SelectScrollButtonImpl = React.memo(
|
|
|
122
103
|
className={`${partClassName} ${className || ''}`.trim()}
|
|
123
104
|
aria-hidden
|
|
124
105
|
{...frameProps}
|
|
106
|
+
// keep hidden buttons measurable so placement reserves their actual height
|
|
107
|
+
visibility={isVisible ? 'visible' : 'hidden'}
|
|
108
|
+
pointerEvents={isVisible ? 'auto' : 'none'}
|
|
125
109
|
zIndex={1000}
|
|
126
110
|
position={strategy}
|
|
127
111
|
left={x || 0}
|
|
128
112
|
top={y || 0}
|
|
129
113
|
width={`calc(${(floatingRef?.current?.offsetWidth ?? 0) - 2}px)`}
|
|
130
114
|
onPointerEnter={composeEventHandlers(onPointerEnter as any, () => {
|
|
131
|
-
statusRef.current = 'active'
|
|
132
115
|
let prevNow = Date.now()
|
|
133
116
|
|
|
134
117
|
function frame() {
|
|
@@ -151,11 +134,11 @@ const SelectScrollButtonImpl = React.memo(
|
|
|
151
134
|
: element.scrollTop + pixelsToScroll <
|
|
152
135
|
element.scrollHeight - element.clientHeight
|
|
153
136
|
|
|
154
|
-
|
|
137
|
+
// the viewport's own scroll handler flips the arrows
|
|
138
|
+
element.scrollTop +=
|
|
155
139
|
dir === 'up'
|
|
156
|
-
? Math.min(pixelsToScroll, remainingPixels)
|
|
157
|
-
: Math.
|
|
158
|
-
)
|
|
140
|
+
? -Math.min(pixelsToScroll, remainingPixels)
|
|
141
|
+
: Math.min(pixelsToScroll, remainingPixels)
|
|
159
142
|
|
|
160
143
|
if (scrollRemaining) {
|
|
161
144
|
frameRef.current = requestAnimationFrame(frame)
|
|
@@ -167,7 +150,6 @@ const SelectScrollButtonImpl = React.memo(
|
|
|
167
150
|
frameRef.current = requestAnimationFrame(frame)
|
|
168
151
|
})}
|
|
169
152
|
onPointerLeave={composeEventHandlers(onPointerLeave as any, () => {
|
|
170
|
-
statusRef.current = 'idle'
|
|
171
153
|
cancelAnimationFrame(frameRef.current)
|
|
172
154
|
})}
|
|
173
155
|
/>
|
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
|
@@ -2,10 +2,9 @@ import { AdaptPortalContents, useAdaptIsActive } from '@tamagui/adapt'
|
|
|
2
2
|
import { AnimatePresence } from '@tamagui/animate-presence'
|
|
3
3
|
import { useComposedRefs } from '@tamagui/compose-refs'
|
|
4
4
|
import { isWeb, useIsomorphicLayoutEffect } from '@tamagui/constants'
|
|
5
|
-
import { createStyledHOC, styled, View } from '@tamagui/core'
|
|
5
|
+
import { createStyledHOC, splitStyleProps, styled, View } from '@tamagui/core'
|
|
6
6
|
import { needsPortalRepropagation } from '@tamagui/portal'
|
|
7
7
|
import { YStack } from '@tamagui/stacks'
|
|
8
|
-
import { startTransition } from '@tamagui/start-transition'
|
|
9
8
|
import * as React from 'react'
|
|
10
9
|
import { VIEWPORT_NAME } from './constants'
|
|
11
10
|
import {
|
|
@@ -27,6 +26,15 @@ export const SelectViewportFrame = styled(View, {
|
|
|
27
26
|
|
|
28
27
|
const needsRepropagation = needsPortalRepropagation()
|
|
29
28
|
|
|
29
|
+
// the viewport's styles dress the floating list (a capped height, a card
|
|
30
|
+
// background, border and shadow). in a sheet the sheet is the surface, so only
|
|
31
|
+
// the element props carry over and the items fill the sheet. a styled() skin
|
|
32
|
+
// hands its styles to this HOC as class names (web) or a style (native)
|
|
33
|
+
const getSheetFrameProps = (props: Record<string, any>) => {
|
|
34
|
+
const { className, style, 'data-tamagui-hoc-class': _hocClassNames, ...rest } = props
|
|
35
|
+
return splitStyleProps(rest)[1]
|
|
36
|
+
}
|
|
37
|
+
|
|
30
38
|
export const SelectViewport = createStyledHOC(
|
|
31
39
|
SelectViewportFrame,
|
|
32
40
|
function SelectViewport(props: SelectViewportProps, forwardedRef) {
|
|
@@ -37,17 +45,13 @@ export const SelectViewport = createStyledHOC(
|
|
|
37
45
|
const viewportRef = React.useRef<any>(null)
|
|
38
46
|
const registeredItemCount = itemContext.registry.getItems().length
|
|
39
47
|
|
|
40
|
-
// lazy mount
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
startTransition(() => {
|
|
48
|
-
setLazyMounted(true)
|
|
49
|
-
})
|
|
50
|
-
}, [context.lazyMount, context.open, lazyMounted])
|
|
48
|
+
// lazy mount keeps the items out of the tree until the first open, but it
|
|
49
|
+
// has to happen in the render that opens, not in an effect after it: mounting
|
|
50
|
+
// a frame later paints an empty popover, then floating-ui positions that empty
|
|
51
|
+
// box and has to jump once the real height arrives. once mounted it stays.
|
|
52
|
+
const hasOpenedRef = React.useRef(!context.lazyMount)
|
|
53
|
+
if (context.open) hasOpenedRef.current = true
|
|
54
|
+
const lazyMounted = hasOpenedRef.current
|
|
51
55
|
|
|
52
56
|
React.useEffect(() => {
|
|
53
57
|
if (!isWeb || !isAdapted || !context.open) return
|
|
@@ -62,7 +66,7 @@ export const SelectViewport = createStyledHOC(
|
|
|
62
66
|
activeItem?.focus()
|
|
63
67
|
})
|
|
64
68
|
return () => cancelAnimationFrame(frame)
|
|
65
|
-
}, [context.
|
|
69
|
+
}, [context.open, isAdapted, registeredItemCount])
|
|
66
70
|
|
|
67
71
|
const composedRefs = useComposedRefs(
|
|
68
72
|
// @ts-ignore react 19 ref type mismatch
|
|
@@ -77,23 +81,24 @@ export const SelectViewport = createStyledHOC(
|
|
|
77
81
|
}
|
|
78
82
|
}, [isAdapted])
|
|
79
83
|
|
|
80
|
-
//
|
|
81
|
-
//
|
|
84
|
+
// items register in layout effects, so the inner middleware can only position
|
|
85
|
+
// against the selected one once the registry has settled
|
|
82
86
|
React.useEffect(() => {
|
|
83
|
-
if (context.lazyMount &&
|
|
87
|
+
if (context.lazyMount && context.open && context.update) {
|
|
84
88
|
const frame = requestAnimationFrame(context.update)
|
|
85
89
|
return () => cancelAnimationFrame(frame)
|
|
86
90
|
}
|
|
87
|
-
}, [
|
|
91
|
+
}, [context.open, registeredItemCount])
|
|
88
92
|
|
|
89
93
|
if (itemContext.shouldRenderWebNative) {
|
|
90
94
|
return <YStack position="relative">{children}</YStack>
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
if (isAdapted || !isWeb) {
|
|
98
|
+
const frameProps = isAdapted ? getSheetFrameProps(viewportProps) : viewportProps
|
|
94
99
|
let content = (
|
|
95
100
|
<SelectViewportFrame
|
|
96
|
-
{...
|
|
101
|
+
{...frameProps}
|
|
97
102
|
{...(isWeb ? (getSelectListboxProps(itemContext.mode) as any) : {})}
|
|
98
103
|
data-select-viewport=""
|
|
99
104
|
ref={composedRefs}
|
|
@@ -115,16 +120,11 @@ export const SelectViewport = createStyledHOC(
|
|
|
115
120
|
)
|
|
116
121
|
}
|
|
117
122
|
|
|
118
|
-
if (!
|
|
123
|
+
if (!context.interactions) {
|
|
119
124
|
return null
|
|
120
125
|
}
|
|
121
126
|
|
|
122
|
-
const
|
|
123
|
-
style,
|
|
124
|
-
// remove this, it was set to "Select" always
|
|
125
|
-
className,
|
|
126
|
-
...floatingProps
|
|
127
|
-
} = itemContext.interactions.getFloatingProps()
|
|
127
|
+
const floatingProps = context.interactions.getFloatingProps()
|
|
128
128
|
|
|
129
129
|
// FloatingFocusManager removed — SelectContent already wraps with FocusScope
|
|
130
130
|
// that handles focus trapping and auto-focus
|
|
@@ -137,7 +137,10 @@ export const SelectViewport = createStyledHOC(
|
|
|
137
137
|
}}
|
|
138
138
|
/>
|
|
139
139
|
)}
|
|
140
|
-
|
|
140
|
+
{/* every styled item reads the presence context, and the default
|
|
141
|
+
(layout-affecting) presence hands out a new context per render, so
|
|
142
|
+
each viewport render would re-render the whole list */}
|
|
143
|
+
<AnimatePresence presenceAffectsLayout={false}>
|
|
141
144
|
{context.open ? (
|
|
142
145
|
<SelectViewportFrame
|
|
143
146
|
key="select-viewport"
|
|
@@ -146,10 +149,11 @@ export const SelectViewport = createStyledHOC(
|
|
|
146
149
|
'data-state': context.open ? 'open' : 'closed',
|
|
147
150
|
})}
|
|
148
151
|
{...viewportProps}
|
|
149
|
-
{...
|
|
152
|
+
{...context.floatingPosition}
|
|
153
|
+
outlineWidth={0}
|
|
150
154
|
{...floatingProps}
|
|
151
155
|
{...getSelectListboxProps(itemContext.mode)}
|
|
152
|
-
overflowY={disableScroll ? undefined :
|
|
156
|
+
overflowY={disableScroll ? undefined : 'auto'}
|
|
153
157
|
ref={composedRefs}
|
|
154
158
|
>
|
|
155
159
|
{lazyMounted ? children : null}
|
|
@@ -171,7 +175,7 @@ const selectViewportCSS = `
|
|
|
171
175
|
[data-select-viewport] {
|
|
172
176
|
scrollbar-width: none;
|
|
173
177
|
-webkit-overflow-scrolling: touch;
|
|
174
|
-
overscroll-behavior:
|
|
178
|
+
overscroll-behavior: none;
|
|
175
179
|
}
|
|
176
180
|
|
|
177
181
|
[data-select-viewport]::-webkit-scrollbar{
|
|
@@ -43,7 +43,12 @@ export function createSelectItemRegistry(onChange?: () => void) {
|
|
|
43
43
|
const itemNodes = new Map<symbol, any>()
|
|
44
44
|
const listeners = new Set<() => void>()
|
|
45
45
|
|
|
46
|
+
// value -> first index, rebuilt after any change: items look themselves up
|
|
47
|
+
// on every active-index emit, so the lookup must not walk the list
|
|
48
|
+
let indexByValue: Map<string, number> | null = null
|
|
49
|
+
|
|
46
50
|
const notify = () => {
|
|
51
|
+
indexByValue = null
|
|
47
52
|
onChange?.()
|
|
48
53
|
listeners.forEach((listener) => listener())
|
|
49
54
|
}
|
|
@@ -52,7 +57,18 @@ export function createSelectItemRegistry(onChange?: () => void) {
|
|
|
52
57
|
|
|
53
58
|
const getItem = (value: string) => items.find((item) => item.value === value)
|
|
54
59
|
|
|
55
|
-
const getIndex = (value: string) =>
|
|
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,18 @@ export interface SelectContextValue {
|
|
|
197
194
|
blockSelection: boolean
|
|
198
195
|
upArrowRef?: MutableRefObject<HTMLDivElement | null>
|
|
199
196
|
downArrowRef?: MutableRefObject<HTMLDivElement | null>
|
|
200
|
-
|
|
201
|
-
|
|
197
|
+
/** re-reads the viewport and flips canScrollUp/Down when they change */
|
|
198
|
+
updateScrollArrows?: () => void
|
|
202
199
|
controlledScrolling?: boolean
|
|
203
200
|
canScrollUp?: boolean
|
|
204
201
|
canScrollDown?: boolean
|
|
205
202
|
floatingContext?: FloatingContext<ReferenceType>
|
|
203
|
+
/** InlineImpl only: where floating-ui put the viewport */
|
|
204
|
+
floatingPosition?: {
|
|
205
|
+
position: 'absolute' | 'fixed'
|
|
206
|
+
top: number | ''
|
|
207
|
+
left: number | ''
|
|
208
|
+
}
|
|
206
209
|
native?: NativeValue
|
|
207
210
|
disablePreventBodyScroll?: boolean
|
|
208
211
|
/** update floating-ui to recalculate */
|
|
@@ -214,7 +217,8 @@ export interface SelectContextValue {
|
|
|
214
217
|
}
|
|
215
218
|
|
|
216
219
|
export type SelectViewportExtraProps = SelectScopedProps<{
|
|
217
|
-
size
|
|
220
|
+
/** a skin-owned size name, threaded opaquely to the skin parts */
|
|
221
|
+
size?: string | boolean
|
|
218
222
|
disableScroll?: boolean
|
|
219
223
|
}>
|
|
220
224
|
|