@tamagui/select 1.0.1-beta.99 → 1.0.1-rc.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.
Files changed (114) hide show
  1. package/dist/cjs/BubbleSelect.js +25 -33
  2. package/dist/cjs/BubbleSelect.js.map +1 -1
  3. package/dist/cjs/Select.js +391 -789
  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 +288 -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 +125 -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 +20 -33
  30. package/dist/esm/BubbleSelect.js.map +1 -1
  31. package/dist/esm/Select.js +379 -803
  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 +273 -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 +94 -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 +20 -18
  58. package/dist/jsx/BubbleSelect.js.map +1 -1
  59. package/dist/jsx/Select.js +259 -656
  60. package/dist/jsx/Select.js.map +3 -3
  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 +246 -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 +1 -0
  80. package/dist/jsx/index.js.map +2 -2
  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 -20
  86. package/src/Select.tsx +210 -906
  87. package/src/SelectContent.native.tsx +5 -0
  88. package/src/SelectContent.tsx +45 -0
  89. package/src/SelectImpl.tsx +344 -0
  90. package/src/SelectScrollButton.native.tsx +7 -0
  91. package/src/SelectScrollButton.tsx +168 -0
  92. package/src/SelectViewport.native.tsx +14 -0
  93. package/src/SelectViewport.tsx +104 -0
  94. package/src/constants.tsx +7 -0
  95. package/src/context.tsx +19 -0
  96. package/src/index.tsx +7 -0
  97. package/src/types.tsx +91 -0
  98. package/src/useSelectBreakpointActive.tsx +15 -0
  99. package/types/BubbleSelect.d.ts +2 -0
  100. package/types/Select.d.ts +449 -599
  101. package/types/SelectContent.d.ts +5 -0
  102. package/types/SelectContent.native.d.ts +4 -0
  103. package/types/SelectImpl.d.ts +9 -0
  104. package/types/SelectScrollButton.d.ts +6 -0
  105. package/types/SelectScrollButton.native.d.ts +4 -0
  106. package/types/SelectViewport.d.ts +127 -0
  107. package/types/SelectViewport.native.d.ts +7 -0
  108. package/types/constants.d.ts +8 -0
  109. package/types/context.d.ts +21 -0
  110. package/types/index.d.ts +2 -0
  111. package/types/types.d.ts +81 -0
  112. package/types/useSelectBreakpointActive.d.ts +4 -0
  113. package/types/Select.d.ts.map +0 -1
  114. package/types/index.d.ts.map +0 -1
@@ -0,0 +1,5 @@
1
+ import { SelectContentProps } from './types'
2
+
3
+ export const SelectContent = ({ children }: SelectContentProps) => {
4
+ return children
5
+ }
@@ -0,0 +1,45 @@
1
+ import { FloatingOverlay, FloatingPortal } from '@floating-ui/react-dom-interactions'
2
+ import { Theme, useIsTouchDevice, useThemeName } from '@tamagui/core'
3
+ import { Dismissable } from '@tamagui/dismissable'
4
+ import { FocusScope, FocusScopeProps } from '@tamagui/focus-scope'
5
+
6
+ import { useSelectContext } from './context'
7
+ import { SelectContentProps } from './types'
8
+ import { useShowSelectSheet } from './useSelectBreakpointActive'
9
+
10
+ /* -------------------------------------------------------------------------------------------------
11
+ * SelectContent
12
+ * -----------------------------------------------------------------------------------------------*/
13
+
14
+ const CONTENT_NAME = 'SelectContent'
15
+
16
+ export const SelectContent = ({
17
+ children,
18
+ __scopeSelect,
19
+ zIndex = 1000,
20
+ ...focusScopeProps
21
+ }: SelectContentProps & FocusScopeProps) => {
22
+ const context = useSelectContext(CONTENT_NAME, __scopeSelect)
23
+ const themeName = useThemeName()
24
+ const showSheet = useShowSelectSheet(context)
25
+ const contents = <Theme name={themeName}>{children}</Theme>
26
+ const touch = useIsTouchDevice()
27
+
28
+ if (showSheet && context.open) {
29
+ return contents
30
+ }
31
+
32
+ return (
33
+ <FloatingPortal>
34
+ {context.open ? (
35
+ <FloatingOverlay style={{ zIndex }} lockScroll={!touch}>
36
+ <FocusScope loop trapped {...focusScopeProps}>
37
+ <Dismissable disableOutsidePointerEvents>{contents}</Dismissable>
38
+ </FocusScope>
39
+ </FloatingOverlay>
40
+ ) : (
41
+ <div style={{ display: 'none' }}>{contents}</div>
42
+ )}
43
+ </FloatingPortal>
44
+ )
45
+ }
@@ -0,0 +1,344 @@
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 { isWeb, 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
+ if (isWeb) {
77
+ React.useEffect(() => {
78
+ if (!open) return
79
+ const mouseUp = (e: MouseEvent) => {
80
+ if (state.current.isMouseOutside) {
81
+ setOpen(false)
82
+ }
83
+ }
84
+ document.addEventListener('mouseup', mouseUp)
85
+ return () => {
86
+ document.removeEventListener('mouseup', mouseUp)
87
+ }
88
+ }, [open])
89
+ }
90
+
91
+ const updateFloatingSize = size({
92
+ apply({
93
+ availableHeight,
94
+ rects: {
95
+ reference: { width },
96
+ },
97
+ }) {
98
+ floatingStyle.current = {
99
+ width: width,
100
+ maxHeight: availableHeight,
101
+ }
102
+ },
103
+ padding: WINDOW_PADDING,
104
+ })
105
+
106
+ const { x, y, reference, floating, strategy, context, refs } = useFloating({
107
+ open,
108
+ onOpenChange: setOpen,
109
+ whileElementsMounted: autoUpdate,
110
+ placement: 'bottom-start',
111
+ middleware: fallback
112
+ ? [
113
+ offset(5),
114
+ ...[
115
+ touch
116
+ ? shift({ crossAxis: true, padding: WINDOW_PADDING })
117
+ : flip({ padding: WINDOW_PADDING }),
118
+ ],
119
+ updateFloatingSize,
120
+ ]
121
+ : [
122
+ inner({
123
+ listRef: listItemsRef,
124
+ overflowRef,
125
+ index: selectedIndex,
126
+ offset: innerOffset,
127
+ onFallbackChange: setFallback,
128
+ padding: 10,
129
+ minItemsVisible: touch ? 10 : 4,
130
+ referenceOverflowThreshold: 20,
131
+ }),
132
+ updateFloatingSize,
133
+ ],
134
+ })
135
+
136
+ const floatingRef = refs.floating
137
+
138
+ const showUpArrow = open && scrollTop > SCROLL_ARROW_THRESHOLD
139
+ const showDownArrow =
140
+ open &&
141
+ floatingRef.current &&
142
+ scrollTop <
143
+ floatingRef.current.scrollHeight - floatingRef.current.clientHeight - SCROLL_ARROW_THRESHOLD
144
+
145
+ const interactions = useInteractions([
146
+ useClick(context, { event: 'mousedown' }),
147
+ useDismiss(context, { outsidePress: false }),
148
+ useRole(context, { role: 'listbox' }),
149
+ useInnerOffset(context, {
150
+ enabled: !fallback,
151
+ onChange: setInnerOffset,
152
+ overflowRef,
153
+ }),
154
+ useListNavigation(context, {
155
+ listRef: listItemsRef,
156
+ activeIndex,
157
+ selectedIndex,
158
+ onNavigate: setActiveIndex,
159
+ }),
160
+ useTypeahead(context, {
161
+ listRef: listContentRef,
162
+ onMatch: open ? setActiveIndex : setSelectedIndex,
163
+ selectedIndex,
164
+ activeIndex,
165
+ }),
166
+ ])
167
+
168
+ const interactionsContext = {
169
+ ...interactions,
170
+ getReferenceProps() {
171
+ return interactions.getReferenceProps({
172
+ ref: reference,
173
+ className: 'SelectTrigger',
174
+ onKeyDown(event) {
175
+ if (event.key === 'Enter' || (event.key === ' ' && !context.dataRef.current.typing)) {
176
+ event.preventDefault()
177
+ setOpen(true)
178
+ }
179
+ },
180
+ })
181
+ },
182
+ getFloatingProps(props) {
183
+ return interactions.getFloatingProps({
184
+ ref: floating,
185
+ className: 'Select',
186
+ ...props,
187
+ style: {
188
+ position: strategy,
189
+ top: y ?? '',
190
+ left: x ?? '',
191
+ outline: 0,
192
+ listStyleType: 'none',
193
+ scrollbarWidth: 'none',
194
+ ...floatingStyle.current,
195
+ ...props?.style,
196
+ },
197
+ onPointerEnter() {
198
+ setControlledScrolling(false)
199
+ state.current.isMouseOutside = false
200
+ },
201
+ onPointerLeave() {
202
+ state.current.isMouseOutside = true
203
+ },
204
+ onPointerMove() {
205
+ state.current.isMouseOutside = false
206
+ setControlledScrolling(false)
207
+ },
208
+ onKeyDown() {
209
+ setControlledScrolling(true)
210
+ },
211
+ onContextMenu(e) {
212
+ e.preventDefault()
213
+ },
214
+ onScroll(event) {
215
+ // In React 18, the ScrollArrows need to synchronously know this value to prevent
216
+ // painting at the wrong time.
217
+ flushSync(() => setScrollTop(event.currentTarget.scrollTop))
218
+ },
219
+ })
220
+ },
221
+ }
222
+
223
+ // effects
224
+
225
+ useIsomorphicLayoutEffect(() => {
226
+ if (open) {
227
+ selectTimeoutRef.current = setTimeout(() => {
228
+ allowSelectRef.current = true
229
+ }, 300)
230
+
231
+ return () => {
232
+ clearTimeout(selectTimeoutRef.current)
233
+ }
234
+ } else {
235
+ allowSelectRef.current = false
236
+ allowMouseUpRef.current = true
237
+ setInnerOffset(0)
238
+ setFallback(false)
239
+ setBlockSelection(false)
240
+ }
241
+ }, [open])
242
+
243
+ // Replacement for `useDismiss` as the arrows are outside of the floating
244
+ // element DOM tree.
245
+ useIsomorphicLayoutEffect(() => {
246
+ function onPointerDown(e: PointerEvent) {
247
+ const target = e.target as Node
248
+ if (
249
+ !refs.floating.current?.contains(target) &&
250
+ !upArrowRef.current?.contains(target) &&
251
+ !downArrowRef.current?.contains(target)
252
+ ) {
253
+ setOpen(false)
254
+ setControlledScrolling(false)
255
+ }
256
+ }
257
+
258
+ if (open) {
259
+ document.addEventListener('pointerdown', onPointerDown)
260
+ return () => {
261
+ document.removeEventListener('pointerdown', onPointerDown)
262
+ }
263
+ }
264
+ }, [open, refs, setOpen])
265
+
266
+ // Scroll the `activeIndex` item into view only in "controlledScrolling"
267
+ // (keyboard nav) mode.
268
+ useIsomorphicLayoutEffect(() => {
269
+ if (open && controlledScrolling) {
270
+ if (activeIndex != null) {
271
+ listItemsRef.current[activeIndex]?.scrollIntoView({ block: 'nearest' })
272
+ }
273
+ }
274
+
275
+ setScrollTop(refs.floating.current?.scrollTop ?? 0)
276
+ }, [open, refs, controlledScrolling, activeIndex])
277
+
278
+ // Scroll the `selectedIndex` into view upon opening the floating element.
279
+ useIsomorphicLayoutEffect(() => {
280
+ if (open && fallback) {
281
+ if (selectedIndex != null) {
282
+ listItemsRef.current[selectedIndex]?.scrollIntoView({ block: 'nearest' })
283
+ }
284
+ }
285
+ }, [open, fallback, selectedIndex])
286
+
287
+ // Unset the height limiting for fallback mode. This gets executed prior to
288
+ // the positioning call.
289
+ useIsomorphicLayoutEffect(() => {
290
+ if (refs.floating.current && fallback) {
291
+ refs.floating.current.style.maxHeight = ''
292
+ }
293
+ }, [refs, fallback])
294
+
295
+ // We set this to true by default so that events bubble to forms without JS (SSR)
296
+ // const isFormControl = trigger ? Boolean(trigger.closest('form')) : true
297
+ // const [bubbleSelect, setBubbleSelect] = React.useState<HTMLSelectElement | null>(null)
298
+ // const triggerPointerDownPosRef = React.useRef<{ x: number; y: number } | null>(null)
299
+
300
+ return (
301
+ <SelectProvider
302
+ scope={__scopeSelect}
303
+ {...(selectContext as Required<typeof selectContext>)}
304
+ setScrollTop={setScrollTop}
305
+ setInnerOffset={setInnerOffset}
306
+ floatingRef={floatingRef}
307
+ setValueAtIndex={(index, value) => {
308
+ listContentRef.current[index] = value
309
+ }}
310
+ fallback={fallback}
311
+ interactions={interactionsContext}
312
+ floatingContext={context}
313
+ activeIndex={activeIndex}
314
+ canScrollDown={!!showDownArrow}
315
+ canScrollUp={!!showUpArrow}
316
+ controlledScrolling
317
+ dataRef={context.dataRef}
318
+ listRef={listItemsRef}
319
+ blockSelection={blockSelection}
320
+ allowMouseUpRef={allowMouseUpRef}
321
+ upArrowRef={upArrowRef}
322
+ downArrowRef={downArrowRef}
323
+ selectTimeoutRef={selectTimeoutRef}
324
+ allowSelectRef={allowSelectRef}
325
+ >
326
+ {children}
327
+ {/* {isFormControl ? (
328
+ <BubbleSelect
329
+ ref={setBubbleSelect}
330
+ aria-hidden
331
+ tabIndex={-1}
332
+ name={name}
333
+ autoComplete={autoComplete}
334
+ value={value}
335
+ // enable form autofill
336
+ onChange={(event) => setValue(event.target.value)}
337
+ />
338
+ ) : null} */}
339
+ </SelectProvider>
340
+ )
341
+ }
342
+
343
+ // Cross browser fixes for pinch-zooming/backdrop-filter 🙄
344
+ 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}>
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'