@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,148 @@
|
|
|
1
|
+
import { useMemo, useRef } from 'react'
|
|
2
|
+
import { isHTMLElement, isMouseLikePointerType, isTypeableElement } from './utils'
|
|
3
|
+
import type { ElementProps, FloatingInteractionContext, UseClickProps } from './types'
|
|
4
|
+
|
|
5
|
+
function isButtonTarget(event: React.KeyboardEvent<Element>) {
|
|
6
|
+
return isHTMLElement(event.target) && event.target.tagName === 'BUTTON'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isAnchorTarget(event: React.KeyboardEvent<Element>) {
|
|
10
|
+
return isHTMLElement(event.target) && event.target.tagName === 'A'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isSpaceIgnored(element: Element | null) {
|
|
14
|
+
return isTypeableElement(element)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// click interaction: toggles open state on click/mousedown
|
|
18
|
+
export function useClick(
|
|
19
|
+
context: FloatingInteractionContext,
|
|
20
|
+
props: UseClickProps = {}
|
|
21
|
+
): ElementProps {
|
|
22
|
+
const {
|
|
23
|
+
open,
|
|
24
|
+
onOpenChange,
|
|
25
|
+
dataRef,
|
|
26
|
+
elements: { domReference },
|
|
27
|
+
} = context
|
|
28
|
+
const {
|
|
29
|
+
enabled = true,
|
|
30
|
+
event: eventOption = 'click',
|
|
31
|
+
toggle = true,
|
|
32
|
+
ignoreMouse = false,
|
|
33
|
+
keyboardHandlers = true,
|
|
34
|
+
stickIfOpen = true,
|
|
35
|
+
} = props
|
|
36
|
+
|
|
37
|
+
const pointerTypeRef = useRef<'mouse' | 'pen' | 'touch' | undefined>(undefined)
|
|
38
|
+
const didKeyDownRef = useRef(false)
|
|
39
|
+
|
|
40
|
+
const reference: ElementProps['reference'] = useMemo(
|
|
41
|
+
() => ({
|
|
42
|
+
onPointerDown(event: any) {
|
|
43
|
+
pointerTypeRef.current = event.pointerType
|
|
44
|
+
},
|
|
45
|
+
onMouseDown(event: any) {
|
|
46
|
+
const pointerType = pointerTypeRef.current
|
|
47
|
+
|
|
48
|
+
// ignore all buttons except for the "main" button
|
|
49
|
+
if (event.button !== 0) return
|
|
50
|
+
if (eventOption === 'click') return
|
|
51
|
+
if (isMouseLikePointerType(pointerType, true) && ignoreMouse) return
|
|
52
|
+
|
|
53
|
+
if (
|
|
54
|
+
open &&
|
|
55
|
+
toggle &&
|
|
56
|
+
(dataRef.current.openEvent && stickIfOpen
|
|
57
|
+
? dataRef.current.openEvent.type === 'mousedown'
|
|
58
|
+
: true)
|
|
59
|
+
) {
|
|
60
|
+
onOpenChange(false, event.nativeEvent || event, 'click')
|
|
61
|
+
} else {
|
|
62
|
+
// prevent stealing focus from the floating element
|
|
63
|
+
event.preventDefault()
|
|
64
|
+
onOpenChange(true, event.nativeEvent || event, 'click')
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
onClick(event: any) {
|
|
68
|
+
const pointerType = pointerTypeRef.current
|
|
69
|
+
|
|
70
|
+
if (eventOption === 'mousedown' && pointerTypeRef.current) {
|
|
71
|
+
pointerTypeRef.current = undefined
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (isMouseLikePointerType(pointerType, true) && ignoreMouse) return
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
open &&
|
|
79
|
+
toggle &&
|
|
80
|
+
(dataRef.current.openEvent && stickIfOpen
|
|
81
|
+
? dataRef.current.openEvent.type === 'click'
|
|
82
|
+
: true)
|
|
83
|
+
) {
|
|
84
|
+
onOpenChange(false, event.nativeEvent || event, 'click')
|
|
85
|
+
} else {
|
|
86
|
+
onOpenChange(true, event.nativeEvent || event, 'click')
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
onKeyDown(event: any) {
|
|
90
|
+
pointerTypeRef.current = undefined
|
|
91
|
+
|
|
92
|
+
if (event.defaultPrevented || !keyboardHandlers || isButtonTarget(event)) {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (event.key === ' ' && !isSpaceIgnored(domReference)) {
|
|
97
|
+
// prevent scrolling
|
|
98
|
+
event.preventDefault()
|
|
99
|
+
didKeyDownRef.current = true
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (isAnchorTarget(event)) {
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (event.key === 'Enter') {
|
|
107
|
+
if (open && toggle) {
|
|
108
|
+
onOpenChange(false, event.nativeEvent || event, 'click')
|
|
109
|
+
} else {
|
|
110
|
+
onOpenChange(true, event.nativeEvent || event, 'click')
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
onKeyUp(event: any) {
|
|
115
|
+
if (
|
|
116
|
+
event.defaultPrevented ||
|
|
117
|
+
!keyboardHandlers ||
|
|
118
|
+
isButtonTarget(event) ||
|
|
119
|
+
isSpaceIgnored(domReference)
|
|
120
|
+
) {
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (event.key === ' ' && didKeyDownRef.current) {
|
|
125
|
+
didKeyDownRef.current = false
|
|
126
|
+
if (open && toggle) {
|
|
127
|
+
onOpenChange(false, event.nativeEvent || event, 'click')
|
|
128
|
+
} else {
|
|
129
|
+
onOpenChange(true, event.nativeEvent || event, 'click')
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
[
|
|
135
|
+
dataRef,
|
|
136
|
+
domReference,
|
|
137
|
+
eventOption,
|
|
138
|
+
ignoreMouse,
|
|
139
|
+
keyboardHandlers,
|
|
140
|
+
onOpenChange,
|
|
141
|
+
open,
|
|
142
|
+
stickIfOpen,
|
|
143
|
+
toggle,
|
|
144
|
+
]
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return useMemo(() => (enabled ? { reference } : {}), [enabled, reference])
|
|
148
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import type { Delay, FloatingInteractionContext } from './types'
|
|
3
|
+
|
|
4
|
+
type DelayGroupContextValue = {
|
|
5
|
+
currentId: string | null | undefined
|
|
6
|
+
setCurrentId: (id: string | null | undefined) => void
|
|
7
|
+
delay: Delay
|
|
8
|
+
timeoutMs: number
|
|
9
|
+
initialDelay: Delay
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const DelayGroupContext = React.createContext<DelayGroupContextValue>({
|
|
13
|
+
currentId: null,
|
|
14
|
+
setCurrentId: () => {},
|
|
15
|
+
delay: 0,
|
|
16
|
+
timeoutMs: 0,
|
|
17
|
+
initialDelay: 0,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export function useDelayGroupContext() {
|
|
21
|
+
return React.useContext(DelayGroupContext)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// provider for coordinated tooltip delay.
|
|
25
|
+
// when one tooltip is already open (currentId is set), subsequent tooltips
|
|
26
|
+
// in the group open instantly instead of with the configured delay.
|
|
27
|
+
export function FloatingDelayGroup({
|
|
28
|
+
children,
|
|
29
|
+
delay,
|
|
30
|
+
timeoutMs = 0,
|
|
31
|
+
}: {
|
|
32
|
+
children: React.ReactNode
|
|
33
|
+
delay: Delay
|
|
34
|
+
timeoutMs?: number
|
|
35
|
+
}) {
|
|
36
|
+
const [currentId, setCurrentIdRaw] = React.useState<string | null | undefined>(null)
|
|
37
|
+
const timeoutRef = React.useRef<ReturnType<typeof setTimeout>>(undefined)
|
|
38
|
+
|
|
39
|
+
const setCurrentId = React.useCallback(
|
|
40
|
+
(id: string | null | undefined) => {
|
|
41
|
+
clearTimeout(timeoutRef.current)
|
|
42
|
+
if (id == null && timeoutMs > 0) {
|
|
43
|
+
// delay clearing so moving between tooltips stays instant
|
|
44
|
+
timeoutRef.current = setTimeout(() => {
|
|
45
|
+
setCurrentIdRaw(null)
|
|
46
|
+
}, timeoutMs)
|
|
47
|
+
} else {
|
|
48
|
+
setCurrentIdRaw(id)
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
[timeoutMs]
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
React.useEffect(() => {
|
|
55
|
+
return () => clearTimeout(timeoutRef.current)
|
|
56
|
+
}, [])
|
|
57
|
+
|
|
58
|
+
const value = React.useMemo(
|
|
59
|
+
() => ({
|
|
60
|
+
currentId,
|
|
61
|
+
setCurrentId,
|
|
62
|
+
delay,
|
|
63
|
+
timeoutMs,
|
|
64
|
+
initialDelay: delay,
|
|
65
|
+
}),
|
|
66
|
+
[currentId, setCurrentId, delay, timeoutMs]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return React.createElement(DelayGroupContext.Provider, { value }, children)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// registers a tooltip with the delay group.
|
|
73
|
+
// returns coordinated delay: instant open when another tooltip in the group is showing.
|
|
74
|
+
export function useDelayGroup(
|
|
75
|
+
context: FloatingInteractionContext,
|
|
76
|
+
options: { id?: string } = {}
|
|
77
|
+
): { delay: Delay; currentId: string | null | undefined } {
|
|
78
|
+
const { id } = options
|
|
79
|
+
const groupContext = React.useContext(DelayGroupContext)
|
|
80
|
+
|
|
81
|
+
// when this tooltip closes, start the timeout to clear the group
|
|
82
|
+
React.useEffect(() => {
|
|
83
|
+
if (!context.open && groupContext.currentId === id) {
|
|
84
|
+
groupContext.setCurrentId(null)
|
|
85
|
+
}
|
|
86
|
+
}, [context.open, id])
|
|
87
|
+
|
|
88
|
+
// when another tooltip in the group opens (currentId changed to someone else),
|
|
89
|
+
// close this one so only one tooltip is visible at a time
|
|
90
|
+
React.useEffect(() => {
|
|
91
|
+
if (groupContext.currentId != null && groupContext.currentId !== id && context.open) {
|
|
92
|
+
context.onOpenChange(false)
|
|
93
|
+
}
|
|
94
|
+
}, [groupContext.currentId, id, context.open])
|
|
95
|
+
|
|
96
|
+
// if another tooltip is currently open (currentId is set and not us),
|
|
97
|
+
// use instant delay for opening
|
|
98
|
+
if (groupContext.currentId != null) {
|
|
99
|
+
return {
|
|
100
|
+
delay: { open: 1, close: getClose(groupContext.initialDelay) },
|
|
101
|
+
currentId: groupContext.currentId,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
delay: groupContext.initialDelay,
|
|
107
|
+
currentId: groupContext.currentId,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getClose(delay: Delay): number {
|
|
112
|
+
if (typeof delay === 'number') return delay
|
|
113
|
+
return delay?.close ?? 0
|
|
114
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ElementProps,
|
|
5
|
+
FloatingInteractionContext,
|
|
6
|
+
OpenChangeReason,
|
|
7
|
+
UseFocusProps,
|
|
8
|
+
} from './types'
|
|
9
|
+
import {
|
|
10
|
+
activeElement,
|
|
11
|
+
clearTimeoutIfSet,
|
|
12
|
+
contains,
|
|
13
|
+
getDocument,
|
|
14
|
+
getTarget,
|
|
15
|
+
isElement,
|
|
16
|
+
isHTMLElement,
|
|
17
|
+
isMac,
|
|
18
|
+
isSafari,
|
|
19
|
+
isTypeableElement,
|
|
20
|
+
matchesFocusVisible,
|
|
21
|
+
} from './utils'
|
|
22
|
+
|
|
23
|
+
function isMacSafari() {
|
|
24
|
+
return isMac() && isSafari()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// focus interaction: opens on focus, closes on blur.
|
|
28
|
+
// ported from floating-ui/react useFocus hook with adaptations for our context.
|
|
29
|
+
export function useFocus(
|
|
30
|
+
context: FloatingInteractionContext,
|
|
31
|
+
props: UseFocusProps = {}
|
|
32
|
+
): ElementProps {
|
|
33
|
+
const { open, onOpenChange, events, dataRef, elements } = context
|
|
34
|
+
const { enabled = true, visibleOnly = true } = props
|
|
35
|
+
|
|
36
|
+
const blockFocusRef = useRef(false)
|
|
37
|
+
const timeoutRef = useRef(-1)
|
|
38
|
+
const keyboardModalityRef = useRef(true)
|
|
39
|
+
|
|
40
|
+
// if the reference was focused and the user left the tab/window, and the
|
|
41
|
+
// floating element was not open, block focus when they return.
|
|
42
|
+
// also tracks keyboard vs pointer modality on mac safari.
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!enabled) return
|
|
45
|
+
|
|
46
|
+
const win = getDocument(elements.domReference).defaultView || window
|
|
47
|
+
|
|
48
|
+
function onBlur() {
|
|
49
|
+
if (
|
|
50
|
+
!open &&
|
|
51
|
+
isHTMLElement(elements.domReference) &&
|
|
52
|
+
elements.domReference === activeElement(getDocument(elements.domReference))
|
|
53
|
+
) {
|
|
54
|
+
blockFocusRef.current = true
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function onKeyDown() {
|
|
59
|
+
keyboardModalityRef.current = true
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function onPointerDown() {
|
|
63
|
+
keyboardModalityRef.current = false
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
win.addEventListener('blur', onBlur)
|
|
67
|
+
|
|
68
|
+
if (isMacSafari()) {
|
|
69
|
+
win.addEventListener('keydown', onKeyDown, true)
|
|
70
|
+
win.addEventListener('pointerdown', onPointerDown, true)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return () => {
|
|
74
|
+
win.removeEventListener('blur', onBlur)
|
|
75
|
+
|
|
76
|
+
if (isMacSafari()) {
|
|
77
|
+
win.removeEventListener('keydown', onKeyDown, true)
|
|
78
|
+
win.removeEventListener('pointerdown', onPointerDown, true)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}, [elements.domReference, open, enabled])
|
|
82
|
+
|
|
83
|
+
// block focus after reference-press or escape-key close,
|
|
84
|
+
// so that focus returning to the trigger doesn't reopen
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!enabled) return
|
|
87
|
+
if (!events) return
|
|
88
|
+
|
|
89
|
+
function handleOpenChange({ reason }: { reason: OpenChangeReason }) {
|
|
90
|
+
if (reason === 'reference-press' || reason === 'escape-key') {
|
|
91
|
+
blockFocusRef.current = true
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
events.on('openchange', handleOpenChange)
|
|
96
|
+
return () => {
|
|
97
|
+
events.off('openchange', handleOpenChange)
|
|
98
|
+
}
|
|
99
|
+
}, [events, enabled])
|
|
100
|
+
|
|
101
|
+
// cleanup timeout on unmount
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
return () => {
|
|
104
|
+
clearTimeoutIfSet(timeoutRef)
|
|
105
|
+
}
|
|
106
|
+
}, [])
|
|
107
|
+
|
|
108
|
+
const reference: ElementProps['reference'] = useMemo(
|
|
109
|
+
() => ({
|
|
110
|
+
onMouseLeave() {
|
|
111
|
+
blockFocusRef.current = false
|
|
112
|
+
},
|
|
113
|
+
onFocus(event: any) {
|
|
114
|
+
if (blockFocusRef.current) return
|
|
115
|
+
|
|
116
|
+
const target = getTarget(event.nativeEvent)
|
|
117
|
+
|
|
118
|
+
if (visibleOnly && isElement(target)) {
|
|
119
|
+
// safari fails to match :focus-visible if focus was initially
|
|
120
|
+
// outside the document
|
|
121
|
+
if (isMacSafari() && !event.relatedTarget) {
|
|
122
|
+
if (!keyboardModalityRef.current && !isTypeableElement(target)) {
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
} else if (!matchesFocusVisible(target)) {
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
onOpenChange(true, event.nativeEvent, 'focus')
|
|
131
|
+
},
|
|
132
|
+
onBlur(event: any) {
|
|
133
|
+
blockFocusRef.current = false
|
|
134
|
+
const relatedTarget = event.relatedTarget as Element | null
|
|
135
|
+
const nativeEvent = event.nativeEvent
|
|
136
|
+
|
|
137
|
+
// wait for the window blur listener to fire
|
|
138
|
+
timeoutRef.current = window.setTimeout(() => {
|
|
139
|
+
const activeEl = activeElement(
|
|
140
|
+
elements.domReference ? elements.domReference.ownerDocument : document
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
// focus left the page, keep it open
|
|
144
|
+
if (!relatedTarget && activeEl === elements.domReference) return
|
|
145
|
+
|
|
146
|
+
// when focusing the reference element then clicking into the floating
|
|
147
|
+
// element, prevent it from hiding. we check activeElement rather than
|
|
148
|
+
// relatedTarget to handle shadow DOM correctly.
|
|
149
|
+
if (
|
|
150
|
+
contains(context.refs.floating.current, activeEl) ||
|
|
151
|
+
contains(elements.domReference, activeEl)
|
|
152
|
+
) {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
onOpenChange(false, nativeEvent, 'focus')
|
|
157
|
+
})
|
|
158
|
+
},
|
|
159
|
+
}),
|
|
160
|
+
[context.refs.floating, elements.domReference, onOpenChange, visibleOnly]
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
return useMemo(() => (enabled ? { reference } : {}), [enabled, reference])
|
|
164
|
+
}
|