@tamagui/select 1.0.1-beta.21 → 1.0.1-beta.211

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 +412 -390
  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 +403 -358
  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 +283 -271
  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 +618 -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 +753 -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,618 @@
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 composedRefs = useComposedRefs(forwardedRef, context.onValueNodeChange)
106
+
107
+ const children = childrenProp ?? context.selectedItem
108
+ const hasChildren = !!children
109
+ const selectValueChildren =
110
+ context.value === undefined && placeholder !== undefined ? placeholder : children
111
+
112
+ useIsomorphicLayoutEffect(() => {
113
+ onValueNodeHasChildrenChange(hasChildren)
114
+ }, [onValueNodeHasChildrenChange, hasChildren])
115
+
116
+ return (
117
+ <SelectValueFrame
118
+ size={context.size}
119
+ ref={composedRefs}
120
+ // we don't want events from the portalled `SelectValue` children to bubble
121
+ // through the item they came from
122
+ pointerEvents="none"
123
+ >
124
+ {selectValueChildren}
125
+ </SelectValueFrame>
126
+ )
127
+ }
128
+ )
129
+ )
130
+
131
+ SelectValue.displayName = VALUE_NAME
132
+
133
+ /* -------------------------------------------------------------------------------------------------
134
+ * SelectIcon
135
+ * -----------------------------------------------------------------------------------------------*/
136
+
137
+ export const SelectIcon = styled(XStack, {
138
+ name: 'SelectIcon',
139
+ // @ts-ignore
140
+ 'aria-hidden': true,
141
+ children: <Paragraph>▼</Paragraph>,
142
+ })
143
+
144
+ /* -------------------------------------------------------------------------------------------------
145
+ * SelectItem
146
+ * -----------------------------------------------------------------------------------------------*/
147
+
148
+ const ITEM_NAME = 'SelectItem'
149
+
150
+ type SelectItemContextValue = {
151
+ value: string
152
+ textId: string
153
+ isSelected: boolean
154
+ }
155
+
156
+ const [SelectItemContextProvider, useSelectItemContext] =
157
+ createSelectContext<SelectItemContextValue>(ITEM_NAME)
158
+
159
+ export interface SelectItemProps extends YStackProps {
160
+ value: string
161
+ index: number
162
+ disabled?: boolean
163
+ textValue?: string
164
+ }
165
+
166
+ export const SelectItem = React.forwardRef<TamaguiElement, SelectItemProps>(
167
+ (props: ScopedProps<SelectItemProps>, forwardedRef) => {
168
+ const {
169
+ __scopeSelect,
170
+ value,
171
+ disabled = false,
172
+ textValue: textValueProp,
173
+ index,
174
+ ...itemProps
175
+ } = props
176
+ const context = useSelectContext(ITEM_NAME, __scopeSelect)
177
+ const isSelected = context.value === value
178
+ const textId = useId()
179
+
180
+ const {
181
+ selectedIndex,
182
+ setSelectedIndex,
183
+ listRef,
184
+ open,
185
+ setOpen,
186
+ onChange,
187
+ setActiveIndex,
188
+ allowMouseUpRef,
189
+ allowSelectRef,
190
+ setValueAtIndex,
191
+ selectTimeoutRef,
192
+ dataRef,
193
+ } = context
194
+
195
+ const composedRefs = useComposedRefs(forwardedRef, (node) => {
196
+ if (!isWeb) return
197
+ if (node instanceof HTMLElement) {
198
+ if (listRef) {
199
+ listRef.current[index] = node
200
+ }
201
+ }
202
+ })
203
+
204
+ React.useEffect(() => {
205
+ setValueAtIndex(index, value)
206
+ }, [index, setValueAtIndex, value])
207
+
208
+ function handleSelect() {
209
+ setSelectedIndex(index)
210
+ onChange(value)
211
+ setOpen(false)
212
+ }
213
+
214
+ const selectItemProps = context.interactions
215
+ ? context.interactions.getItemProps({
216
+ onTouchStart() {
217
+ allowSelectRef!.current = true
218
+ allowMouseUpRef!.current = false
219
+ },
220
+ onKeyDown(event) {
221
+ if (event.key === 'Enter' || (event.key === ' ' && !dataRef?.current.typing)) {
222
+ event.preventDefault()
223
+ handleSelect()
224
+ } else {
225
+ allowSelectRef!.current = true
226
+ }
227
+ },
228
+ onClick() {
229
+ if (allowSelectRef!.current) {
230
+ setSelectedIndex(index)
231
+ setOpen(false)
232
+ }
233
+ },
234
+ onMouseUp() {
235
+ if (!allowMouseUpRef!.current) {
236
+ return
237
+ }
238
+
239
+ if (allowSelectRef!.current) {
240
+ handleSelect()
241
+ }
242
+
243
+ // On touch devices, prevent the element from
244
+ // immediately closing `onClick` by deferring it
245
+ clearTimeout(selectTimeoutRef!.current)
246
+ selectTimeoutRef!.current = setTimeout(() => {
247
+ allowSelectRef!.current = true
248
+ })
249
+ },
250
+ })
251
+ : {
252
+ onPress: handleSelect,
253
+ }
254
+
255
+ return (
256
+ <SelectItemContextProvider
257
+ scope={__scopeSelect}
258
+ value={value}
259
+ textId={textId || ''}
260
+ isSelected={isSelected}
261
+ >
262
+ <ListItem
263
+ backgrounded
264
+ pressTheme
265
+ cursor=""
266
+ focusTheme
267
+ componentName={ITEM_NAME}
268
+ ref={composedRefs}
269
+ aria-labelledby={textId}
270
+ aria-selected={isSelected}
271
+ data-state={isSelected ? 'active' : 'inactive'}
272
+ aria-disabled={disabled || undefined}
273
+ data-disabled={disabled ? '' : undefined}
274
+ tabIndex={disabled ? undefined : -1}
275
+ size={context.size}
276
+ {...itemProps}
277
+ {...selectItemProps}
278
+ />
279
+ </SelectItemContextProvider>
280
+ )
281
+ }
282
+ )
283
+
284
+ SelectItem.displayName = ITEM_NAME
285
+
286
+ /* -------------------------------------------------------------------------------------------------
287
+ * SelectItemText
288
+ * -----------------------------------------------------------------------------------------------*/
289
+
290
+ const ITEM_TEXT_NAME = 'SelectItemText'
291
+
292
+ export const SelectItemTextFrame = styled(Paragraph, {
293
+ name: ITEM_TEXT_NAME,
294
+ selectable: false,
295
+ })
296
+
297
+ type SelectItemTextProps = GetProps<typeof SelectItemTextFrame>
298
+
299
+ const SelectItemText = React.forwardRef<TamaguiElement, SelectItemTextProps>(
300
+ (props: ScopedProps<SelectItemTextProps>, forwardedRef) => {
301
+ const { __scopeSelect, className, ...itemTextProps } = props
302
+ const context = useSelectContext(ITEM_TEXT_NAME, __scopeSelect)
303
+ const itemContext = useSelectItemContext(ITEM_TEXT_NAME, __scopeSelect)
304
+ const ref = React.useRef<TamaguiElement | null>(null)
305
+ const composedRefs = useComposedRefs(forwardedRef, ref)
306
+ const isSelected = Boolean(itemContext.isSelected && context.valueNode)
307
+
308
+ const contents = React.useMemo(
309
+ () => (
310
+ <SelectItemTextFrame
311
+ className={className}
312
+ size={context.size}
313
+ id={itemContext.textId}
314
+ {...itemTextProps}
315
+ ref={composedRefs}
316
+ />
317
+ ),
318
+ // eslint-disable-next-line react-hooks/exhaustive-deps
319
+ [props, context.size, className, itemContext.textId]
320
+ )
321
+
322
+ // until portals work in sub-trees on RN, use this just for native:
323
+ React.useEffect(() => {
324
+ if (isSelected) {
325
+ context.setSelectedItem(contents)
326
+ }
327
+ // eslint-disable-next-line react-hooks/exhaustive-deps
328
+ }, [isSelected, contents])
329
+
330
+ return (
331
+ <>
332
+ {contents}
333
+
334
+ {/* Portal the select item text into the trigger value node */}
335
+ {/* this needs some extra stability between renders */}
336
+ {/* {isWeb && isSelected
337
+ ? ReactDOM.createPortal(itemTextProps.children, context.valueNode!)
338
+ : null} */}
339
+
340
+ {/* Portal an option in the bubble select */}
341
+ {/* {context.bubbleSelect
342
+ ? ReactDOM.createPortal(
343
+ // we use `.textContent` because `option` only support `string` or `number`
344
+ <option value={itemContext.value}>{ref.current?.textContent}</option>,
345
+ context.bubbleSelect
346
+ )
347
+ : null} */}
348
+ </>
349
+ )
350
+ }
351
+ )
352
+
353
+ SelectItemText.displayName = ITEM_TEXT_NAME
354
+
355
+ /* -------------------------------------------------------------------------------------------------
356
+ * SelectItemIndicator
357
+ * -----------------------------------------------------------------------------------------------*/
358
+
359
+ const ITEM_INDICATOR_NAME = 'SelectItemIndicator'
360
+
361
+ const SelectItemIndicatorFrame = styled(XStack, {
362
+ name: ITEM_TEXT_NAME,
363
+ })
364
+
365
+ type SelectItemIndicatorProps = GetProps<typeof SelectItemIndicatorFrame>
366
+
367
+ const SelectItemIndicator = React.forwardRef<TamaguiElement, SelectItemIndicatorProps>(
368
+ (props: ScopedProps<SelectItemIndicatorProps>, forwardedRef) => {
369
+ const { __scopeSelect, ...itemIndicatorProps } = props
370
+ const itemContext = useSelectItemContext(ITEM_INDICATOR_NAME, __scopeSelect)
371
+ return itemContext.isSelected ? (
372
+ <SelectItemIndicatorFrame aria-hidden {...itemIndicatorProps} ref={forwardedRef} />
373
+ ) : null
374
+ }
375
+ )
376
+
377
+ SelectItemIndicator.displayName = ITEM_INDICATOR_NAME
378
+
379
+ /* -------------------------------------------------------------------------------------------------
380
+ * SelectGroup
381
+ * -----------------------------------------------------------------------------------------------*/
382
+
383
+ const GROUP_NAME = 'SelectGroup'
384
+
385
+ type SelectGroupContextValue = { id: string }
386
+
387
+ const [SelectGroupContextProvider, useSelectGroupContext] =
388
+ createSelectContext<SelectGroupContextValue>(GROUP_NAME)
389
+
390
+ export const SelectGroupFrame = styled(YStack, {
391
+ name: GROUP_NAME,
392
+ width: '100%',
393
+ })
394
+
395
+ type SelectGroupProps = GetProps<typeof SelectGroupFrame>
396
+
397
+ const SelectGroup = React.forwardRef<TamaguiElement, SelectGroupProps>(
398
+ (props: ScopedProps<SelectGroupProps>, forwardedRef) => {
399
+ const { __scopeSelect, ...groupProps } = props
400
+ const groupId = useId()
401
+ return (
402
+ <SelectGroupContextProvider scope={__scopeSelect} id={groupId || ''}>
403
+ <SelectGroupFrame
404
+ // @ts-ignore
405
+ role="group"
406
+ aria-labelledby={groupId}
407
+ {...groupProps}
408
+ ref={forwardedRef}
409
+ />
410
+ </SelectGroupContextProvider>
411
+ )
412
+ }
413
+ )
414
+
415
+ SelectGroup.displayName = GROUP_NAME
416
+
417
+ /* -------------------------------------------------------------------------------------------------
418
+ * SelectLabel
419
+ * -----------------------------------------------------------------------------------------------*/
420
+
421
+ const LABEL_NAME = 'SelectLabel'
422
+
423
+ export type SelectLabelProps = ListItemProps
424
+
425
+ const SelectLabel = React.forwardRef<TamaguiElement, SelectLabelProps>(
426
+ (props: ScopedProps<SelectLabelProps>, forwardedRef) => {
427
+ const { __scopeSelect, ...labelProps } = props
428
+ const context = useSelectContext(LABEL_NAME, __scopeSelect)
429
+ const groupContext = useSelectGroupContext(LABEL_NAME, __scopeSelect)
430
+ return (
431
+ <ListItem
432
+ componentName={LABEL_NAME}
433
+ fontWeight="800"
434
+ id={groupContext.id}
435
+ size={context.size}
436
+ {...labelProps}
437
+ ref={forwardedRef}
438
+ />
439
+ )
440
+ }
441
+ )
442
+
443
+ SelectLabel.displayName = LABEL_NAME
444
+
445
+ /* -------------------------------------------------------------------------------------------------
446
+ * SelectSeparator
447
+ * -----------------------------------------------------------------------------------------------*/
448
+
449
+ export const SelectSeparator = styled(Separator, {
450
+ name: 'SelectSeparator',
451
+ })
452
+
453
+ const SelectSheetController = (
454
+ props: ScopedProps<{}> & {
455
+ children: React.ReactNode
456
+ onOpenChange: React.Dispatch<React.SetStateAction<boolean>>
457
+ }
458
+ ) => {
459
+ const context = useSelectContext('SelectSheetController', props.__scopeSelect)
460
+ const showSheet = useShowSelectSheet(context)
461
+ const breakpointActive = useSelectBreakpointActive(context.sheetBreakpoint)
462
+ const getShowSheet = useGet(showSheet)
463
+
464
+ return (
465
+ <SheetController
466
+ onOpenChange={(val) => {
467
+ if (getShowSheet()) {
468
+ props.onOpenChange(val)
469
+ }
470
+ }}
471
+ open={context.open}
472
+ hidden={breakpointActive === false}
473
+ >
474
+ {props.children}
475
+ </SheetController>
476
+ )
477
+ }
478
+
479
+ /* -------------------------------------------------------------------------------------------------
480
+ * SelectSheetContents
481
+ * -----------------------------------------------------------------------------------------------*/
482
+
483
+ const SHEET_CONTENTS_NAME = 'SelectSheetContents'
484
+
485
+ export const SelectSheetContents = ({ __scopeSelect }: ScopedProps<{}>) => {
486
+ const context = useSelectContext(SHEET_CONTENTS_NAME, __scopeSelect)
487
+ return <PortalHost name={`${context.scopeKey}SheetContents`} />
488
+ }
489
+
490
+ SelectSheetContents.displayName = SHEET_CONTENTS_NAME
491
+
492
+ const SelectSheetImpl = (props: SelectImplProps) => {
493
+ return <>{props.children}</>
494
+ }
495
+
496
+ /* -------------------------------------------------------------------------------------------------
497
+ * Select
498
+ * -----------------------------------------------------------------------------------------------*/
499
+
500
+ export const Select = withStaticProperties(
501
+ (props: ScopedProps<SelectProps>) => {
502
+ const {
503
+ __scopeSelect,
504
+ children,
505
+ open: openProp,
506
+ defaultOpen,
507
+ onOpenChange,
508
+ value: valueProp,
509
+ defaultValue,
510
+ onValueChange,
511
+ size: sizeProp = '$4',
512
+ sheetBreakpoint = false,
513
+ dir,
514
+ } = props
515
+
516
+ const isSheet = useSelectBreakpointActive(sheetBreakpoint)
517
+ const SelectImpl = isSheet ? SelectSheetImpl : SelectInlineImpl
518
+ const forceUpdate = React.useReducer(() => ({}), {})[1]
519
+ const [selectedItem, setSelectedItem] = React.useState<React.ReactNode>(null)
520
+
521
+ const [open, setOpen] = useControllableState({
522
+ prop: openProp,
523
+ defaultProp: defaultOpen || false,
524
+ onChange: onOpenChange,
525
+ strategy: 'most-recent-wins',
526
+ })
527
+
528
+ const [value, setValue] = useControllableState({
529
+ prop: valueProp,
530
+ defaultProp: defaultValue || '',
531
+ onChange: onValueChange,
532
+ strategy: 'most-recent-wins',
533
+ })
534
+
535
+ const [activeIndex, setActiveIndex] = React.useState<number | null>(null)
536
+ const selectedIndexRef = React.useRef<number | null>(null)
537
+ const activeIndexRef = React.useRef<number | null>(null)
538
+
539
+ const listContentRef = React.useRef<string[]>([])
540
+
541
+ const [selectedIndex, setSelectedIndex] = React.useState(
542
+ Math.max(0, listContentRef.current.indexOf(value))
543
+ )
544
+
545
+ const [valueNode, setValueNode] = React.useState<HTMLElement | null>(null)
546
+ const [valueNodeHasChildren, setValueNodeHasChildren] = React.useState(false)
547
+
548
+ useIsomorphicLayoutEffect(() => {
549
+ selectedIndexRef.current = selectedIndex
550
+ activeIndexRef.current = activeIndex
551
+ })
552
+
553
+ const id = useId()
554
+ const scopeKey = __scopeSelect ? Object.keys(__scopeSelect)[0] ?? id : id
555
+
556
+ return (
557
+ <SelectProvider
558
+ dir={dir}
559
+ blockSelection={false}
560
+ size={sizeProp}
561
+ fallback={false}
562
+ selectedItem={selectedItem}
563
+ setSelectedItem={setSelectedItem}
564
+ forceUpdate={forceUpdate}
565
+ valueNode={valueNode}
566
+ onValueNodeChange={setValueNode}
567
+ onValueNodeHasChildrenChange={setValueNodeHasChildren}
568
+ valueNodeHasChildren={valueNodeHasChildren}
569
+ scopeKey={scopeKey}
570
+ sheetBreakpoint={sheetBreakpoint}
571
+ scope={__scopeSelect}
572
+ setValueAtIndex={(index, value) => {
573
+ listContentRef.current[index] = value
574
+ }}
575
+ activeIndex={activeIndex}
576
+ onChange={setValue}
577
+ selectedIndex={selectedIndex}
578
+ setActiveIndex={setActiveIndex}
579
+ setOpen={setOpen}
580
+ setSelectedIndex={setSelectedIndex}
581
+ value={value}
582
+ open={open}
583
+ >
584
+ <SelectSheetController onOpenChange={setOpen} __scopeSelect={__scopeSelect}>
585
+ <SelectImpl
586
+ activeIndexRef={activeIndexRef}
587
+ listContentRef={listContentRef}
588
+ selectedIndexRef={selectedIndexRef}
589
+ {...props}
590
+ open={open}
591
+ value={value}
592
+ >
593
+ {children}
594
+ </SelectImpl>
595
+ </SelectSheetController>
596
+ </SelectProvider>
597
+ )
598
+ },
599
+ {
600
+ Content: SelectContent,
601
+ Group: SelectGroup,
602
+ Icon: SelectIcon,
603
+ Item: SelectItem,
604
+ ItemIndicator: SelectItemIndicator,
605
+ ItemText: SelectItemText,
606
+ Label: SelectLabel,
607
+ ScrollDownButton: SelectScrollDownButton,
608
+ ScrollUpButton: SelectScrollUpButton,
609
+ Trigger: SelectTrigger,
610
+ Value: SelectValue,
611
+ Viewport: SelectViewport,
612
+ SheetContents: SelectSheetContents,
613
+ Sheet: ControlledSheet,
614
+ }
615
+ )
616
+
617
+ // @ts-ignore
618
+ 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
+ }