@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,500 @@
1
+ import type { HandleCloseFn, SafePolygonOptions } from './types'
2
+ import { clearTimeoutIfSet, contains, getTarget } from './utils'
3
+
4
+ type Point = [number, number]
5
+ type Polygon = Point[]
6
+ type Side = 'top' | 'bottom' | 'left' | 'right'
7
+ type Rect = { x: number; y: number; width: number; height: number }
8
+
9
+ function isPointInPolygon(point: Point, polygon: Polygon) {
10
+ const [x, y] = point
11
+ let isInside = false
12
+ const length = polygon.length
13
+ for (let i = 0, j = length - 1; i < length; j = i++) {
14
+ const [xi, yi] = polygon[i] || [0, 0]
15
+ const [xj, yj] = polygon[j] || [0, 0]
16
+ const intersect = yi >= y !== yj >= y && x <= ((xj - xi) * (y - yi)) / (yj - yi) + xi
17
+ if (intersect) {
18
+ isInside = !isInside
19
+ }
20
+ }
21
+ return isInside
22
+ }
23
+
24
+ function isInside(point: Point, rect: Rect) {
25
+ return (
26
+ point[0] >= rect.x &&
27
+ point[0] <= rect.x + rect.width &&
28
+ point[1] >= rect.y &&
29
+ point[1] <= rect.y + rect.height
30
+ )
31
+ }
32
+
33
+ export type { SafePolygonOptions }
34
+
35
+ // generates a safe polygon area that the user can traverse without closing the
36
+ // floating element once leaving the reference element.
37
+ // ported from @floating-ui/react with full polygon geometry.
38
+ //
39
+ // the returned HandleCloseFn is a closure factory: called once on mouseleave
40
+ // with the leave position (x, y), it returns a handler that runs on each
41
+ // subsequent document mousemove. the original leave position is baked into
42
+ // the closure so the polygon anchor stays fixed.
43
+ //
44
+ // unlike @floating-ui/react, we do NOT add a documentElement mouseleave
45
+ // listener, which fixes the window-blur-closing-popover bug.
46
+ // debug overlay — renders polygon + trough as SVG on top of everything
47
+ let debugSvg: SVGSVGElement | null = null
48
+ function debugDrawPolygon(
49
+ polygon: Point[],
50
+ trough: Point[],
51
+ cursor: Point,
52
+ anchor: Point
53
+ ) {
54
+ if (!debugSvg) {
55
+ debugSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
56
+ debugSvg.id = '__safe-polygon-debug'
57
+ Object.assign(debugSvg.style, {
58
+ position: 'fixed',
59
+ inset: '0',
60
+ width: '100vw',
61
+ height: '100vh',
62
+ pointerEvents: 'none',
63
+ zIndex: '999999',
64
+ })
65
+ document.body.appendChild(debugSvg)
66
+ }
67
+ debugSvg.innerHTML = ''
68
+
69
+ // trough rectangle (blue)
70
+ if (trough.length) {
71
+ const troughEl = document.createElementNS('http://www.w3.org/2000/svg', 'polygon')
72
+ troughEl.setAttribute('points', trough.map((p) => p.join(',')).join(' '))
73
+ troughEl.setAttribute('fill', 'rgba(0,100,255,0.15)')
74
+ troughEl.setAttribute('stroke', 'rgba(0,100,255,0.6)')
75
+ troughEl.setAttribute('stroke-width', '1')
76
+ debugSvg.appendChild(troughEl)
77
+ }
78
+
79
+ // safe polygon (red)
80
+ if (polygon.length) {
81
+ const polyEl = document.createElementNS('http://www.w3.org/2000/svg', 'polygon')
82
+ polyEl.setAttribute('points', polygon.map((p) => p.join(',')).join(' '))
83
+ polyEl.setAttribute('fill', 'rgba(255,50,50,0.2)')
84
+ polyEl.setAttribute('stroke', 'rgba(255,50,50,0.8)')
85
+ polyEl.setAttribute('stroke-width', '1.5')
86
+ debugSvg.appendChild(polyEl)
87
+ }
88
+
89
+ // anchor point (green circle)
90
+ const anchorCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle')
91
+ anchorCircle.setAttribute('cx', String(anchor[0]))
92
+ anchorCircle.setAttribute('cy', String(anchor[1]))
93
+ anchorCircle.setAttribute('r', '5')
94
+ anchorCircle.setAttribute('fill', 'lime')
95
+ anchorCircle.setAttribute('stroke', 'darkgreen')
96
+ anchorCircle.setAttribute('stroke-width', '1.5')
97
+ debugSvg.appendChild(anchorCircle)
98
+
99
+ // cursor point (yellow circle)
100
+ const cursorCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle')
101
+ cursorCircle.setAttribute('cx', String(cursor[0]))
102
+ cursorCircle.setAttribute('cy', String(cursor[1]))
103
+ cursorCircle.setAttribute('r', '4')
104
+ cursorCircle.setAttribute('fill', 'yellow')
105
+ cursorCircle.setAttribute('stroke', 'orange')
106
+ cursorCircle.setAttribute('stroke-width', '1.5')
107
+ debugSvg.appendChild(cursorCircle)
108
+ }
109
+
110
+ function debugClear() {
111
+ if (debugSvg) {
112
+ debugSvg.remove()
113
+ debugSvg = null
114
+ }
115
+ }
116
+
117
+ export function safePolygon(options: SafePolygonOptions = {}): HandleCloseFn {
118
+ const {
119
+ buffer = 0.5,
120
+ blockPointerEvents = false,
121
+ requireIntent = true,
122
+ __debug = false,
123
+ } = options
124
+
125
+ const timeoutRef = { current: -1 }
126
+
127
+ let hasLanded = false
128
+ let lastX: number | null = null
129
+ let lastY: number | null = null
130
+ let lastCursorTime = typeof performance !== 'undefined' ? performance.now() : 0
131
+
132
+ function getCursorSpeed(x: number, y: number): number | null {
133
+ const currentTime = performance.now()
134
+ const elapsedTime = currentTime - lastCursorTime
135
+
136
+ if (lastX === null || lastY === null || elapsedTime === 0) {
137
+ lastX = x
138
+ lastY = y
139
+ lastCursorTime = currentTime
140
+ return null
141
+ }
142
+
143
+ const deltaX = x - lastX
144
+ const deltaY = y - lastY
145
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
146
+ const speed = distance / elapsedTime
147
+
148
+ lastX = x
149
+ lastY = y
150
+ lastCursorTime = currentTime
151
+
152
+ return speed
153
+ }
154
+
155
+ // called once on mouseleave. x, y = cursor position when leaving.
156
+ // returns the mousemove handler that checks each subsequent position
157
+ // against the polygon anchored at (x, y).
158
+ const fn: HandleCloseFn = ({ x, y, placement, elements, onClose }) => {
159
+ // reset on each new handler creation — each leave starts a fresh session
160
+ hasLanded = false
161
+ lastX = null
162
+ lastY = null
163
+
164
+ return function onMouseMove(event: MouseEvent) {
165
+ function close() {
166
+ clearTimeoutIfSet(timeoutRef)
167
+ onClose()
168
+ }
169
+
170
+ clearTimeoutIfSet(timeoutRef)
171
+
172
+ const domReference = elements.domReference ?? elements.reference
173
+
174
+ if (
175
+ !domReference ||
176
+ !elements.floating ||
177
+ placement == null ||
178
+ x == null ||
179
+ y == null
180
+ ) {
181
+ return
182
+ }
183
+
184
+ const { clientX, clientY } = event
185
+ const clientPoint: Point = [clientX, clientY]
186
+ const target = getTarget(event) as Element | null
187
+ const isLeave = event.type === 'mouseleave'
188
+ const isOverFloatingEl = contains(elements.floating, target)
189
+ const isOverReferenceEl = contains(domReference, target)
190
+ const refRect = domReference.getBoundingClientRect()
191
+ const rect = elements.floating.getBoundingClientRect()
192
+ const side = placement.split('-')[0] as Side
193
+
194
+ // x, y are from the closure — the position when cursor LEFT the reference.
195
+ // cursorLeaveFromRight/Bottom determine which side of the floating el
196
+ // the leave point is on, to shape the polygon correctly.
197
+ const cursorLeaveFromRight = x > rect.right - rect.width / 2
198
+ const cursorLeaveFromBottom = y > rect.bottom - rect.height / 2
199
+ const isOverReferenceRect = isInside(clientPoint, refRect)
200
+ const isFloatingWider = rect.width > refRect.width
201
+ const isFloatingTaller = rect.height > refRect.height
202
+ const left = (isFloatingWider ? refRect : rect).left
203
+ const right = (isFloatingWider ? refRect : rect).right
204
+ const top = (isFloatingTaller ? refRect : rect).top
205
+ const bottom = (isFloatingTaller ? refRect : rect).bottom
206
+
207
+ if (isOverFloatingEl) {
208
+ hasLanded = true
209
+
210
+ if (!isLeave) {
211
+ return
212
+ }
213
+ }
214
+
215
+ if (isOverReferenceEl) {
216
+ hasLanded = false
217
+ }
218
+
219
+ if (isOverReferenceEl && !isLeave) {
220
+ hasLanded = true
221
+ return
222
+ }
223
+
224
+ // cursor in reference bounding box but outside DOM element (rounded corners)
225
+ if (!isOverReferenceEl && isOverReferenceRect && !isLeave) {
226
+ return
227
+ }
228
+
229
+ // prevent overlapping floating element from being stuck in an open-close
230
+ // loop: https://github.com/floating-ui/floating-ui/issues/1910
231
+ if (
232
+ isLeave &&
233
+ event.relatedTarget &&
234
+ contains(elements.floating, event.relatedTarget as Element)
235
+ ) {
236
+ return
237
+ }
238
+
239
+ // if the pointer is leaving from the opposite side, the "buffer" logic
240
+ // creates a point where the floating element remains open, but should be
241
+ // ignored. a constant of 1 handles floating point rounding errors.
242
+ if (
243
+ (side === 'top' && y >= refRect.bottom - 1) ||
244
+ (side === 'bottom' && y <= refRect.top + 1) ||
245
+ (side === 'left' && x >= refRect.right - 1) ||
246
+ (side === 'right' && x <= refRect.left + 1)
247
+ ) {
248
+ return close()
249
+ }
250
+
251
+ // ignore when the cursor is within the rectangular trough between the
252
+ // two elements. since the polygon is created from the cursor point,
253
+ // which can start beyond the ref element's edge, traversing back and
254
+ // forth from the ref to the floating element can cause it to close. this
255
+ // ensures it always remains open in that case.
256
+ let rectPoly: Point[] = []
257
+
258
+ switch (side) {
259
+ case 'top':
260
+ rectPoly = [
261
+ [left, refRect.top + 1],
262
+ [left, rect.bottom - 1],
263
+ [right, rect.bottom - 1],
264
+ [right, refRect.top + 1],
265
+ ]
266
+ break
267
+ case 'bottom':
268
+ rectPoly = [
269
+ [left, rect.top + 1],
270
+ [left, refRect.bottom - 1],
271
+ [right, refRect.bottom - 1],
272
+ [right, rect.top + 1],
273
+ ]
274
+ break
275
+ case 'left':
276
+ rectPoly = [
277
+ [rect.right - 1, bottom],
278
+ [rect.right - 1, top],
279
+ [refRect.left + 1, top],
280
+ [refRect.left + 1, bottom],
281
+ ]
282
+ break
283
+ case 'right':
284
+ rectPoly = [
285
+ [refRect.right - 1, bottom],
286
+ [refRect.right - 1, top],
287
+ [rect.left + 1, top],
288
+ [rect.left + 1, bottom],
289
+ ]
290
+ break
291
+ }
292
+
293
+ // getPolygon uses the closure's (x, y) — the LEAVE position — as the
294
+ // polygon anchor point, NOT the current cursor position. this creates
295
+ // a stable triangular/trapezoidal safe zone from the leave point toward
296
+ // the floating element's edges.
297
+ function getPolygon([x, y]: Point): Array<Point> {
298
+ switch (side) {
299
+ case 'top': {
300
+ const cursorPointOne: Point = [
301
+ isFloatingWider
302
+ ? x + buffer / 2
303
+ : cursorLeaveFromRight
304
+ ? x + buffer * 4
305
+ : x - buffer * 4,
306
+ y + buffer + 1,
307
+ ]
308
+ const cursorPointTwo: Point = [
309
+ isFloatingWider
310
+ ? x - buffer / 2
311
+ : cursorLeaveFromRight
312
+ ? x + buffer * 4
313
+ : x - buffer * 4,
314
+ y + buffer + 1,
315
+ ]
316
+ const commonPoints: [Point, Point] = [
317
+ [
318
+ rect.left,
319
+ cursorLeaveFromRight
320
+ ? rect.bottom - buffer
321
+ : isFloatingWider
322
+ ? rect.bottom - buffer
323
+ : rect.top,
324
+ ],
325
+ [
326
+ rect.right,
327
+ cursorLeaveFromRight
328
+ ? isFloatingWider
329
+ ? rect.bottom - buffer
330
+ : rect.top
331
+ : rect.bottom - buffer,
332
+ ],
333
+ ]
334
+
335
+ return [cursorPointOne, cursorPointTwo, ...commonPoints]
336
+ }
337
+ case 'bottom': {
338
+ const cursorPointOne: Point = [
339
+ isFloatingWider
340
+ ? x + buffer / 2
341
+ : cursorLeaveFromRight
342
+ ? x + buffer * 4
343
+ : x - buffer * 4,
344
+ y - buffer,
345
+ ]
346
+ const cursorPointTwo: Point = [
347
+ isFloatingWider
348
+ ? x - buffer / 2
349
+ : cursorLeaveFromRight
350
+ ? x + buffer * 4
351
+ : x - buffer * 4,
352
+ y - buffer,
353
+ ]
354
+ const commonPoints: [Point, Point] = [
355
+ [
356
+ rect.left,
357
+ cursorLeaveFromRight
358
+ ? rect.top + buffer
359
+ : isFloatingWider
360
+ ? rect.top + buffer
361
+ : rect.bottom,
362
+ ],
363
+ [
364
+ rect.right,
365
+ cursorLeaveFromRight
366
+ ? isFloatingWider
367
+ ? rect.top + buffer
368
+ : rect.bottom
369
+ : rect.top + buffer,
370
+ ],
371
+ ]
372
+
373
+ return [cursorPointOne, cursorPointTwo, ...commonPoints]
374
+ }
375
+ case 'left': {
376
+ const cursorPointOne: Point = [
377
+ x + buffer + 1,
378
+ isFloatingTaller
379
+ ? y + buffer / 2
380
+ : cursorLeaveFromBottom
381
+ ? y + buffer * 4
382
+ : y - buffer * 4,
383
+ ]
384
+ const cursorPointTwo: Point = [
385
+ x + buffer + 1,
386
+ isFloatingTaller
387
+ ? y - buffer / 2
388
+ : cursorLeaveFromBottom
389
+ ? y + buffer * 4
390
+ : y - buffer * 4,
391
+ ]
392
+ const commonPoints: [Point, Point] = [
393
+ [
394
+ cursorLeaveFromBottom
395
+ ? rect.right - buffer
396
+ : isFloatingTaller
397
+ ? rect.right - buffer
398
+ : rect.left,
399
+ rect.top,
400
+ ],
401
+ [
402
+ cursorLeaveFromBottom
403
+ ? isFloatingTaller
404
+ ? rect.right - buffer
405
+ : rect.left
406
+ : rect.right - buffer,
407
+ rect.bottom,
408
+ ],
409
+ ]
410
+
411
+ return [...commonPoints, cursorPointOne, cursorPointTwo]
412
+ }
413
+ case 'right': {
414
+ const cursorPointOne: Point = [
415
+ x - buffer,
416
+ isFloatingTaller
417
+ ? y + buffer / 2
418
+ : cursorLeaveFromBottom
419
+ ? y + buffer * 4
420
+ : y - buffer * 4,
421
+ ]
422
+ const cursorPointTwo: Point = [
423
+ x - buffer,
424
+ isFloatingTaller
425
+ ? y - buffer / 2
426
+ : cursorLeaveFromBottom
427
+ ? y + buffer * 4
428
+ : y - buffer * 4,
429
+ ]
430
+ const commonPoints: [Point, Point] = [
431
+ [
432
+ cursorLeaveFromBottom
433
+ ? rect.left + buffer
434
+ : isFloatingTaller
435
+ ? rect.left + buffer
436
+ : rect.right,
437
+ rect.top,
438
+ ],
439
+ [
440
+ cursorLeaveFromBottom
441
+ ? isFloatingTaller
442
+ ? rect.left + buffer
443
+ : rect.right
444
+ : rect.left + buffer,
445
+ rect.bottom,
446
+ ],
447
+ ]
448
+
449
+ return [cursorPointOne, cursorPointTwo, ...commonPoints]
450
+ }
451
+ }
452
+ }
453
+
454
+ const poly = getPolygon([x, y])
455
+
456
+ if (__debug) {
457
+ debugDrawPolygon(poly, rectPoly, clientPoint, [x, y])
458
+ }
459
+
460
+ if (isPointInPolygon([clientX, clientY], rectPoly)) {
461
+ return
462
+ }
463
+
464
+ if (hasLanded && !isOverReferenceRect) {
465
+ if (__debug) debugClear()
466
+ return close()
467
+ }
468
+
469
+ // polygon check first — inside polygon = safe.
470
+ // the polygon geometry itself limits the safe zone to valid cursor
471
+ // paths toward the floating element, so no timeout is needed here.
472
+ // a previous 40ms requireIntent timeout caused premature closures
473
+ // during natural mouse pauses (humans routinely pause >40ms while
474
+ // moving diagonally from trigger to content).
475
+ if (isPointInPolygon([clientX, clientY], poly)) {
476
+ return
477
+ }
478
+
479
+ // speed check only applies outside polygon
480
+ if (!isLeave && requireIntent) {
481
+ const cursorSpeed = getCursorSpeed(clientX, clientY)
482
+ const cursorSpeedThreshold = 0.1
483
+ if (cursorSpeed !== null && cursorSpeed < cursorSpeedThreshold) {
484
+ if (__debug) debugClear()
485
+ return close()
486
+ }
487
+ }
488
+
489
+ // outside polygon — close
490
+ if (__debug) debugClear()
491
+ close()
492
+ }
493
+ }
494
+
495
+ fn.__options = {
496
+ blockPointerEvents,
497
+ }
498
+
499
+ return fn
500
+ }
@@ -0,0 +1,165 @@
1
+ import type { HTMLProps, RefObject } from 'react'
2
+ import type { FloatingEvents } from './createFloatingEvents'
3
+ import type { PopupTriggerMap } from './PopupTriggerMap'
4
+
5
+ export type { FloatingEvents }
6
+
7
+ export type ElementProps = {
8
+ reference?: HTMLProps<Element>
9
+ floating?: HTMLProps<HTMLElement>
10
+ item?:
11
+ | HTMLProps<HTMLElement>
12
+ | ((props: { active?: boolean; selected?: boolean }) => HTMLProps<HTMLElement>)
13
+ }
14
+
15
+ export type OpenChangeReason =
16
+ | 'hover'
17
+ | 'focus'
18
+ | 'click'
19
+ | 'dismiss'
20
+ | 'list-navigation'
21
+ | 'escape-key'
22
+ | 'reference-press'
23
+ | 'safe-polygon'
24
+
25
+ export interface FloatingInteractionContext {
26
+ open: boolean
27
+ onOpenChange: (open: boolean, event?: Event, reason?: OpenChangeReason) => void
28
+ refs: {
29
+ reference: RefObject<Element | null>
30
+ floating: RefObject<HTMLElement | null>
31
+ domReference: RefObject<Element | null>
32
+ }
33
+ elements: {
34
+ reference: Element | null
35
+ floating: HTMLElement | null
36
+ domReference: Element | null
37
+ }
38
+ dataRef: RefObject<{
39
+ openEvent?: Event
40
+ // placement from the floating positioning — needed for safePolygon
41
+ placement?: string
42
+ // whether the user is currently typing (typeahead)
43
+ typing?: boolean
44
+ }>
45
+ events?: FloatingEvents
46
+ triggerElements?: PopupTriggerMap
47
+ // set by useHover when safePolygon's document mousemove handler is active.
48
+ // checked by onLeaveReference fallback timer to avoid racing safePolygon.
49
+ handleCloseActiveRef?: RefObject<boolean>
50
+ }
51
+
52
+ export type Delay =
53
+ | number
54
+ | Partial<{
55
+ open: number
56
+ close: number
57
+ }>
58
+
59
+ export interface UseHoverProps {
60
+ enabled?: boolean
61
+ delay?: Delay
62
+ restMs?: number
63
+ move?: boolean
64
+ handleClose?: HandleCloseFn | null
65
+ mouseOnly?: boolean
66
+ }
67
+
68
+ // called once on mouseleave with the leave position, returns a handler
69
+ // that runs on each subsequent document mousemove event. the returned
70
+ // handler has the original leave x/y baked into its closure so the
71
+ // polygon anchor stays fixed.
72
+ export type HandleCloseFn = {
73
+ (context: {
74
+ x: number
75
+ y: number
76
+ placement: string
77
+ elements: {
78
+ reference: Element
79
+ floating: HTMLElement
80
+ domReference: Element
81
+ }
82
+ onClose: () => void
83
+ tree?: any
84
+ leave?: boolean
85
+ }): (event: MouseEvent) => void
86
+ __options?: SafePolygonOptions
87
+ }
88
+
89
+ export interface SafePolygonOptions {
90
+ requireIntent?: boolean
91
+ buffer?: number
92
+ blockPointerEvents?: boolean
93
+ /** render the safe polygon on screen for debugging */
94
+ __debug?: boolean
95
+ }
96
+
97
+ export interface UseFocusProps {
98
+ enabled?: boolean
99
+ visibleOnly?: boolean
100
+ }
101
+
102
+ export interface UseRoleProps {
103
+ enabled?: boolean
104
+ role?:
105
+ | 'dialog'
106
+ | 'tooltip'
107
+ | 'alertdialog'
108
+ | 'menu'
109
+ | 'listbox'
110
+ | 'grid'
111
+ | 'tree'
112
+ | 'select'
113
+ | 'combobox'
114
+ | 'label'
115
+ }
116
+
117
+ export interface UseClickProps {
118
+ enabled?: boolean
119
+ event?: 'click' | 'mousedown'
120
+ toggle?: boolean
121
+ ignoreMouse?: boolean
122
+ keyboardHandlers?: boolean
123
+ stickIfOpen?: boolean
124
+ }
125
+
126
+ export interface UseListNavigationProps {
127
+ listRef: RefObject<Array<HTMLElement | null>>
128
+ activeIndex: number | null
129
+ selectedIndex?: number | null
130
+ onNavigate?: (index: number | null) => void
131
+ enabled?: boolean
132
+ loop?: boolean
133
+ nested?: boolean
134
+ rtl?: boolean
135
+ virtual?: boolean
136
+ focusItemOnOpen?: boolean | 'auto'
137
+ focusItemOnHover?: boolean
138
+ openOnArrowKeyDown?: boolean
139
+ scrollItemIntoView?: boolean | ScrollIntoViewOptions
140
+ allowEscape?: boolean
141
+ orientation?: 'vertical' | 'horizontal' | 'both'
142
+ disabledIndices?: Array<number> | ((index: number) => boolean)
143
+ cols?: number
144
+ }
145
+
146
+ export interface UseTypeaheadProps {
147
+ listRef: RefObject<Array<string | null>>
148
+ activeIndex: number | null
149
+ selectedIndex?: number | null
150
+ onMatch?: (index: number) => void
151
+ onTypingChange?: (isTyping: boolean) => void
152
+ enabled?: boolean
153
+ findMatch?:
154
+ | null
155
+ | ((list: Array<string | null>, typedString: string) => string | null | undefined)
156
+ resetMs?: number
157
+ ignoreKeys?: string[]
158
+ }
159
+
160
+ export interface UseInnerOffsetProps {
161
+ enabled?: boolean
162
+ onChange: (offset: number | ((prev: number) => number)) => void
163
+ overflowRef: RefObject<any>
164
+ scrollRef?: RefObject<HTMLElement | null>
165
+ }