@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.
- package/dist/cjs/Floating.cjs +7 -5
- package/dist/cjs/Floating.native.js +19 -13
- package/dist/cjs/Floating.native.js.map +1 -1
- package/dist/cjs/index.cjs +46 -13
- package/dist/cjs/index.native.js +46 -13
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/interactions/PopupTriggerMap.cjs +49 -0
- package/dist/cjs/interactions/PopupTriggerMap.native.js +97 -0
- package/dist/cjs/interactions/PopupTriggerMap.native.js.map +1 -0
- package/dist/cjs/interactions/createFloatingEvents.cjs +50 -0
- package/dist/cjs/interactions/createFloatingEvents.native.js +56 -0
- package/dist/cjs/interactions/createFloatingEvents.native.js.map +1 -0
- package/dist/cjs/interactions/safePolygon.cjs +273 -0
- package/dist/cjs/interactions/safePolygon.native.js +284 -0
- package/dist/cjs/interactions/safePolygon.native.js.map +1 -0
- package/dist/cjs/interactions/types.cjs +18 -0
- package/dist/cjs/interactions/types.native.js +21 -0
- package/dist/cjs/interactions/types.native.js.map +1 -0
- package/dist/cjs/interactions/useClick.cjs +124 -0
- package/dist/cjs/interactions/useClick.native.js +132 -0
- package/dist/cjs/interactions/useClick.native.js.map +1 -0
- package/dist/cjs/interactions/useDelayGroup.cjs +115 -0
- package/dist/cjs/interactions/useDelayGroup.native.js +125 -0
- package/dist/cjs/interactions/useDelayGroup.native.js.map +1 -0
- package/dist/cjs/interactions/useFocus.cjs +130 -0
- package/dist/cjs/interactions/useFocus.native.js +139 -0
- package/dist/cjs/interactions/useFocus.native.js.map +1 -0
- package/dist/cjs/interactions/useHover.cjs +357 -0
- package/dist/cjs/interactions/useHover.native.js +373 -0
- package/dist/cjs/interactions/useHover.native.js.map +1 -0
- package/dist/cjs/interactions/useInnerOffset.cjs +128 -0
- package/dist/cjs/interactions/useInnerOffset.native.js +141 -0
- package/dist/cjs/interactions/useInnerOffset.native.js.map +1 -0
- package/dist/cjs/interactions/useInteractions.cjs +105 -0
- package/dist/cjs/interactions/useInteractions.native.js +216 -0
- package/dist/cjs/interactions/useInteractions.native.js.map +1 -0
- package/dist/cjs/interactions/useListNavigation.cjs +418 -0
- package/dist/cjs/interactions/useListNavigation.native.js +433 -0
- package/dist/cjs/interactions/useListNavigation.native.js.map +1 -0
- package/dist/cjs/interactions/useRole.cjs +122 -0
- package/dist/cjs/interactions/useRole.native.js +136 -0
- package/dist/cjs/interactions/useRole.native.js.map +1 -0
- package/dist/cjs/interactions/useTypeahead.cjs +143 -0
- package/dist/cjs/interactions/useTypeahead.native.js +159 -0
- package/dist/cjs/interactions/useTypeahead.native.js.map +1 -0
- package/dist/cjs/interactions/utils.cjs +208 -0
- package/dist/cjs/interactions/utils.native.js +227 -0
- package/dist/cjs/interactions/utils.native.js.map +1 -0
- package/dist/cjs/middleware/inner.cjs +118 -0
- package/dist/cjs/middleware/inner.native.js +130 -0
- package/dist/cjs/middleware/inner.native.js.map +1 -0
- package/dist/cjs/useFloating.cjs +35 -28
- package/dist/cjs/useFloating.native.js +51 -47
- package/dist/cjs/useFloating.native.js.map +1 -1
- package/dist/esm/Floating.native.js +6 -3
- package/dist/esm/Floating.native.js.map +1 -1
- package/dist/esm/index.js +17 -34
- package/dist/esm/index.js.map +1 -6
- package/dist/esm/index.mjs +16 -2
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +16 -2
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/interactions/PopupTriggerMap.mjs +24 -0
- package/dist/esm/interactions/PopupTriggerMap.mjs.map +1 -0
- package/dist/esm/interactions/PopupTriggerMap.native.js +69 -0
- package/dist/esm/interactions/PopupTriggerMap.native.js.map +1 -0
- package/dist/esm/interactions/createFloatingEvents.mjs +25 -0
- package/dist/esm/interactions/createFloatingEvents.mjs.map +1 -0
- package/dist/esm/interactions/createFloatingEvents.native.js +28 -0
- package/dist/esm/interactions/createFloatingEvents.native.js.map +1 -0
- package/dist/esm/interactions/safePolygon.mjs +248 -0
- package/dist/esm/interactions/safePolygon.mjs.map +1 -0
- package/dist/esm/interactions/safePolygon.native.js +256 -0
- package/dist/esm/interactions/safePolygon.native.js.map +1 -0
- package/dist/esm/interactions/types.mjs +2 -0
- package/dist/esm/interactions/types.mjs.map +1 -0
- package/dist/esm/interactions/types.native.js +2 -0
- package/dist/esm/interactions/types.native.js.map +1 -0
- package/dist/esm/interactions/useClick.mjs +99 -0
- package/dist/esm/interactions/useClick.mjs.map +1 -0
- package/dist/esm/interactions/useClick.native.js +104 -0
- package/dist/esm/interactions/useClick.native.js.map +1 -0
- package/dist/esm/interactions/useDelayGroup.mjs +77 -0
- package/dist/esm/interactions/useDelayGroup.mjs.map +1 -0
- package/dist/esm/interactions/useDelayGroup.native.js +84 -0
- package/dist/esm/interactions/useDelayGroup.native.js.map +1 -0
- package/dist/esm/interactions/useFocus.mjs +105 -0
- package/dist/esm/interactions/useFocus.mjs.map +1 -0
- package/dist/esm/interactions/useFocus.native.js +111 -0
- package/dist/esm/interactions/useFocus.native.js.map +1 -0
- package/dist/esm/interactions/useHover.mjs +320 -0
- package/dist/esm/interactions/useHover.mjs.map +1 -0
- package/dist/esm/interactions/useHover.native.js +333 -0
- package/dist/esm/interactions/useHover.native.js.map +1 -0
- package/dist/esm/interactions/useInnerOffset.mjs +92 -0
- package/dist/esm/interactions/useInnerOffset.mjs.map +1 -0
- package/dist/esm/interactions/useInnerOffset.native.js +102 -0
- package/dist/esm/interactions/useInnerOffset.native.js.map +1 -0
- package/dist/esm/interactions/useInteractions.mjs +80 -0
- package/dist/esm/interactions/useInteractions.mjs.map +1 -0
- package/dist/esm/interactions/useInteractions.native.js +188 -0
- package/dist/esm/interactions/useInteractions.native.js.map +1 -0
- package/dist/esm/interactions/useListNavigation.mjs +393 -0
- package/dist/esm/interactions/useListNavigation.mjs.map +1 -0
- package/dist/esm/interactions/useListNavigation.native.js +405 -0
- package/dist/esm/interactions/useListNavigation.native.js.map +1 -0
- package/dist/esm/interactions/useRole.mjs +86 -0
- package/dist/esm/interactions/useRole.mjs.map +1 -0
- package/dist/esm/interactions/useRole.native.js +97 -0
- package/dist/esm/interactions/useRole.native.js.map +1 -0
- package/dist/esm/interactions/useTypeahead.mjs +118 -0
- package/dist/esm/interactions/useTypeahead.mjs.map +1 -0
- package/dist/esm/interactions/useTypeahead.native.js +131 -0
- package/dist/esm/interactions/useTypeahead.native.js.map +1 -0
- package/dist/esm/interactions/utils.mjs +162 -0
- package/dist/esm/interactions/utils.mjs.map +1 -0
- package/dist/esm/interactions/utils.native.js +178 -0
- package/dist/esm/interactions/utils.native.js.map +1 -0
- package/dist/esm/middleware/inner.mjs +82 -0
- package/dist/esm/middleware/inner.mjs.map +1 -0
- package/dist/esm/middleware/inner.native.js +91 -0
- package/dist/esm/middleware/inner.native.js.map +1 -0
- package/dist/esm/useFloating.mjs +8 -3
- package/dist/esm/useFloating.mjs.map +1 -1
- package/dist/esm/useFloating.native.js +25 -23
- package/dist/esm/useFloating.native.js.map +1 -1
- package/package.json +8 -10
- package/src/Floating.native.tsx +1 -0
- package/src/index.ts +49 -0
- package/src/interactions/PopupTriggerMap.ts +30 -0
- package/src/interactions/createFloatingEvents.ts +34 -0
- package/src/interactions/safePolygon.ts +500 -0
- package/src/interactions/types.ts +165 -0
- package/src/interactions/useClick.ts +148 -0
- package/src/interactions/useDelayGroup.ts +114 -0
- package/src/interactions/useFocus.ts +164 -0
- package/src/interactions/useHover.ts +453 -0
- package/src/interactions/useInnerOffset.ts +116 -0
- package/src/interactions/useInteractions.ts +101 -0
- package/src/interactions/useListNavigation.ts +578 -0
- package/src/interactions/useRole.ts +103 -0
- package/src/interactions/useTypeahead.ts +173 -0
- package/src/interactions/utils.ts +234 -0
- package/src/middleware/inner.ts +141 -0
- package/src/useFloating.tsx +13 -1
- package/types/Floating.native.d.ts +1 -0
- package/types/Floating.native.d.ts.map +1 -1
- package/types/index.d.ts +17 -2
- package/types/index.d.ts.map +1 -1
- package/types/interactions/PopupTriggerMap.d.ts +8 -0
- package/types/interactions/PopupTriggerMap.d.ts.map +1 -0
- package/types/interactions/createFloatingEvents.d.ts +7 -0
- package/types/interactions/createFloatingEvents.d.ts.map +1 -0
- package/types/interactions/safePolygon.d.ts +4 -0
- package/types/interactions/safePolygon.d.ts.map +1 -0
- package/types/interactions/types.d.ts +123 -0
- package/types/interactions/types.d.ts.map +1 -0
- package/types/interactions/useClick.d.ts +3 -0
- package/types/interactions/useClick.d.ts.map +1 -0
- package/types/interactions/useDelayGroup.d.ts +23 -0
- package/types/interactions/useDelayGroup.d.ts.map +1 -0
- package/types/interactions/useFocus.d.ts +3 -0
- package/types/interactions/useFocus.d.ts.map +1 -0
- package/types/interactions/useHover.d.ts +6 -0
- package/types/interactions/useHover.d.ts.map +1 -0
- package/types/interactions/useInnerOffset.d.ts +3 -0
- package/types/interactions/useInnerOffset.d.ts.map +1 -0
- package/types/interactions/useInteractions.d.ts +8 -0
- package/types/interactions/useInteractions.d.ts.map +1 -0
- package/types/interactions/useListNavigation.d.ts +3 -0
- package/types/interactions/useListNavigation.d.ts.map +1 -0
- package/types/interactions/useRole.d.ts +3 -0
- package/types/interactions/useRole.d.ts.map +1 -0
- package/types/interactions/useTypeahead.d.ts +3 -0
- package/types/interactions/useTypeahead.d.ts.map +1 -0
- package/types/interactions/utils.d.ts +46 -0
- package/types/interactions/utils.d.ts.map +1 -0
- package/types/middleware/inner.d.ts +14 -0
- package/types/middleware/inner.d.ts.map +1 -0
- package/types/useFloating.d.ts +7 -1
- package/types/useFloating.d.ts.map +1 -1
- package/dist/cjs/Floating.js +0 -15
- package/dist/cjs/Floating.js.map +0 -6
- package/dist/cjs/index.js +0 -34
- package/dist/cjs/index.js.map +0 -6
- package/dist/cjs/useFloating.js +0 -46
- package/dist/cjs/useFloating.js.map +0 -6
- package/dist/esm/Floating.js +0 -2
- package/dist/esm/Floating.js.map +0 -6
- package/dist/esm/useFloating.js +0 -23
- 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
|
+
}
|