@tamagui/select 1.0.1-beta.18 → 1.0.1-beta.180

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dist/cjs/BubbleSelect.js +43 -0
  2. package/dist/cjs/BubbleSelect.js.map +7 -0
  3. package/dist/cjs/Select.js +413 -389
  4. package/dist/cjs/Select.js.map +3 -3
  5. package/dist/cjs/SelectContent.js +72 -0
  6. package/dist/cjs/SelectContent.js.map +7 -0
  7. package/dist/cjs/SelectContent.native.js +31 -0
  8. package/dist/cjs/SelectContent.native.js.map +7 -0
  9. package/dist/cjs/SelectImpl.js +286 -0
  10. package/dist/cjs/SelectImpl.js.map +7 -0
  11. package/dist/cjs/SelectScrollButton.js +157 -0
  12. package/dist/cjs/SelectScrollButton.js.map +7 -0
  13. package/dist/cjs/SelectScrollButton.native.js +32 -0
  14. package/dist/cjs/SelectScrollButton.native.js.map +7 -0
  15. package/dist/cjs/SelectViewport.js +126 -0
  16. package/dist/cjs/SelectViewport.js.map +7 -0
  17. package/dist/cjs/SelectViewport.native.js +41 -0
  18. package/dist/cjs/SelectViewport.native.js.map +7 -0
  19. package/dist/cjs/constants.js +47 -0
  20. package/dist/cjs/constants.js.map +7 -0
  21. package/dist/cjs/context.js +49 -0
  22. package/dist/cjs/context.js.map +7 -0
  23. package/dist/cjs/index.js +2 -0
  24. package/dist/cjs/index.js.map +2 -2
  25. package/dist/cjs/types.js +17 -0
  26. package/dist/cjs/types.js.map +7 -0
  27. package/dist/cjs/useSelectBreakpointActive.js +39 -0
  28. package/dist/cjs/useSelectBreakpointActive.js.map +7 -0
  29. package/dist/esm/BubbleSelect.js +24 -0
  30. package/dist/esm/BubbleSelect.js.map +7 -0
  31. package/dist/esm/Select.js +404 -357
  32. package/dist/esm/Select.js.map +3 -3
  33. package/dist/esm/SelectContent.js +48 -0
  34. package/dist/esm/SelectContent.js.map +7 -0
  35. package/dist/esm/SelectContent.native.js +7 -0
  36. package/dist/esm/SelectContent.native.js.map +7 -0
  37. package/dist/esm/SelectImpl.js +271 -0
  38. package/dist/esm/SelectImpl.js.map +7 -0
  39. package/dist/esm/SelectScrollButton.js +126 -0
  40. package/dist/esm/SelectScrollButton.js.map +7 -0
  41. package/dist/esm/SelectScrollButton.native.js +7 -0
  42. package/dist/esm/SelectScrollButton.native.js.map +7 -0
  43. package/dist/esm/SelectViewport.js +95 -0
  44. package/dist/esm/SelectViewport.js.map +7 -0
  45. package/dist/esm/SelectViewport.native.js +17 -0
  46. package/dist/esm/SelectViewport.native.js.map +7 -0
  47. package/dist/esm/constants.js +17 -0
  48. package/dist/esm/constants.js.map +7 -0
  49. package/dist/esm/context.js +21 -0
  50. package/dist/esm/context.js.map +7 -0
  51. package/dist/esm/index.js +1 -0
  52. package/dist/esm/index.js.map +2 -2
  53. package/dist/esm/types.js +1 -0
  54. package/dist/esm/types.js.map +7 -0
  55. package/dist/esm/useSelectBreakpointActive.js +14 -0
  56. package/dist/esm/useSelectBreakpointActive.js.map +7 -0
  57. package/dist/jsx/BubbleSelect.js +24 -0
  58. package/dist/jsx/BubbleSelect.js.map +7 -0
  59. package/dist/jsx/Select.js +284 -270
  60. package/dist/jsx/Select.js.map +7 -0
  61. package/dist/jsx/SelectContent.js +27 -0
  62. package/dist/jsx/SelectContent.js.map +7 -0
  63. package/dist/jsx/SelectContent.native.js +7 -0
  64. package/dist/jsx/SelectContent.native.js.map +7 -0
  65. package/dist/jsx/SelectImpl.js +244 -0
  66. package/dist/jsx/SelectImpl.js.map +7 -0
  67. package/dist/jsx/SelectScrollButton.js +99 -0
  68. package/dist/jsx/SelectScrollButton.js.map +7 -0
  69. package/dist/jsx/SelectScrollButton.native.js +7 -0
  70. package/dist/jsx/SelectScrollButton.native.js.map +7 -0
  71. package/dist/jsx/SelectViewport.js +72 -0
  72. package/dist/jsx/SelectViewport.js.map +7 -0
  73. package/dist/jsx/SelectViewport.native.js +13 -0
  74. package/dist/jsx/SelectViewport.native.js.map +7 -0
  75. package/dist/jsx/constants.js +17 -0
  76. package/dist/jsx/constants.js.map +7 -0
  77. package/dist/jsx/context.js +15 -0
  78. package/dist/jsx/context.js.map +7 -0
  79. package/dist/jsx/index.js +2 -0
  80. package/dist/jsx/index.js.map +7 -0
  81. package/dist/jsx/types.js +1 -0
  82. package/dist/jsx/types.js.map +7 -0
  83. package/dist/jsx/useSelectBreakpointActive.js +14 -0
  84. package/dist/jsx/useSelectBreakpointActive.js.map +7 -0
  85. package/package.json +26 -16
  86. package/src/BubbleSelect.tsx +46 -0
  87. package/src/Select.tsx +613 -0
  88. package/src/SelectContent.native.tsx +5 -0
  89. package/src/SelectContent.tsx +45 -0
  90. package/src/SelectImpl.tsx +342 -0
  91. package/src/SelectScrollButton.native.tsx +7 -0
  92. package/src/SelectScrollButton.tsx +168 -0
  93. package/src/SelectViewport.native.tsx +14 -0
  94. package/src/SelectViewport.tsx +104 -0
  95. package/src/constants.tsx +7 -0
  96. package/src/context.tsx +19 -0
  97. package/src/index.tsx +8 -0
  98. package/src/types.tsx +94 -0
  99. package/src/useSelectBreakpointActive.tsx +15 -0
  100. package/types/BubbleSelect.d.ts +2 -0
  101. package/types/Select.d.ts +750 -0
  102. package/types/SelectContent.d.ts +5 -0
  103. package/types/SelectContent.native.d.ts +4 -0
  104. package/types/SelectImpl.d.ts +9 -0
  105. package/types/SelectScrollButton.d.ts +6 -0
  106. package/types/SelectScrollButton.native.d.ts +4 -0
  107. package/types/SelectViewport.d.ts +127 -0
  108. package/types/SelectViewport.native.d.ts +7 -0
  109. package/types/constants.d.ts +8 -0
  110. package/types/context.d.ts +21 -0
  111. package/types/index.d.ts +2 -0
  112. package/types/types.d.ts +84 -0
  113. package/types/useSelectBreakpointActive.d.ts +4 -0
  114. package/types/index.d.ts.map +0 -1
package/src/Select.tsx ADDED
@@ -0,0 +1,613 @@
1
+ import { useComposedRefs } from '@tamagui/compose-refs'
2
+ import { GetProps, TamaguiElement, isClient, isWeb } from '@tamagui/core'
3
+ import { styled, useGet, useIsomorphicLayoutEffect, withStaticProperties } from '@tamagui/core'
4
+ import { useId } from '@tamagui/core'
5
+ import { ListItem, ListItemProps } from '@tamagui/list-item'
6
+ import { PortalHost } from '@tamagui/portal'
7
+ import { Separator } from '@tamagui/separator'
8
+ import { ControlledSheet, SheetController } from '@tamagui/sheet'
9
+ import { XStack, YStack, YStackProps } from '@tamagui/stacks'
10
+ import { Paragraph } from '@tamagui/text'
11
+ import { useControllableState } from '@tamagui/use-controllable-state'
12
+ import * as React from 'react'
13
+ import * as ReactDOM from 'react-dom'
14
+
15
+ import { SELECT_NAME } from './constants'
16
+ import { SelectProvider, createSelectContext, useSelectContext } from './context'
17
+ import { SelectContent } from './SelectContent'
18
+ import { SelectImplProps, SelectInlineImpl } from './SelectImpl'
19
+ import { SelectScrollDownButton, SelectScrollUpButton } from './SelectScrollButton'
20
+ import { SelectViewport } from './SelectViewport'
21
+ import { ScopedProps, SelectProps } from './types'
22
+ import { useSelectBreakpointActive, useShowSelectSheet } from './useSelectBreakpointActive'
23
+
24
+ /* -------------------------------------------------------------------------------------------------
25
+ * SelectTrigger
26
+ * -----------------------------------------------------------------------------------------------*/
27
+
28
+ const TRIGGER_NAME = 'SelectTrigger'
29
+
30
+ export type SelectTriggerProps = ListItemProps
31
+
32
+ export const SelectTrigger = React.forwardRef<TamaguiElement, SelectTriggerProps>(
33
+ (props: ScopedProps<SelectTriggerProps>, forwardedRef) => {
34
+ const {
35
+ __scopeSelect,
36
+ disabled = false,
37
+ // @ts-ignore
38
+ 'aria-labelledby': ariaLabelledby,
39
+ ...triggerProps
40
+ } = props
41
+ const context = useSelectContext(TRIGGER_NAME, __scopeSelect)
42
+ // const composedRefs = useComposedRefs(forwardedRef, context.onTriggerChange)
43
+ // const getItems = useCollection(__scopeSelect)
44
+ // const labelId = useLabelContext(context.trigger)
45
+ const labelledBy = ariaLabelledby // || labelId
46
+
47
+ return (
48
+ <ListItem
49
+ componentName={TRIGGER_NAME}
50
+ backgrounded
51
+ radiused
52
+ hoverTheme
53
+ pressTheme
54
+ focusTheme
55
+ focusable
56
+ borderWidth={1}
57
+ size={context.size}
58
+ // aria-controls={context.contentId}
59
+ aria-expanded={context.open}
60
+ aria-autocomplete="none"
61
+ aria-labelledby={labelledBy}
62
+ dir={context.dir}
63
+ disabled={disabled}
64
+ data-disabled={disabled ? '' : undefined}
65
+ {...triggerProps}
66
+ ref={forwardedRef}
67
+ {...(process.env.TAMAGUI_TARGET === 'web' && context.interactions
68
+ ? context.interactions.getReferenceProps()
69
+ : {
70
+ onPress() {
71
+ context.setOpen(!context.open)
72
+ },
73
+ })}
74
+ />
75
+ )
76
+ }
77
+ )
78
+
79
+ SelectTrigger.displayName = TRIGGER_NAME
80
+
81
+ /* -------------------------------------------------------------------------------------------------
82
+ * SelectValue
83
+ * -----------------------------------------------------------------------------------------------*/
84
+
85
+ const VALUE_NAME = 'SelectValue'
86
+
87
+ const SelectValueFrame = styled(Paragraph, {
88
+ name: VALUE_NAME,
89
+ selectable: false,
90
+ })
91
+
92
+ type SelectValueProps = GetProps<typeof SelectValueFrame> & {
93
+ placeholder?: React.ReactNode
94
+ }
95
+
96
+ const SelectValue = SelectValueFrame.extractable(
97
+ React.forwardRef<TamaguiElement, SelectValueProps>(
98
+ (
99
+ { __scopeSelect, children: childrenProp, placeholder }: ScopedProps<SelectValueProps>,
100
+ forwardedRef
101
+ ) => {
102
+ // We ignore `className` and `style` as this part shouldn't be styled.
103
+ const context = useSelectContext(VALUE_NAME, __scopeSelect)
104
+ const { onValueNodeHasChildrenChange } = context
105
+ const hasChildren = childrenProp !== undefined
106
+ const composedRefs = useComposedRefs(forwardedRef, context.onValueNodeChange)
107
+
108
+ useIsomorphicLayoutEffect(() => {
109
+ onValueNodeHasChildrenChange(hasChildren)
110
+ }, [onValueNodeHasChildrenChange, hasChildren])
111
+
112
+ const children = childrenProp ?? context.selectedItem
113
+
114
+ return (
115
+ <SelectValueFrame
116
+ size={context.size}
117
+ ref={composedRefs}
118
+ // we don't want events from the portalled `SelectValue` children to bubble
119
+ // through the item they came from
120
+ pointerEvents="none"
121
+ >
122
+ {context.value === undefined && placeholder !== undefined ? placeholder : children}
123
+ </SelectValueFrame>
124
+ )
125
+ }
126
+ )
127
+ )
128
+
129
+ SelectValue.displayName = VALUE_NAME
130
+
131
+ /* -------------------------------------------------------------------------------------------------
132
+ * SelectIcon
133
+ * -----------------------------------------------------------------------------------------------*/
134
+
135
+ export const SelectIcon = styled(XStack, {
136
+ name: 'SelectIcon',
137
+ // @ts-ignore
138
+ 'aria-hidden': true,
139
+ children: <Paragraph>▼</Paragraph>,
140
+ })
141
+
142
+ /* -------------------------------------------------------------------------------------------------
143
+ * SelectItem
144
+ * -----------------------------------------------------------------------------------------------*/
145
+
146
+ const ITEM_NAME = 'SelectItem'
147
+
148
+ type SelectItemContextValue = {
149
+ value: string
150
+ textId: string
151
+ isSelected: boolean
152
+ }
153
+
154
+ const [SelectItemContextProvider, useSelectItemContext] =
155
+ createSelectContext<SelectItemContextValue>(ITEM_NAME)
156
+
157
+ export interface SelectItemProps extends YStackProps {
158
+ value: string
159
+ index: number
160
+ disabled?: boolean
161
+ textValue?: string
162
+ }
163
+
164
+ export const SelectItem = React.forwardRef<TamaguiElement, SelectItemProps>(
165
+ (props: ScopedProps<SelectItemProps>, forwardedRef) => {
166
+ const {
167
+ __scopeSelect,
168
+ value,
169
+ disabled = false,
170
+ textValue: textValueProp,
171
+ index,
172
+ ...itemProps
173
+ } = props
174
+ const context = useSelectContext(ITEM_NAME, __scopeSelect)
175
+ const isSelected = context.value === value
176
+ const textId = useId()
177
+
178
+ const {
179
+ selectedIndex,
180
+ setSelectedIndex,
181
+ listRef,
182
+ open,
183
+ setOpen,
184
+ onChange,
185
+ setActiveIndex,
186
+ allowMouseUpRef,
187
+ allowSelectRef,
188
+ setValueAtIndex,
189
+ selectTimeoutRef,
190
+ dataRef,
191
+ } = context
192
+
193
+ const composedRefs = useComposedRefs(forwardedRef, (node) => {
194
+ if (!isWeb) return
195
+ if (node instanceof HTMLElement) {
196
+ if (listRef) {
197
+ listRef.current[index] = node
198
+ }
199
+ }
200
+ })
201
+
202
+ React.useEffect(() => {
203
+ setValueAtIndex(index, value)
204
+ }, [index, setValueAtIndex, value])
205
+
206
+ function handleSelect() {
207
+ setSelectedIndex(index)
208
+ onChange(value)
209
+ setOpen(false)
210
+ }
211
+
212
+ const selectItemProps = context.interactions
213
+ ? context.interactions.getItemProps({
214
+ onTouchStart() {
215
+ allowSelectRef!.current = true
216
+ allowMouseUpRef!.current = false
217
+ },
218
+ onKeyDown(event) {
219
+ if (event.key === 'Enter' || (event.key === ' ' && !dataRef?.current.typing)) {
220
+ event.preventDefault()
221
+ handleSelect()
222
+ } else {
223
+ allowSelectRef!.current = true
224
+ }
225
+ },
226
+ onClick() {
227
+ if (allowSelectRef!.current) {
228
+ setSelectedIndex(index)
229
+ setOpen(false)
230
+ }
231
+ },
232
+ onMouseUp() {
233
+ if (!allowMouseUpRef!.current) {
234
+ return
235
+ }
236
+
237
+ if (allowSelectRef!.current) {
238
+ handleSelect()
239
+ }
240
+
241
+ // On touch devices, prevent the element from
242
+ // immediately closing `onClick` by deferring it
243
+ clearTimeout(selectTimeoutRef!.current)
244
+ selectTimeoutRef!.current = setTimeout(() => {
245
+ allowSelectRef!.current = true
246
+ })
247
+ },
248
+ })
249
+ : {
250
+ onPress: handleSelect,
251
+ }
252
+
253
+ return (
254
+ <SelectItemContextProvider
255
+ scope={__scopeSelect}
256
+ value={value}
257
+ textId={textId || ''}
258
+ isSelected={isSelected}
259
+ >
260
+ <ListItem
261
+ backgrounded
262
+ pressTheme
263
+ cursor=""
264
+ focusTheme
265
+ componentName={ITEM_NAME}
266
+ ref={composedRefs}
267
+ aria-labelledby={textId}
268
+ aria-selected={isSelected}
269
+ data-state={isSelected ? 'active' : 'inactive'}
270
+ aria-disabled={disabled || undefined}
271
+ data-disabled={disabled ? '' : undefined}
272
+ tabIndex={disabled ? undefined : -1}
273
+ size={context.size}
274
+ {...itemProps}
275
+ {...selectItemProps}
276
+ />
277
+ </SelectItemContextProvider>
278
+ )
279
+ }
280
+ )
281
+
282
+ SelectItem.displayName = ITEM_NAME
283
+
284
+ /* -------------------------------------------------------------------------------------------------
285
+ * SelectItemText
286
+ * -----------------------------------------------------------------------------------------------*/
287
+
288
+ const ITEM_TEXT_NAME = 'SelectItemText'
289
+
290
+ export const SelectItemTextFrame = styled(Paragraph, {
291
+ name: ITEM_TEXT_NAME,
292
+ selectable: false,
293
+ })
294
+
295
+ type SelectItemTextProps = GetProps<typeof SelectItemTextFrame>
296
+
297
+ const SelectItemText = React.forwardRef<TamaguiElement, SelectItemTextProps>(
298
+ (props: ScopedProps<SelectItemTextProps>, forwardedRef) => {
299
+ const { __scopeSelect, className, ...itemTextProps } = props
300
+ const context = useSelectContext(ITEM_TEXT_NAME, __scopeSelect)
301
+ const itemContext = useSelectItemContext(ITEM_TEXT_NAME, __scopeSelect)
302
+ const ref = React.useRef<TamaguiElement | null>(null)
303
+ const composedRefs = useComposedRefs(forwardedRef, ref)
304
+ const isSelected = itemContext.isSelected && context.valueNode && !context.valueNodeHasChildren
305
+
306
+ const contents = (
307
+ <SelectItemTextFrame
308
+ className={className}
309
+ size={context.size}
310
+ id={itemContext.textId}
311
+ {...itemTextProps}
312
+ ref={composedRefs}
313
+ />
314
+ )
315
+
316
+ // until portals work in sub-trees on RN, use this just for native:
317
+ if (!isWeb) {
318
+ React.useEffect(() => {
319
+ if (isSelected) {
320
+ context.setSelectedItem(contents)
321
+ }
322
+ // eslint-disable-next-line react-hooks/exhaustive-deps
323
+ }, [isSelected])
324
+ }
325
+
326
+ return (
327
+ <>
328
+ {contents}
329
+
330
+ {/* Portal the select item text into the trigger value node */}
331
+ {isWeb && isSelected
332
+ ? ReactDOM.createPortal(itemTextProps.children, context.valueNode!)
333
+ : null}
334
+
335
+ {/* Portal an option in the bubble select */}
336
+ {/* {context.bubbleSelect
337
+ ? ReactDOM.createPortal(
338
+ // we use `.textContent` because `option` only support `string` or `number`
339
+ <option value={itemContext.value}>{ref.current?.textContent}</option>,
340
+ context.bubbleSelect
341
+ )
342
+ : null} */}
343
+ </>
344
+ )
345
+ }
346
+ )
347
+
348
+ SelectItemText.displayName = ITEM_TEXT_NAME
349
+
350
+ /* -------------------------------------------------------------------------------------------------
351
+ * SelectItemIndicator
352
+ * -----------------------------------------------------------------------------------------------*/
353
+
354
+ const ITEM_INDICATOR_NAME = 'SelectItemIndicator'
355
+
356
+ const SelectItemIndicatorFrame = styled(XStack, {
357
+ name: ITEM_TEXT_NAME,
358
+ })
359
+
360
+ type SelectItemIndicatorProps = GetProps<typeof SelectItemIndicatorFrame>
361
+
362
+ const SelectItemIndicator = React.forwardRef<TamaguiElement, SelectItemIndicatorProps>(
363
+ (props: ScopedProps<SelectItemIndicatorProps>, forwardedRef) => {
364
+ const { __scopeSelect, ...itemIndicatorProps } = props
365
+ const itemContext = useSelectItemContext(ITEM_INDICATOR_NAME, __scopeSelect)
366
+ return itemContext.isSelected ? (
367
+ <SelectItemIndicatorFrame aria-hidden {...itemIndicatorProps} ref={forwardedRef} />
368
+ ) : null
369
+ }
370
+ )
371
+
372
+ SelectItemIndicator.displayName = ITEM_INDICATOR_NAME
373
+
374
+ /* -------------------------------------------------------------------------------------------------
375
+ * SelectGroup
376
+ * -----------------------------------------------------------------------------------------------*/
377
+
378
+ const GROUP_NAME = 'SelectGroup'
379
+
380
+ type SelectGroupContextValue = { id: string }
381
+
382
+ const [SelectGroupContextProvider, useSelectGroupContext] =
383
+ createSelectContext<SelectGroupContextValue>(GROUP_NAME)
384
+
385
+ export const SelectGroupFrame = styled(YStack, {
386
+ name: GROUP_NAME,
387
+ width: '100%',
388
+ })
389
+
390
+ type SelectGroupProps = GetProps<typeof SelectGroupFrame>
391
+
392
+ const SelectGroup = React.forwardRef<TamaguiElement, SelectGroupProps>(
393
+ (props: ScopedProps<SelectGroupProps>, forwardedRef) => {
394
+ const { __scopeSelect, ...groupProps } = props
395
+ const groupId = useId()
396
+ return (
397
+ <SelectGroupContextProvider scope={__scopeSelect} id={groupId || ''}>
398
+ <SelectGroupFrame
399
+ // @ts-ignore
400
+ role="group"
401
+ aria-labelledby={groupId}
402
+ {...groupProps}
403
+ ref={forwardedRef}
404
+ />
405
+ </SelectGroupContextProvider>
406
+ )
407
+ }
408
+ )
409
+
410
+ SelectGroup.displayName = GROUP_NAME
411
+
412
+ /* -------------------------------------------------------------------------------------------------
413
+ * SelectLabel
414
+ * -----------------------------------------------------------------------------------------------*/
415
+
416
+ const LABEL_NAME = 'SelectLabel'
417
+
418
+ export type SelectLabelProps = ListItemProps
419
+
420
+ const SelectLabel = React.forwardRef<TamaguiElement, SelectLabelProps>(
421
+ (props: ScopedProps<SelectLabelProps>, forwardedRef) => {
422
+ const { __scopeSelect, ...labelProps } = props
423
+ const context = useSelectContext(LABEL_NAME, __scopeSelect)
424
+ const groupContext = useSelectGroupContext(LABEL_NAME, __scopeSelect)
425
+ return (
426
+ <ListItem
427
+ componentName={LABEL_NAME}
428
+ fontWeight="800"
429
+ id={groupContext.id}
430
+ size={context.size}
431
+ {...labelProps}
432
+ ref={forwardedRef}
433
+ />
434
+ )
435
+ }
436
+ )
437
+
438
+ SelectLabel.displayName = LABEL_NAME
439
+
440
+ /* -------------------------------------------------------------------------------------------------
441
+ * SelectSeparator
442
+ * -----------------------------------------------------------------------------------------------*/
443
+
444
+ export const SelectSeparator = styled(Separator, {
445
+ name: 'SelectSeparator',
446
+ })
447
+
448
+ const SelectSheetController = (
449
+ props: ScopedProps<{}> & {
450
+ children: React.ReactNode
451
+ onOpenChange: React.Dispatch<React.SetStateAction<boolean>>
452
+ }
453
+ ) => {
454
+ const context = useSelectContext('SelectSheetController', props.__scopeSelect)
455
+ const showSheet = useShowSelectSheet(context)
456
+ const breakpointActive = useSelectBreakpointActive(context.sheetBreakpoint)
457
+ const getShowSheet = useGet(showSheet)
458
+
459
+ return (
460
+ <SheetController
461
+ onOpenChange={(val) => {
462
+ if (getShowSheet()) {
463
+ props.onOpenChange(val)
464
+ }
465
+ }}
466
+ open={context.open}
467
+ hidden={breakpointActive === false}
468
+ >
469
+ {props.children}
470
+ </SheetController>
471
+ )
472
+ }
473
+
474
+ /* -------------------------------------------------------------------------------------------------
475
+ * SelectSheetContents
476
+ * -----------------------------------------------------------------------------------------------*/
477
+
478
+ const SHEET_CONTENTS_NAME = 'SelectSheetContents'
479
+
480
+ export const SelectSheetContents = ({ __scopeSelect }: ScopedProps<{}>) => {
481
+ const context = useSelectContext(SHEET_CONTENTS_NAME, __scopeSelect)
482
+ return <PortalHost name={`${context.scopeKey}SheetContents`} />
483
+ }
484
+
485
+ SelectSheetContents.displayName = SHEET_CONTENTS_NAME
486
+
487
+ const SelectSheetImpl = (props: SelectImplProps) => {
488
+ return <>{props.children}</>
489
+ }
490
+
491
+ /* -------------------------------------------------------------------------------------------------
492
+ * Select
493
+ * -----------------------------------------------------------------------------------------------*/
494
+
495
+ export const Select = withStaticProperties(
496
+ (props: ScopedProps<SelectProps>) => {
497
+ const {
498
+ __scopeSelect,
499
+ children,
500
+ open: openProp,
501
+ defaultOpen,
502
+ onOpenChange,
503
+ value: valueProp,
504
+ defaultValue,
505
+ onValueChange,
506
+ size: sizeProp = '$4',
507
+ sheetBreakpoint = false,
508
+ dir,
509
+ } = props
510
+
511
+ const isSheet = useSelectBreakpointActive(sheetBreakpoint)
512
+ const SelectImpl = isSheet ? SelectSheetImpl : SelectInlineImpl
513
+ const forceUpdate = React.useReducer(() => ({}), {})[1]
514
+ const [selectedItem, setSelectedItem] = React.useState<React.ReactNode>(null)
515
+
516
+ const [open, setOpen] = useControllableState({
517
+ prop: openProp,
518
+ defaultProp: defaultOpen || false,
519
+ onChange: onOpenChange,
520
+ strategy: 'most-recent-wins',
521
+ })
522
+
523
+ const [value, setValue] = useControllableState({
524
+ prop: valueProp,
525
+ defaultProp: defaultValue || '',
526
+ onChange: onValueChange,
527
+ strategy: 'most-recent-wins',
528
+ })
529
+
530
+ const [activeIndex, setActiveIndex] = React.useState<number | null>(null)
531
+ const selectedIndexRef = React.useRef<number | null>(null)
532
+ const activeIndexRef = React.useRef<number | null>(null)
533
+
534
+ const listContentRef = React.useRef<string[]>([])
535
+
536
+ const [selectedIndex, setSelectedIndex] = React.useState(
537
+ Math.max(0, listContentRef.current.indexOf(value))
538
+ )
539
+
540
+ const [valueNode, setValueNode] = React.useState<HTMLElement | null>(null)
541
+ const [valueNodeHasChildren, setValueNodeHasChildren] = React.useState(false)
542
+
543
+ useIsomorphicLayoutEffect(() => {
544
+ selectedIndexRef.current = selectedIndex
545
+ activeIndexRef.current = activeIndex
546
+ })
547
+
548
+ const id = useId()
549
+ const scopeKey = __scopeSelect ? Object.keys(__scopeSelect)[0] ?? id : id
550
+
551
+ return (
552
+ <SelectProvider
553
+ dir={dir}
554
+ blockSelection={false}
555
+ size={sizeProp}
556
+ fallback={false}
557
+ selectedItem={selectedItem}
558
+ setSelectedItem={setSelectedItem}
559
+ forceUpdate={forceUpdate}
560
+ valueNode={valueNode}
561
+ onValueNodeChange={setValueNode}
562
+ onValueNodeHasChildrenChange={setValueNodeHasChildren}
563
+ valueNodeHasChildren={valueNodeHasChildren}
564
+ scopeKey={scopeKey}
565
+ sheetBreakpoint={sheetBreakpoint}
566
+ scope={__scopeSelect}
567
+ setValueAtIndex={(index, value) => {
568
+ listContentRef.current[index] = value
569
+ }}
570
+ activeIndex={activeIndex}
571
+ onChange={setValue}
572
+ selectedIndex={selectedIndex}
573
+ setActiveIndex={setActiveIndex}
574
+ setOpen={setOpen}
575
+ setSelectedIndex={setSelectedIndex}
576
+ value={value}
577
+ open={open}
578
+ >
579
+ <SelectSheetController onOpenChange={setOpen} __scopeSelect={__scopeSelect}>
580
+ <SelectImpl
581
+ activeIndexRef={activeIndexRef}
582
+ listContentRef={listContentRef}
583
+ selectedIndexRef={selectedIndexRef}
584
+ {...props}
585
+ open={open}
586
+ value={value}
587
+ >
588
+ {children}
589
+ </SelectImpl>
590
+ </SelectSheetController>
591
+ </SelectProvider>
592
+ )
593
+ },
594
+ {
595
+ Content: SelectContent,
596
+ Group: SelectGroup,
597
+ Icon: SelectIcon,
598
+ Item: SelectItem,
599
+ ItemIndicator: SelectItemIndicator,
600
+ ItemText: SelectItemText,
601
+ Label: SelectLabel,
602
+ ScrollDownButton: SelectScrollDownButton,
603
+ ScrollUpButton: SelectScrollUpButton,
604
+ Trigger: SelectTrigger,
605
+ Value: SelectValue,
606
+ Viewport: SelectViewport,
607
+ SheetContents: SelectSheetContents,
608
+ Sheet: ControlledSheet,
609
+ }
610
+ )
611
+
612
+ // @ts-ignore
613
+ Select.displayName = SELECT_NAME
@@ -0,0 +1,5 @@
1
+ import { SelectContentProps } from './types'
2
+
3
+ export const SelectContent = ({ children }: SelectContentProps) => {
4
+ return children
5
+ }
@@ -0,0 +1,45 @@
1
+ import { FloatingOverlay, FloatingPortal } from '@floating-ui/react-dom-interactions'
2
+ import { Theme, useIsTouchDevice, useThemeName } from '@tamagui/core'
3
+ import { Dismissable } from '@tamagui/dismissable'
4
+ import { FocusScope, FocusScopeProps } from '@tamagui/focus-scope'
5
+
6
+ import { useSelectContext } from './context'
7
+ import { SelectContentProps } from './types'
8
+ import { useShowSelectSheet } from './useSelectBreakpointActive'
9
+
10
+ /* -------------------------------------------------------------------------------------------------
11
+ * SelectContent
12
+ * -----------------------------------------------------------------------------------------------*/
13
+
14
+ const CONTENT_NAME = 'SelectContent'
15
+
16
+ export const SelectContent = ({
17
+ children,
18
+ __scopeSelect,
19
+ zIndex = 1000,
20
+ ...focusScopeProps
21
+ }: SelectContentProps & FocusScopeProps) => {
22
+ const context = useSelectContext(CONTENT_NAME, __scopeSelect)
23
+ const themeName = useThemeName()
24
+ const showSheet = useShowSelectSheet(context)
25
+ const contents = <Theme name={themeName}>{children}</Theme>
26
+ const touch = useIsTouchDevice()
27
+
28
+ if (showSheet && context.open) {
29
+ return contents
30
+ }
31
+
32
+ return (
33
+ <FloatingPortal>
34
+ {context.open ? (
35
+ <FloatingOverlay style={{ zIndex }} lockScroll={!touch}>
36
+ <FocusScope loop trapped {...focusScopeProps}>
37
+ <Dismissable disableOutsidePointerEvents>{contents}</Dismissable>
38
+ </FocusScope>
39
+ </FloatingOverlay>
40
+ ) : (
41
+ <div style={{ display: 'none' }}>{contents}</div>
42
+ )}
43
+ </FloatingPortal>
44
+ )
45
+ }