@tamagui/select 1.0.1-beta.21 → 1.0.1-beta.211

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 (114) hide show
  1. package/dist/cjs/BubbleSelect.js +43 -0
  2. package/dist/cjs/BubbleSelect.js.map +7 -0
  3. package/dist/cjs/Select.js +412 -390
  4. package/dist/cjs/Select.js.map +3 -3
  5. package/dist/cjs/SelectContent.js +72 -0
  6. package/dist/cjs/SelectContent.js.map +7 -0
  7. package/dist/cjs/SelectContent.native.js +31 -0
  8. package/dist/cjs/SelectContent.native.js.map +7 -0
  9. package/dist/cjs/SelectImpl.js +286 -0
  10. package/dist/cjs/SelectImpl.js.map +7 -0
  11. package/dist/cjs/SelectScrollButton.js +157 -0
  12. package/dist/cjs/SelectScrollButton.js.map +7 -0
  13. package/dist/cjs/SelectScrollButton.native.js +32 -0
  14. package/dist/cjs/SelectScrollButton.native.js.map +7 -0
  15. package/dist/cjs/SelectViewport.js +126 -0
  16. package/dist/cjs/SelectViewport.js.map +7 -0
  17. package/dist/cjs/SelectViewport.native.js +41 -0
  18. package/dist/cjs/SelectViewport.native.js.map +7 -0
  19. package/dist/cjs/constants.js +47 -0
  20. package/dist/cjs/constants.js.map +7 -0
  21. package/dist/cjs/context.js +49 -0
  22. package/dist/cjs/context.js.map +7 -0
  23. package/dist/cjs/index.js +2 -0
  24. package/dist/cjs/index.js.map +2 -2
  25. package/dist/cjs/types.js +17 -0
  26. package/dist/cjs/types.js.map +7 -0
  27. package/dist/cjs/useSelectBreakpointActive.js +39 -0
  28. package/dist/cjs/useSelectBreakpointActive.js.map +7 -0
  29. package/dist/esm/BubbleSelect.js +24 -0
  30. package/dist/esm/BubbleSelect.js.map +7 -0
  31. package/dist/esm/Select.js +403 -358
  32. package/dist/esm/Select.js.map +3 -3
  33. package/dist/esm/SelectContent.js +48 -0
  34. package/dist/esm/SelectContent.js.map +7 -0
  35. package/dist/esm/SelectContent.native.js +7 -0
  36. package/dist/esm/SelectContent.native.js.map +7 -0
  37. package/dist/esm/SelectImpl.js +271 -0
  38. package/dist/esm/SelectImpl.js.map +7 -0
  39. package/dist/esm/SelectScrollButton.js +126 -0
  40. package/dist/esm/SelectScrollButton.js.map +7 -0
  41. package/dist/esm/SelectScrollButton.native.js +7 -0
  42. package/dist/esm/SelectScrollButton.native.js.map +7 -0
  43. package/dist/esm/SelectViewport.js +95 -0
  44. package/dist/esm/SelectViewport.js.map +7 -0
  45. package/dist/esm/SelectViewport.native.js +17 -0
  46. package/dist/esm/SelectViewport.native.js.map +7 -0
  47. package/dist/esm/constants.js +17 -0
  48. package/dist/esm/constants.js.map +7 -0
  49. package/dist/esm/context.js +21 -0
  50. package/dist/esm/context.js.map +7 -0
  51. package/dist/esm/index.js +1 -0
  52. package/dist/esm/index.js.map +2 -2
  53. package/dist/esm/types.js +1 -0
  54. package/dist/esm/types.js.map +7 -0
  55. package/dist/esm/useSelectBreakpointActive.js +14 -0
  56. package/dist/esm/useSelectBreakpointActive.js.map +7 -0
  57. package/dist/jsx/BubbleSelect.js +24 -0
  58. package/dist/jsx/BubbleSelect.js.map +7 -0
  59. package/dist/jsx/Select.js +283 -271
  60. package/dist/jsx/Select.js.map +7 -0
  61. package/dist/jsx/SelectContent.js +27 -0
  62. package/dist/jsx/SelectContent.js.map +7 -0
  63. package/dist/jsx/SelectContent.native.js +7 -0
  64. package/dist/jsx/SelectContent.native.js.map +7 -0
  65. package/dist/jsx/SelectImpl.js +244 -0
  66. package/dist/jsx/SelectImpl.js.map +7 -0
  67. package/dist/jsx/SelectScrollButton.js +99 -0
  68. package/dist/jsx/SelectScrollButton.js.map +7 -0
  69. package/dist/jsx/SelectScrollButton.native.js +7 -0
  70. package/dist/jsx/SelectScrollButton.native.js.map +7 -0
  71. package/dist/jsx/SelectViewport.js +72 -0
  72. package/dist/jsx/SelectViewport.js.map +7 -0
  73. package/dist/jsx/SelectViewport.native.js +13 -0
  74. package/dist/jsx/SelectViewport.native.js.map +7 -0
  75. package/dist/jsx/constants.js +17 -0
  76. package/dist/jsx/constants.js.map +7 -0
  77. package/dist/jsx/context.js +15 -0
  78. package/dist/jsx/context.js.map +7 -0
  79. package/dist/jsx/index.js +2 -0
  80. package/dist/jsx/index.js.map +7 -0
  81. package/dist/jsx/types.js +1 -0
  82. package/dist/jsx/types.js.map +7 -0
  83. package/dist/jsx/useSelectBreakpointActive.js +14 -0
  84. package/dist/jsx/useSelectBreakpointActive.js.map +7 -0
  85. package/package.json +26 -16
  86. package/src/BubbleSelect.tsx +46 -0
  87. package/src/Select.tsx +618 -0
  88. package/src/SelectContent.native.tsx +5 -0
  89. package/src/SelectContent.tsx +45 -0
  90. package/src/SelectImpl.tsx +342 -0
  91. package/src/SelectScrollButton.native.tsx +7 -0
  92. package/src/SelectScrollButton.tsx +168 -0
  93. package/src/SelectViewport.native.tsx +14 -0
  94. package/src/SelectViewport.tsx +104 -0
  95. package/src/constants.tsx +7 -0
  96. package/src/context.tsx +19 -0
  97. package/src/index.tsx +8 -0
  98. package/src/types.tsx +94 -0
  99. package/src/useSelectBreakpointActive.tsx +15 -0
  100. package/types/BubbleSelect.d.ts +2 -0
  101. package/types/Select.d.ts +753 -0
  102. package/types/SelectContent.d.ts +5 -0
  103. package/types/SelectContent.native.d.ts +4 -0
  104. package/types/SelectImpl.d.ts +9 -0
  105. package/types/SelectScrollButton.d.ts +6 -0
  106. package/types/SelectScrollButton.native.d.ts +4 -0
  107. package/types/SelectViewport.d.ts +127 -0
  108. package/types/SelectViewport.native.d.ts +7 -0
  109. package/types/constants.d.ts +8 -0
  110. package/types/context.d.ts +21 -0
  111. package/types/index.d.ts +2 -0
  112. package/types/types.d.ts +84 -0
  113. package/types/useSelectBreakpointActive.d.ts +4 -0
  114. package/types/index.d.ts.map +0 -1
@@ -0,0 +1,342 @@
1
+ import {
2
+ SideObject,
3
+ autoUpdate,
4
+ flip,
5
+ inner,
6
+ offset,
7
+ shift,
8
+ size,
9
+ useClick,
10
+ useDismiss,
11
+ useFloating,
12
+ useInnerOffset,
13
+ useInteractions,
14
+ useListNavigation,
15
+ useRole,
16
+ useTypeahead,
17
+ } from '@floating-ui/react-dom-interactions'
18
+ import { useIsTouchDevice, useIsomorphicLayoutEffect } from '@tamagui/core'
19
+ import * as React from 'react'
20
+ import { flushSync } from 'react-dom'
21
+
22
+ import { SCROLL_ARROW_THRESHOLD, WINDOW_PADDING } from './constants'
23
+ import { SelectProvider, useSelectContext } from './context'
24
+ import { ScopedProps, SelectProps } from './types'
25
+
26
+ export type SelectImplProps = ScopedProps<SelectProps> & {
27
+ activeIndexRef: any
28
+ selectedIndexRef: any
29
+ listContentRef: any
30
+ }
31
+
32
+ // TODO use id for focusing from label
33
+ export const SelectInlineImpl = (props: SelectImplProps) => {
34
+ const { __scopeSelect, children, open = false, selectedIndexRef, listContentRef } = props
35
+
36
+ const selectContext = useSelectContext('SelectSheetImpl', __scopeSelect)
37
+ const { setActiveIndex, setOpen, setSelectedIndex, selectedIndex, activeIndex, forceUpdate } =
38
+ selectContext
39
+ const [scrollTop, setScrollTop] = React.useState(0)
40
+ const touch = useIsTouchDevice()
41
+
42
+ const listItemsRef = React.useRef<Array<HTMLElement | null>>([])
43
+ const overflowRef = React.useRef<null | SideObject>(null)
44
+ const upArrowRef = React.useRef<HTMLDivElement | null>(null)
45
+ const downArrowRef = React.useRef<HTMLDivElement | null>(null)
46
+ const allowSelectRef = React.useRef(false)
47
+ const allowMouseUpRef = React.useRef(true)
48
+ const selectTimeoutRef = React.useRef<any>()
49
+ const state = React.useRef({
50
+ isMouseOutside: false,
51
+ })
52
+
53
+ const [controlledScrolling, setControlledScrolling] = React.useState(false)
54
+ const [fallback, setFallback] = React.useState(false)
55
+ const [innerOffset, setInnerOffset] = React.useState(0)
56
+ const [blockSelection, setBlockSelection] = React.useState(false)
57
+ const floatingStyle = React.useRef({})
58
+
59
+ // Wait for scroll position to settle before showing arrows to prevent
60
+ // interference with pointer events.
61
+ React.useEffect(() => {
62
+ const frame = requestAnimationFrame(() => {
63
+ if (!open) {
64
+ setScrollTop(0)
65
+ setFallback(false)
66
+ setActiveIndex(null)
67
+ setControlledScrolling(false)
68
+ }
69
+ })
70
+ return () => {
71
+ cancelAnimationFrame(frame)
72
+ }
73
+ }, [open, setActiveIndex])
74
+
75
+ // close when mouseup outside select
76
+ React.useEffect(() => {
77
+ if (!open) return
78
+ const mouseUp = (e: MouseEvent) => {
79
+ if (state.current.isMouseOutside) {
80
+ setOpen(false)
81
+ }
82
+ }
83
+ document.addEventListener('mouseup', mouseUp)
84
+ return () => {
85
+ document.removeEventListener('mouseup', mouseUp)
86
+ }
87
+ }, [open])
88
+
89
+ const updateFloatingSize = size({
90
+ apply({
91
+ availableHeight,
92
+ rects: {
93
+ reference: { width },
94
+ },
95
+ }) {
96
+ floatingStyle.current = {
97
+ width: width,
98
+ maxHeight: availableHeight,
99
+ }
100
+ },
101
+ padding: WINDOW_PADDING,
102
+ })
103
+
104
+ const { x, y, reference, floating, strategy, context, refs } = useFloating({
105
+ open,
106
+ onOpenChange: setOpen,
107
+ whileElementsMounted: autoUpdate,
108
+ placement: 'bottom-start',
109
+ middleware: fallback
110
+ ? [
111
+ offset(5),
112
+ ...[
113
+ touch
114
+ ? shift({ crossAxis: true, padding: WINDOW_PADDING })
115
+ : flip({ padding: WINDOW_PADDING }),
116
+ ],
117
+ updateFloatingSize,
118
+ ]
119
+ : [
120
+ inner({
121
+ listRef: listItemsRef,
122
+ overflowRef,
123
+ index: selectedIndex,
124
+ offset: innerOffset,
125
+ onFallbackChange: setFallback,
126
+ padding: 10,
127
+ minItemsVisible: touch ? 10 : 4,
128
+ referenceOverflowThreshold: 20,
129
+ }),
130
+ updateFloatingSize,
131
+ ],
132
+ })
133
+
134
+ const floatingRef = refs.floating
135
+
136
+ const showUpArrow = open && scrollTop > SCROLL_ARROW_THRESHOLD
137
+ const showDownArrow =
138
+ open &&
139
+ floatingRef.current &&
140
+ scrollTop <
141
+ floatingRef.current.scrollHeight - floatingRef.current.clientHeight - SCROLL_ARROW_THRESHOLD
142
+
143
+ const interactions = useInteractions([
144
+ useClick(context, { event: 'mousedown' }),
145
+ useDismiss(context, { outsidePress: false }),
146
+ useRole(context, { role: 'listbox' }),
147
+ useInnerOffset(context, {
148
+ enabled: !fallback,
149
+ onChange: setInnerOffset,
150
+ overflowRef,
151
+ }),
152
+ useListNavigation(context, {
153
+ listRef: listItemsRef,
154
+ activeIndex,
155
+ selectedIndex,
156
+ onNavigate: setActiveIndex,
157
+ }),
158
+ useTypeahead(context, {
159
+ listRef: listContentRef,
160
+ onMatch: open ? setActiveIndex : setSelectedIndex,
161
+ selectedIndex,
162
+ activeIndex,
163
+ }),
164
+ ])
165
+
166
+ const interactionsContext = {
167
+ ...interactions,
168
+ getReferenceProps() {
169
+ return interactions.getReferenceProps({
170
+ ref: reference,
171
+ className: 'SelectTrigger',
172
+ onKeyDown(event) {
173
+ if (event.key === 'Enter' || (event.key === ' ' && !context.dataRef.current.typing)) {
174
+ event.preventDefault()
175
+ setOpen(true)
176
+ }
177
+ },
178
+ })
179
+ },
180
+ getFloatingProps(props) {
181
+ return interactions.getFloatingProps({
182
+ ref: floating,
183
+ className: 'Select',
184
+ ...props,
185
+ style: {
186
+ position: strategy,
187
+ top: y ?? '',
188
+ left: x ?? '',
189
+ outline: 0,
190
+ listStyleType: 'none',
191
+ scrollbarWidth: 'none',
192
+ ...floatingStyle.current,
193
+ ...props?.style,
194
+ },
195
+ onPointerEnter() {
196
+ setControlledScrolling(false)
197
+ state.current.isMouseOutside = false
198
+ },
199
+ onPointerLeave() {
200
+ state.current.isMouseOutside = true
201
+ },
202
+ onPointerMove() {
203
+ state.current.isMouseOutside = false
204
+ setControlledScrolling(false)
205
+ },
206
+ onKeyDown() {
207
+ setControlledScrolling(true)
208
+ },
209
+ onContextMenu(e) {
210
+ e.preventDefault()
211
+ },
212
+ onScroll(event) {
213
+ // In React 18, the ScrollArrows need to synchronously know this value to prevent
214
+ // painting at the wrong time.
215
+ flushSync(() => setScrollTop(event.currentTarget.scrollTop))
216
+ },
217
+ })
218
+ },
219
+ }
220
+
221
+ // effects
222
+
223
+ useIsomorphicLayoutEffect(() => {
224
+ if (open) {
225
+ selectTimeoutRef.current = setTimeout(() => {
226
+ allowSelectRef.current = true
227
+ }, 300)
228
+
229
+ return () => {
230
+ clearTimeout(selectTimeoutRef.current)
231
+ }
232
+ } else {
233
+ allowSelectRef.current = false
234
+ allowMouseUpRef.current = true
235
+ setInnerOffset(0)
236
+ setFallback(false)
237
+ setBlockSelection(false)
238
+ }
239
+ }, [open])
240
+
241
+ // Replacement for `useDismiss` as the arrows are outside of the floating
242
+ // element DOM tree.
243
+ useIsomorphicLayoutEffect(() => {
244
+ function onPointerDown(e: PointerEvent) {
245
+ const target = e.target as Node
246
+ if (
247
+ !refs.floating.current?.contains(target) &&
248
+ !upArrowRef.current?.contains(target) &&
249
+ !downArrowRef.current?.contains(target)
250
+ ) {
251
+ setOpen(false)
252
+ setControlledScrolling(false)
253
+ }
254
+ }
255
+
256
+ if (open) {
257
+ document.addEventListener('pointerdown', onPointerDown)
258
+ return () => {
259
+ document.removeEventListener('pointerdown', onPointerDown)
260
+ }
261
+ }
262
+ }, [open, refs, setOpen])
263
+
264
+ // Scroll the `activeIndex` item into view only in "controlledScrolling"
265
+ // (keyboard nav) mode.
266
+ useIsomorphicLayoutEffect(() => {
267
+ if (open && controlledScrolling) {
268
+ if (activeIndex != null) {
269
+ listItemsRef.current[activeIndex]?.scrollIntoView({ block: 'nearest' })
270
+ }
271
+ }
272
+
273
+ setScrollTop(refs.floating.current?.scrollTop ?? 0)
274
+ }, [open, refs, controlledScrolling, activeIndex])
275
+
276
+ // Scroll the `selectedIndex` into view upon opening the floating element.
277
+ useIsomorphicLayoutEffect(() => {
278
+ if (open && fallback) {
279
+ if (selectedIndex != null) {
280
+ listItemsRef.current[selectedIndex]?.scrollIntoView({ block: 'nearest' })
281
+ }
282
+ }
283
+ }, [open, fallback, selectedIndex])
284
+
285
+ // Unset the height limiting for fallback mode. This gets executed prior to
286
+ // the positioning call.
287
+ useIsomorphicLayoutEffect(() => {
288
+ if (refs.floating.current && fallback) {
289
+ refs.floating.current.style.maxHeight = ''
290
+ }
291
+ }, [refs, fallback])
292
+
293
+ // We set this to true by default so that events bubble to forms without JS (SSR)
294
+ // const isFormControl = trigger ? Boolean(trigger.closest('form')) : true
295
+ // const [bubbleSelect, setBubbleSelect] = React.useState<HTMLSelectElement | null>(null)
296
+ // const triggerPointerDownPosRef = React.useRef<{ x: number; y: number } | null>(null)
297
+
298
+ return (
299
+ <SelectProvider
300
+ scope={__scopeSelect}
301
+ {...(selectContext as Required<typeof selectContext>)}
302
+ setScrollTop={setScrollTop}
303
+ setInnerOffset={setInnerOffset}
304
+ floatingRef={floatingRef}
305
+ setValueAtIndex={(index, value) => {
306
+ listContentRef.current[index] = value
307
+ }}
308
+ fallback={fallback}
309
+ interactions={interactionsContext}
310
+ floatingContext={context}
311
+ activeIndex={activeIndex}
312
+ canScrollDown={!!showDownArrow}
313
+ canScrollUp={!!showUpArrow}
314
+ controlledScrolling
315
+ dataRef={context.dataRef}
316
+ listRef={listItemsRef}
317
+ blockSelection={blockSelection}
318
+ allowMouseUpRef={allowMouseUpRef}
319
+ upArrowRef={upArrowRef}
320
+ downArrowRef={downArrowRef}
321
+ selectTimeoutRef={selectTimeoutRef}
322
+ allowSelectRef={allowSelectRef}
323
+ >
324
+ {children}
325
+ {/* {isFormControl ? (
326
+ <BubbleSelect
327
+ ref={setBubbleSelect}
328
+ aria-hidden
329
+ tabIndex={-1}
330
+ name={name}
331
+ autoComplete={autoComplete}
332
+ value={value}
333
+ // enable form autofill
334
+ onChange={(event) => setValue(event.target.value)}
335
+ />
336
+ ) : null} */}
337
+ </SelectProvider>
338
+ )
339
+ }
340
+
341
+ // Cross browser fixes for pinch-zooming/backdrop-filter 🙄
342
+ const userAgent = (typeof navigator !== 'undefined' && navigator.userAgent) || ''
@@ -0,0 +1,7 @@
1
+ import { ScopedProps, SelectScrollButtonProps } from './types'
2
+
3
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
4
+ export const SelectScrollUpButton = (_: ScopedProps<SelectScrollButtonProps>) => null
5
+
6
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
7
+ export const SelectScrollDownButton = (_: ScopedProps<SelectScrollButtonProps>) => null
@@ -0,0 +1,168 @@
1
+ import { autoUpdate, offset, useFloating } from '@floating-ui/react-dom-interactions'
2
+ import { useComposedRefs } from '@tamagui/compose-refs'
3
+ import { TamaguiElement, useIsomorphicLayoutEffect } from '@tamagui/core'
4
+ import { YStack } from '@tamagui/stacks'
5
+ import * as React from 'react'
6
+ import { flushSync } from 'react-dom'
7
+
8
+ import { useSelectContext } from './context'
9
+ import { ScopedProps, SelectScrollButtonImplProps, SelectScrollButtonProps } from './types'
10
+
11
+ /* -------------------------------------------------------------------------------------------------
12
+ * SelectScrollUpButton
13
+ * -----------------------------------------------------------------------------------------------*/
14
+
15
+ const SCROLL_UP_BUTTON_NAME = 'SelectScrollUpButton'
16
+
17
+ export const SelectScrollUpButton = React.forwardRef<TamaguiElement, SelectScrollButtonProps>(
18
+ (props: ScopedProps<SelectScrollButtonProps>, forwardedRef) => {
19
+ return (
20
+ <SelectScrollButtonImpl
21
+ componentName={SCROLL_UP_BUTTON_NAME}
22
+ {...props}
23
+ dir="up"
24
+ ref={forwardedRef}
25
+ />
26
+ )
27
+ }
28
+ )
29
+
30
+ SelectScrollUpButton.displayName = SCROLL_UP_BUTTON_NAME
31
+
32
+ /* -------------------------------------------------------------------------------------------------
33
+ * SelectScrollDownButton
34
+ * -----------------------------------------------------------------------------------------------*/
35
+
36
+ const SCROLL_DOWN_BUTTON_NAME = 'SelectScrollDownButton'
37
+
38
+ export const SelectScrollDownButton = React.forwardRef<TamaguiElement, SelectScrollButtonProps>(
39
+ (props: ScopedProps<SelectScrollButtonProps>, forwardedRef) => {
40
+ return (
41
+ <SelectScrollButtonImpl
42
+ componentName={SCROLL_DOWN_BUTTON_NAME}
43
+ {...props}
44
+ dir="down"
45
+ ref={forwardedRef}
46
+ />
47
+ )
48
+ }
49
+ )
50
+
51
+ SelectScrollDownButton.displayName = SCROLL_DOWN_BUTTON_NAME
52
+
53
+ type SelectScrollButtonImplElement = TamaguiElement
54
+
55
+ const SelectScrollButtonImpl = React.memo(
56
+ React.forwardRef<SelectScrollButtonImplElement, SelectScrollButtonImplProps>(
57
+ (props: ScopedProps<SelectScrollButtonImplProps>, forwardedRef) => {
58
+ const { __scopeSelect, dir, componentName, ...scrollIndicatorProps } = props
59
+ const { floatingRef, forceUpdate, open, fallback, setScrollTop, setInnerOffset, ...context } =
60
+ useSelectContext(componentName, __scopeSelect)
61
+
62
+ const [element, setElement] = React.useState<HTMLElement | null>(null)
63
+ const statusRef = React.useRef<'idle' | 'active'>('idle')
64
+ const isVisible = context[dir === 'down' ? 'canScrollDown' : 'canScrollUp']
65
+ const frameRef = React.useRef<any>()
66
+
67
+ const { x, y, reference, floating, strategy, update, refs } = useFloating({
68
+ strategy: 'fixed',
69
+ placement: dir === 'up' ? 'top' : 'bottom',
70
+ middleware: [offset(({ rects }) => -rects.floating.height)],
71
+ whileElementsMounted: (...args) => autoUpdate(...args, { animationFrame: true }),
72
+ })
73
+
74
+ const composedRef = useComposedRefs(forwardedRef, floating)
75
+
76
+ if (floatingRef) {
77
+ if (open) {
78
+ if (element !== floatingRef.current) {
79
+ setElement(floatingRef.current)
80
+ reference(floatingRef.current)
81
+ requestAnimationFrame(update)
82
+ }
83
+ } else {
84
+ cancelAnimationFrame(frameRef.current)
85
+ }
86
+ }
87
+
88
+ useIsomorphicLayoutEffect(() => {
89
+ return () => {
90
+ // eslint-disable-next-line react-hooks/exhaustive-deps
91
+ cancelAnimationFrame(frameRef.current)
92
+ }
93
+ }, [])
94
+
95
+ if (!isVisible || !floatingRef) {
96
+ return null
97
+ }
98
+
99
+ const onScroll = (amount: number) => {
100
+ if (fallback) {
101
+ if (refs.floating.current) {
102
+ refs.floating.current.scrollTop -= amount
103
+ flushSync(() => setScrollTop!(refs.floating.current?.scrollTop ?? 0))
104
+ }
105
+ } else {
106
+ flushSync(() => setInnerOffset!((value) => value - amount))
107
+ }
108
+ }
109
+
110
+ return (
111
+ <YStack
112
+ ref={composedRef}
113
+ componentName={componentName}
114
+ aria-hidden
115
+ {...scrollIndicatorProps}
116
+ zIndex={1000}
117
+ // @ts-expect-error
118
+ position={strategy}
119
+ left={x || 0}
120
+ top={y || 0}
121
+ width={`calc(${(floatingRef?.current?.offsetWidth ?? 0) - 2}px)`}
122
+ onPointerEnter={() => {
123
+ statusRef.current = 'active'
124
+ let prevNow = Date.now()
125
+
126
+ function frame() {
127
+ if (element) {
128
+ const currentNow = Date.now()
129
+ const msElapsed = currentNow - prevNow
130
+ prevNow = currentNow
131
+
132
+ const pixelsToScroll = msElapsed / 2
133
+
134
+ const remainingPixels =
135
+ dir === 'up'
136
+ ? element.scrollTop
137
+ : element.scrollHeight - element.clientHeight - element.scrollTop
138
+
139
+ const scrollRemaining =
140
+ dir === 'up'
141
+ ? element.scrollTop - pixelsToScroll > 0
142
+ : element.scrollTop + pixelsToScroll <
143
+ element.scrollHeight - element.clientHeight
144
+
145
+ onScroll(
146
+ dir === 'up'
147
+ ? Math.min(pixelsToScroll, remainingPixels)
148
+ : Math.max(-pixelsToScroll, -remainingPixels)
149
+ )
150
+
151
+ if (scrollRemaining) {
152
+ frameRef.current = requestAnimationFrame(frame)
153
+ }
154
+ }
155
+ }
156
+
157
+ cancelAnimationFrame(frameRef.current)
158
+ frameRef.current = requestAnimationFrame(frame)
159
+ }}
160
+ onPointerLeave={() => {
161
+ statusRef.current = 'idle'
162
+ cancelAnimationFrame(frameRef.current)
163
+ }}
164
+ />
165
+ )
166
+ }
167
+ )
168
+ )
@@ -0,0 +1,14 @@
1
+ import { PortalItem } from '@tamagui/portal'
2
+ import * as React from 'react'
3
+
4
+ import { VIEWPORT_NAME } from './constants'
5
+ import { useSelectContext } from './context'
6
+ import { ScopedProps, SelectViewportProps } from './types'
7
+
8
+ export const SelectViewport = (props: ScopedProps<SelectViewportProps>) => {
9
+ const { __scopeSelect, children } = props
10
+ const context = useSelectContext(VIEWPORT_NAME, __scopeSelect)
11
+ return <PortalItem hostName={`${context.scopeKey}SheetContents`}>{children}</PortalItem>
12
+ }
13
+
14
+ SelectViewport.displayName = VIEWPORT_NAME
@@ -0,0 +1,104 @@
1
+ import { FloatingFocusManager } from '@floating-ui/react-dom-interactions'
2
+ import { TamaguiElement, isWeb } from '@tamagui/core'
3
+ import { styled } from '@tamagui/core'
4
+ import { PortalItem } from '@tamagui/portal'
5
+ import { ThemeableStack } from '@tamagui/stacks'
6
+ import * as React from 'react'
7
+
8
+ import { VIEWPORT_NAME } from './constants'
9
+ import { ForwardSelectContext, useSelectContext } from './context'
10
+ import { ScopedProps, SelectViewportProps } from './types'
11
+ import { useSelectBreakpointActive } from './useSelectBreakpointActive'
12
+
13
+ /* -------------------------------------------------------------------------------------------------
14
+ * SelectViewport
15
+ * -----------------------------------------------------------------------------------------------*/
16
+
17
+ export const SelectViewportFrame = styled(ThemeableStack, {
18
+ name: VIEWPORT_NAME,
19
+ backgroundColor: '$background',
20
+ elevate: true,
21
+ bordered: true,
22
+ overflow: 'scroll',
23
+ userSelect: 'none',
24
+
25
+ variants: {
26
+ size: {
27
+ '...size': (val, { tokens }) => {
28
+ return {
29
+ borderRadius: tokens.radius[val] ?? val,
30
+ }
31
+ },
32
+ },
33
+ } as const,
34
+
35
+ defaultVariants: {
36
+ size: '$2',
37
+ },
38
+ })
39
+
40
+ export const SelectViewport = React.forwardRef<TamaguiElement, SelectViewportProps>(
41
+ (props: ScopedProps<SelectViewportProps>, forwardedRef) => {
42
+ const { __scopeSelect, children, ...viewportProps } = props
43
+ const context = useSelectContext(VIEWPORT_NAME, __scopeSelect)
44
+ const breakpointActive = useSelectBreakpointActive(context.sheetBreakpoint)
45
+
46
+ if (breakpointActive || !isWeb) {
47
+ return (
48
+ <PortalItem hostName={`${context.scopeKey}SheetContents`}>
49
+ <ForwardSelectContext context={context}>{children}</ForwardSelectContext>
50
+ </PortalItem>
51
+ )
52
+ }
53
+
54
+ if (!context.floatingContext) {
55
+ return null
56
+ }
57
+
58
+ if (!context.open) {
59
+ return children
60
+ }
61
+
62
+ const {
63
+ style: { scrollbarWidth, listStyleType, ...restStyle },
64
+ ...floatingProps
65
+ } = context.interactions!.getFloatingProps()
66
+
67
+ return (
68
+ <>
69
+ <style
70
+ dangerouslySetInnerHTML={{
71
+ __html: selectViewportCSS,
72
+ }}
73
+ />
74
+ <FloatingFocusManager context={context.floatingContext} preventTabbing>
75
+ <SelectViewportFrame
76
+ size={context.size}
77
+ // @ts-ignore
78
+ role="presentation"
79
+ {...viewportProps}
80
+ ref={forwardedRef}
81
+ {...floatingProps}
82
+ {...restStyle}
83
+ >
84
+ {children}
85
+ </SelectViewportFrame>
86
+ </FloatingFocusManager>
87
+ </>
88
+ )
89
+ }
90
+ )
91
+
92
+ SelectViewport.displayName = VIEWPORT_NAME
93
+
94
+ const selectViewportCSS = `
95
+ .is_SelectViewport {
96
+ scrollbar-width: none;
97
+ -webkit-overflow-scrolling: touch;
98
+ overscroll-behavior: contain;
99
+ }
100
+
101
+ .is_SelectViewport::-webkit-scrollbar{
102
+ display:none
103
+ }
104
+ `
@@ -0,0 +1,7 @@
1
+ export const SELECT_NAME = 'Select'
2
+ export const WINDOW_PADDING = 8
3
+ export const SCROLL_ARROW_VELOCITY = 8
4
+ export const SCROLL_ARROW_THRESHOLD = 8
5
+ export const MIN_HEIGHT = 80
6
+ export const FALLBACK_THRESHOLD = 16
7
+ export const VIEWPORT_NAME = 'SelectViewport'
@@ -0,0 +1,19 @@
1
+ import { createContextScope } from '@tamagui/create-context'
2
+
3
+ import { SELECT_NAME } from './constants'
4
+ import { ScopedProps, SelectContextValue } from './types'
5
+
6
+ export const [createSelectContext, createSelectScope] = createContextScope(SELECT_NAME)
7
+
8
+ export const [SelectProvider, useSelectContext] =
9
+ createSelectContext<SelectContextValue>(SELECT_NAME)
10
+
11
+ export const ForwardSelectContext = (
12
+ props: ScopedProps<{ children?: any; context: SelectContextValue }>
13
+ ) => {
14
+ return (
15
+ <SelectProvider isInSheet scope={props.__scopeSelect} {...props.context}>
16
+ {props.children}
17
+ </SelectProvider>
18
+ )
19
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,8 @@
1
+ export * from './Select'
2
+ export type {
3
+ SelectContentProps,
4
+ SelectProps,
5
+ SelectScrollButtonProps,
6
+ SelectViewportProps,
7
+ } from './types'
8
+ export * from './context'