@tamagui/select 1.0.1-beta.56 → 1.0.1-beta.60

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