@tamagui/select 1.0.1-beta.99 → 1.0.1-rc.0

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