@tamagui/select 1.0.0-beta.17 → 1.0.1-beta.102

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/src/Select.tsx ADDED
@@ -0,0 +1,1320 @@
1
+ import {
2
+ ContextData,
3
+ FloatingContext,
4
+ FloatingFocusManager,
5
+ FloatingOverlay,
6
+ FloatingPortal,
7
+ ReferenceType,
8
+ autoUpdate,
9
+ detectOverflow,
10
+ flip,
11
+ offset,
12
+ size,
13
+ useClick,
14
+ useDismiss,
15
+ useFloating,
16
+ useInteractions,
17
+ useListNavigation,
18
+ useRole,
19
+ useTypeahead,
20
+ } from '@floating-ui/react-dom-interactions'
21
+ import { usePrevious } from '@radix-ui/react-use-previous'
22
+ import { useComposedRefs } from '@tamagui/compose-refs'
23
+ import {
24
+ GetProps,
25
+ SizeTokens,
26
+ Theme,
27
+ styled,
28
+ useIsomorphicLayoutEffect,
29
+ useThemeName,
30
+ withStaticProperties,
31
+ } from '@tamagui/core'
32
+ import { useId } from '@tamagui/core'
33
+ import { createContextScope } from '@tamagui/create-context'
34
+ import type { Scope } from '@tamagui/create-context'
35
+ import { ListItem, ListItemProps } from '@tamagui/list-item'
36
+ import { Separator } from '@tamagui/separator'
37
+ import { ThemeableStack, XStack, YStack, YStackProps } from '@tamagui/stacks'
38
+ import { Paragraph } from '@tamagui/text'
39
+ import { useControllableState } from '@tamagui/use-controllable-state'
40
+ import * as React from 'react'
41
+ import * as ReactDOM from 'react-dom'
42
+ import { View } from 'react-native'
43
+
44
+ type TamaguiElement = HTMLElement | View
45
+
46
+ // Cross browser fixes for pinch-zooming/backdrop-filter 🙄
47
+ const userAgent = (typeof navigator !== 'undefined' && navigator.userAgent) || ''
48
+ const isFirefox = userAgent.toLowerCase().includes('firefox')
49
+ if (isFirefox) {
50
+ document.body.classList.add('firefox')
51
+ }
52
+ function getVisualOffsetTop() {
53
+ return !/^((?!chrome|android).)*safari/i.test(userAgent) ? visualViewport?.offsetTop ?? 0 : 0
54
+ }
55
+
56
+ /* -------------------------------------------------------------------------------------------------
57
+ * SelectContext
58
+ * -----------------------------------------------------------------------------------------------*/
59
+
60
+ const SELECT_NAME = 'Select'
61
+ const WINDOW_PADDING = 8
62
+ const SCROLL_ARROW_VELOCITY = 8
63
+ const SCROLL_ARROW_THRESHOLD = 8
64
+ const MIN_HEIGHT = 80
65
+ const FALLBACK_THRESHOLD = 16
66
+
67
+ interface SelectContextValue {
68
+ size?: SizeTokens
69
+ value: any
70
+ selectedIndex: number
71
+ setSelectedIndex: (index: number) => void
72
+ activeIndex: number | null
73
+ setActiveIndex: (index: number | null) => void
74
+ setValueAtIndex: (index: number, value: string) => void
75
+ listRef: React.MutableRefObject<Array<HTMLElement | null>>
76
+ floatingRef: React.MutableRefObject<HTMLElement | null>
77
+ open: boolean
78
+ setOpen: (open: boolean) => void
79
+ onChange: (value: string) => void
80
+ dataRef: React.MutableRefObject<ContextData>
81
+ controlledScrolling: boolean
82
+ valueNode: Element | null
83
+ onValueNodeChange(node: HTMLElement): void
84
+ valueNodeHasChildren: boolean
85
+ onValueNodeHasChildrenChange(hasChildren: boolean): void
86
+ canScrollUp: boolean
87
+ canScrollDown: boolean
88
+ floatingContext: FloatingContext<ReferenceType>
89
+ increaseHeight: (floating: HTMLElement, amount?: any) => number | undefined
90
+ forceUpdate: React.DispatchWithoutAction
91
+ interactions: {
92
+ getReferenceProps: (userProps?: React.HTMLProps<Element> | undefined) => any
93
+ getFloatingProps: (userProps?: React.HTMLProps<HTMLElement> | undefined) => any
94
+ getItemProps: (userProps?: React.HTMLProps<HTMLElement> | undefined) => any
95
+ }
96
+ }
97
+
98
+ type ScopedProps<P> = P & { __scopeSelect?: Scope }
99
+
100
+ const [createSelectContext, createSelectScope] = createContextScope(SELECT_NAME)
101
+ const [SelectProvider, useSelectContext] = createSelectContext<SelectContextValue>(SELECT_NAME)
102
+
103
+ // const [SelectContentContextProvider, useSelectContentContext] =
104
+ // createSelectContext<SelectContentContextValue>(CONTENT_NAME);
105
+
106
+ type GenericElement = HTMLElement | View
107
+
108
+ type Direction = 'ltr' | 'rtl'
109
+
110
+ /* -------------------------------------------------------------------------------------------------
111
+ * SelectTrigger
112
+ * -----------------------------------------------------------------------------------------------*/
113
+
114
+ const TRIGGER_NAME = 'SelectTrigger'
115
+
116
+ export type SelectTriggerProps = ListItemProps
117
+
118
+ export const SelectTrigger = React.forwardRef<GenericElement, SelectTriggerProps>(
119
+ (props: ScopedProps<SelectTriggerProps>, forwardedRef) => {
120
+ const {
121
+ __scopeSelect,
122
+ disabled = false,
123
+ // @ts-ignore
124
+ 'aria-labelledby': ariaLabelledby,
125
+ ...triggerProps
126
+ } = props
127
+ const context = useSelectContext(TRIGGER_NAME, __scopeSelect)
128
+ // const composedRefs = useComposedRefs(forwardedRef, context.onTriggerChange)
129
+ // const getItems = useCollection(__scopeSelect)
130
+ // const labelId = useLabelContext(context.trigger)
131
+ // const labelledBy = ariaLabelledby || labelId
132
+
133
+ return (
134
+ <ListItem
135
+ backgrounded
136
+ radiused
137
+ hoverTheme
138
+ pressTheme
139
+ focusTheme
140
+ focusable={false}
141
+ borderWidth={1}
142
+ componentName={TRIGGER_NAME}
143
+ size={context.size}
144
+ // aria-controls={context.contentId}
145
+ aria-expanded={context.open}
146
+ aria-autocomplete="none"
147
+ // aria-labelledby={labelledBy}
148
+ // dir={context.dir}
149
+ disabled={disabled}
150
+ data-disabled={disabled ? '' : undefined}
151
+ {...triggerProps}
152
+ ref={forwardedRef}
153
+ {...context.interactions.getReferenceProps()}
154
+ />
155
+ )
156
+ }
157
+ )
158
+
159
+ SelectTrigger.displayName = TRIGGER_NAME
160
+
161
+ /* -------------------------------------------------------------------------------------------------
162
+ * SelectValue
163
+ * -----------------------------------------------------------------------------------------------*/
164
+
165
+ const VALUE_NAME = 'SelectValue'
166
+
167
+ const SelectValueFrame = styled(Paragraph, {
168
+ name: VALUE_NAME,
169
+ selectable: false,
170
+ })
171
+
172
+ type SelectValueProps = GetProps<typeof SelectValueFrame> & {
173
+ placeholder?: React.ReactNode
174
+ }
175
+
176
+ const SelectValue = SelectValueFrame.extractable(
177
+ React.forwardRef<TamaguiElement, SelectValueProps>(
178
+ ({ __scopeSelect, children, placeholder }: ScopedProps<SelectValueProps>, forwardedRef) => {
179
+ // We ignore `className` and `style` as this part shouldn't be styled.
180
+ const context = useSelectContext(VALUE_NAME, __scopeSelect)
181
+ const { onValueNodeHasChildrenChange } = context
182
+ const hasChildren = children !== undefined
183
+ const composedRefs = useComposedRefs(forwardedRef, context.onValueNodeChange)
184
+
185
+ React.useLayoutEffect(() => {
186
+ onValueNodeHasChildrenChange(hasChildren)
187
+ }, [onValueNodeHasChildrenChange, hasChildren])
188
+
189
+ return (
190
+ <SelectValueFrame
191
+ size={context.size}
192
+ ref={composedRefs}
193
+ // we don't want events from the portalled `SelectValue` children to bubble
194
+ // through the item they came from
195
+ pointerEvents="none"
196
+ >
197
+ {context.value === undefined && placeholder !== undefined ? placeholder : children}
198
+ </SelectValueFrame>
199
+ )
200
+ }
201
+ )
202
+ )
203
+
204
+ SelectValue.displayName = VALUE_NAME
205
+
206
+ /* -------------------------------------------------------------------------------------------------
207
+ * SelectIcon
208
+ * -----------------------------------------------------------------------------------------------*/
209
+
210
+ export const SelectIcon = styled(XStack, {
211
+ name: 'SelectIcon',
212
+ // @ts-ignore
213
+ 'aria-hidden': true,
214
+ children: <Paragraph>▼</Paragraph>,
215
+ })
216
+
217
+ /* -------------------------------------------------------------------------------------------------
218
+ * SelectContent
219
+ * -----------------------------------------------------------------------------------------------*/
220
+
221
+ const CONTENT_NAME = 'SelectContent'
222
+
223
+ export type SelectContentProps = { children?: React.ReactNode }
224
+
225
+ const SelectContent = ({ children, __scopeSelect }: ScopedProps<SelectContentProps>) => {
226
+ const context = useSelectContext(CONTENT_NAME, __scopeSelect)
227
+ const themeName = useThemeName()
228
+
229
+ const contents = <Theme name={themeName}>{children}</Theme>
230
+
231
+ return (
232
+ <FloatingPortal>
233
+ {context.open ? (
234
+ <FloatingOverlay lockScroll>{contents}</FloatingOverlay>
235
+ ) : (
236
+ <div style={{ display: 'none' }}>{contents}</div>
237
+ )}
238
+ </FloatingPortal>
239
+ )
240
+ }
241
+
242
+ /* -------------------------------------------------------------------------------------------------
243
+ * SelectViewport
244
+ * -----------------------------------------------------------------------------------------------*/
245
+
246
+ const VIEWPORT_NAME = 'SelectViewport'
247
+
248
+ export const SelectViewportFrame = styled(ThemeableStack, {
249
+ name: VIEWPORT_NAME,
250
+ backgroundColor: '$background',
251
+ elevate: true,
252
+ bordered: true,
253
+ overflow: 'scroll',
254
+ userSelect: 'none',
255
+
256
+ variants: {
257
+ size: {
258
+ '...size': (val, { tokens }) => {
259
+ return {
260
+ borderRadius: tokens.radius[val] ?? val,
261
+ }
262
+ },
263
+ },
264
+ },
265
+
266
+ defaultVariants: {
267
+ size: '$2',
268
+ },
269
+ })
270
+
271
+ export type SelectViewportProps = GetProps<typeof SelectViewportFrame>
272
+
273
+ export const SelectViewport = React.forwardRef<TamaguiElement, SelectViewportProps>(
274
+ (props: ScopedProps<SelectViewportProps>, forwardedRef) => {
275
+ const { __scopeSelect, children, ...viewportProps } = props
276
+ const context = useSelectContext(VIEWPORT_NAME, __scopeSelect)
277
+ const { style, ...floatingProps } = context.interactions.getFloatingProps()
278
+ delete style['scrollbarWidth']
279
+ delete style['listStyleType']
280
+ return (
281
+ <>
282
+ {/* Hide scrollbars cross-browser and enable momentum scroll for touch devices */}
283
+ <style
284
+ dangerouslySetInnerHTML={{
285
+ __html: `[data-tamagui-select-viewport]{scrollbar-width:none;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;}[data-tamagui-select-viewport]::-webkit-scrollbar{display:none}`,
286
+ }}
287
+ />
288
+ <FloatingFocusManager context={context.floatingContext} preventTabbing>
289
+ <SelectViewportFrame
290
+ size={context.size}
291
+ data-tamagui-select-viewport=""
292
+ // @ts-ignore
293
+ role="presentation"
294
+ {...viewportProps}
295
+ ref={forwardedRef}
296
+ {...floatingProps}
297
+ {...style}
298
+ >
299
+ {children}
300
+ </SelectViewportFrame>
301
+ </FloatingFocusManager>
302
+ </>
303
+ )
304
+ }
305
+ )
306
+
307
+ SelectViewport.displayName = VIEWPORT_NAME
308
+
309
+ /* -------------------------------------------------------------------------------------------------
310
+ * SelectItem
311
+ * -----------------------------------------------------------------------------------------------*/
312
+
313
+ const ITEM_NAME = 'SelectItem'
314
+
315
+ type SelectItemContextValue = {
316
+ value: string
317
+ textId: string
318
+ isSelected: boolean
319
+ onItemTextChange(node: TamaguiElement | null): void
320
+ }
321
+
322
+ const [SelectItemContextProvider, useSelectItemContext] =
323
+ createSelectContext<SelectItemContextValue>(ITEM_NAME)
324
+
325
+ export interface SelectItemProps extends YStackProps {
326
+ value: string
327
+ index: number
328
+ disabled?: boolean
329
+ textValue?: string
330
+ }
331
+
332
+ // TODO styled(ListItem, { name: 'SelectItem' })
333
+
334
+ export const SelectItem = React.forwardRef<TamaguiElement, SelectItemProps>(
335
+ (props: ScopedProps<SelectItemProps>, forwardedRef) => {
336
+ const {
337
+ __scopeSelect,
338
+ value,
339
+ disabled = false,
340
+ textValue: textValueProp,
341
+ index,
342
+ ...itemProps
343
+ } = props
344
+ const context = useSelectContext(ITEM_NAME, __scopeSelect)
345
+ const isSelected = context.value === value
346
+ const [textValue, setTextValue] = React.useState(textValueProp ?? '')
347
+ const [isFocused, setIsFocused] = React.useState(false)
348
+ const textId = useId()
349
+
350
+ const {
351
+ selectedIndex,
352
+ setSelectedIndex,
353
+ listRef,
354
+ open,
355
+ setOpen,
356
+ onChange,
357
+ activeIndex,
358
+ setActiveIndex,
359
+ setValueAtIndex,
360
+ dataRef,
361
+ } = context
362
+
363
+ const composedRefs = useComposedRefs(
364
+ forwardedRef,
365
+ (node) => {
366
+ if (node instanceof HTMLElement) {
367
+ listRef.current[index] = node
368
+ }
369
+ }
370
+ // isSelected ? context.onSelectedItemChange : undefined
371
+ )
372
+
373
+ React.useEffect(() => {
374
+ setValueAtIndex(index, value)
375
+ }, [index])
376
+
377
+ const timeoutRef = React.useRef<any>()
378
+ const [allowMouseUp, setAllowMouseUp] = React.useState(false)
379
+
380
+ function handleSelect() {
381
+ setSelectedIndex(index)
382
+ onChange(value)
383
+ setOpen(false)
384
+ }
385
+
386
+ React.useLayoutEffect(() => {
387
+ clearTimeout(timeoutRef.current)
388
+ if (open) {
389
+ if (selectedIndex !== index) {
390
+ setAllowMouseUp(true)
391
+ } else {
392
+ timeoutRef.current = setTimeout(() => {
393
+ setAllowMouseUp(true)
394
+ }, 300)
395
+ }
396
+ } else {
397
+ setAllowMouseUp(false)
398
+ }
399
+ }, [open, index, setActiveIndex, selectedIndex])
400
+
401
+ function handleKeyDown(event: React.KeyboardEvent) {
402
+ if (event.key === 'Enter' || (event.key === ' ' && !dataRef.current.typing)) {
403
+ event.preventDefault()
404
+ handleSelect()
405
+ }
406
+ }
407
+
408
+ const selectItemProps = context.interactions.getItemProps({
409
+ onClick: allowMouseUp ? handleSelect : undefined,
410
+ onMouseUp: allowMouseUp ? handleSelect : undefined,
411
+ onKeyDown: handleKeyDown,
412
+ })
413
+
414
+ return (
415
+ <SelectItemContextProvider
416
+ scope={__scopeSelect}
417
+ value={value}
418
+ textId={textId || ''}
419
+ isSelected={isSelected}
420
+ onItemTextChange={React.useCallback((node) => {
421
+ // @ts-ignore
422
+ setTextValue((prevTextValue) => prevTextValue || (node?.textContent ?? '').trim())
423
+ }, [])}
424
+ >
425
+ <ListItem
426
+ backgrounded
427
+ pressTheme
428
+ focusTheme
429
+ componentName={ITEM_NAME}
430
+ ref={composedRefs}
431
+ aria-labelledby={textId}
432
+ // `isFocused` caveat fixes stuttering in VoiceOver
433
+ aria-selected={isSelected && isFocused}
434
+ data-state={isSelected ? 'active' : 'inactive'}
435
+ aria-disabled={disabled || undefined}
436
+ data-disabled={disabled ? '' : undefined}
437
+ tabIndex={disabled ? undefined : -1}
438
+ size={context.size}
439
+ {...itemProps}
440
+ {...selectItemProps}
441
+ />
442
+ </SelectItemContextProvider>
443
+ )
444
+ }
445
+ )
446
+
447
+ SelectItem.displayName = ITEM_NAME
448
+
449
+ /* -------------------------------------------------------------------------------------------------
450
+ * SelectItemText
451
+ * -----------------------------------------------------------------------------------------------*/
452
+
453
+ const ITEM_TEXT_NAME = 'SelectItemText'
454
+
455
+ export const SelectItemTextFrame = styled(Paragraph, {
456
+ name: ITEM_TEXT_NAME,
457
+ selectable: false,
458
+ })
459
+
460
+ type SelectItemTextProps = GetProps<typeof SelectItemTextFrame>
461
+
462
+ const SelectItemText = React.forwardRef<TamaguiElement, SelectItemTextProps>(
463
+ (props: ScopedProps<SelectItemTextProps>, forwardedRef) => {
464
+ // We ignore `className` and `style` as this part shouldn't be styled.
465
+ const { __scopeSelect, className, style, ...itemTextProps } = props
466
+ const context = useSelectContext(ITEM_TEXT_NAME, __scopeSelect)
467
+ const itemContext = useSelectItemContext(ITEM_TEXT_NAME, __scopeSelect)
468
+ const ref = React.useRef<TamaguiElement | null>(null)
469
+ const composedRefs = useComposedRefs(forwardedRef, ref, itemContext.onItemTextChange)
470
+
471
+ return (
472
+ <>
473
+ <SelectItemTextFrame
474
+ size={context.size}
475
+ id={itemContext.textId}
476
+ {...itemTextProps}
477
+ ref={composedRefs}
478
+ />
479
+
480
+ {/* Portal the select item text into the trigger value node */}
481
+ {itemContext.isSelected && context.valueNode && !context.valueNodeHasChildren
482
+ ? ReactDOM.createPortal(itemTextProps.children, context.valueNode)
483
+ : null}
484
+
485
+ {/* Portal an option in the bubble select */}
486
+ {/* {context.bubbleSelect
487
+ ? ReactDOM.createPortal(
488
+ // we use `.textContent` because `option` only support `string` or `number`
489
+ <option value={itemContext.value}>{ref.current?.textContent}</option>,
490
+ context.bubbleSelect
491
+ )
492
+ : null} */}
493
+ </>
494
+ )
495
+ }
496
+ )
497
+
498
+ SelectItemText.displayName = ITEM_TEXT_NAME
499
+
500
+ /* -------------------------------------------------------------------------------------------------
501
+ * SelectItemIndicator
502
+ * -----------------------------------------------------------------------------------------------*/
503
+
504
+ const ITEM_INDICATOR_NAME = 'SelectItemIndicator'
505
+
506
+ const SelectItemIndicatorFrame = styled(XStack, {
507
+ name: ITEM_TEXT_NAME,
508
+ })
509
+
510
+ type SelectItemIndicatorProps = GetProps<typeof SelectItemIndicatorFrame>
511
+
512
+ const SelectItemIndicator = React.forwardRef<TamaguiElement, SelectItemIndicatorProps>(
513
+ (props: ScopedProps<SelectItemIndicatorProps>, forwardedRef) => {
514
+ const { __scopeSelect, ...itemIndicatorProps } = props
515
+ const itemContext = useSelectItemContext(ITEM_INDICATOR_NAME, __scopeSelect)
516
+ return itemContext.isSelected ? (
517
+ <SelectItemIndicatorFrame aria-hidden {...itemIndicatorProps} ref={forwardedRef} />
518
+ ) : null
519
+ }
520
+ )
521
+
522
+ SelectItemIndicator.displayName = ITEM_INDICATOR_NAME
523
+
524
+ /* -------------------------------------------------------------------------------------------------
525
+ * SelectGroup
526
+ * -----------------------------------------------------------------------------------------------*/
527
+
528
+ const GROUP_NAME = 'SelectGroup'
529
+
530
+ type SelectGroupContextValue = { id: string }
531
+
532
+ const [SelectGroupContextProvider, useSelectGroupContext] =
533
+ createSelectContext<SelectGroupContextValue>(GROUP_NAME)
534
+
535
+ export const SelectGroupFrame = styled(YStack, {
536
+ name: GROUP_NAME,
537
+ width: '100%',
538
+ })
539
+
540
+ type SelectGroupProps = GetProps<typeof SelectGroupFrame>
541
+
542
+ const SelectGroup = React.forwardRef<TamaguiElement, SelectGroupProps>(
543
+ (props: ScopedProps<SelectGroupProps>, forwardedRef) => {
544
+ const { __scopeSelect, ...groupProps } = props
545
+ const groupId = useId()
546
+ return (
547
+ <SelectGroupContextProvider scope={__scopeSelect} id={groupId || ''}>
548
+ <SelectGroupFrame
549
+ // @ts-ignore
550
+ role="group"
551
+ aria-labelledby={groupId}
552
+ {...groupProps}
553
+ ref={forwardedRef}
554
+ />
555
+ </SelectGroupContextProvider>
556
+ )
557
+ }
558
+ )
559
+
560
+ SelectGroup.displayName = GROUP_NAME
561
+
562
+ /* -------------------------------------------------------------------------------------------------
563
+ * SelectLabel
564
+ * -----------------------------------------------------------------------------------------------*/
565
+
566
+ const LABEL_NAME = 'SelectLabel'
567
+
568
+ export type SelectLabelProps = ListItemProps
569
+
570
+ const SelectLabel = React.forwardRef<TamaguiElement, SelectLabelProps>(
571
+ (props: ScopedProps<SelectLabelProps>, forwardedRef) => {
572
+ const { __scopeSelect, ...labelProps } = props
573
+ const context = useSelectContext(LABEL_NAME, __scopeSelect)
574
+ const groupContext = useSelectGroupContext(LABEL_NAME, __scopeSelect)
575
+ return (
576
+ <ListItem
577
+ componentName={LABEL_NAME}
578
+ fontWeight="800"
579
+ id={groupContext.id}
580
+ size={context.size}
581
+ {...labelProps}
582
+ // @ts-expect-error
583
+ ref={forwardedRef}
584
+ />
585
+ )
586
+ }
587
+ )
588
+
589
+ SelectLabel.displayName = LABEL_NAME
590
+
591
+ /* -------------------------------------------------------------------------------------------------
592
+ * SelectScrollUpButton
593
+ * -----------------------------------------------------------------------------------------------*/
594
+
595
+ const SCROLL_UP_BUTTON_NAME = 'SelectScrollUpButton'
596
+
597
+ interface SelectScrollButtonProps
598
+ extends Omit<SelectScrollButtonImplProps, 'dir' | 'componentName'> {}
599
+
600
+ const SelectScrollUpButton = React.forwardRef<TamaguiElement, SelectScrollButtonProps>(
601
+ (props: ScopedProps<SelectScrollButtonProps>, forwardedRef) => {
602
+ return (
603
+ <SelectScrollButtonImpl
604
+ componentName={SCROLL_UP_BUTTON_NAME}
605
+ {...props}
606
+ dir="up"
607
+ ref={forwardedRef}
608
+ />
609
+ )
610
+ }
611
+ )
612
+
613
+ SelectScrollUpButton.displayName = SCROLL_UP_BUTTON_NAME
614
+
615
+ /* -------------------------------------------------------------------------------------------------
616
+ * SelectScrollDownButton
617
+ * -----------------------------------------------------------------------------------------------*/
618
+
619
+ const SCROLL_DOWN_BUTTON_NAME = 'SelectScrollDownButton'
620
+
621
+ const SelectScrollDownButton = React.forwardRef<TamaguiElement, SelectScrollButtonProps>(
622
+ (props: ScopedProps<SelectScrollButtonProps>, forwardedRef) => {
623
+ return (
624
+ <SelectScrollButtonImpl
625
+ componentName={SCROLL_DOWN_BUTTON_NAME}
626
+ {...props}
627
+ dir="down"
628
+ ref={forwardedRef}
629
+ />
630
+ )
631
+ }
632
+ )
633
+
634
+ SelectScrollDownButton.displayName = SCROLL_DOWN_BUTTON_NAME
635
+
636
+ type SelectScrollButtonImplElement = TamaguiElement
637
+ interface SelectScrollButtonImplProps extends YStackProps {
638
+ dir: 'up' | 'down'
639
+ componentName: string
640
+ }
641
+
642
+ const SelectScrollButtonImpl = React.forwardRef<
643
+ SelectScrollButtonImplElement,
644
+ SelectScrollButtonImplProps
645
+ >((props: ScopedProps<SelectScrollButtonImplProps>, forwardedRef) => {
646
+ const { __scopeSelect, dir, componentName, ...scrollIndicatorProps } = props
647
+ const { floatingRef, increaseHeight, forceUpdate, ...context } = useSelectContext(
648
+ componentName,
649
+ __scopeSelect
650
+ )
651
+ const intervalRef = React.useRef<any>()
652
+ const loopingRef = React.useRef(false)
653
+ const isVisible = context[dir === 'down' ? 'canScrollDown' : 'canScrollUp']
654
+
655
+ const { x, y, reference, floating, strategy, update, refs } = useFloating({
656
+ strategy: 'fixed',
657
+ placement: dir === 'up' ? 'top' : 'bottom',
658
+ middleware: [offset(({ rects }) => -rects.floating.height)],
659
+ })
660
+
661
+ const composedRefs = useComposedRefs(forwardedRef, floating)
662
+
663
+ React.useLayoutEffect(() => {
664
+ reference(floatingRef.current)
665
+ // eslint-disable-next-line react-hooks/exhaustive-deps
666
+ }, [reference, floatingRef.current])
667
+
668
+ React.useEffect(() => {
669
+ if (!refs.reference.current || !refs.floating.current || !isVisible) {
670
+ return
671
+ }
672
+
673
+ const cleanup = autoUpdate(refs.reference.current, refs.floating.current, update, {
674
+ animationFrame: true,
675
+ })
676
+
677
+ return () => {
678
+ clearInterval(intervalRef.current)
679
+ loopingRef.current = false
680
+ cleanup()
681
+ }
682
+ }, [isVisible, update, refs.floating, refs.reference])
683
+
684
+ if (!isVisible) {
685
+ return null
686
+ }
687
+
688
+ const handleScrollArrowChange = () => {
689
+ const floating = floatingRef.current
690
+ const isUp = dir === 'up'
691
+
692
+ if (floating) {
693
+ const value = isUp ? -SCROLL_ARROW_VELOCITY : SCROLL_ARROW_VELOCITY
694
+ const multi =
695
+ (isUp && floating.scrollTop <= SCROLL_ARROW_THRESHOLD * 2) ||
696
+ (!isUp &&
697
+ floating.scrollTop >=
698
+ floating.scrollHeight - floating.clientHeight - SCROLL_ARROW_THRESHOLD * 2)
699
+ ? 2
700
+ : 1
701
+
702
+ floating.scrollTop += multi * (isUp ? -SCROLL_ARROW_VELOCITY : SCROLL_ARROW_VELOCITY)
703
+
704
+ increaseHeight(floating, multi === 2 ? value * 2 : value)
705
+ // Ensure derived data (scroll arrows) is fresh
706
+ forceUpdate()
707
+ }
708
+ }
709
+
710
+ return (
711
+ <YStack
712
+ ref={composedRefs}
713
+ componentName={componentName}
714
+ aria-hidden
715
+ {...scrollIndicatorProps}
716
+ zIndex={1000}
717
+ // @ts-expect-error
718
+ position={strategy}
719
+ left={x || 0}
720
+ top={y || 0}
721
+ width={`calc(${(floatingRef.current?.offsetWidth ?? 0) - 2}px)`}
722
+ onPointerMove={() => {
723
+ if (!loopingRef.current) {
724
+ intervalRef.current = setInterval(handleScrollArrowChange, 1000 / 60)
725
+ loopingRef.current = true
726
+ }
727
+ }}
728
+ onPointerLeave={() => {
729
+ loopingRef.current = false
730
+ clearInterval(intervalRef.current)
731
+ }}
732
+ />
733
+ )
734
+ })
735
+
736
+ /* -------------------------------------------------------------------------------------------------
737
+ * SelectSeparator
738
+ * -----------------------------------------------------------------------------------------------*/
739
+
740
+ export const SelectSeparator = styled(Separator, {
741
+ name: 'SelectSeparator',
742
+ })
743
+
744
+ /* -------------------------------------------------------------------------------------------------
745
+ * Select
746
+ * -----------------------------------------------------------------------------------------------*/
747
+
748
+ export interface SelectProps {
749
+ id?: string
750
+ children?: React.ReactNode
751
+ value?: string
752
+ defaultValue?: string
753
+ onValueChange?(value: string): void
754
+ open?: boolean
755
+ defaultOpen?: boolean
756
+ onOpenChange?(open: boolean): void
757
+ dir?: Direction
758
+ name?: string
759
+ autoComplete?: string
760
+ size?: SizeTokens
761
+ }
762
+
763
+ export const Select = withStaticProperties(
764
+ (props: ScopedProps<SelectProps>) => {
765
+ const {
766
+ // TODO use id for focusing from label
767
+ id,
768
+ __scopeSelect,
769
+ children,
770
+ open: openProp,
771
+ defaultOpen,
772
+ onOpenChange,
773
+ value: valueProp,
774
+ defaultValue,
775
+ onValueChange,
776
+ size: sizeProp,
777
+ dir,
778
+ name,
779
+ autoComplete,
780
+ } = props
781
+
782
+ // const [trigger, setTrigger] = React.useState<SelectTriggerElement | null>(null)
783
+
784
+ const [open, setOpen] = useControllableState({
785
+ prop: openProp,
786
+ defaultProp: defaultOpen || false,
787
+ onChange: onOpenChange,
788
+ })
789
+
790
+ const [value, setValue] = useControllableState({
791
+ prop: valueProp,
792
+ defaultProp: defaultValue || '',
793
+ onChange: onValueChange,
794
+ })
795
+
796
+ const [activeIndex, setActiveIndex] = React.useState<number | null>(null)
797
+ const selectedIndexRef = React.useRef<number | null>(null)
798
+ const activeIndexRef = React.useRef<number | null>(null)
799
+ const prevActiveIndex = usePrevious<number | null>(activeIndex)
800
+
801
+ const [showArrows, setShowArrows] = React.useState(false)
802
+ const [scrollTop, setScrollTop] = React.useState(0)
803
+ const listItemsRef = React.useRef<Array<HTMLElement | null>>([])
804
+ const listContentRef = React.useRef<string[]>([])
805
+
806
+ const [selectedIndex, setSelectedIndex] = React.useState(
807
+ Math.max(0, listContentRef.current.indexOf(value))
808
+ )
809
+
810
+ const [controlledScrolling, setControlledScrolling] = React.useState(false)
811
+ const [middlewareType, setMiddlewareType] = React.useState<'align' | 'fallback'>('align')
812
+
813
+ useIsomorphicLayoutEffect(() => {
814
+ selectedIndexRef.current = selectedIndex
815
+ activeIndexRef.current = activeIndex
816
+ })
817
+
818
+ // Wait for scroll position to settle before showing arrows to prevent
819
+ // interference with pointer events.
820
+ React.useEffect(() => {
821
+ const frame = requestAnimationFrame(() => {
822
+ setShowArrows(open)
823
+
824
+ if (!open) {
825
+ setScrollTop(0)
826
+ setMiddlewareType('align')
827
+ setActiveIndex(null)
828
+ setControlledScrolling(false)
829
+ }
830
+ })
831
+ return () => {
832
+ cancelAnimationFrame(frame)
833
+ }
834
+ }, [open])
835
+
836
+ function getFloatingPadding(floating: HTMLElement | null) {
837
+ if (!floating) {
838
+ return 0
839
+ }
840
+ return Number(getComputedStyle(floating).paddingLeft?.replace('px', ''))
841
+ }
842
+
843
+ const { x, y, reference, floating, strategy, context, refs, middlewareData, update } =
844
+ useFloating({
845
+ open,
846
+ onOpenChange: setOpen,
847
+ placement: 'bottom',
848
+ middleware:
849
+ middlewareType === 'align'
850
+ ? [
851
+ offset(({ rects }) => {
852
+ const index = activeIndexRef.current ?? selectedIndexRef.current
853
+
854
+ if (index == null) {
855
+ return 0
856
+ }
857
+
858
+ const item = listItemsRef.current[index]
859
+
860
+ if (item == null) {
861
+ return 0
862
+ }
863
+
864
+ const offsetTop = item.offsetTop
865
+ const itemHeight = item.offsetHeight
866
+ const height = rects.reference.height
867
+
868
+ return -offsetTop - height - (itemHeight - height) / 2
869
+ }),
870
+ // Custom `size` that can handle the opposite direction of the placement
871
+ {
872
+ name: 'size',
873
+ async fn(args) {
874
+ const {
875
+ elements: { floating },
876
+ rects: { reference },
877
+ middlewareData,
878
+ } = args
879
+
880
+ const overflow = await detectOverflow(args, {
881
+ padding: WINDOW_PADDING,
882
+ })
883
+
884
+ const top = Math.max(0, overflow.top)
885
+ const bottom = Math.max(0, overflow.bottom)
886
+ const nextY = args.y + top
887
+
888
+ if (middlewareData.size?.skip) {
889
+ return {
890
+ y: nextY,
891
+ data: {
892
+ y: middlewareData.size.y,
893
+ },
894
+ }
895
+ }
896
+
897
+ Object.assign(floating.style, {
898
+ maxHeight: `${floating.scrollHeight - Math.abs(top + bottom)}px`,
899
+ minWidth: `${reference.width + getFloatingPadding(floating) * 2}px`,
900
+ })
901
+
902
+ return {
903
+ y: nextY,
904
+ data: {
905
+ y: top,
906
+ skip: true,
907
+ },
908
+ reset: {
909
+ rects: true,
910
+ },
911
+ }
912
+ },
913
+ },
914
+ ]
915
+ : [
916
+ offset(5),
917
+ flip(),
918
+ size({
919
+ apply({ rects, availableHeight, elements }) {
920
+ Object.assign(elements.floating.style, {
921
+ width: `${rects.reference.width}px`,
922
+ maxHeight: `${availableHeight}px`,
923
+ })
924
+ },
925
+ padding: WINDOW_PADDING,
926
+ }),
927
+ ],
928
+ })
929
+
930
+ const floatingRef = refs.floating
931
+ const forceUpdate = React.useReducer(() => ({}), {})[1]
932
+
933
+ const showUpArrow = showArrows && scrollTop > SCROLL_ARROW_THRESHOLD
934
+ const showDownArrow =
935
+ showArrows &&
936
+ floatingRef.current &&
937
+ scrollTop <
938
+ floatingRef.current.scrollHeight - floatingRef.current.clientHeight - SCROLL_ARROW_THRESHOLD
939
+
940
+ const interactions = useInteractions([
941
+ useClick(context, { pointerDown: true }),
942
+ useRole(context, { role: 'listbox' }),
943
+ useDismiss(context),
944
+ useListNavigation(context, {
945
+ listRef: listItemsRef,
946
+ activeIndex,
947
+ selectedIndex,
948
+ onNavigate: setActiveIndex,
949
+ }),
950
+ useTypeahead(context, {
951
+ listRef: listContentRef,
952
+ onMatch: open ? setActiveIndex : setSelectedIndex,
953
+ selectedIndex,
954
+ activeIndex,
955
+ }),
956
+ ])
957
+
958
+ const increaseHeight = React.useCallback(
959
+ (floating: HTMLElement, amount = 0) => {
960
+ if (middlewareType === 'fallback') {
961
+ return
962
+ }
963
+
964
+ const currentMaxHeight = Number(floating.style.maxHeight.replace('px', ''))
965
+ const currentTop = Number(floating.style.top.replace('px', ''))
966
+ const rect = floating.getBoundingClientRect()
967
+ const rectTop = rect.top
968
+ const rectBottom = rect.bottom
969
+ const viewportHeight = visualViewport?.height ?? 0
970
+ const visualMaxHeight = viewportHeight - WINDOW_PADDING * 2
971
+
972
+ if (
973
+ amount < 0 &&
974
+ selectedIndexRef.current != null &&
975
+ Math.round(rectBottom) <
976
+ Math.round(viewportHeight + getVisualOffsetTop() - WINDOW_PADDING)
977
+ ) {
978
+ floating.style.maxHeight = `${Math.min(visualMaxHeight, currentMaxHeight - amount)}px`
979
+ }
980
+
981
+ if (
982
+ amount > 0 &&
983
+ Math.round(rectTop) > Math.round(WINDOW_PADDING - getVisualOffsetTop()) &&
984
+ floating.scrollHeight > floating.offsetHeight
985
+ ) {
986
+ const nextTop = Math.max(WINDOW_PADDING + getVisualOffsetTop(), currentTop - amount)
987
+
988
+ const nextMaxHeight = Math.min(visualMaxHeight, currentMaxHeight + amount)
989
+
990
+ Object.assign(floating.style, {
991
+ maxHeight: `${nextMaxHeight}px`,
992
+ top: `${nextTop}px`,
993
+ })
994
+
995
+ if (nextTop - WINDOW_PADDING > getVisualOffsetTop()) {
996
+ floating.scrollTop -= nextMaxHeight - currentMaxHeight + getFloatingPadding(floating)
997
+ }
998
+
999
+ return currentTop - nextTop
1000
+ }
1001
+ },
1002
+ [middlewareType]
1003
+ )
1004
+
1005
+ const touchPageYRef = React.useRef<number | null>(null)
1006
+
1007
+ const handleWheel = React.useCallback(
1008
+ (event: WheelEvent | TouchEvent) => {
1009
+ const pinching = event.ctrlKey
1010
+
1011
+ const currentTarget = event.currentTarget as HTMLElement
1012
+
1013
+ function isWheelEvent(event: any): event is WheelEvent {
1014
+ return typeof event.deltaY === 'number'
1015
+ }
1016
+
1017
+ function isTouchEvent(event: any): event is TouchEvent {
1018
+ return event.touches != null
1019
+ }
1020
+
1021
+ if (
1022
+ Math.abs(
1023
+ (currentTarget?.offsetHeight ?? 0) -
1024
+ ((visualViewport?.height ?? 0) - WINDOW_PADDING * 2)
1025
+ ) > 1 &&
1026
+ !pinching
1027
+ ) {
1028
+ event.preventDefault()
1029
+ } else if (isWheelEvent(event) && isFirefox) {
1030
+ // Firefox needs this to propagate scrolling
1031
+ // during momentum scrolling phase if the
1032
+ // height reached its maximum (at boundaries)
1033
+ currentTarget.scrollTop += event.deltaY
1034
+ }
1035
+
1036
+ if (!pinching) {
1037
+ let delta = 5
1038
+
1039
+ if (isTouchEvent(event)) {
1040
+ const currentPageY = touchPageYRef.current
1041
+ const pageY = event.touches[0]?.pageY
1042
+
1043
+ if (pageY != null) {
1044
+ touchPageYRef.current = pageY
1045
+
1046
+ if (currentPageY != null) {
1047
+ delta = currentPageY - pageY
1048
+ }
1049
+ }
1050
+ }
1051
+
1052
+ increaseHeight(currentTarget, isWheelEvent(event) ? event.deltaY : delta)
1053
+ setScrollTop(currentTarget.scrollTop)
1054
+ // Ensure derived data (scroll arrows) is fresh
1055
+ forceUpdate()
1056
+ }
1057
+ },
1058
+ [increaseHeight, forceUpdate]
1059
+ )
1060
+
1061
+ // Handle `onWheel` event in an effect to remove the `passive` option so we
1062
+ // can .preventDefault() it
1063
+ React.useEffect(() => {
1064
+ function onTouchEnd() {
1065
+ touchPageYRef.current = null
1066
+ }
1067
+
1068
+ const floating = floatingRef.current
1069
+ if (open && floating && middlewareType === 'align') {
1070
+ floating.addEventListener('wheel', handleWheel)
1071
+ floating.addEventListener('touchmove', handleWheel)
1072
+ floating.addEventListener('touchend', onTouchEnd, { passive: true })
1073
+ return () => {
1074
+ floating.removeEventListener('wheel', handleWheel)
1075
+ floating.removeEventListener('touchmove', handleWheel)
1076
+ floating.removeEventListener('touchend', onTouchEnd)
1077
+ }
1078
+ }
1079
+ }, [open, floatingRef, handleWheel, middlewareType])
1080
+
1081
+ // Ensure the menu remains attached to the reference element when resizing.
1082
+ React.useEffect(() => {
1083
+ window.addEventListener('resize', update)
1084
+ return () => {
1085
+ window.removeEventListener('resize', update)
1086
+ }
1087
+ }, [update])
1088
+
1089
+ // Scroll the active or selected item into view when in `controlledScrolling`
1090
+ // mode (i.e. arrow key nav).
1091
+ React.useLayoutEffect(() => {
1092
+ const floating = floatingRef.current
1093
+
1094
+ if (open && controlledScrolling && floating) {
1095
+ const item =
1096
+ activeIndex != null
1097
+ ? listItemsRef.current[activeIndex]
1098
+ : selectedIndex != null
1099
+ ? listItemsRef.current[selectedIndex]
1100
+ : null
1101
+
1102
+ if (item && prevActiveIndex != null) {
1103
+ const itemHeight = listItemsRef.current[prevActiveIndex]?.offsetHeight ?? 0
1104
+
1105
+ const floatingHeight = floating.offsetHeight
1106
+ const top = item.offsetTop
1107
+ const bottom = top + itemHeight
1108
+
1109
+ if (top < floating.scrollTop + 20) {
1110
+ const diff = floating.scrollTop - top + 20
1111
+ floating.scrollTop -= diff
1112
+
1113
+ if (activeIndex != selectedIndex && activeIndex != null) {
1114
+ increaseHeight(floating, -diff)
1115
+ }
1116
+ } else if (bottom > floatingHeight + floating.scrollTop - 20) {
1117
+ const diff = bottom - floatingHeight - floating.scrollTop + 20
1118
+
1119
+ floating.scrollTop += diff
1120
+
1121
+ if (activeIndex != selectedIndex && activeIndex != null) {
1122
+ floating.scrollTop -= increaseHeight(floating, diff) ?? 0
1123
+ }
1124
+ }
1125
+ }
1126
+ }
1127
+ }, [
1128
+ open,
1129
+ controlledScrolling,
1130
+ prevActiveIndex,
1131
+ activeIndex,
1132
+ selectedIndex,
1133
+ floatingRef,
1134
+ increaseHeight,
1135
+ ])
1136
+
1137
+ // Sync the height and the scrollTop values and device whether to use fallback
1138
+ // positioning.
1139
+ React.useLayoutEffect(() => {
1140
+ const floating = refs.floating.current
1141
+ const reference = refs.reference.current
1142
+
1143
+ if (open && floating && reference && floating.offsetHeight < floating.scrollHeight) {
1144
+ const referenceRect = reference.getBoundingClientRect()
1145
+
1146
+ if (middlewareType === 'fallback') {
1147
+ const item = listItemsRef.current[selectedIndex]
1148
+ if (item) {
1149
+ floating.scrollTop = item.offsetTop - floating.clientHeight + referenceRect.height
1150
+ }
1151
+ return
1152
+ }
1153
+
1154
+ floating.scrollTop = middlewareData.size?.y
1155
+
1156
+ const closeToBottom =
1157
+ (visualViewport?.height ?? 0) + getVisualOffsetTop() - referenceRect.bottom <
1158
+ FALLBACK_THRESHOLD
1159
+ const closeToTop = referenceRect.top < FALLBACK_THRESHOLD
1160
+
1161
+ if (floating.offsetHeight < MIN_HEIGHT || closeToTop || closeToBottom) {
1162
+ setMiddlewareType('fallback')
1163
+ }
1164
+ }
1165
+ }, [
1166
+ open,
1167
+ increaseHeight,
1168
+ selectedIndex,
1169
+ middlewareType,
1170
+ refs.floating,
1171
+ refs.reference,
1172
+ // Always re-run this effect when the position has been computed so the
1173
+ // .scrollTop change works with fresh sizing.
1174
+ middlewareData,
1175
+ ])
1176
+
1177
+ React.useLayoutEffect(() => {
1178
+ if (open && selectedIndex != null) {
1179
+ requestAnimationFrame(() => {
1180
+ listItemsRef.current[selectedIndex]?.focus({ preventScroll: true })
1181
+ })
1182
+ }
1183
+ }, [listItemsRef, selectedIndex, open])
1184
+
1185
+ // Wait for scroll position to settle before showing arrows to prevent
1186
+ // interference with pointer events.
1187
+ React.useEffect(() => {
1188
+ const frame = requestAnimationFrame(() => {
1189
+ setShowArrows(open)
1190
+
1191
+ if (!open) {
1192
+ setScrollTop(0)
1193
+ setMiddlewareType('align')
1194
+ setActiveIndex(null)
1195
+ setControlledScrolling(false)
1196
+ }
1197
+ })
1198
+ return () => cancelAnimationFrame(frame)
1199
+ }, [open])
1200
+
1201
+ // We set this to true by default so that events bubble to forms without JS (SSR)
1202
+ // const isFormControl = trigger ? Boolean(trigger.closest('form')) : true
1203
+ // const [bubbleSelect, setBubbleSelect] = React.useState<HTMLSelectElement | null>(null)
1204
+ // const triggerPointerDownPosRef = React.useRef<{ x: number; y: number } | null>(null)
1205
+
1206
+ const [valueNode, setValueNode] = React.useState<HTMLElement | null>(null)
1207
+ const [valueNodeHasChildren, setValueNodeHasChildren] = React.useState(false)
1208
+
1209
+ return (
1210
+ <SelectProvider
1211
+ size={sizeProp}
1212
+ scope={__scopeSelect}
1213
+ increaseHeight={increaseHeight}
1214
+ forceUpdate={forceUpdate}
1215
+ floatingRef={floatingRef}
1216
+ valueNode={valueNode}
1217
+ onValueNodeChange={setValueNode}
1218
+ valueNodeHasChildren={valueNodeHasChildren}
1219
+ onValueNodeHasChildrenChange={setValueNodeHasChildren}
1220
+ setValueAtIndex={(index, value) => {
1221
+ listContentRef.current[index] = value
1222
+ }}
1223
+ interactions={{
1224
+ ...interactions,
1225
+ getReferenceProps() {
1226
+ return interactions.getReferenceProps({
1227
+ ref: reference,
1228
+ className: 'SelectTrigger',
1229
+ onKeyDown(event) {
1230
+ if (
1231
+ event.key === 'Enter' ||
1232
+ (event.key === ' ' && !context.dataRef.current.typing)
1233
+ ) {
1234
+ event.preventDefault()
1235
+ setOpen(true)
1236
+ }
1237
+ },
1238
+ })
1239
+ },
1240
+ getFloatingProps(props) {
1241
+ return interactions.getFloatingProps({
1242
+ ref: floating,
1243
+ className: 'Select',
1244
+ ...props,
1245
+ style: {
1246
+ position: strategy,
1247
+ top: y ?? '',
1248
+ left: x ?? '',
1249
+ outline: 0,
1250
+ listStyleType: 'none',
1251
+ scrollbarWidth: 'none',
1252
+ userSelect: 'none',
1253
+ ...props?.style,
1254
+ },
1255
+ onPointerEnter() {
1256
+ setControlledScrolling(false)
1257
+ },
1258
+ onPointerMove() {
1259
+ setControlledScrolling(false)
1260
+ },
1261
+ onKeyDown() {
1262
+ setControlledScrolling(true)
1263
+ },
1264
+ onScroll(event) {
1265
+ setScrollTop(event.currentTarget.scrollTop)
1266
+ },
1267
+ })
1268
+ },
1269
+ }}
1270
+ floatingContext={context}
1271
+ activeIndex={activeIndex}
1272
+ canScrollDown={!!showDownArrow}
1273
+ canScrollUp={!!showUpArrow}
1274
+ controlledScrolling
1275
+ dataRef={context.dataRef}
1276
+ listRef={listItemsRef}
1277
+ onChange={setValue}
1278
+ selectedIndex={selectedIndex}
1279
+ setActiveIndex={setActiveIndex}
1280
+ setOpen={setOpen}
1281
+ setSelectedIndex={setSelectedIndex}
1282
+ value={value}
1283
+ open={open}
1284
+ >
1285
+ {children}
1286
+ {/* {isFormControl ? (
1287
+ <BubbleSelect
1288
+ ref={setBubbleSelect}
1289
+ aria-hidden
1290
+ tabIndex={-1}
1291
+ name={name}
1292
+ autoComplete={autoComplete}
1293
+ value={value}
1294
+ // enable form autofill
1295
+ onChange={(event) => setValue(event.target.value)}
1296
+ />
1297
+ ) : null} */}
1298
+ </SelectProvider>
1299
+ )
1300
+ },
1301
+ {
1302
+ Content: SelectContent,
1303
+ Group: SelectGroup,
1304
+ Icon: SelectIcon,
1305
+ Item: SelectItem,
1306
+ ItemIndicator: SelectItemIndicator,
1307
+ ItemText: SelectItemText,
1308
+ Label: SelectLabel,
1309
+ ScrollDownButton: SelectScrollDownButton,
1310
+ ScrollUpButton: SelectScrollUpButton,
1311
+ Trigger: SelectTrigger,
1312
+ Value: SelectValue,
1313
+ Viewport: SelectViewport,
1314
+ }
1315
+ )
1316
+
1317
+ // @ts-ignore
1318
+ Select.displayName = SELECT_NAME
1319
+
1320
+ export { createSelectScope }