@tamagui/floating 2.0.0-rc.3 → 2.0.0-rc.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/dist/cjs/Floating.native.js +4 -0
  2. package/dist/cjs/Floating.native.js.map +1 -1
  3. package/dist/cjs/index.cjs +33 -2
  4. package/dist/cjs/index.js +49 -12
  5. package/dist/cjs/index.js.map +2 -2
  6. package/dist/cjs/index.native.js +33 -2
  7. package/dist/cjs/index.native.js.map +1 -1
  8. package/dist/cjs/interactions/PopupTriggerMap.cjs +40 -0
  9. package/dist/cjs/interactions/PopupTriggerMap.native.js +73 -0
  10. package/dist/cjs/interactions/PopupTriggerMap.native.js.map +1 -0
  11. package/dist/cjs/interactions/createFloatingEvents.cjs +41 -0
  12. package/dist/cjs/interactions/createFloatingEvents.native.js +47 -0
  13. package/dist/cjs/interactions/createFloatingEvents.native.js.map +1 -0
  14. package/dist/cjs/interactions/safePolygon.cjs +191 -0
  15. package/dist/cjs/interactions/safePolygon.native.js +205 -0
  16. package/dist/cjs/interactions/safePolygon.native.js.map +1 -0
  17. package/dist/cjs/interactions/types.cjs +16 -0
  18. package/dist/cjs/interactions/types.native.js +19 -0
  19. package/dist/cjs/interactions/types.native.js.map +1 -0
  20. package/dist/cjs/interactions/useClick.cjs +82 -0
  21. package/dist/cjs/interactions/useClick.native.js +90 -0
  22. package/dist/cjs/interactions/useClick.native.js.map +1 -0
  23. package/dist/cjs/interactions/useDelayGroup.cjs +96 -0
  24. package/dist/cjs/interactions/useDelayGroup.native.js +109 -0
  25. package/dist/cjs/interactions/useDelayGroup.native.js.map +1 -0
  26. package/dist/cjs/interactions/useFocus.cjs +101 -0
  27. package/dist/cjs/interactions/useFocus.native.js +113 -0
  28. package/dist/cjs/interactions/useFocus.native.js.map +1 -0
  29. package/dist/cjs/interactions/useHover.cjs +224 -0
  30. package/dist/cjs/interactions/useHover.native.js +248 -0
  31. package/dist/cjs/interactions/useHover.native.js.map +1 -0
  32. package/dist/cjs/interactions/useInnerOffset.cjs +104 -0
  33. package/dist/cjs/interactions/useInnerOffset.native.js +118 -0
  34. package/dist/cjs/interactions/useInnerOffset.native.js.map +1 -0
  35. package/dist/cjs/interactions/useInteractions.cjs +76 -0
  36. package/dist/cjs/interactions/useInteractions.native.js +164 -0
  37. package/dist/cjs/interactions/useInteractions.native.js.map +1 -0
  38. package/dist/cjs/interactions/useListNavigation.cjs +253 -0
  39. package/dist/cjs/interactions/useListNavigation.native.js +297 -0
  40. package/dist/cjs/interactions/useListNavigation.native.js.map +1 -0
  41. package/dist/cjs/interactions/useRole.cjs +112 -0
  42. package/dist/cjs/interactions/useRole.native.js +128 -0
  43. package/dist/cjs/interactions/useRole.native.js.map +1 -0
  44. package/dist/cjs/interactions/useTypeahead.cjs +93 -0
  45. package/dist/cjs/interactions/useTypeahead.native.js +114 -0
  46. package/dist/cjs/interactions/useTypeahead.native.js.map +1 -0
  47. package/dist/cjs/interactions/utils.cjs +178 -0
  48. package/dist/cjs/interactions/utils.native.js +188 -0
  49. package/dist/cjs/interactions/utils.native.js.map +1 -0
  50. package/dist/cjs/middleware/inner.cjs +106 -0
  51. package/dist/cjs/middleware/inner.native.js +120 -0
  52. package/dist/cjs/middleware/inner.native.js.map +1 -0
  53. package/dist/cjs/useFloating.cjs +21 -17
  54. package/dist/cjs/useFloating.native.js +2 -0
  55. package/dist/cjs/useFloating.native.js.map +1 -1
  56. package/dist/esm/Floating.native.js +4 -1
  57. package/dist/esm/Floating.native.js.map +1 -1
  58. package/dist/esm/index.js +17 -34
  59. package/dist/esm/index.js.map +1 -6
  60. package/dist/esm/index.mjs +16 -2
  61. package/dist/esm/index.mjs.map +1 -1
  62. package/dist/esm/index.native.js +16 -2
  63. package/dist/esm/index.native.js.map +1 -1
  64. package/dist/esm/interactions/PopupTriggerMap.mjs +17 -0
  65. package/dist/esm/interactions/PopupTriggerMap.mjs.map +1 -0
  66. package/dist/esm/interactions/PopupTriggerMap.native.js +47 -0
  67. package/dist/esm/interactions/PopupTriggerMap.native.js.map +1 -0
  68. package/dist/esm/interactions/createFloatingEvents.mjs +18 -0
  69. package/dist/esm/interactions/createFloatingEvents.mjs.map +1 -0
  70. package/dist/esm/interactions/createFloatingEvents.native.js +21 -0
  71. package/dist/esm/interactions/createFloatingEvents.native.js.map +1 -0
  72. package/dist/esm/interactions/safePolygon.mjs +168 -0
  73. package/dist/esm/interactions/safePolygon.mjs.map +1 -0
  74. package/dist/esm/interactions/safePolygon.native.js +179 -0
  75. package/dist/esm/interactions/safePolygon.native.js.map +1 -0
  76. package/dist/esm/interactions/types.mjs +2 -0
  77. package/dist/esm/interactions/types.mjs.map +1 -0
  78. package/dist/esm/interactions/types.native.js +2 -0
  79. package/dist/esm/interactions/types.native.js.map +1 -0
  80. package/dist/esm/interactions/useClick.mjs +59 -0
  81. package/dist/esm/interactions/useClick.mjs.map +1 -0
  82. package/dist/esm/interactions/useClick.native.js +64 -0
  83. package/dist/esm/interactions/useClick.native.js.map +1 -0
  84. package/dist/esm/interactions/useDelayGroup.mjs +60 -0
  85. package/dist/esm/interactions/useDelayGroup.mjs.map +1 -0
  86. package/dist/esm/interactions/useDelayGroup.native.js +70 -0
  87. package/dist/esm/interactions/useDelayGroup.native.js.map +1 -0
  88. package/dist/esm/interactions/useFocus.mjs +78 -0
  89. package/dist/esm/interactions/useFocus.mjs.map +1 -0
  90. package/dist/esm/interactions/useFocus.native.js +87 -0
  91. package/dist/esm/interactions/useFocus.native.js.map +1 -0
  92. package/dist/esm/interactions/useHover.mjs +189 -0
  93. package/dist/esm/interactions/useHover.mjs.map +1 -0
  94. package/dist/esm/interactions/useHover.native.js +210 -0
  95. package/dist/esm/interactions/useHover.native.js.map +1 -0
  96. package/dist/esm/interactions/useInnerOffset.mjs +70 -0
  97. package/dist/esm/interactions/useInnerOffset.mjs.map +1 -0
  98. package/dist/esm/interactions/useInnerOffset.native.js +81 -0
  99. package/dist/esm/interactions/useInnerOffset.native.js.map +1 -0
  100. package/dist/esm/interactions/useInteractions.mjs +53 -0
  101. package/dist/esm/interactions/useInteractions.mjs.map +1 -0
  102. package/dist/esm/interactions/useInteractions.native.js +138 -0
  103. package/dist/esm/interactions/useInteractions.native.js.map +1 -0
  104. package/dist/esm/interactions/useListNavigation.mjs +230 -0
  105. package/dist/esm/interactions/useListNavigation.mjs.map +1 -0
  106. package/dist/esm/interactions/useListNavigation.native.js +271 -0
  107. package/dist/esm/interactions/useListNavigation.native.js.map +1 -0
  108. package/dist/esm/interactions/useRole.mjs +78 -0
  109. package/dist/esm/interactions/useRole.mjs.map +1 -0
  110. package/dist/esm/interactions/useRole.native.js +91 -0
  111. package/dist/esm/interactions/useRole.native.js.map +1 -0
  112. package/dist/esm/interactions/useTypeahead.mjs +70 -0
  113. package/dist/esm/interactions/useTypeahead.mjs.map +1 -0
  114. package/dist/esm/interactions/useTypeahead.native.js +88 -0
  115. package/dist/esm/interactions/useTypeahead.native.js.map +1 -0
  116. package/dist/esm/interactions/utils.mjs +134 -0
  117. package/dist/esm/interactions/utils.mjs.map +1 -0
  118. package/dist/esm/interactions/utils.native.js +141 -0
  119. package/dist/esm/interactions/utils.native.js.map +1 -0
  120. package/dist/esm/middleware/inner.mjs +72 -0
  121. package/dist/esm/middleware/inner.mjs.map +1 -0
  122. package/dist/esm/middleware/inner.native.js +83 -0
  123. package/dist/esm/middleware/inner.native.js.map +1 -0
  124. package/dist/esm/useFloating.mjs +21 -17
  125. package/dist/esm/useFloating.mjs.map +1 -1
  126. package/dist/esm/useFloating.native.js +2 -0
  127. package/dist/esm/useFloating.native.js.map +1 -1
  128. package/package.json +8 -10
  129. package/src/Floating.native.tsx +1 -0
  130. package/src/index.ts +49 -0
  131. package/src/interactions/PopupTriggerMap.ts +30 -0
  132. package/src/interactions/createFloatingEvents.ts +34 -0
  133. package/src/interactions/safePolygon.ts +500 -0
  134. package/src/interactions/types.ts +165 -0
  135. package/src/interactions/useClick.ts +148 -0
  136. package/src/interactions/useDelayGroup.ts +114 -0
  137. package/src/interactions/useFocus.ts +164 -0
  138. package/src/interactions/useHover.ts +453 -0
  139. package/src/interactions/useInnerOffset.ts +116 -0
  140. package/src/interactions/useInteractions.ts +101 -0
  141. package/src/interactions/useListNavigation.ts +578 -0
  142. package/src/interactions/useRole.ts +103 -0
  143. package/src/interactions/useTypeahead.ts +173 -0
  144. package/src/interactions/utils.ts +234 -0
  145. package/src/middleware/inner.ts +142 -0
  146. package/src/useFloating.tsx +13 -1
  147. package/types/Floating.native.d.ts +1 -0
  148. package/types/Floating.native.d.ts.map +1 -1
  149. package/types/index.d.ts +17 -2
  150. package/types/index.d.ts.map +1 -1
  151. package/types/interactions/PopupTriggerMap.d.ts +8 -0
  152. package/types/interactions/PopupTriggerMap.d.ts.map +1 -0
  153. package/types/interactions/createFloatingEvents.d.ts +7 -0
  154. package/types/interactions/createFloatingEvents.d.ts.map +1 -0
  155. package/types/interactions/safePolygon.d.ts +4 -0
  156. package/types/interactions/safePolygon.d.ts.map +1 -0
  157. package/types/interactions/types.d.ts +123 -0
  158. package/types/interactions/types.d.ts.map +1 -0
  159. package/types/interactions/useClick.d.ts +3 -0
  160. package/types/interactions/useClick.d.ts.map +1 -0
  161. package/types/interactions/useDelayGroup.d.ts +23 -0
  162. package/types/interactions/useDelayGroup.d.ts.map +1 -0
  163. package/types/interactions/useFocus.d.ts +3 -0
  164. package/types/interactions/useFocus.d.ts.map +1 -0
  165. package/types/interactions/useHover.d.ts +6 -0
  166. package/types/interactions/useHover.d.ts.map +1 -0
  167. package/types/interactions/useInnerOffset.d.ts +3 -0
  168. package/types/interactions/useInnerOffset.d.ts.map +1 -0
  169. package/types/interactions/useInteractions.d.ts +8 -0
  170. package/types/interactions/useInteractions.d.ts.map +1 -0
  171. package/types/interactions/useListNavigation.d.ts +3 -0
  172. package/types/interactions/useListNavigation.d.ts.map +1 -0
  173. package/types/interactions/useRole.d.ts +3 -0
  174. package/types/interactions/useRole.d.ts.map +1 -0
  175. package/types/interactions/useTypeahead.d.ts +3 -0
  176. package/types/interactions/useTypeahead.d.ts.map +1 -0
  177. package/types/interactions/utils.d.ts +46 -0
  178. package/types/interactions/utils.d.ts.map +1 -0
  179. package/types/middleware/inner.d.ts +14 -0
  180. package/types/middleware/inner.d.ts.map +1 -0
  181. package/types/useFloating.d.ts +7 -1
  182. package/types/useFloating.d.ts.map +1 -1
  183. package/dist/cjs/Floating.js +0 -15
  184. package/dist/cjs/Floating.js.map +0 -6
  185. package/dist/cjs/useFloating.js +0 -46
  186. package/dist/cjs/useFloating.js.map +0 -6
  187. package/dist/esm/Floating.js +0 -2
  188. package/dist/esm/Floating.js.map +0 -6
  189. package/dist/esm/useFloating.js +0 -23
  190. package/dist/esm/useFloating.js.map +0 -6
@@ -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
+ }
@@ -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
+ }