@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,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
|
+
}
|