@tamagui/select 1.0.1-beta.113 → 1.0.1-beta.114

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 (109) hide show
  1. package/dist/cjs/Select.js +50 -558
  2. package/dist/cjs/Select.js.map +2 -2
  3. package/dist/cjs/SelectContent.js +53 -0
  4. package/dist/cjs/SelectContent.js.map +7 -0
  5. package/dist/cjs/SelectContent.native.js +31 -0
  6. package/dist/cjs/SelectContent.native.js.map +7 -0
  7. package/dist/cjs/SelectImpl.js +393 -0
  8. package/dist/cjs/SelectImpl.js.map +7 -0
  9. package/dist/cjs/SelectScrollButton.js +128 -0
  10. package/dist/cjs/SelectScrollButton.js.map +7 -0
  11. package/dist/cjs/SelectScrollButton.native.js +32 -0
  12. package/dist/cjs/SelectScrollButton.native.js.map +7 -0
  13. package/dist/cjs/SelectViewport.js +96 -0
  14. package/dist/cjs/SelectViewport.js.map +7 -0
  15. package/dist/cjs/SelectViewport.native.js +43 -0
  16. package/dist/cjs/SelectViewport.native.js.map +7 -0
  17. package/dist/cjs/constants.js +47 -0
  18. package/dist/cjs/constants.js.map +7 -0
  19. package/dist/cjs/context.js +38 -0
  20. package/dist/cjs/context.js.map +7 -0
  21. package/dist/cjs/index.js +19 -0
  22. package/dist/cjs/index.js.map +2 -2
  23. package/dist/cjs/types.js +17 -0
  24. package/dist/cjs/types.js.map +7 -0
  25. package/dist/esm/Select.js +33 -565
  26. package/dist/esm/Select.js.map +2 -2
  27. package/dist/esm/SelectContent.js +26 -0
  28. package/dist/esm/SelectContent.js.map +7 -0
  29. package/dist/esm/SelectContent.native.js +7 -0
  30. package/dist/esm/SelectContent.native.js.map +7 -0
  31. package/dist/esm/SelectImpl.js +378 -0
  32. package/dist/esm/SelectImpl.js.map +7 -0
  33. package/dist/esm/SelectScrollButton.js +100 -0
  34. package/dist/esm/SelectScrollButton.js.map +7 -0
  35. package/dist/esm/SelectScrollButton.native.js +7 -0
  36. package/dist/esm/SelectScrollButton.native.js.map +7 -0
  37. package/dist/esm/SelectViewport.js +68 -0
  38. package/dist/esm/SelectViewport.js.map +7 -0
  39. package/dist/esm/SelectViewport.native.js +16 -0
  40. package/dist/esm/SelectViewport.native.js.map +7 -0
  41. package/dist/esm/constants.js +17 -0
  42. package/dist/esm/constants.js.map +7 -0
  43. package/dist/esm/context.js +11 -0
  44. package/dist/esm/context.js.map +7 -0
  45. package/dist/esm/index.js +13 -0
  46. package/dist/esm/index.js.map +2 -2
  47. package/dist/esm/types.js +1 -0
  48. package/dist/esm/types.js.map +7 -0
  49. package/dist/jsx/Select.js +32 -515
  50. package/dist/jsx/Select.js.map +2 -2
  51. package/dist/jsx/SelectContent.js +19 -0
  52. package/dist/jsx/SelectContent.js.map +7 -0
  53. package/dist/jsx/SelectContent.native.js +7 -0
  54. package/dist/jsx/SelectContent.native.js.map +7 -0
  55. package/dist/jsx/SelectImpl.js +366 -0
  56. package/dist/jsx/SelectImpl.js.map +7 -0
  57. package/dist/jsx/SelectScrollButton.js +78 -0
  58. package/dist/jsx/SelectScrollButton.js.map +7 -0
  59. package/dist/jsx/SelectScrollButton.native.js +7 -0
  60. package/dist/jsx/SelectScrollButton.native.js.map +7 -0
  61. package/dist/jsx/SelectViewport.js +57 -0
  62. package/dist/jsx/SelectViewport.js.map +7 -0
  63. package/dist/jsx/SelectViewport.native.js +14 -0
  64. package/dist/jsx/SelectViewport.native.js.map +7 -0
  65. package/dist/jsx/constants.js +17 -0
  66. package/dist/jsx/constants.js.map +7 -0
  67. package/dist/jsx/context.js +11 -0
  68. package/dist/jsx/context.js.map +7 -0
  69. package/dist/jsx/index.js +13 -0
  70. package/dist/jsx/index.js.map +2 -2
  71. package/dist/jsx/types.js +1 -0
  72. package/dist/jsx/types.js.map +7 -0
  73. package/package.json +12 -12
  74. package/src/Select.tsx +57 -881
  75. package/src/SelectContent.native.tsx +5 -0
  76. package/src/SelectContent.tsx +34 -0
  77. package/src/SelectImpl.tsx +511 -0
  78. package/src/SelectScrollButton.native.tsx +7 -0
  79. package/src/SelectScrollButton.tsx +150 -0
  80. package/src/SelectViewport.native.tsx +17 -0
  81. package/src/SelectViewport.tsx +86 -0
  82. package/src/constants.tsx +7 -0
  83. package/src/context.tsx +9 -0
  84. package/src/index.tsx +7 -0
  85. package/src/types.tsx +83 -0
  86. package/types/Select.d.ts +140 -298
  87. package/types/Select.d.ts.map +1 -1
  88. package/types/SelectContent.d.ts +4 -0
  89. package/types/SelectContent.d.ts.map +1 -0
  90. package/types/SelectContent.native.d.ts +4 -0
  91. package/types/SelectContent.native.d.ts.map +1 -0
  92. package/types/SelectImpl.d.ts +9 -0
  93. package/types/SelectImpl.d.ts.map +1 -0
  94. package/types/SelectScrollButton.d.ts +6 -0
  95. package/types/SelectScrollButton.d.ts.map +1 -0
  96. package/types/SelectScrollButton.native.d.ts +4 -0
  97. package/types/SelectScrollButton.native.d.ts.map +1 -0
  98. package/types/SelectViewport.d.ts +127 -0
  99. package/types/SelectViewport.d.ts.map +1 -0
  100. package/types/SelectViewport.native.d.ts +54 -0
  101. package/types/SelectViewport.native.d.ts.map +1 -0
  102. package/types/constants.d.ts +8 -0
  103. package/types/constants.d.ts.map +1 -0
  104. package/types/context.d.ts +17 -0
  105. package/types/context.d.ts.map +1 -0
  106. package/types/index.d.ts +2 -0
  107. package/types/index.d.ts.map +1 -1
  108. package/types/types.d.ts +74 -0
  109. package/types/types.d.ts.map +1 -0
@@ -0,0 +1,5 @@
1
+ import { SelectContentProps } from './types'
2
+
3
+ export const SelectContent = ({ children }: SelectContentProps) => {
4
+ return children
5
+ }
@@ -0,0 +1,34 @@
1
+ import { FloatingOverlay, FloatingPortal } from '@floating-ui/react-dom-interactions'
2
+ import { Theme, useThemeName } from '@tamagui/core'
3
+ import * as React from 'react'
4
+
5
+ import { useSelectContext } from './context'
6
+ import { useShowSelectSheet } from './Select'
7
+ import { SelectContentProps } from './types'
8
+
9
+ /* -------------------------------------------------------------------------------------------------
10
+ * SelectContent
11
+ * -----------------------------------------------------------------------------------------------*/
12
+
13
+ const CONTENT_NAME = 'SelectContent'
14
+
15
+ export const SelectContent = ({ children, __scopeSelect }: SelectContentProps) => {
16
+ const context = useSelectContext(CONTENT_NAME, __scopeSelect)
17
+ const themeName = useThemeName()
18
+ const showSheet = useShowSelectSheet(context)
19
+ const contents = <Theme name={themeName}>{children}</Theme>
20
+
21
+ if (showSheet && context.open) {
22
+ return contents
23
+ }
24
+
25
+ return (
26
+ <FloatingPortal>
27
+ {context.open ? (
28
+ <FloatingOverlay lockScroll>{contents}</FloatingOverlay>
29
+ ) : (
30
+ <div style={{ display: 'none' }}>{contents}</div>
31
+ )}
32
+ </FloatingPortal>
33
+ )
34
+ }
@@ -0,0 +1,511 @@
1
+ import {
2
+ detectOverflow,
3
+ flip,
4
+ offset,
5
+ size,
6
+ useClick,
7
+ useDismiss,
8
+ useFloating,
9
+ useInteractions,
10
+ useListNavigation,
11
+ useRole,
12
+ useTypeahead,
13
+ } from '@floating-ui/react-dom-interactions'
14
+ import { usePrevious } from '@radix-ui/react-use-previous'
15
+ import * as React from 'react'
16
+
17
+ import { FALLBACK_THRESHOLD, MIN_HEIGHT, SCROLL_ARROW_THRESHOLD, WINDOW_PADDING } from './constants'
18
+ import { SelectProvider, useSelectContext } from './context'
19
+ import { getVisualOffsetTop, isFirefox } from './Select'
20
+ import { ScopedProps, SelectProps } from './types'
21
+
22
+ export type SelectImplProps = ScopedProps<SelectProps> & {
23
+ activeIndexRef: any
24
+ selectedIndexRef: any
25
+ listContentRef: any
26
+ }
27
+
28
+ // TODO use id for focusing from label
29
+ export const SelectInlineImpl = (props: SelectImplProps) => {
30
+ const {
31
+ __scopeSelect,
32
+ children,
33
+ open = false,
34
+ activeIndexRef,
35
+ selectedIndexRef,
36
+ listContentRef,
37
+ } = props
38
+
39
+ const selectContext = useSelectContext('SelectSheetImpl', __scopeSelect)
40
+ const { setActiveIndex, setOpen, setSelectedIndex, selectedIndex, activeIndex, forceUpdate } =
41
+ selectContext
42
+ const [showArrows, setShowArrows] = React.useState(false)
43
+ const [scrollTop, setScrollTop] = React.useState(0)
44
+ const prevActiveIndex = usePrevious<number | null>(activeIndex)
45
+
46
+ const listItemsRef = React.useRef<Array<HTMLElement | null>>([])
47
+
48
+ const [controlledScrolling, setControlledScrolling] = React.useState(false)
49
+ const [middlewareType, setMiddlewareType] = React.useState<'align' | 'fallback'>('align')
50
+
51
+ // Wait for scroll position to settle before showing arrows to prevent
52
+ // interference with pointer events.
53
+ React.useEffect(() => {
54
+ const frame = requestAnimationFrame(() => {
55
+ setShowArrows(open)
56
+
57
+ if (!open) {
58
+ setScrollTop(0)
59
+ setMiddlewareType('align')
60
+ setActiveIndex(null)
61
+ setControlledScrolling(false)
62
+ }
63
+ })
64
+ return () => {
65
+ cancelAnimationFrame(frame)
66
+ }
67
+ }, [open, setActiveIndex])
68
+
69
+ function getFloatingPadding(floating: HTMLElement | null) {
70
+ if (!floating) {
71
+ return 0
72
+ }
73
+ return Number(getComputedStyle(floating).paddingLeft?.replace('px', ''))
74
+ }
75
+
76
+ const { x, y, reference, floating, strategy, context, refs, middlewareData, update } =
77
+ useFloating({
78
+ open,
79
+ onOpenChange: setOpen,
80
+ placement: 'bottom',
81
+ middleware:
82
+ middlewareType === 'align'
83
+ ? [
84
+ offset(({ rects }) => {
85
+ const index = activeIndexRef.current ?? selectedIndexRef.current
86
+
87
+ if (index == null) {
88
+ return 0
89
+ }
90
+
91
+ const item = listItemsRef.current[index]
92
+
93
+ if (item == null) {
94
+ return 0
95
+ }
96
+
97
+ const offsetTop = item.offsetTop
98
+ const itemHeight = item.offsetHeight
99
+ const height = rects.reference.height
100
+
101
+ return -offsetTop - height - (itemHeight - height) / 2
102
+ }),
103
+ // Custom `size` that can handle the opposite direction of the placement
104
+ {
105
+ name: 'size',
106
+ async fn(args) {
107
+ const {
108
+ elements: { floating },
109
+ rects: { reference },
110
+ middlewareData,
111
+ } = args
112
+
113
+ const overflow = await detectOverflow(args, {
114
+ padding: WINDOW_PADDING,
115
+ })
116
+
117
+ const top = Math.max(0, overflow.top)
118
+ const bottom = Math.max(0, overflow.bottom)
119
+ const nextY = args.y + top
120
+
121
+ if (middlewareData.size?.skip) {
122
+ return {
123
+ y: nextY,
124
+ data: {
125
+ y: middlewareData.size.y,
126
+ },
127
+ }
128
+ }
129
+
130
+ Object.assign(floating.style, {
131
+ maxHeight: `${floating.scrollHeight - Math.abs(top + bottom)}px`,
132
+ minWidth: `${reference.width + getFloatingPadding(floating) * 2}px`,
133
+ })
134
+
135
+ return {
136
+ y: nextY,
137
+ data: {
138
+ y: top,
139
+ skip: true,
140
+ },
141
+ reset: {
142
+ rects: true,
143
+ },
144
+ }
145
+ },
146
+ },
147
+ ]
148
+ : [
149
+ offset(5),
150
+ flip(),
151
+ size({
152
+ apply({ rects, availableHeight, elements }) {
153
+ Object.assign(elements.floating.style, {
154
+ width: `${rects.reference.width}px`,
155
+ maxHeight: `${availableHeight}px`,
156
+ })
157
+ },
158
+ padding: WINDOW_PADDING,
159
+ }),
160
+ ],
161
+ })
162
+
163
+ const floatingRef = refs.floating
164
+
165
+ const showUpArrow = showArrows && scrollTop > SCROLL_ARROW_THRESHOLD
166
+ const showDownArrow =
167
+ showArrows &&
168
+ floatingRef.current &&
169
+ scrollTop <
170
+ floatingRef.current.scrollHeight - floatingRef.current.clientHeight - SCROLL_ARROW_THRESHOLD
171
+
172
+ const interactions = useInteractions([
173
+ useClick(context, { pointerDown: true }),
174
+ useRole(context, { role: 'listbox' }),
175
+ useDismiss(context),
176
+ useListNavigation(context, {
177
+ listRef: listItemsRef,
178
+ activeIndex,
179
+ selectedIndex,
180
+ onNavigate: setActiveIndex,
181
+ }),
182
+ useTypeahead(context, {
183
+ listRef: listContentRef,
184
+ onMatch: open ? setActiveIndex : setSelectedIndex,
185
+ selectedIndex,
186
+ activeIndex,
187
+ }),
188
+ ])
189
+
190
+ const increaseHeight = React.useCallback(
191
+ (floating: HTMLElement, amount = 0) => {
192
+ if (middlewareType === 'fallback') {
193
+ return
194
+ }
195
+
196
+ const currentMaxHeight = Number(floating.style.maxHeight.replace('px', ''))
197
+ const currentTop = Number(floating.style.top.replace('px', ''))
198
+ const rect = floating.getBoundingClientRect()
199
+ const rectTop = rect.top
200
+ const rectBottom = rect.bottom
201
+ const viewportHeight = visualViewport?.height ?? 0
202
+ const visualMaxHeight = viewportHeight - WINDOW_PADDING * 2
203
+
204
+ if (
205
+ amount < 0 &&
206
+ selectedIndexRef.current != null &&
207
+ Math.round(rectBottom) < Math.round(viewportHeight + getVisualOffsetTop() - WINDOW_PADDING)
208
+ ) {
209
+ floating.style.maxHeight = `${Math.min(visualMaxHeight, currentMaxHeight - amount)}px`
210
+ }
211
+
212
+ if (
213
+ amount > 0 &&
214
+ Math.round(rectTop) > Math.round(WINDOW_PADDING - getVisualOffsetTop()) &&
215
+ floating.scrollHeight > floating.offsetHeight
216
+ ) {
217
+ const nextTop = Math.max(WINDOW_PADDING + getVisualOffsetTop(), currentTop - amount)
218
+
219
+ const nextMaxHeight = Math.min(visualMaxHeight, currentMaxHeight + amount)
220
+
221
+ Object.assign(floating.style, {
222
+ maxHeight: `${nextMaxHeight}px`,
223
+ top: `${nextTop}px`,
224
+ })
225
+
226
+ if (nextTop - WINDOW_PADDING > getVisualOffsetTop()) {
227
+ floating.scrollTop -= nextMaxHeight - currentMaxHeight + getFloatingPadding(floating)
228
+ }
229
+
230
+ return currentTop - nextTop
231
+ }
232
+ },
233
+ [middlewareType, selectedIndexRef]
234
+ )
235
+
236
+ const touchPageYRef = React.useRef<number | null>(null)
237
+
238
+ const handleWheel = React.useCallback(
239
+ (event: WheelEvent | TouchEvent) => {
240
+ const pinching = event.ctrlKey
241
+
242
+ const currentTarget = event.currentTarget as HTMLElement
243
+
244
+ function isWheelEvent(event: any): event is WheelEvent {
245
+ return typeof event.deltaY === 'number'
246
+ }
247
+
248
+ function isTouchEvent(event: any): event is TouchEvent {
249
+ return event.touches != null
250
+ }
251
+
252
+ if (
253
+ Math.abs(
254
+ (currentTarget?.offsetHeight ?? 0) - ((visualViewport?.height ?? 0) - WINDOW_PADDING * 2)
255
+ ) > 1 &&
256
+ !pinching
257
+ ) {
258
+ event.preventDefault()
259
+ } else if (isWheelEvent(event) && isFirefox) {
260
+ // Firefox needs this to propagate scrolling
261
+ // during momentum scrolling phase if the
262
+ // height reached its maximum (at boundaries)
263
+ currentTarget.scrollTop += event.deltaY
264
+ }
265
+
266
+ if (!pinching) {
267
+ let delta = 5
268
+
269
+ if (isTouchEvent(event)) {
270
+ const currentPageY = touchPageYRef.current
271
+ const pageY = event.touches[0]?.pageY
272
+
273
+ if (pageY != null) {
274
+ touchPageYRef.current = pageY
275
+
276
+ if (currentPageY != null) {
277
+ delta = currentPageY - pageY
278
+ }
279
+ }
280
+ }
281
+
282
+ increaseHeight(currentTarget, isWheelEvent(event) ? event.deltaY : delta)
283
+ setScrollTop(currentTarget.scrollTop)
284
+ // Ensure derived data (scroll arrows) is fresh
285
+ forceUpdate()
286
+ }
287
+ },
288
+ [forceUpdate, increaseHeight]
289
+ )
290
+
291
+ // Handle `onWheel` event in an effect to remove the `passive` option so we
292
+ // can .preventDefault() it
293
+ React.useEffect(() => {
294
+ function onTouchEnd() {
295
+ touchPageYRef.current = null
296
+ }
297
+
298
+ const floating = floatingRef.current
299
+ if (open && floating && middlewareType === 'align') {
300
+ floating.addEventListener('wheel', handleWheel)
301
+ floating.addEventListener('touchmove', handleWheel)
302
+ floating.addEventListener('touchend', onTouchEnd, { passive: true })
303
+ return () => {
304
+ floating.removeEventListener('wheel', handleWheel)
305
+ floating.removeEventListener('touchmove', handleWheel)
306
+ floating.removeEventListener('touchend', onTouchEnd)
307
+ }
308
+ }
309
+ }, [open, floatingRef, handleWheel, middlewareType])
310
+
311
+ // Ensure the menu remains attached to the reference element when resizing.
312
+ React.useEffect(() => {
313
+ window.addEventListener('resize', update)
314
+ return () => {
315
+ window.removeEventListener('resize', update)
316
+ }
317
+ }, [update])
318
+
319
+ // Scroll the active or selected item into view when in `controlledScrolling`
320
+ // mode (i.e. arrow key nav).
321
+ React.useLayoutEffect(() => {
322
+ const floating = floatingRef.current
323
+
324
+ if (open && controlledScrolling && floating) {
325
+ const item =
326
+ activeIndex != null
327
+ ? listItemsRef.current[activeIndex]
328
+ : selectedIndex != null
329
+ ? listItemsRef.current[selectedIndex]
330
+ : null
331
+
332
+ if (item && prevActiveIndex != null) {
333
+ const itemHeight = listItemsRef.current[prevActiveIndex]?.offsetHeight ?? 0
334
+
335
+ const floatingHeight = floating.offsetHeight
336
+ const top = item.offsetTop
337
+ const bottom = top + itemHeight
338
+
339
+ if (top < floating.scrollTop + 20) {
340
+ const diff = floating.scrollTop - top + 20
341
+ floating.scrollTop -= diff
342
+
343
+ if (activeIndex != selectedIndex && activeIndex != null) {
344
+ increaseHeight(floating, -diff)
345
+ }
346
+ } else if (bottom > floatingHeight + floating.scrollTop - 20) {
347
+ const diff = bottom - floatingHeight - floating.scrollTop + 20
348
+
349
+ floating.scrollTop += diff
350
+
351
+ if (activeIndex != selectedIndex && activeIndex != null) {
352
+ floating.scrollTop -= increaseHeight(floating, diff) ?? 0
353
+ }
354
+ }
355
+ }
356
+ }
357
+ }, [
358
+ open,
359
+ controlledScrolling,
360
+ prevActiveIndex,
361
+ activeIndex,
362
+ selectedIndex,
363
+ floatingRef,
364
+ increaseHeight,
365
+ ])
366
+
367
+ // Sync the height and the scrollTop values and device whether to use fallback
368
+ // positioning.
369
+ React.useLayoutEffect(() => {
370
+ const floating = refs.floating.current
371
+ const reference = refs.reference.current
372
+
373
+ if (open && floating && reference && floating.offsetHeight < floating.scrollHeight) {
374
+ const referenceRect = reference.getBoundingClientRect()
375
+
376
+ if (middlewareType === 'fallback') {
377
+ const item = listItemsRef.current[selectedIndex]
378
+ if (item) {
379
+ floating.scrollTop = item.offsetTop - floating.clientHeight + referenceRect.height
380
+ }
381
+ return
382
+ }
383
+
384
+ floating.scrollTop = middlewareData.size?.y
385
+
386
+ const closeToBottom =
387
+ (visualViewport?.height ?? 0) + getVisualOffsetTop() - referenceRect.bottom <
388
+ FALLBACK_THRESHOLD
389
+ const closeToTop = referenceRect.top < FALLBACK_THRESHOLD
390
+
391
+ if (floating.offsetHeight < MIN_HEIGHT || closeToTop || closeToBottom) {
392
+ setMiddlewareType('fallback')
393
+ }
394
+ }
395
+ }, [
396
+ open,
397
+ increaseHeight,
398
+ selectedIndex,
399
+ middlewareType,
400
+ refs.floating,
401
+ refs.reference,
402
+ // Always re-run this effect when the position has been computed so the
403
+ // .scrollTop change works with fresh sizing.
404
+ middlewareData,
405
+ ])
406
+
407
+ React.useLayoutEffect(() => {
408
+ if (open && selectedIndex != null) {
409
+ requestAnimationFrame(() => {
410
+ listItemsRef.current[selectedIndex]?.focus({ preventScroll: true })
411
+ })
412
+ }
413
+ }, [listItemsRef, selectedIndex, open])
414
+
415
+ // Wait for scroll position to settle before showing arrows to prevent
416
+ // interference with pointer events.
417
+ React.useEffect(() => {
418
+ const frame = requestAnimationFrame(() => {
419
+ setShowArrows(open)
420
+
421
+ if (!open) {
422
+ setScrollTop(0)
423
+ setMiddlewareType('align')
424
+ setActiveIndex(null)
425
+ setControlledScrolling(false)
426
+ }
427
+ })
428
+ return () => cancelAnimationFrame(frame)
429
+ }, [open, setActiveIndex])
430
+
431
+ // We set this to true by default so that events bubble to forms without JS (SSR)
432
+ // const isFormControl = trigger ? Boolean(trigger.closest('form')) : true
433
+ // const [bubbleSelect, setBubbleSelect] = React.useState<HTMLSelectElement | null>(null)
434
+ // const triggerPointerDownPosRef = React.useRef<{ x: number; y: number } | null>(null)
435
+ return (
436
+ <SelectProvider
437
+ scope={__scopeSelect}
438
+ {...(selectContext as Required<typeof selectContext>)}
439
+ increaseHeight={increaseHeight}
440
+ floatingRef={floatingRef}
441
+ setValueAtIndex={(index, value) => {
442
+ listContentRef.current[index] = value
443
+ }}
444
+ interactions={{
445
+ ...interactions,
446
+ getReferenceProps() {
447
+ return interactions.getReferenceProps({
448
+ ref: reference,
449
+ className: 'SelectTrigger',
450
+ onKeyDown(event) {
451
+ if (event.key === 'Enter' || (event.key === ' ' && !context.dataRef.current.typing)) {
452
+ event.preventDefault()
453
+ setOpen(true)
454
+ }
455
+ },
456
+ })
457
+ },
458
+ getFloatingProps(props) {
459
+ return interactions.getFloatingProps({
460
+ ref: floating,
461
+ className: 'Select',
462
+ ...props,
463
+ style: {
464
+ position: strategy,
465
+ top: y ?? '',
466
+ left: x ?? '',
467
+ outline: 0,
468
+ listStyleType: 'none',
469
+ scrollbarWidth: 'none',
470
+ userSelect: 'none',
471
+ ...props?.style,
472
+ },
473
+ onPointerEnter() {
474
+ setControlledScrolling(false)
475
+ },
476
+ onPointerMove() {
477
+ setControlledScrolling(false)
478
+ },
479
+ onKeyDown() {
480
+ setControlledScrolling(true)
481
+ },
482
+ onScroll(event) {
483
+ setScrollTop(event.currentTarget.scrollTop)
484
+ },
485
+ })
486
+ },
487
+ }}
488
+ floatingContext={context}
489
+ activeIndex={activeIndex}
490
+ canScrollDown={!!showDownArrow}
491
+ canScrollUp={!!showUpArrow}
492
+ controlledScrolling
493
+ dataRef={context.dataRef}
494
+ listRef={listItemsRef}
495
+ >
496
+ {children}
497
+ {/* {isFormControl ? (
498
+ <BubbleSelect
499
+ ref={setBubbleSelect}
500
+ aria-hidden
501
+ tabIndex={-1}
502
+ name={name}
503
+ autoComplete={autoComplete}
504
+ value={value}
505
+ // enable form autofill
506
+ onChange={(event) => setValue(event.target.value)}
507
+ />
508
+ ) : null} */}
509
+ </SelectProvider>
510
+ )
511
+ }
@@ -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,150 @@
1
+ import { autoUpdate, offset, useFloating } from '@floating-ui/react-dom-interactions'
2
+ import { useComposedRefs } from '@tamagui/compose-refs'
3
+ import { TamaguiElement } from '@tamagui/core'
4
+ import { YStack } from '@tamagui/stacks'
5
+ import * as React from 'react'
6
+
7
+ import { SCROLL_ARROW_THRESHOLD, SCROLL_ARROW_VELOCITY } from './constants'
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, increaseHeight, forceUpdate, ...context } = useSelectContext(
60
+ componentName,
61
+ __scopeSelect
62
+ )
63
+ const intervalRef = React.useRef<any>()
64
+ const loopingRef = React.useRef(false)
65
+ const isVisible = context[dir === 'down' ? 'canScrollDown' : 'canScrollUp']
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
+ })
72
+
73
+ const composedRefs = useComposedRefs(forwardedRef, floating)
74
+
75
+ React.useLayoutEffect(() => {
76
+ if (!floatingRef) return
77
+ reference(floatingRef.current)
78
+ // eslint-disable-next-line react-hooks/exhaustive-deps
79
+ }, [reference, floatingRef?.current])
80
+
81
+ React.useEffect(() => {
82
+ if (!refs.reference.current || !refs.floating.current || !isVisible) {
83
+ return
84
+ }
85
+
86
+ const cleanup = autoUpdate(refs.reference.current, refs.floating.current, update, {
87
+ animationFrame: true,
88
+ })
89
+
90
+ return () => {
91
+ clearInterval(intervalRef.current)
92
+ loopingRef.current = false
93
+ cleanup()
94
+ }
95
+ }, [isVisible, update, refs.floating, refs.reference])
96
+
97
+ if (!isVisible) {
98
+ return null
99
+ }
100
+
101
+ const handleScrollArrowChange = () => {
102
+ if (!floatingRef) return
103
+ const floating = floatingRef.current
104
+ const isUp = dir === 'up'
105
+
106
+ if (floating) {
107
+ const value = isUp ? -SCROLL_ARROW_VELOCITY : SCROLL_ARROW_VELOCITY
108
+ const multi =
109
+ (isUp && floating.scrollTop <= SCROLL_ARROW_THRESHOLD * 2) ||
110
+ (!isUp &&
111
+ floating.scrollTop >=
112
+ floating.scrollHeight - floating.clientHeight - SCROLL_ARROW_THRESHOLD * 2)
113
+ ? 2
114
+ : 1
115
+
116
+ floating.scrollTop += multi * (isUp ? -SCROLL_ARROW_VELOCITY : SCROLL_ARROW_VELOCITY)
117
+
118
+ increaseHeight?.(floating, multi === 2 ? value * 2 : value)
119
+ // Ensure derived data (scroll arrows) is fresh
120
+ forceUpdate()
121
+ }
122
+ }
123
+
124
+ return (
125
+ <YStack
126
+ ref={composedRefs}
127
+ componentName={componentName}
128
+ aria-hidden
129
+ {...scrollIndicatorProps}
130
+ zIndex={1000}
131
+ // @ts-expect-error
132
+ position={strategy}
133
+ left={x || 0}
134
+ top={y || 0}
135
+ width={`calc(${(floatingRef?.current?.offsetWidth ?? 0) - 2}px)`}
136
+ onPointerMove={() => {
137
+ if (!loopingRef.current) {
138
+ intervalRef.current = setInterval(handleScrollArrowChange, 1000 / 60)
139
+ loopingRef.current = true
140
+ }
141
+ }}
142
+ onPointerLeave={() => {
143
+ loopingRef.current = false
144
+ clearInterval(intervalRef.current)
145
+ }}
146
+ />
147
+ )
148
+ }
149
+ )
150
+ )