@tamagui/select 2.7.7 → 3.0.0-beta.1093.1

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 (248) hide show
  1. package/dist/cjs/Select.cjs +406 -467
  2. package/dist/cjs/Select.native.cjs +495 -0
  3. package/dist/cjs/Select.native.js +445 -492
  4. package/dist/cjs/Select.native.js.map +1 -1
  5. package/dist/cjs/SelectContent.cjs +79 -79
  6. package/dist/cjs/SelectContent.native.cjs +29 -0
  7. package/dist/cjs/SelectContent.native.js +17 -22
  8. package/dist/cjs/SelectContent.native.js.map +1 -1
  9. package/dist/cjs/SelectImpl.cjs +305 -320
  10. package/dist/cjs/SelectImpl.native.cjs +29 -0
  11. package/dist/cjs/SelectImpl.native.js +16 -24
  12. package/dist/cjs/SelectImpl.native.js.map +1 -1
  13. package/dist/cjs/SelectItem.cjs +256 -205
  14. package/dist/cjs/SelectItem.native.cjs +288 -0
  15. package/dist/cjs/SelectItem.native.js +264 -213
  16. package/dist/cjs/SelectItem.native.js.map +1 -1
  17. package/dist/cjs/SelectItemText.cjs +48 -79
  18. package/dist/cjs/SelectItemText.native.cjs +73 -0
  19. package/dist/cjs/SelectItemText.native.js +49 -79
  20. package/dist/cjs/SelectItemText.native.js.map +1 -1
  21. package/dist/cjs/SelectScrollButton.cjs +110 -130
  22. package/dist/cjs/SelectScrollButton.native.cjs +37 -0
  23. package/dist/cjs/SelectScrollButton.native.js +24 -22
  24. package/dist/cjs/SelectScrollButton.native.js.map +1 -1
  25. package/dist/cjs/SelectTrigger.cjs +68 -84
  26. package/dist/cjs/SelectTrigger.native.cjs +90 -0
  27. package/dist/cjs/SelectTrigger.native.js +70 -66
  28. package/dist/cjs/SelectTrigger.native.js.map +1 -1
  29. package/dist/cjs/SelectViewport.cjs +121 -142
  30. package/dist/cjs/SelectViewport.native.cjs +72 -0
  31. package/dist/cjs/SelectViewport.native.js +53 -50
  32. package/dist/cjs/SelectViewport.native.js.map +1 -1
  33. package/dist/cjs/constants.cjs +20 -22
  34. package/dist/cjs/constants.native.cjs +40 -0
  35. package/dist/cjs/constants.native.js +20 -21
  36. package/dist/cjs/constants.native.js.map +1 -1
  37. package/dist/cjs/context.cjs +37 -48
  38. package/dist/cjs/context.native.cjs +57 -0
  39. package/dist/cjs/context.native.js +39 -49
  40. package/dist/cjs/context.native.js.map +1 -1
  41. package/dist/cjs/index.cjs +14 -11
  42. package/dist/cjs/index.native.cjs +26 -0
  43. package/dist/cjs/index.native.js +14 -10
  44. package/dist/cjs/index.native.js.map +1 -1
  45. package/dist/cjs/selectionController.cjs +343 -0
  46. package/dist/cjs/selectionController.native.cjs +407 -0
  47. package/dist/cjs/selectionController.native.js +409 -0
  48. package/dist/cjs/selectionController.native.js.map +1 -0
  49. package/dist/cjs/types.cjs +9 -11
  50. package/dist/cjs/types.native.cjs +18 -0
  51. package/dist/cjs/types.native.js +9 -10
  52. package/dist/cjs/types.native.js.map +1 -1
  53. package/dist/esm/Select.mjs +381 -443
  54. package/dist/esm/Select.mjs.map +1 -1
  55. package/dist/esm/Select.native.js +420 -469
  56. package/dist/esm/Select.native.js.map +1 -1
  57. package/dist/esm/SelectContent.mjs +67 -61
  58. package/dist/esm/SelectContent.mjs.map +1 -1
  59. package/dist/esm/SelectContent.native.js +4 -5
  60. package/dist/esm/SelectContent.native.js.map +1 -1
  61. package/dist/esm/SelectImpl.mjs +291 -295
  62. package/dist/esm/SelectImpl.mjs.map +1 -1
  63. package/dist/esm/SelectImpl.native.js +5 -8
  64. package/dist/esm/SelectImpl.native.js.map +1 -1
  65. package/dist/esm/SelectItem.mjs +240 -181
  66. package/dist/esm/SelectItem.mjs.map +1 -1
  67. package/dist/esm/SelectItem.native.js +249 -191
  68. package/dist/esm/SelectItem.native.js.map +1 -1
  69. package/dist/esm/SelectItemText.mjs +33 -55
  70. package/dist/esm/SelectItemText.mjs.map +1 -1
  71. package/dist/esm/SelectItemText.native.js +34 -56
  72. package/dist/esm/SelectItemText.native.js.map +1 -1
  73. package/dist/esm/SelectScrollButton.mjs +95 -107
  74. package/dist/esm/SelectScrollButton.mjs.map +1 -1
  75. package/dist/esm/SelectScrollButton.native.js +11 -7
  76. package/dist/esm/SelectScrollButton.native.js.map +1 -1
  77. package/dist/esm/SelectTrigger.mjs +56 -58
  78. package/dist/esm/SelectTrigger.mjs.map +1 -1
  79. package/dist/esm/SelectTrigger.native.js +54 -44
  80. package/dist/esm/SelectTrigger.native.js.map +1 -1
  81. package/dist/esm/SelectViewport.mjs +107 -119
  82. package/dist/esm/SelectViewport.mjs.map +1 -1
  83. package/dist/esm/SelectViewport.native.js +44 -39
  84. package/dist/esm/SelectViewport.native.js.map +1 -1
  85. package/dist/esm/constants.mjs +1 -0
  86. package/dist/esm/constants.mjs.map +1 -1
  87. package/dist/esm/constants.native.js +1 -0
  88. package/dist/esm/constants.native.js.map +1 -1
  89. package/dist/esm/context.mjs +20 -28
  90. package/dist/esm/context.mjs.map +1 -1
  91. package/dist/esm/context.native.js +23 -31
  92. package/dist/esm/context.native.js.map +1 -1
  93. package/dist/esm/index.js +13 -3
  94. package/dist/esm/index.mjs +13 -3
  95. package/dist/esm/index.native.js +13 -3
  96. package/dist/esm/selectionController.mjs +315 -0
  97. package/dist/esm/selectionController.mjs.map +1 -0
  98. package/dist/esm/selectionController.native.js +377 -0
  99. package/dist/esm/selectionController.native.js.map +1 -0
  100. package/dist/esm/types.mjs +0 -2
  101. package/dist/esm/types.native.js +0 -2
  102. package/dist/jsx/Select.mjs +381 -443
  103. package/dist/jsx/Select.mjs.map +1 -1
  104. package/dist/jsx/Select.native.cjs +495 -0
  105. package/dist/jsx/Select.native.js +445 -492
  106. package/dist/jsx/Select.native.js.map +1 -1
  107. package/dist/jsx/SelectContent.mjs +67 -61
  108. package/dist/jsx/SelectContent.mjs.map +1 -1
  109. package/dist/jsx/SelectContent.native.cjs +29 -0
  110. package/dist/jsx/SelectContent.native.js +17 -22
  111. package/dist/jsx/SelectContent.native.js.map +1 -1
  112. package/dist/jsx/SelectImpl.mjs +291 -295
  113. package/dist/jsx/SelectImpl.mjs.map +1 -1
  114. package/dist/jsx/SelectImpl.native.cjs +29 -0
  115. package/dist/jsx/SelectImpl.native.js +16 -24
  116. package/dist/jsx/SelectImpl.native.js.map +1 -1
  117. package/dist/jsx/SelectItem.mjs +240 -181
  118. package/dist/jsx/SelectItem.mjs.map +1 -1
  119. package/dist/jsx/SelectItem.native.cjs +288 -0
  120. package/dist/jsx/SelectItem.native.js +264 -213
  121. package/dist/jsx/SelectItem.native.js.map +1 -1
  122. package/dist/jsx/SelectItemText.mjs +33 -55
  123. package/dist/jsx/SelectItemText.mjs.map +1 -1
  124. package/dist/jsx/SelectItemText.native.cjs +73 -0
  125. package/dist/jsx/SelectItemText.native.js +49 -79
  126. package/dist/jsx/SelectItemText.native.js.map +1 -1
  127. package/dist/jsx/SelectScrollButton.mjs +95 -107
  128. package/dist/jsx/SelectScrollButton.mjs.map +1 -1
  129. package/dist/jsx/SelectScrollButton.native.cjs +37 -0
  130. package/dist/jsx/SelectScrollButton.native.js +24 -22
  131. package/dist/jsx/SelectScrollButton.native.js.map +1 -1
  132. package/dist/jsx/SelectTrigger.mjs +56 -58
  133. package/dist/jsx/SelectTrigger.mjs.map +1 -1
  134. package/dist/jsx/SelectTrigger.native.cjs +90 -0
  135. package/dist/jsx/SelectTrigger.native.js +70 -66
  136. package/dist/jsx/SelectTrigger.native.js.map +1 -1
  137. package/dist/jsx/SelectViewport.mjs +107 -119
  138. package/dist/jsx/SelectViewport.mjs.map +1 -1
  139. package/dist/jsx/SelectViewport.native.cjs +72 -0
  140. package/dist/jsx/SelectViewport.native.js +53 -50
  141. package/dist/jsx/SelectViewport.native.js.map +1 -1
  142. package/dist/jsx/constants.mjs +1 -0
  143. package/dist/jsx/constants.mjs.map +1 -1
  144. package/dist/jsx/constants.native.cjs +40 -0
  145. package/dist/jsx/constants.native.js +20 -21
  146. package/dist/jsx/constants.native.js.map +1 -1
  147. package/dist/jsx/context.mjs +20 -28
  148. package/dist/jsx/context.mjs.map +1 -1
  149. package/dist/jsx/context.native.cjs +57 -0
  150. package/dist/jsx/context.native.js +39 -49
  151. package/dist/jsx/context.native.js.map +1 -1
  152. package/dist/jsx/index.js +13 -3
  153. package/dist/jsx/index.mjs +13 -3
  154. package/dist/jsx/index.native.cjs +26 -0
  155. package/dist/jsx/index.native.js +14 -10
  156. package/dist/jsx/index.native.js.map +1 -1
  157. package/dist/jsx/selectionController.mjs +315 -0
  158. package/dist/jsx/selectionController.mjs.map +1 -0
  159. package/dist/jsx/selectionController.native.cjs +407 -0
  160. package/dist/jsx/selectionController.native.js +409 -0
  161. package/dist/jsx/selectionController.native.js.map +1 -0
  162. package/dist/jsx/types.mjs +0 -2
  163. package/dist/jsx/types.native.cjs +18 -0
  164. package/dist/jsx/types.native.js +9 -10
  165. package/dist/jsx/types.native.js.map +1 -1
  166. package/package.json +31 -30
  167. package/src/Select.tsx +478 -378
  168. package/src/SelectContent.tsx +46 -16
  169. package/src/SelectImpl.native.tsx +0 -8
  170. package/src/SelectImpl.tsx +149 -160
  171. package/src/SelectItem.tsx +280 -159
  172. package/src/SelectItemText.tsx +16 -33
  173. package/src/SelectScrollButton.native.tsx +13 -4
  174. package/src/SelectScrollButton.tsx +57 -42
  175. package/src/SelectTrigger.tsx +77 -53
  176. package/src/SelectViewport.native.tsx +42 -25
  177. package/src/SelectViewport.tsx +60 -60
  178. package/src/context.tsx +6 -2
  179. package/src/index.tsx +5 -2
  180. package/src/selectionController.ts +404 -0
  181. package/src/types.tsx +105 -59
  182. package/types/Select.d.ts +139 -201
  183. package/types/Select.d.ts.map +1 -1
  184. package/types/SelectContent.d.ts +1 -1
  185. package/types/SelectContent.d.ts.map +1 -1
  186. package/types/SelectContent.native.d.ts.map +1 -1
  187. package/types/SelectImpl.d.ts +2 -1
  188. package/types/SelectImpl.d.ts.map +1 -1
  189. package/types/SelectImpl.native.d.ts +1 -5
  190. package/types/SelectImpl.native.d.ts.map +1 -1
  191. package/types/SelectItem.d.ts +13 -16
  192. package/types/SelectItem.d.ts.map +1 -1
  193. package/types/SelectItemText.d.ts +9 -13
  194. package/types/SelectItemText.d.ts.map +1 -1
  195. package/types/SelectScrollButton.d.ts +15 -2
  196. package/types/SelectScrollButton.d.ts.map +1 -1
  197. package/types/SelectScrollButton.native.d.ts +16 -3
  198. package/types/SelectScrollButton.native.d.ts.map +1 -1
  199. package/types/SelectTrigger.d.ts +11 -23
  200. package/types/SelectTrigger.d.ts.map +1 -1
  201. package/types/SelectViewport.d.ts +11 -38
  202. package/types/SelectViewport.d.ts.map +1 -1
  203. package/types/SelectViewport.native.d.ts +15 -4
  204. package/types/SelectViewport.native.d.ts.map +1 -1
  205. package/types/context.d.ts +2 -0
  206. package/types/context.d.ts.map +1 -1
  207. package/types/index.d.ts +5 -2
  208. package/types/index.d.ts.map +1 -1
  209. package/types/selectionController.d.ts +90 -0
  210. package/types/selectionController.d.ts.map +1 -0
  211. package/types/types.d.ts +66 -50
  212. package/types/types.d.ts.map +1 -1
  213. package/dist/cjs/BubbleSelect.cjs +0 -50
  214. package/dist/cjs/BubbleSelect.native.js +0 -30
  215. package/dist/cjs/BubbleSelect.native.js.map +0 -1
  216. package/dist/cjs/useSelectBreakpointActive.cjs +0 -32
  217. package/dist/cjs/useSelectBreakpointActive.native.js +0 -35
  218. package/dist/cjs/useSelectBreakpointActive.native.js.map +0 -1
  219. package/dist/esm/BubbleSelect.mjs +0 -27
  220. package/dist/esm/BubbleSelect.mjs.map +0 -1
  221. package/dist/esm/BubbleSelect.native.js +0 -4
  222. package/dist/esm/BubbleSelect.native.js.map +0 -1
  223. package/dist/esm/index.js.map +0 -1
  224. package/dist/esm/index.mjs.map +0 -1
  225. package/dist/esm/index.native.js.map +0 -1
  226. package/dist/esm/types.mjs.map +0 -1
  227. package/dist/esm/types.native.js.map +0 -1
  228. package/dist/esm/useSelectBreakpointActive.mjs +0 -7
  229. package/dist/esm/useSelectBreakpointActive.mjs.map +0 -1
  230. package/dist/esm/useSelectBreakpointActive.native.js +0 -7
  231. package/dist/esm/useSelectBreakpointActive.native.js.map +0 -1
  232. package/dist/jsx/BubbleSelect.mjs +0 -27
  233. package/dist/jsx/BubbleSelect.mjs.map +0 -1
  234. package/dist/jsx/BubbleSelect.native.js +0 -30
  235. package/dist/jsx/BubbleSelect.native.js.map +0 -1
  236. package/dist/jsx/index.js.map +0 -1
  237. package/dist/jsx/index.mjs.map +0 -1
  238. package/dist/jsx/types.mjs.map +0 -1
  239. package/dist/jsx/useSelectBreakpointActive.mjs +0 -7
  240. package/dist/jsx/useSelectBreakpointActive.mjs.map +0 -1
  241. package/dist/jsx/useSelectBreakpointActive.native.js +0 -35
  242. package/dist/jsx/useSelectBreakpointActive.native.js.map +0 -1
  243. package/src/BubbleSelect.tsx +0 -50
  244. package/src/useSelectBreakpointActive.tsx +0 -7
  245. package/types/BubbleSelect.d.ts +0 -2
  246. package/types/BubbleSelect.d.ts.map +0 -1
  247. package/types/useSelectBreakpointActive.d.ts +0 -3
  248. package/types/useSelectBreakpointActive.d.ts.map +0 -1
@@ -1,7 +1,8 @@
1
- import { isWeb } from '@tamagui/core'
1
+ import { useAdaptContext, useAdaptIsActive } from '@tamagui/adapt'
2
2
  import { Dismissable } from '@tamagui/dismissable'
3
3
  import type { FocusScopeProps } from '@tamagui/focus-scope'
4
4
  import { FocusScope } from '@tamagui/focus-scope'
5
+ import { composeEventHandlers } from '@tamagui/helpers'
5
6
 
6
7
  import { Portal } from '@tamagui/portal'
7
8
  import { RemoveScroll } from '@tamagui/remove-scroll'
@@ -12,7 +13,7 @@ import {
12
13
  useSelectItemParentContext,
13
14
  } from './context'
14
15
  import type { SelectContentProps } from './types'
15
- import { useShowSelectSheet } from './useSelectBreakpointActive'
16
+ import type { SelectOpenChangeDetails } from './types'
16
17
 
17
18
  /* -------------------------------------------------------------------------------------------------
18
19
  * SelectContent
@@ -21,12 +22,17 @@ import { useShowSelectSheet } from './useSelectBreakpointActive'
21
22
  export const SelectContent = ({
22
23
  children,
23
24
  scope,
25
+ onEscapeKeyDown,
26
+ onPointerDownOutside,
27
+ onFocusOutside,
28
+ onInteractOutside,
24
29
  ...focusScopeProps
25
30
  }: SelectContentProps & FocusScopeProps) => {
26
31
  const context = useSelectContext(scope)
27
32
  const itemParentContext = useSelectItemParentContext(scope)
28
33
  const zIndex = useContext(SelectZIndexContext)
29
- const showSheet = useShowSelectSheet(context)
34
+ const isAdapted = useAdaptIsActive(context.adaptScope)
35
+ const adaptContext = useAdaptContext(context.adaptScope)
30
36
 
31
37
  const contents = children
32
38
 
@@ -34,8 +40,11 @@ export const SelectContent = ({
34
40
  return <>{children}</>
35
41
  }
36
42
 
37
- if (showSheet) {
38
- if (!context.open) {
43
+ if (isAdapted) {
44
+ // content is published into the Sheet via SelectViewport's AdaptPortalContents;
45
+ // keep it mounted through the sheet slide-out (adaptContext.targetFullyHidden),
46
+ // otherwise the sheet body vanishes mid-slide on close. mirrors Popover/Dialog.
47
+ if (!context.open && adaptContext.targetFullyHidden) {
39
48
  return null
40
49
  }
41
50
  return <>{contents}</>
@@ -47,13 +56,35 @@ export const SelectContent = ({
47
56
  <Dismissable
48
57
  asChild
49
58
  forceUnmount={!context.open}
50
- onDismiss={() => itemParentContext.setOpen(false)}
51
- // prevent focus-outside and pointer-outside from triggering dismiss:
52
- // SelectImpl has its own document pointerdown listener for outside clicks,
53
- // and focus changes during open (e.g. FocusScope trapping) shouldn't dismiss.
54
- // only escape key should trigger onDismiss here.
55
- onFocusOutside={(e) => e.preventDefault()}
56
- onPointerDownOutside={(e) => e.preventDefault()}
59
+ onDismiss={(details) => {
60
+ if (details.reason !== 'focus-out') {
61
+ itemParentContext.requestOpenChange(
62
+ false,
63
+ details as SelectOpenChangeDetails
64
+ )
65
+ }
66
+ }}
67
+ onEscapeKeyDown={(details) => {
68
+ const event = details.event as unknown as KeyboardEvent | undefined
69
+ event?.stopImmediatePropagation()
70
+ onEscapeKeyDown?.(details)
71
+ }}
72
+ onInteractOutside={onInteractOutside}
73
+ // focus changes during open should not dismiss the list
74
+ onFocusOutside={composeEventHandlers(onFocusOutside, (e) => e.cancel(), {
75
+ checkDefaultPrevented: false,
76
+ })}
77
+ onPointerDownOutside={composeEventHandlers(
78
+ onPointerDownOutside,
79
+ (details) => {
80
+ const trigger = context.floatingContext?.refs?.reference?.current
81
+ const target = details.event?.target
82
+ if (trigger instanceof HTMLElement && target instanceof Node) {
83
+ if (trigger.contains(target)) details.cancel()
84
+ }
85
+ },
86
+ { checkDefaultPrevented: false }
87
+ )}
57
88
  >
58
89
  <FocusScope
59
90
  {...focusScopeProps}
@@ -61,19 +92,18 @@ export const SelectContent = ({
61
92
  trapped
62
93
  onMountAutoFocus={(e) => {
63
94
  // prevent FocusScope from auto-focusing - floating-ui handles focus in SelectItem
64
- e.preventDefault()
95
+ e.cancel()
65
96
  }}
66
97
  onUnmountAutoFocus={(e) => {
67
98
  // return focus to trigger on close
68
- e.preventDefault()
99
+ e.cancel()
69
100
  const trigger = context.floatingContext?.refs?.reference?.current
70
101
  if (trigger instanceof HTMLElement) {
71
102
  trigger.focus()
72
103
  }
73
104
  }}
74
105
  >
75
- {/* div needed for FocusScope ref, display:contents keeps layout neutral */}
76
- {isWeb ? <div style={{ display: 'contents' }}>{contents}</div> : contents}
106
+ {contents}
77
107
  </FocusScope>
78
108
  </Dismissable>
79
109
  </RemoveScroll>
@@ -1,13 +1,5 @@
1
1
  import type { SelectImplProps } from './types'
2
2
 
3
- /**
4
- * TODO can just make it a sheet for native
5
- * later on can have `native` prop
6
- */
7
-
8
3
  export const SelectInlineImpl = (props: SelectImplProps) => {
9
- if (process.env.NODE_ENV === 'development') {
10
- console.warn('Sheet not implemented inline on native')
11
- }
12
4
  return <>{props.children}</>
13
5
  }
@@ -1,9 +1,7 @@
1
1
  import {
2
2
  autoUpdate,
3
- flip,
4
3
  inner,
5
4
  offset,
6
- shift,
7
5
  size,
8
6
  useClick,
9
7
  useFloatingRaw as useFloatingDom,
@@ -16,38 +14,41 @@ import {
16
14
  type SideObject,
17
15
  } from '@tamagui/floating'
18
16
  import { useIsomorphicLayoutEffect } from '@tamagui/constants'
19
- import { useEvent, useIsTouchDevice } from '@tamagui/core'
17
+ import { createChangeEventDetails, useEvent, useIsTouchDevice } from '@tamagui/core'
18
+ import { composeEventHandlers } from '@tamagui/helpers'
20
19
  import * as React from 'react'
21
- import { flushSync } from 'react-dom'
22
- import { SCROLL_ARROW_THRESHOLD, WINDOW_PADDING } from './constants'
20
+ import { SCROLL_ARROW_THRESHOLD } from './constants'
23
21
  import {
24
22
  SelectItemParentProvider,
25
23
  SelectProvider,
26
24
  useSelectContext,
27
25
  useSelectItemParentContext,
28
26
  } from './context'
29
- import type { SelectImplProps } from './types'
27
+ import type {
28
+ SelectActiveChangeDetails,
29
+ SelectImplProps,
30
+ SelectOpenChangeDetails,
31
+ } from './types'
30
32
 
31
- // TODO use id for focusing from label
32
33
  export const SelectInlineImpl = (props: SelectImplProps) => {
33
- const { scope, children, open = false, listContentRef, setActiveIndexFast } = props
34
+ const { scope, children, open = false, listContentRef } = props
34
35
 
35
36
  const selectContext = useSelectContext(scope)
36
37
  const selectItemParentContext = useSelectItemParentContext(scope)
37
38
  const { setActiveIndex, selectedIndex, activeIndexRef } = selectContext
38
39
 
39
- const { setOpen, setSelectedIndex } = selectItemParentContext
40
+ const { requestOpenChange, registry } = selectItemParentContext
40
41
 
41
- const [scrollTop, setScrollTop] = React.useState(0)
42
42
  const touch = useIsTouchDevice()
43
43
 
44
- const listItemsRef = React.useRef<Array<HTMLElement | null>>([])
44
+ const listItemsRef = selectItemParentContext.listRef!
45
45
  const overflowRef = React.useRef<null | SideObject>(null)
46
46
  const upArrowRef = React.useRef<HTMLDivElement | null>(null)
47
47
  const downArrowRef = React.useRef<HTMLDivElement | null>(null)
48
48
  const allowSelectRef = React.useRef(false)
49
49
  const allowMouseUpRef = React.useRef(true)
50
50
  const selectTimeoutRef = React.useRef<any>(null)
51
+ const latestKeyboardEventRef = React.useRef<Event | undefined>(undefined)
51
52
  const state = React.useRef({
52
53
  isMouseOutside: false,
53
54
  isTyping: false,
@@ -57,19 +58,15 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
57
58
  const [fallback, setFallback] = React.useState(false)
58
59
  const [innerOffset, setInnerOffset] = React.useState(0)
59
60
  const [blockSelection, setBlockSelection] = React.useState(false)
60
- const floatingStyle = React.useRef({})
61
61
 
62
- // sync activeIndex on open/close
63
62
  React.useEffect(() => {
64
- if (open) {
65
- setActiveIndexFast(selectedIndex ?? 0)
66
- } else {
67
- setScrollTop(0)
63
+ if (!open) {
64
+ setCanScrollUp(false)
65
+ setCanScrollDown(false)
68
66
  setFallback(false)
69
- setActiveIndexFast(null)
70
67
  setControlledScrolling(false)
71
68
  }
72
- }, [open, selectedIndex, setActiveIndexFast])
69
+ }, [open])
73
70
 
74
71
  // close when mouseup outside select
75
72
  if (process.env.TAMAGUI_TARGET === 'web') {
@@ -77,7 +74,10 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
77
74
  if (!open) return
78
75
  const mouseUp = (e: MouseEvent) => {
79
76
  if (state.current.isMouseOutside) {
80
- setOpen(false)
77
+ requestOpenChange(
78
+ false,
79
+ createChangeEventDetails('outside-press', e) as SelectOpenChangeDetails
80
+ )
81
81
  }
82
82
  }
83
83
  document.addEventListener('mouseup', mouseUp)
@@ -98,103 +98,97 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
98
98
  open,
99
99
  placement: 'bottom-start',
100
100
  whileElementsMounted: autoUpdate,
101
- // eslint-disable-next-line no-constant-condition
102
- middleware: false
103
- ? // this is the logic from floating-ui
104
- // but i find it causes issues (open, drag select, close, then re-open its not positioned "over")
105
- // https://github.com/floating-ui/floating-ui/blob/master/packages/react/test/visual/components/MacSelect.tsx
106
- [
107
- offset(5),
108
- touch
109
- ? shift({ crossAxis: true, padding: WINDOW_PADDING })
110
- : flip({ padding: WINDOW_PADDING }),
111
- size({
112
- apply({ availableHeight, rects }) {
113
- Object.assign(floatingStyle.current, {
114
- maxHeight: `${availableHeight}px`,
115
- minWidth: `${rects.reference.width}px`,
116
- })
117
- if (refs.floating.current) {
118
- Object.assign(refs.floating.current.style, floatingStyle.current)
119
- }
120
- },
121
- padding: 10,
122
- }),
123
- ]
124
- : [
125
- size({
126
- apply({
127
- rects: {
128
- reference: { width },
129
- },
130
- }) {
131
- Object.assign(floatingStyle.current, {
132
- minWidth: width + 8,
133
- })
134
- if (refs.floating.current) {
135
- Object.assign(refs.floating.current.style, floatingStyle.current)
136
- }
137
- },
138
- }),
139
- inner({
140
- listRef: listItemsRef,
141
- overflowRef,
142
- index: selectedIndex,
143
- offset: innerOffset,
144
- onFallbackChange: setFallback,
145
- padding: 10,
146
- minItemsVisible: touch ? 10 : 4,
147
- referenceOverflowThreshold: 20,
148
- }),
149
- offset({ crossAxis: -5 }),
150
- ],
101
+ // The removed alternate stack was the logic from floating-ui,
102
+ // but it causes issues (open, drag select, close, then re-open its not positioned "over")
103
+ // https://github.com/floating-ui/floating-ui/blob/master/packages/react/test/visual/components/MacSelect.tsx
104
+ middleware: [
105
+ size({
106
+ apply({ rects, elements }) {
107
+ // the list overhangs the trigger by 4px a side (with the -4 cross offset)
108
+ elements.floating.style.minWidth = `${rects.reference.width + 8}px`
109
+ },
110
+ }),
111
+ inner({
112
+ listRef: listItemsRef,
113
+ overflowRef,
114
+ index: selectedIndex,
115
+ offset: innerOffset,
116
+ onFallbackChange: setFallback,
117
+ padding: 10,
118
+ minItemsVisible: touch ? 10 : 4,
119
+ referenceOverflowThreshold: 20,
120
+ }),
121
+ offset({ crossAxis: -4 }),
122
+ ],
151
123
  } as any)
152
124
 
153
- const floatingRef = refs.floating
154
-
155
- const showUpArrow = open && scrollTop > SCROLL_ARROW_THRESHOLD
156
- const showDownArrow =
157
- open &&
158
- floatingRef.current &&
159
- scrollTop <
160
- floatingRef.current.scrollHeight -
161
- floatingRef.current.clientHeight -
162
- SCROLL_ARROW_THRESHOLD
163
-
125
+ // the arrows are booleans that flip at a threshold, never the scroll offset
126
+ // itself, so a scroll event costs no render unless an arrow appears or goes
127
+ const [canScrollUp, setCanScrollUp] = React.useState(false)
128
+ const [canScrollDown, setCanScrollDown] = React.useState(false)
129
+ const updateScrollArrows = React.useCallback(() => {
130
+ const el = refs.floating.current
131
+ if (!el) return
132
+ setCanScrollUp(el.scrollTop > SCROLL_ARROW_THRESHOLD)
133
+ setCanScrollDown(
134
+ el.scrollTop < el.scrollHeight - el.clientHeight - SCROLL_ARROW_THRESHOLD
135
+ )
136
+ }, [refs])
137
+
138
+ const showUpArrow = open && canScrollUp
139
+ const showDownArrow = open && canScrollDown
164
140
  const isScrollable = showDownArrow || showUpArrow
165
141
 
142
+ // autoUpdate handles resize and scroll while mounted; this covers the open
166
143
  useIsomorphicLayoutEffect(() => {
167
- if (typeof window === 'undefined') return
168
- window.addEventListener('resize', update)
169
- if (open) {
170
- update()
171
- }
172
- return () => window.removeEventListener('resize', update)
144
+ if (open) update()
173
145
  }, [update, open])
174
146
 
175
147
  const onMatch = useEvent((index: number) => {
176
- const fn = open ? setActiveIndex : setSelectedIndex
177
- return fn(index)
148
+ if (!open) return
149
+ setActiveIndex(index, {
150
+ reason: 'keyboard',
151
+ event: latestKeyboardEventRef.current,
152
+ trigger: undefined,
153
+ index,
154
+ } as SelectActiveChangeDetails)
178
155
  })
179
156
 
180
- // construct interaction context for our custom hooks
157
+ // the interaction context for the floating hooks. every hook memoizes its
158
+ // props on this object, and those props sit in the item context, so a new
159
+ // identity here re-renders every item
181
160
  const dataRef = React.useRef<{ openEvent?: Event; placement?: string }>({})
182
161
  dataRef.current.placement = computedPlacement
183
- const interactionContext: FloatingInteractionContext = {
184
- open,
185
- onOpenChange: (val) => setOpen(val),
186
- refs: {
187
- reference: refs.reference as any,
188
- floating: refs.floating,
189
- domReference: refs.reference as any,
190
- },
191
- elements: {
192
- reference: (refs.reference?.current as Element) || null,
193
- floating: refs.floating?.current || null,
194
- domReference: (refs.reference?.current as Element) || null,
195
- },
196
- dataRef,
197
- }
162
+ const onOpenChange = useEvent((nextOpen: boolean, event?: Event, reason?: string) => {
163
+ requestOpenChange(
164
+ nextOpen,
165
+ createChangeEventDetails(
166
+ reason === 'list-navigation' ? 'keyboard' : 'trigger-press',
167
+ event,
168
+ refs.reference.current
169
+ ) as SelectOpenChangeDetails
170
+ )
171
+ })
172
+ const referenceElement = (refs.reference.current as Element | null) || null
173
+ const floatingElement = refs.floating.current || null
174
+ const interactionContext: FloatingInteractionContext = React.useMemo(
175
+ () => ({
176
+ open,
177
+ onOpenChange,
178
+ refs: {
179
+ reference: refs.reference as any,
180
+ floating: refs.floating,
181
+ domReference: refs.reference as any,
182
+ },
183
+ elements: {
184
+ reference: referenceElement,
185
+ floating: floatingElement,
186
+ domReference: referenceElement,
187
+ },
188
+ dataRef,
189
+ }),
190
+ [open, onOpenChange, refs, referenceElement, floatingElement]
191
+ )
198
192
 
199
193
  const interactionsProps = [
200
194
  useClick(interactionContext, { event: 'mousedown', keyboardHandlers: false }),
@@ -208,20 +202,31 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
208
202
  }),
209
203
  useListNavigation(interactionContext, {
210
204
  listRef: listItemsRef,
211
- activeIndex: selectContext.activeIndex ?? 0,
205
+ // items focus themselves from the active-index emitter, and the hook's
206
+ // own index syncs from item focus, so it never needs the value
207
+ activeIndex: null,
212
208
  selectedIndex,
213
209
  onNavigate: (index) => {
214
210
  if (index !== null) {
215
- setActiveIndex(index)
211
+ setActiveIndex(index, {
212
+ reason: 'list-navigation',
213
+ event: latestKeyboardEventRef.current,
214
+ trigger: undefined,
215
+ index,
216
+ } as SelectActiveChangeDetails)
216
217
  }
217
218
  },
219
+ disabledIndices: registry.getDisabledIndices(),
218
220
  scrollItemIntoView: false,
221
+ // items own hover: the hook's leave handler would focus the viewport when
222
+ // a scroll moves the list under a still pointer, losing the keyboard position
223
+ focusItemOnHover: false,
219
224
  }),
220
225
  useTypeahead(interactionContext, {
221
226
  listRef: listContentRef,
222
227
  onMatch,
223
228
  selectedIndex,
224
- activeIndex: selectContext.activeIndex,
229
+ activeIndex: null,
225
230
  onTypingChange: (e) => {
226
231
  state.current.isTyping = e
227
232
  },
@@ -234,39 +239,38 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
234
239
  }, interactionsProps)
235
240
  )
236
241
 
242
+ // the trigger and viewport getters. items get getItemProps alone, which is
243
+ // stable once the list mounts, so the floating element mounting (a new
244
+ // identity here) never re-renders the list
237
245
  const interactionsContext = React.useMemo(() => {
238
246
  return {
239
- ...interactions,
240
- getReferenceProps() {
247
+ getReferenceProps(props: Record<string, any> = {}) {
241
248
  return interactions.getReferenceProps({
242
- ref: refs.reference as any,
243
- className: 'SelectTrigger',
244
- onKeyDown(event) {
249
+ ...props,
250
+ onKeyDown: composeEventHandlers(props.onKeyDown, (event: any) => {
251
+ latestKeyboardEventRef.current = event.nativeEvent || event
245
252
  if (
246
253
  event.key === 'Enter' ||
247
254
  event.code === 'Space' ||
248
255
  (event.key === ' ' && !state.current.isTyping)
249
256
  ) {
250
257
  event.preventDefault()
251
- setOpen(true)
258
+ requestOpenChange(
259
+ true,
260
+ createChangeEventDetails(
261
+ 'keyboard',
262
+ event.nativeEvent || event,
263
+ refs.reference.current
264
+ ) as SelectOpenChangeDetails
265
+ )
252
266
  }
253
- },
267
+ }),
254
268
  })
255
269
  },
256
270
  getFloatingProps(props) {
257
271
  return interactions.getFloatingProps({
258
272
  ref: refs.floating,
259
- className: 'Select',
260
273
  ...props,
261
- style: {
262
- position: strategy,
263
- top: y ?? '',
264
- left: x ?? '',
265
- outline: 0,
266
- scrollbarWidth: 'none',
267
- ...floatingStyle.current,
268
- ...props?.style,
269
- },
270
274
  onPointerEnter() {
271
275
  setControlledScrolling(false)
272
276
  state.current.isMouseOutside = false
@@ -278,21 +282,23 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
278
282
  state.current.isMouseOutside = false
279
283
  setControlledScrolling(false)
280
284
  },
281
- onKeyDown() {
285
+ onKeyDown(event) {
286
+ latestKeyboardEventRef.current = event.nativeEvent || event
282
287
  setControlledScrolling(true)
283
288
  },
284
289
  onContextMenu(e) {
285
290
  e.preventDefault()
286
291
  },
287
- onScroll(event) {
288
- flushSync(() => {
289
- setScrollTop(event.currentTarget.scrollTop)
290
- })
291
- },
292
+ onScroll: updateScrollArrows,
292
293
  })
293
294
  },
294
295
  }
295
- }, [refs.reference.current, x, y, refs.floating.current, interactions])
296
+ }, [refs, interactions, requestOpenChange, updateScrollArrows])
297
+
298
+ const floatingPosition = React.useMemo(
299
+ () => ({ position: strategy, top: y ?? '', left: x ?? '' }),
300
+ [strategy, x, y]
301
+ )
296
302
 
297
303
  // effects
298
304
 
@@ -322,30 +328,6 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
322
328
  }
323
329
  }, [open])
324
330
 
325
- // dismiss on outside pointer down (arrows are outside the floating DOM tree)
326
- useIsomorphicLayoutEffect(() => {
327
- function onPointerDown(e: PointerEvent) {
328
- const target = e.target as Node
329
- if (
330
- !(
331
- refs.floating.current?.contains(target) ||
332
- upArrowRef.current?.contains(target) ||
333
- downArrowRef.current?.contains(target)
334
- )
335
- ) {
336
- setOpen(false)
337
- setControlledScrolling(false)
338
- }
339
- }
340
-
341
- if (open) {
342
- document.addEventListener('pointerdown', onPointerDown)
343
- return () => {
344
- document.removeEventListener('pointerdown', onPointerDown)
345
- }
346
- }
347
- }, [open, refs, setOpen])
348
-
349
331
  // scroll activeIndex into view during keyboard nav
350
332
  React.useEffect(() => {
351
333
  if (!open) return
@@ -354,13 +336,18 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
354
336
  if (controlledScrolling && index != null) {
355
337
  listItemsRef.current[index]?.scrollIntoView({ block: 'nearest' })
356
338
  }
357
- setScrollTop(refs.floating.current?.scrollTop ?? 0)
339
+ updateScrollArrows()
358
340
  }
359
341
 
360
342
  scrollActiveIntoView(activeIndexRef.current)
361
343
 
362
344
  return selectItemParentContext.activeIndexSubscribe(scrollActiveIntoView)
363
- }, [open, refs, controlledScrolling, selectItemParentContext.activeIndexSubscribe])
345
+ }, [
346
+ open,
347
+ controlledScrolling,
348
+ selectItemParentContext.activeIndexSubscribe,
349
+ updateScrollArrows,
350
+ ])
364
351
 
365
352
  React.useEffect(() => {
366
353
  if (open && fallback) {
@@ -389,10 +376,12 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
389
376
  <SelectProvider
390
377
  scope={scope}
391
378
  {...(selectContext as Required<typeof selectContext>)}
392
- setScrollTop={setScrollTop}
379
+ updateScrollArrows={updateScrollArrows}
393
380
  setInnerOffset={setInnerOffset}
394
381
  fallback={fallback}
395
382
  floatingContext={floatingContext as any}
383
+ interactions={interactionsContext}
384
+ floatingPosition={floatingPosition}
396
385
  canScrollDown={!!showDownArrow}
397
386
  canScrollUp={!!showUpArrow}
398
387
  controlledScrolling={controlledScrolling}
@@ -407,7 +396,7 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
407
396
  allowMouseUpRef={allowMouseUpRef}
408
397
  allowSelectRef={allowSelectRef}
409
398
  dataRef={dataRef as any}
410
- interactions={interactionsContext}
399
+ getItemProps={interactions.getItemProps}
411
400
  listRef={listItemsRef}
412
401
  selectTimeoutRef={selectTimeoutRef}
413
402
  >