@tamagui/floating 2.0.0-rc.4 → 2.0.0-rc.40

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 (191) hide show
  1. package/dist/cjs/Floating.cjs +7 -5
  2. package/dist/cjs/Floating.native.js +19 -13
  3. package/dist/cjs/Floating.native.js.map +1 -1
  4. package/dist/cjs/index.cjs +46 -13
  5. package/dist/cjs/index.native.js +46 -13
  6. package/dist/cjs/index.native.js.map +1 -1
  7. package/dist/cjs/interactions/PopupTriggerMap.cjs +49 -0
  8. package/dist/cjs/interactions/PopupTriggerMap.native.js +97 -0
  9. package/dist/cjs/interactions/PopupTriggerMap.native.js.map +1 -0
  10. package/dist/cjs/interactions/createFloatingEvents.cjs +50 -0
  11. package/dist/cjs/interactions/createFloatingEvents.native.js +56 -0
  12. package/dist/cjs/interactions/createFloatingEvents.native.js.map +1 -0
  13. package/dist/cjs/interactions/safePolygon.cjs +273 -0
  14. package/dist/cjs/interactions/safePolygon.native.js +284 -0
  15. package/dist/cjs/interactions/safePolygon.native.js.map +1 -0
  16. package/dist/cjs/interactions/types.cjs +18 -0
  17. package/dist/cjs/interactions/types.native.js +21 -0
  18. package/dist/cjs/interactions/types.native.js.map +1 -0
  19. package/dist/cjs/interactions/useClick.cjs +124 -0
  20. package/dist/cjs/interactions/useClick.native.js +132 -0
  21. package/dist/cjs/interactions/useClick.native.js.map +1 -0
  22. package/dist/cjs/interactions/useDelayGroup.cjs +115 -0
  23. package/dist/cjs/interactions/useDelayGroup.native.js +125 -0
  24. package/dist/cjs/interactions/useDelayGroup.native.js.map +1 -0
  25. package/dist/cjs/interactions/useFocus.cjs +130 -0
  26. package/dist/cjs/interactions/useFocus.native.js +139 -0
  27. package/dist/cjs/interactions/useFocus.native.js.map +1 -0
  28. package/dist/cjs/interactions/useHover.cjs +357 -0
  29. package/dist/cjs/interactions/useHover.native.js +373 -0
  30. package/dist/cjs/interactions/useHover.native.js.map +1 -0
  31. package/dist/cjs/interactions/useInnerOffset.cjs +128 -0
  32. package/dist/cjs/interactions/useInnerOffset.native.js +141 -0
  33. package/dist/cjs/interactions/useInnerOffset.native.js.map +1 -0
  34. package/dist/cjs/interactions/useInteractions.cjs +105 -0
  35. package/dist/cjs/interactions/useInteractions.native.js +216 -0
  36. package/dist/cjs/interactions/useInteractions.native.js.map +1 -0
  37. package/dist/cjs/interactions/useListNavigation.cjs +418 -0
  38. package/dist/cjs/interactions/useListNavigation.native.js +433 -0
  39. package/dist/cjs/interactions/useListNavigation.native.js.map +1 -0
  40. package/dist/cjs/interactions/useRole.cjs +122 -0
  41. package/dist/cjs/interactions/useRole.native.js +136 -0
  42. package/dist/cjs/interactions/useRole.native.js.map +1 -0
  43. package/dist/cjs/interactions/useTypeahead.cjs +143 -0
  44. package/dist/cjs/interactions/useTypeahead.native.js +159 -0
  45. package/dist/cjs/interactions/useTypeahead.native.js.map +1 -0
  46. package/dist/cjs/interactions/utils.cjs +208 -0
  47. package/dist/cjs/interactions/utils.native.js +227 -0
  48. package/dist/cjs/interactions/utils.native.js.map +1 -0
  49. package/dist/cjs/middleware/inner.cjs +118 -0
  50. package/dist/cjs/middleware/inner.native.js +130 -0
  51. package/dist/cjs/middleware/inner.native.js.map +1 -0
  52. package/dist/cjs/useFloating.cjs +35 -28
  53. package/dist/cjs/useFloating.native.js +51 -47
  54. package/dist/cjs/useFloating.native.js.map +1 -1
  55. package/dist/esm/Floating.native.js +6 -3
  56. package/dist/esm/Floating.native.js.map +1 -1
  57. package/dist/esm/index.js +17 -34
  58. package/dist/esm/index.js.map +1 -6
  59. package/dist/esm/index.mjs +16 -2
  60. package/dist/esm/index.mjs.map +1 -1
  61. package/dist/esm/index.native.js +16 -2
  62. package/dist/esm/index.native.js.map +1 -1
  63. package/dist/esm/interactions/PopupTriggerMap.mjs +24 -0
  64. package/dist/esm/interactions/PopupTriggerMap.mjs.map +1 -0
  65. package/dist/esm/interactions/PopupTriggerMap.native.js +69 -0
  66. package/dist/esm/interactions/PopupTriggerMap.native.js.map +1 -0
  67. package/dist/esm/interactions/createFloatingEvents.mjs +25 -0
  68. package/dist/esm/interactions/createFloatingEvents.mjs.map +1 -0
  69. package/dist/esm/interactions/createFloatingEvents.native.js +28 -0
  70. package/dist/esm/interactions/createFloatingEvents.native.js.map +1 -0
  71. package/dist/esm/interactions/safePolygon.mjs +248 -0
  72. package/dist/esm/interactions/safePolygon.mjs.map +1 -0
  73. package/dist/esm/interactions/safePolygon.native.js +256 -0
  74. package/dist/esm/interactions/safePolygon.native.js.map +1 -0
  75. package/dist/esm/interactions/types.mjs +2 -0
  76. package/dist/esm/interactions/types.mjs.map +1 -0
  77. package/dist/esm/interactions/types.native.js +2 -0
  78. package/dist/esm/interactions/types.native.js.map +1 -0
  79. package/dist/esm/interactions/useClick.mjs +99 -0
  80. package/dist/esm/interactions/useClick.mjs.map +1 -0
  81. package/dist/esm/interactions/useClick.native.js +104 -0
  82. package/dist/esm/interactions/useClick.native.js.map +1 -0
  83. package/dist/esm/interactions/useDelayGroup.mjs +77 -0
  84. package/dist/esm/interactions/useDelayGroup.mjs.map +1 -0
  85. package/dist/esm/interactions/useDelayGroup.native.js +84 -0
  86. package/dist/esm/interactions/useDelayGroup.native.js.map +1 -0
  87. package/dist/esm/interactions/useFocus.mjs +105 -0
  88. package/dist/esm/interactions/useFocus.mjs.map +1 -0
  89. package/dist/esm/interactions/useFocus.native.js +111 -0
  90. package/dist/esm/interactions/useFocus.native.js.map +1 -0
  91. package/dist/esm/interactions/useHover.mjs +320 -0
  92. package/dist/esm/interactions/useHover.mjs.map +1 -0
  93. package/dist/esm/interactions/useHover.native.js +333 -0
  94. package/dist/esm/interactions/useHover.native.js.map +1 -0
  95. package/dist/esm/interactions/useInnerOffset.mjs +92 -0
  96. package/dist/esm/interactions/useInnerOffset.mjs.map +1 -0
  97. package/dist/esm/interactions/useInnerOffset.native.js +102 -0
  98. package/dist/esm/interactions/useInnerOffset.native.js.map +1 -0
  99. package/dist/esm/interactions/useInteractions.mjs +80 -0
  100. package/dist/esm/interactions/useInteractions.mjs.map +1 -0
  101. package/dist/esm/interactions/useInteractions.native.js +188 -0
  102. package/dist/esm/interactions/useInteractions.native.js.map +1 -0
  103. package/dist/esm/interactions/useListNavigation.mjs +393 -0
  104. package/dist/esm/interactions/useListNavigation.mjs.map +1 -0
  105. package/dist/esm/interactions/useListNavigation.native.js +405 -0
  106. package/dist/esm/interactions/useListNavigation.native.js.map +1 -0
  107. package/dist/esm/interactions/useRole.mjs +86 -0
  108. package/dist/esm/interactions/useRole.mjs.map +1 -0
  109. package/dist/esm/interactions/useRole.native.js +97 -0
  110. package/dist/esm/interactions/useRole.native.js.map +1 -0
  111. package/dist/esm/interactions/useTypeahead.mjs +118 -0
  112. package/dist/esm/interactions/useTypeahead.mjs.map +1 -0
  113. package/dist/esm/interactions/useTypeahead.native.js +131 -0
  114. package/dist/esm/interactions/useTypeahead.native.js.map +1 -0
  115. package/dist/esm/interactions/utils.mjs +162 -0
  116. package/dist/esm/interactions/utils.mjs.map +1 -0
  117. package/dist/esm/interactions/utils.native.js +178 -0
  118. package/dist/esm/interactions/utils.native.js.map +1 -0
  119. package/dist/esm/middleware/inner.mjs +82 -0
  120. package/dist/esm/middleware/inner.mjs.map +1 -0
  121. package/dist/esm/middleware/inner.native.js +91 -0
  122. package/dist/esm/middleware/inner.native.js.map +1 -0
  123. package/dist/esm/useFloating.mjs +8 -3
  124. package/dist/esm/useFloating.mjs.map +1 -1
  125. package/dist/esm/useFloating.native.js +25 -23
  126. package/dist/esm/useFloating.native.js.map +1 -1
  127. package/package.json +8 -10
  128. package/src/Floating.native.tsx +1 -0
  129. package/src/index.ts +49 -0
  130. package/src/interactions/PopupTriggerMap.ts +30 -0
  131. package/src/interactions/createFloatingEvents.ts +34 -0
  132. package/src/interactions/safePolygon.ts +500 -0
  133. package/src/interactions/types.ts +165 -0
  134. package/src/interactions/useClick.ts +148 -0
  135. package/src/interactions/useDelayGroup.ts +114 -0
  136. package/src/interactions/useFocus.ts +164 -0
  137. package/src/interactions/useHover.ts +453 -0
  138. package/src/interactions/useInnerOffset.ts +116 -0
  139. package/src/interactions/useInteractions.ts +101 -0
  140. package/src/interactions/useListNavigation.ts +578 -0
  141. package/src/interactions/useRole.ts +103 -0
  142. package/src/interactions/useTypeahead.ts +173 -0
  143. package/src/interactions/utils.ts +234 -0
  144. package/src/middleware/inner.ts +141 -0
  145. package/src/useFloating.tsx +13 -1
  146. package/types/Floating.native.d.ts +1 -0
  147. package/types/Floating.native.d.ts.map +1 -1
  148. package/types/index.d.ts +17 -2
  149. package/types/index.d.ts.map +1 -1
  150. package/types/interactions/PopupTriggerMap.d.ts +8 -0
  151. package/types/interactions/PopupTriggerMap.d.ts.map +1 -0
  152. package/types/interactions/createFloatingEvents.d.ts +7 -0
  153. package/types/interactions/createFloatingEvents.d.ts.map +1 -0
  154. package/types/interactions/safePolygon.d.ts +4 -0
  155. package/types/interactions/safePolygon.d.ts.map +1 -0
  156. package/types/interactions/types.d.ts +123 -0
  157. package/types/interactions/types.d.ts.map +1 -0
  158. package/types/interactions/useClick.d.ts +3 -0
  159. package/types/interactions/useClick.d.ts.map +1 -0
  160. package/types/interactions/useDelayGroup.d.ts +23 -0
  161. package/types/interactions/useDelayGroup.d.ts.map +1 -0
  162. package/types/interactions/useFocus.d.ts +3 -0
  163. package/types/interactions/useFocus.d.ts.map +1 -0
  164. package/types/interactions/useHover.d.ts +6 -0
  165. package/types/interactions/useHover.d.ts.map +1 -0
  166. package/types/interactions/useInnerOffset.d.ts +3 -0
  167. package/types/interactions/useInnerOffset.d.ts.map +1 -0
  168. package/types/interactions/useInteractions.d.ts +8 -0
  169. package/types/interactions/useInteractions.d.ts.map +1 -0
  170. package/types/interactions/useListNavigation.d.ts +3 -0
  171. package/types/interactions/useListNavigation.d.ts.map +1 -0
  172. package/types/interactions/useRole.d.ts +3 -0
  173. package/types/interactions/useRole.d.ts.map +1 -0
  174. package/types/interactions/useTypeahead.d.ts +3 -0
  175. package/types/interactions/useTypeahead.d.ts.map +1 -0
  176. package/types/interactions/utils.d.ts +46 -0
  177. package/types/interactions/utils.d.ts.map +1 -0
  178. package/types/middleware/inner.d.ts +14 -0
  179. package/types/middleware/inner.d.ts.map +1 -0
  180. package/types/useFloating.d.ts +7 -1
  181. package/types/useFloating.d.ts.map +1 -1
  182. package/dist/cjs/Floating.js +0 -15
  183. package/dist/cjs/Floating.js.map +0 -6
  184. package/dist/cjs/index.js +0 -34
  185. package/dist/cjs/index.js.map +0 -6
  186. package/dist/cjs/useFloating.js +0 -46
  187. package/dist/cjs/useFloating.js.map +0 -6
  188. package/dist/esm/Floating.js +0 -2
  189. package/dist/esm/Floating.js.map +0 -6
  190. package/dist/esm/useFloating.js +0 -23
  191. package/dist/esm/useFloating.js.map +0 -6
@@ -0,0 +1,453 @@
1
+ import * as React from 'react'
2
+ import { useEvent } from '@tamagui/use-event'
3
+
4
+ import type {
5
+ Delay,
6
+ ElementProps,
7
+ FloatingInteractionContext,
8
+ OpenChangeReason,
9
+ UseHoverProps,
10
+ } from './types'
11
+ import {
12
+ clearTimeoutIfSet,
13
+ contains,
14
+ getDocument,
15
+ isElement,
16
+ isMouseLikePointerType,
17
+ } from './utils'
18
+
19
+ const safePolygonIdentifier = 'data-floating-ui-safe-polygon'
20
+
21
+ export function getDelay(
22
+ value: Delay | undefined,
23
+ prop: 'open' | 'close',
24
+ pointerType?: string
25
+ ) {
26
+ if (pointerType && !isMouseLikePointerType(pointerType)) {
27
+ return 0
28
+ }
29
+
30
+ if (typeof value === 'number') {
31
+ return value
32
+ }
33
+
34
+ return value?.[prop]
35
+ }
36
+
37
+ export interface UseHoverReturn extends ElementProps {}
38
+
39
+ // hover interaction hook ported from @floating-ui/react.
40
+ // uses raw DOM addEventListener on the reference element (NOT react delegation)
41
+ // so that mouseenter fires even when cursor moves from a disabled element.
42
+ //
43
+ // intentionally does NOT listen for documentElement mouseleave, which
44
+ // fixes the window-blur-closing-popover bug from @floating-ui/react.
45
+ export function useHover(
46
+ context: FloatingInteractionContext,
47
+ props: UseHoverProps = {}
48
+ ): ElementProps {
49
+ const { open, onOpenChange, dataRef, events, elements } = context
50
+ const {
51
+ enabled = true,
52
+ delay = 0,
53
+ handleClose = null,
54
+ mouseOnly = false,
55
+ restMs = 0,
56
+ move = true,
57
+ } = props
58
+
59
+ // latest-value refs to avoid stale closures in raw DOM handlers
60
+ const handleCloseRef = React.useRef(handleClose)
61
+ handleCloseRef.current = handleClose
62
+ const delayRef = React.useRef(delay)
63
+ delayRef.current = delay
64
+ const openRef = React.useRef(open)
65
+ openRef.current = open
66
+ const restMsRef = React.useRef(restMs)
67
+ restMsRef.current = restMs
68
+ // stable callback that always calls the latest onOpenChange
69
+ const stableOnOpenChange = useEvent(onOpenChange)
70
+
71
+ const pointerTypeRef = React.useRef<string | undefined>(undefined)
72
+ const timeoutRef = React.useRef(-1)
73
+ const handlerRef = React.useRef<((event: MouseEvent) => void) | undefined>(undefined)
74
+ const restTimeoutRef = React.useRef(-1)
75
+ const blockMouseMoveRef = React.useRef(true)
76
+ const performedPointerEventsMutationRef = React.useRef(false)
77
+ const unbindMouseMoveRef = React.useRef(() => {})
78
+ const restTimeoutPendingRef = React.useRef(false)
79
+
80
+ const isHoverOpen = useEvent(() => {
81
+ const type = dataRef.current.openEvent?.type
82
+ return type?.includes('mouse') && type !== 'mousedown'
83
+ })
84
+
85
+ // when closing before opening, clear the delay timeouts to cancel it
86
+ // from showing.
87
+ React.useEffect(() => {
88
+ if (!enabled) return
89
+ if (!events) return
90
+
91
+ function onOpenChange({ open }: { open: boolean }) {
92
+ if (!open) {
93
+ clearTimeoutIfSet(timeoutRef)
94
+ clearTimeoutIfSet(restTimeoutRef)
95
+ blockMouseMoveRef.current = true
96
+ restTimeoutPendingRef.current = false
97
+ }
98
+ }
99
+
100
+ events.on('openchange', onOpenChange)
101
+ return () => {
102
+ events.off('openchange', onOpenChange)
103
+ }
104
+ }, [enabled, events])
105
+
106
+ // NOTE: intentionally skipping the documentElement mouseleave handler
107
+ // from upstream. this is our fix for the window-blur-closing-popover bug.
108
+
109
+ const closeWithDelay = useEvent(
110
+ (event: Event, runElseBranch = true, reason: OpenChangeReason = 'hover') => {
111
+ const closeDelay = getDelay(delayRef.current, 'close', pointerTypeRef.current)
112
+ if (closeDelay && !handlerRef.current) {
113
+ clearTimeoutIfSet(timeoutRef)
114
+ timeoutRef.current = window.setTimeout(
115
+ () => stableOnOpenChange(false, event, reason),
116
+ closeDelay
117
+ ) as unknown as number
118
+ } else if (runElseBranch) {
119
+ clearTimeoutIfSet(timeoutRef)
120
+ stableOnOpenChange(false, event, reason)
121
+ }
122
+ }
123
+ )
124
+
125
+ const cleanupMouseMoveHandler = useEvent(() => {
126
+ unbindMouseMoveRef.current()
127
+ handlerRef.current = undefined
128
+ if (context.handleCloseActiveRef) {
129
+ context.handleCloseActiveRef.current = false
130
+ }
131
+ })
132
+
133
+ const clearPointerEvents = useEvent(() => {
134
+ if (performedPointerEventsMutationRef.current) {
135
+ const body = getDocument(elements.floating).body
136
+ body.style.pointerEvents = ''
137
+ body.removeAttribute(safePolygonIdentifier)
138
+ performedPointerEventsMutationRef.current = false
139
+ }
140
+ })
141
+
142
+ const isClickLikeOpenEvent = useEvent(() => {
143
+ return dataRef.current.openEvent
144
+ ? ['click', 'mousedown'].includes(dataRef.current.openEvent.type)
145
+ : false
146
+ })
147
+
148
+ // registering the mouse events on the reference directly to bypass React's
149
+ // delegation system. if the cursor was on a disabled element and then entered
150
+ // the reference (no gap), mouseenter doesn't fire in the delegation system.
151
+ React.useEffect(() => {
152
+ if (!enabled) return
153
+
154
+ function onReferenceMouseEnter(event: MouseEvent) {
155
+ clearTimeoutIfSet(timeoutRef)
156
+ blockMouseMoveRef.current = false
157
+
158
+ if (
159
+ (mouseOnly && !isMouseLikePointerType(pointerTypeRef.current)) ||
160
+ (restMsRef.current > 0 && !getDelay(delayRef.current, 'open'))
161
+ ) {
162
+ return
163
+ }
164
+
165
+ const openDelay = getDelay(delayRef.current, 'open', pointerTypeRef.current)
166
+
167
+ if (openDelay) {
168
+ timeoutRef.current = window.setTimeout(() => {
169
+ if (!openRef.current) {
170
+ stableOnOpenChange(true, event, 'hover')
171
+ }
172
+ }, openDelay) as unknown as number
173
+ } else if (!open) {
174
+ stableOnOpenChange(true, event, 'hover')
175
+ }
176
+ }
177
+
178
+ function onReferenceMouseLeave(event: MouseEvent) {
179
+ if (isClickLikeOpenEvent()) {
180
+ clearPointerEvents()
181
+ return
182
+ }
183
+
184
+ // moving to a sibling trigger in a multi-trigger pattern — suppress close
185
+ if (context.triggerElements?.hasElement(event.relatedTarget as Element)) {
186
+ return
187
+ }
188
+
189
+ unbindMouseMoveRef.current()
190
+
191
+ const doc = getDocument(elements.floating)
192
+ clearTimeoutIfSet(restTimeoutRef)
193
+ restTimeoutPendingRef.current = false
194
+
195
+ if (handleCloseRef.current) {
196
+ // prevent clearing onScrollMouseLeave timeout
197
+ if (!open) {
198
+ clearTimeoutIfSet(timeoutRef)
199
+ }
200
+
201
+ const placement = dataRef.current.placement || 'bottom'
202
+ const reference = elements.domReference
203
+ const floating = elements.floating
204
+
205
+ if (!reference || !floating) return
206
+
207
+ // call handleClose once with the leave position — it returns a
208
+ // mousemove handler with the leave (x, y) baked into its closure
209
+ // so the polygon anchor stays fixed at the leave point.
210
+ handlerRef.current = handleCloseRef.current({
211
+ x: event.clientX,
212
+ y: event.clientY,
213
+ placement,
214
+ elements: {
215
+ reference: reference as Element,
216
+ floating: floating as HTMLElement,
217
+ domReference: reference as Element,
218
+ },
219
+ onClose() {
220
+ if (context.handleCloseActiveRef) {
221
+ context.handleCloseActiveRef.current = false
222
+ }
223
+ clearPointerEvents()
224
+ cleanupMouseMoveHandler()
225
+ if (!isClickLikeOpenEvent()) {
226
+ closeWithDelay(event, true, 'safe-polygon')
227
+ }
228
+ },
229
+ })
230
+
231
+ if (context.handleCloseActiveRef) {
232
+ context.handleCloseActiveRef.current = true
233
+ }
234
+
235
+ const handler = handlerRef.current
236
+
237
+ doc.addEventListener('mousemove', handler)
238
+ unbindMouseMoveRef.current = () => {
239
+ doc.removeEventListener('mousemove', handler)
240
+ }
241
+
242
+ return
243
+ }
244
+
245
+ // allow interactivity without safePolygon on touch devices. with a
246
+ // pointer, a short close delay is an alternative.
247
+ const shouldClose =
248
+ pointerTypeRef.current === 'touch'
249
+ ? !contains(elements.floating, event.relatedTarget as Element | null)
250
+ : true
251
+ if (shouldClose) {
252
+ closeWithDelay(event)
253
+ }
254
+ }
255
+
256
+ // ensure the floating element closes after scrolling even if the pointer
257
+ // did not move.
258
+ function onScrollMouseLeave(event: MouseEvent) {
259
+ if (isClickLikeOpenEvent()) return
260
+
261
+ // moving to a sibling trigger in a multi-trigger pattern — suppress close
262
+ if (context.triggerElements?.hasElement(event.relatedTarget as Element)) {
263
+ return
264
+ }
265
+
266
+ const placement = dataRef.current.placement || 'bottom'
267
+ const reference = elements.domReference
268
+ const floating = elements.floating
269
+
270
+ if (!reference || !floating) return
271
+
272
+ // call handleClose to get a handler, then immediately invoke it
273
+ // with this scroll-leave event
274
+ handleCloseRef.current?.({
275
+ x: event.clientX,
276
+ y: event.clientY,
277
+ placement,
278
+ elements: {
279
+ reference: reference as Element,
280
+ floating: floating as HTMLElement,
281
+ domReference: reference as Element,
282
+ },
283
+ onClose() {
284
+ clearPointerEvents()
285
+ cleanupMouseMoveHandler()
286
+ if (!isClickLikeOpenEvent()) {
287
+ closeWithDelay(event)
288
+ }
289
+ },
290
+ })(event)
291
+ }
292
+
293
+ function onFloatingMouseEnter() {
294
+ clearTimeoutIfSet(timeoutRef)
295
+ }
296
+
297
+ function onFloatingMouseLeave(event: MouseEvent) {
298
+ if (isClickLikeOpenEvent()) return
299
+
300
+ // moving to a sibling trigger in a multi-trigger pattern — suppress close
301
+ if (context.triggerElements?.hasElement(event.relatedTarget as Element)) {
302
+ return
303
+ }
304
+
305
+ closeWithDelay(event, false)
306
+ }
307
+
308
+ if (isElement(elements.domReference)) {
309
+ const reference = elements.domReference as unknown as HTMLElement
310
+ const floating = elements.floating
311
+
312
+ if (open) {
313
+ reference.addEventListener('mouseleave', onScrollMouseLeave)
314
+ }
315
+
316
+ if (move) {
317
+ reference.addEventListener('mousemove', onReferenceMouseEnter, {
318
+ once: true,
319
+ })
320
+ }
321
+
322
+ reference.addEventListener('mouseenter', onReferenceMouseEnter)
323
+ reference.addEventListener('mouseleave', onReferenceMouseLeave)
324
+
325
+ if (floating) {
326
+ floating.addEventListener('mouseleave', onScrollMouseLeave)
327
+ floating.addEventListener('mouseenter', onFloatingMouseEnter)
328
+ floating.addEventListener('mouseleave', onFloatingMouseLeave)
329
+ }
330
+
331
+ return () => {
332
+ if (open) {
333
+ reference.removeEventListener('mouseleave', onScrollMouseLeave)
334
+ }
335
+
336
+ if (move) {
337
+ reference.removeEventListener('mousemove', onReferenceMouseEnter)
338
+ }
339
+
340
+ reference.removeEventListener('mouseenter', onReferenceMouseEnter)
341
+ reference.removeEventListener('mouseleave', onReferenceMouseLeave)
342
+
343
+ if (floating) {
344
+ floating.removeEventListener('mouseleave', onScrollMouseLeave)
345
+ floating.removeEventListener('mouseenter', onFloatingMouseEnter)
346
+ floating.removeEventListener('mouseleave', onFloatingMouseLeave)
347
+ }
348
+
349
+ // clean up safePolygon's document mousemove handler when reference
350
+ // changes — without this, handleCloseActiveRef stays true
351
+ // indefinitely after rapid trigger switching
352
+ cleanupMouseMoveHandler()
353
+ }
354
+ }
355
+ }, [elements, enabled, context, mouseOnly, move, open, dataRef])
356
+
357
+ // block pointer-events of every element other than the reference and floating
358
+ // while the floating element is open and has a handleClose handler with
359
+ // blockPointerEvents enabled.
360
+ React.useLayoutEffect(() => {
361
+ if (!enabled) return
362
+
363
+ if (open && handleCloseRef.current?.__options?.blockPointerEvents && isHoverOpen()) {
364
+ performedPointerEventsMutationRef.current = true
365
+ const floatingEl = elements.floating
366
+
367
+ if (isElement(elements.domReference) && floatingEl) {
368
+ const body = getDocument(elements.floating).body
369
+ body.setAttribute(safePolygonIdentifier, '')
370
+
371
+ const ref = elements.domReference as unknown as HTMLElement | SVGSVGElement
372
+
373
+ body.style.pointerEvents = 'none'
374
+ ref.style.pointerEvents = 'auto'
375
+ floatingEl.style.pointerEvents = 'auto'
376
+
377
+ return () => {
378
+ body.style.pointerEvents = ''
379
+ ref.style.pointerEvents = ''
380
+ floatingEl.style.pointerEvents = ''
381
+ }
382
+ }
383
+ }
384
+ }, [enabled, open, elements, isHoverOpen])
385
+
386
+ React.useLayoutEffect(() => {
387
+ if (!open) {
388
+ pointerTypeRef.current = undefined
389
+ restTimeoutPendingRef.current = false
390
+ cleanupMouseMoveHandler()
391
+ clearPointerEvents()
392
+ }
393
+ }, [open])
394
+
395
+ React.useEffect(() => {
396
+ return () => {
397
+ cleanupMouseMoveHandler()
398
+ clearTimeoutIfSet(timeoutRef)
399
+ clearTimeoutIfSet(restTimeoutRef)
400
+ clearPointerEvents()
401
+ }
402
+ }, [enabled, elements.domReference])
403
+
404
+ const reference: ElementProps['reference'] = React.useMemo(() => {
405
+ function setPointerRef(event: React.PointerEvent) {
406
+ pointerTypeRef.current = event.pointerType
407
+ }
408
+
409
+ return {
410
+ onPointerDown: setPointerRef,
411
+ onPointerEnter: setPointerRef,
412
+ onMouseMove(event: React.MouseEvent) {
413
+ const { nativeEvent } = event
414
+
415
+ function handleMouseMove() {
416
+ if (!blockMouseMoveRef.current && !openRef.current) {
417
+ stableOnOpenChange(true, nativeEvent, 'hover')
418
+ }
419
+ }
420
+
421
+ if (mouseOnly && !isMouseLikePointerType(pointerTypeRef.current)) {
422
+ return
423
+ }
424
+
425
+ if (open || restMsRef.current === 0) {
426
+ return
427
+ }
428
+
429
+ // ignore insignificant movements to account for tremors
430
+ if (
431
+ restTimeoutPendingRef.current &&
432
+ event.movementX ** 2 + event.movementY ** 2 < 2
433
+ ) {
434
+ return
435
+ }
436
+
437
+ clearTimeoutIfSet(restTimeoutRef)
438
+
439
+ if (pointerTypeRef.current === 'touch') {
440
+ handleMouseMove()
441
+ } else {
442
+ restTimeoutPendingRef.current = true
443
+ restTimeoutRef.current = window.setTimeout(
444
+ handleMouseMove,
445
+ restMsRef.current
446
+ ) as unknown as number
447
+ }
448
+ },
449
+ }
450
+ }, [mouseOnly, open])
451
+
452
+ return React.useMemo(() => (enabled ? { reference } : {}), [enabled, reference])
453
+ }
@@ -0,0 +1,116 @@
1
+ import * as React from 'react'
2
+ import * as ReactDOM from 'react-dom'
3
+ import { useEvent } from '@tamagui/use-event'
4
+ import type {
5
+ ElementProps,
6
+ FloatingInteractionContext,
7
+ UseInnerOffsetProps,
8
+ } from './types'
9
+
10
+ // ported from floating-ui/react/_deprecated-inner.ts useInnerOffset
11
+ // changes the inner middleware's offset upon wheel events to expand the
12
+ // floating element's height, revealing more list items.
13
+ export function useInnerOffset(
14
+ context: FloatingInteractionContext,
15
+ props: UseInnerOffsetProps
16
+ ): ElementProps {
17
+ const { open, elements } = context
18
+ const { enabled = true, overflowRef, scrollRef, onChange: unstable_onChange } = props
19
+
20
+ const onChange = useEvent(unstable_onChange)
21
+ const controlledScrollingRef = React.useRef(false)
22
+ const prevScrollTopRef = React.useRef<number | null>(null)
23
+ const initialOverflowRef = React.useRef<any>(null)
24
+
25
+ React.useEffect(() => {
26
+ if (!enabled) return
27
+
28
+ function onWheel(e: WheelEvent) {
29
+ if (e.ctrlKey || !el || overflowRef.current == null) {
30
+ return
31
+ }
32
+
33
+ const dY = e.deltaY
34
+ const isAtTop = overflowRef.current.top >= -0.5
35
+ const isAtBottom = overflowRef.current.bottom >= -0.5
36
+ const remainingScroll = el.scrollHeight - el.clientHeight
37
+ const sign = dY < 0 ? -1 : 1
38
+ const method = dY < 0 ? 'max' : 'min'
39
+
40
+ if (el.scrollHeight <= el.clientHeight) {
41
+ return
42
+ }
43
+
44
+ if ((!isAtTop && dY > 0) || (!isAtBottom && dY < 0)) {
45
+ e.preventDefault()
46
+ ReactDOM.flushSync(() => {
47
+ onChange((d: number) => d + Math[method](dY, remainingScroll * sign))
48
+ })
49
+ } else if (/firefox/i.test(navigator.userAgent)) {
50
+ // propagate scrolling during momentum phase once limited by boundary
51
+ el.scrollTop += dY
52
+ }
53
+ }
54
+
55
+ const el = scrollRef?.current || elements.floating
56
+
57
+ if (open && el) {
58
+ el.addEventListener('wheel', onWheel)
59
+
60
+ // wait for the position to be ready
61
+ requestAnimationFrame(() => {
62
+ prevScrollTopRef.current = el.scrollTop
63
+
64
+ if (overflowRef.current != null) {
65
+ initialOverflowRef.current = { ...overflowRef.current }
66
+ }
67
+ })
68
+
69
+ return () => {
70
+ prevScrollTopRef.current = null
71
+ initialOverflowRef.current = null
72
+ el.removeEventListener('wheel', onWheel)
73
+ }
74
+ }
75
+ }, [enabled, open, elements.floating, overflowRef, scrollRef, onChange])
76
+
77
+ const floating: ElementProps['floating'] = React.useMemo(
78
+ () => ({
79
+ onKeyDown() {
80
+ controlledScrollingRef.current = true
81
+ },
82
+ onWheel() {
83
+ controlledScrollingRef.current = false
84
+ },
85
+ onPointerMove() {
86
+ controlledScrollingRef.current = false
87
+ },
88
+ onScroll() {
89
+ const el = scrollRef?.current || elements.floating
90
+
91
+ if (!overflowRef.current || !el || !controlledScrollingRef.current) {
92
+ return
93
+ }
94
+
95
+ if (prevScrollTopRef.current !== null) {
96
+ const scrollDiff = el.scrollTop - prevScrollTopRef.current
97
+
98
+ if (
99
+ (overflowRef.current.bottom < -0.5 && scrollDiff < -1) ||
100
+ (overflowRef.current.top < -0.5 && scrollDiff > 1)
101
+ ) {
102
+ ReactDOM.flushSync(() => onChange((d: number) => d + scrollDiff))
103
+ }
104
+ }
105
+
106
+ // [firefox] wait for the height change to have been applied
107
+ requestAnimationFrame(() => {
108
+ prevScrollTopRef.current = el!.scrollTop
109
+ })
110
+ },
111
+ }),
112
+ [elements.floating, onChange, overflowRef, scrollRef]
113
+ )
114
+
115
+ return React.useMemo(() => (enabled ? { floating } : {}), [enabled, floating])
116
+ }
@@ -0,0 +1,101 @@
1
+ import type { HTMLProps } from 'react'
2
+ import type { ElementProps } from './types'
3
+
4
+ // merges prop getters from multiple interaction hooks.
5
+ // event handlers are chained (all run), first non-undefined return value wins.
6
+ // non-function props: user props override hook props.
7
+ export function useInteractions(propsList: Array<ElementProps | void>) {
8
+ const filtered = propsList.filter(Boolean) as ElementProps[]
9
+
10
+ // collect all event handlers by event name for each element type
11
+ const referenceFns = new Map<string, Array<(...args: any[]) => any>>()
12
+ const floatingFns = new Map<string, Array<(...args: any[]) => any>>()
13
+ const itemFns = new Map<string, Array<(...args: any[]) => any>>()
14
+
15
+ const referenceStatic: Record<string, any> = {}
16
+ const floatingStatic: Record<string, any> = {}
17
+
18
+ for (const props of filtered) {
19
+ if (props.reference) {
20
+ collectProps(props.reference as any, referenceFns, referenceStatic)
21
+ }
22
+ if (props.floating) {
23
+ collectProps(props.floating as any, floatingFns, floatingStatic)
24
+ }
25
+ if (props.item && typeof props.item === 'object') {
26
+ collectProps(props.item as any, itemFns, {})
27
+ }
28
+ }
29
+
30
+ return {
31
+ getReferenceProps(userProps?: HTMLProps<Element>) {
32
+ return buildProps(referenceFns, referenceStatic, userProps)
33
+ },
34
+ getFloatingProps(userProps?: HTMLProps<HTMLElement>) {
35
+ return buildProps(floatingFns, floatingStatic, userProps)
36
+ },
37
+ getItemProps(userProps?: HTMLProps<HTMLElement>) {
38
+ return buildProps(itemFns, {}, userProps)
39
+ },
40
+ }
41
+ }
42
+
43
+ function collectProps(
44
+ props: Record<string, any>,
45
+ fnMap: Map<string, Array<(...args: any[]) => any>>,
46
+ staticMap: Record<string, any>
47
+ ) {
48
+ for (const key of Object.keys(props)) {
49
+ if (typeof props[key] === 'function') {
50
+ let arr = fnMap.get(key)
51
+ if (!arr) {
52
+ arr = []
53
+ fnMap.set(key, arr)
54
+ }
55
+ arr.push(props[key])
56
+ } else {
57
+ staticMap[key] = props[key]
58
+ }
59
+ }
60
+ }
61
+
62
+ function buildProps(
63
+ fnMap: Map<string, Array<(...args: any[]) => any>>,
64
+ staticProps: Record<string, any>,
65
+ userProps?: Record<string, any>
66
+ ): Record<string, any> {
67
+ // hook static props first, then user props override
68
+ const result: Record<string, any> = { ...staticProps }
69
+
70
+ // merge event handlers from hooks
71
+ for (const [key, fns] of fnMap) {
72
+ const hookHandler = (...args: any[]) => {
73
+ for (const fn of fns) {
74
+ const result = fn(...args)
75
+ if (result !== undefined) return result
76
+ }
77
+ }
78
+
79
+ result[key] = hookHandler
80
+ }
81
+
82
+ // user props override everything — but chain event handlers
83
+ if (userProps) {
84
+ for (const key of Object.keys(userProps)) {
85
+ if (key === 'style') {
86
+ result.style = { ...result.style, ...userProps.style }
87
+ } else if (typeof userProps[key] === 'function' && result[key]) {
88
+ const hookFn = result[key]
89
+ const userFn = userProps[key]
90
+ result[key] = (...args: any[]) => {
91
+ userFn(...args)
92
+ hookFn(...args)
93
+ }
94
+ } else {
95
+ result[key] = userProps[key]
96
+ }
97
+ }
98
+ }
99
+
100
+ return result
101
+ }