@tamagui/floating 2.0.0-rc.4 → 2.0.0-rc.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/dist/cjs/Floating.cjs +7 -5
  2. package/dist/cjs/Floating.native.js +19 -13
  3. package/dist/cjs/Floating.native.js.map +1 -1
  4. package/dist/cjs/index.cjs +46 -13
  5. package/dist/cjs/index.native.js +46 -13
  6. package/dist/cjs/index.native.js.map +1 -1
  7. package/dist/cjs/interactions/PopupTriggerMap.cjs +49 -0
  8. package/dist/cjs/interactions/PopupTriggerMap.native.js +97 -0
  9. package/dist/cjs/interactions/PopupTriggerMap.native.js.map +1 -0
  10. package/dist/cjs/interactions/createFloatingEvents.cjs +50 -0
  11. package/dist/cjs/interactions/createFloatingEvents.native.js +56 -0
  12. package/dist/cjs/interactions/createFloatingEvents.native.js.map +1 -0
  13. package/dist/cjs/interactions/safePolygon.cjs +273 -0
  14. package/dist/cjs/interactions/safePolygon.native.js +284 -0
  15. package/dist/cjs/interactions/safePolygon.native.js.map +1 -0
  16. package/dist/cjs/interactions/types.cjs +18 -0
  17. package/dist/cjs/interactions/types.native.js +21 -0
  18. package/dist/cjs/interactions/types.native.js.map +1 -0
  19. package/dist/cjs/interactions/useClick.cjs +124 -0
  20. package/dist/cjs/interactions/useClick.native.js +132 -0
  21. package/dist/cjs/interactions/useClick.native.js.map +1 -0
  22. package/dist/cjs/interactions/useDelayGroup.cjs +115 -0
  23. package/dist/cjs/interactions/useDelayGroup.native.js +125 -0
  24. package/dist/cjs/interactions/useDelayGroup.native.js.map +1 -0
  25. package/dist/cjs/interactions/useFocus.cjs +130 -0
  26. package/dist/cjs/interactions/useFocus.native.js +139 -0
  27. package/dist/cjs/interactions/useFocus.native.js.map +1 -0
  28. package/dist/cjs/interactions/useHover.cjs +357 -0
  29. package/dist/cjs/interactions/useHover.native.js +373 -0
  30. package/dist/cjs/interactions/useHover.native.js.map +1 -0
  31. package/dist/cjs/interactions/useInnerOffset.cjs +128 -0
  32. package/dist/cjs/interactions/useInnerOffset.native.js +141 -0
  33. package/dist/cjs/interactions/useInnerOffset.native.js.map +1 -0
  34. package/dist/cjs/interactions/useInteractions.cjs +105 -0
  35. package/dist/cjs/interactions/useInteractions.native.js +216 -0
  36. package/dist/cjs/interactions/useInteractions.native.js.map +1 -0
  37. package/dist/cjs/interactions/useListNavigation.cjs +418 -0
  38. package/dist/cjs/interactions/useListNavigation.native.js +433 -0
  39. package/dist/cjs/interactions/useListNavigation.native.js.map +1 -0
  40. package/dist/cjs/interactions/useRole.cjs +122 -0
  41. package/dist/cjs/interactions/useRole.native.js +136 -0
  42. package/dist/cjs/interactions/useRole.native.js.map +1 -0
  43. package/dist/cjs/interactions/useTypeahead.cjs +143 -0
  44. package/dist/cjs/interactions/useTypeahead.native.js +159 -0
  45. package/dist/cjs/interactions/useTypeahead.native.js.map +1 -0
  46. package/dist/cjs/interactions/utils.cjs +208 -0
  47. package/dist/cjs/interactions/utils.native.js +227 -0
  48. package/dist/cjs/interactions/utils.native.js.map +1 -0
  49. package/dist/cjs/middleware/inner.cjs +118 -0
  50. package/dist/cjs/middleware/inner.native.js +130 -0
  51. package/dist/cjs/middleware/inner.native.js.map +1 -0
  52. package/dist/cjs/useFloating.cjs +35 -28
  53. package/dist/cjs/useFloating.native.js +51 -47
  54. package/dist/cjs/useFloating.native.js.map +1 -1
  55. package/dist/esm/Floating.native.js +6 -3
  56. package/dist/esm/Floating.native.js.map +1 -1
  57. package/dist/esm/index.js +17 -34
  58. package/dist/esm/index.js.map +1 -6
  59. package/dist/esm/index.mjs +16 -2
  60. package/dist/esm/index.mjs.map +1 -1
  61. package/dist/esm/index.native.js +16 -2
  62. package/dist/esm/index.native.js.map +1 -1
  63. package/dist/esm/interactions/PopupTriggerMap.mjs +24 -0
  64. package/dist/esm/interactions/PopupTriggerMap.mjs.map +1 -0
  65. package/dist/esm/interactions/PopupTriggerMap.native.js +69 -0
  66. package/dist/esm/interactions/PopupTriggerMap.native.js.map +1 -0
  67. package/dist/esm/interactions/createFloatingEvents.mjs +25 -0
  68. package/dist/esm/interactions/createFloatingEvents.mjs.map +1 -0
  69. package/dist/esm/interactions/createFloatingEvents.native.js +28 -0
  70. package/dist/esm/interactions/createFloatingEvents.native.js.map +1 -0
  71. package/dist/esm/interactions/safePolygon.mjs +248 -0
  72. package/dist/esm/interactions/safePolygon.mjs.map +1 -0
  73. package/dist/esm/interactions/safePolygon.native.js +256 -0
  74. package/dist/esm/interactions/safePolygon.native.js.map +1 -0
  75. package/dist/esm/interactions/types.mjs +2 -0
  76. package/dist/esm/interactions/types.mjs.map +1 -0
  77. package/dist/esm/interactions/types.native.js +2 -0
  78. package/dist/esm/interactions/types.native.js.map +1 -0
  79. package/dist/esm/interactions/useClick.mjs +99 -0
  80. package/dist/esm/interactions/useClick.mjs.map +1 -0
  81. package/dist/esm/interactions/useClick.native.js +104 -0
  82. package/dist/esm/interactions/useClick.native.js.map +1 -0
  83. package/dist/esm/interactions/useDelayGroup.mjs +77 -0
  84. package/dist/esm/interactions/useDelayGroup.mjs.map +1 -0
  85. package/dist/esm/interactions/useDelayGroup.native.js +84 -0
  86. package/dist/esm/interactions/useDelayGroup.native.js.map +1 -0
  87. package/dist/esm/interactions/useFocus.mjs +105 -0
  88. package/dist/esm/interactions/useFocus.mjs.map +1 -0
  89. package/dist/esm/interactions/useFocus.native.js +111 -0
  90. package/dist/esm/interactions/useFocus.native.js.map +1 -0
  91. package/dist/esm/interactions/useHover.mjs +320 -0
  92. package/dist/esm/interactions/useHover.mjs.map +1 -0
  93. package/dist/esm/interactions/useHover.native.js +333 -0
  94. package/dist/esm/interactions/useHover.native.js.map +1 -0
  95. package/dist/esm/interactions/useInnerOffset.mjs +92 -0
  96. package/dist/esm/interactions/useInnerOffset.mjs.map +1 -0
  97. package/dist/esm/interactions/useInnerOffset.native.js +102 -0
  98. package/dist/esm/interactions/useInnerOffset.native.js.map +1 -0
  99. package/dist/esm/interactions/useInteractions.mjs +80 -0
  100. package/dist/esm/interactions/useInteractions.mjs.map +1 -0
  101. package/dist/esm/interactions/useInteractions.native.js +188 -0
  102. package/dist/esm/interactions/useInteractions.native.js.map +1 -0
  103. package/dist/esm/interactions/useListNavigation.mjs +393 -0
  104. package/dist/esm/interactions/useListNavigation.mjs.map +1 -0
  105. package/dist/esm/interactions/useListNavigation.native.js +405 -0
  106. package/dist/esm/interactions/useListNavigation.native.js.map +1 -0
  107. package/dist/esm/interactions/useRole.mjs +86 -0
  108. package/dist/esm/interactions/useRole.mjs.map +1 -0
  109. package/dist/esm/interactions/useRole.native.js +97 -0
  110. package/dist/esm/interactions/useRole.native.js.map +1 -0
  111. package/dist/esm/interactions/useTypeahead.mjs +118 -0
  112. package/dist/esm/interactions/useTypeahead.mjs.map +1 -0
  113. package/dist/esm/interactions/useTypeahead.native.js +131 -0
  114. package/dist/esm/interactions/useTypeahead.native.js.map +1 -0
  115. package/dist/esm/interactions/utils.mjs +162 -0
  116. package/dist/esm/interactions/utils.mjs.map +1 -0
  117. package/dist/esm/interactions/utils.native.js +178 -0
  118. package/dist/esm/interactions/utils.native.js.map +1 -0
  119. package/dist/esm/middleware/inner.mjs +82 -0
  120. package/dist/esm/middleware/inner.mjs.map +1 -0
  121. package/dist/esm/middleware/inner.native.js +91 -0
  122. package/dist/esm/middleware/inner.native.js.map +1 -0
  123. package/dist/esm/useFloating.mjs +8 -3
  124. package/dist/esm/useFloating.mjs.map +1 -1
  125. package/dist/esm/useFloating.native.js +25 -23
  126. package/dist/esm/useFloating.native.js.map +1 -1
  127. package/package.json +8 -10
  128. package/src/Floating.native.tsx +1 -0
  129. package/src/index.ts +49 -0
  130. package/src/interactions/PopupTriggerMap.ts +30 -0
  131. package/src/interactions/createFloatingEvents.ts +34 -0
  132. package/src/interactions/safePolygon.ts +500 -0
  133. package/src/interactions/types.ts +165 -0
  134. package/src/interactions/useClick.ts +148 -0
  135. package/src/interactions/useDelayGroup.ts +114 -0
  136. package/src/interactions/useFocus.ts +164 -0
  137. package/src/interactions/useHover.ts +453 -0
  138. package/src/interactions/useInnerOffset.ts +116 -0
  139. package/src/interactions/useInteractions.ts +101 -0
  140. package/src/interactions/useListNavigation.ts +578 -0
  141. package/src/interactions/useRole.ts +103 -0
  142. package/src/interactions/useTypeahead.ts +173 -0
  143. package/src/interactions/utils.ts +234 -0
  144. package/src/middleware/inner.ts +141 -0
  145. package/src/useFloating.tsx +13 -1
  146. package/types/Floating.native.d.ts +1 -0
  147. package/types/Floating.native.d.ts.map +1 -1
  148. package/types/index.d.ts +17 -2
  149. package/types/index.d.ts.map +1 -1
  150. package/types/interactions/PopupTriggerMap.d.ts +8 -0
  151. package/types/interactions/PopupTriggerMap.d.ts.map +1 -0
  152. package/types/interactions/createFloatingEvents.d.ts +7 -0
  153. package/types/interactions/createFloatingEvents.d.ts.map +1 -0
  154. package/types/interactions/safePolygon.d.ts +4 -0
  155. package/types/interactions/safePolygon.d.ts.map +1 -0
  156. package/types/interactions/types.d.ts +123 -0
  157. package/types/interactions/types.d.ts.map +1 -0
  158. package/types/interactions/useClick.d.ts +3 -0
  159. package/types/interactions/useClick.d.ts.map +1 -0
  160. package/types/interactions/useDelayGroup.d.ts +23 -0
  161. package/types/interactions/useDelayGroup.d.ts.map +1 -0
  162. package/types/interactions/useFocus.d.ts +3 -0
  163. package/types/interactions/useFocus.d.ts.map +1 -0
  164. package/types/interactions/useHover.d.ts +6 -0
  165. package/types/interactions/useHover.d.ts.map +1 -0
  166. package/types/interactions/useInnerOffset.d.ts +3 -0
  167. package/types/interactions/useInnerOffset.d.ts.map +1 -0
  168. package/types/interactions/useInteractions.d.ts +8 -0
  169. package/types/interactions/useInteractions.d.ts.map +1 -0
  170. package/types/interactions/useListNavigation.d.ts +3 -0
  171. package/types/interactions/useListNavigation.d.ts.map +1 -0
  172. package/types/interactions/useRole.d.ts +3 -0
  173. package/types/interactions/useRole.d.ts.map +1 -0
  174. package/types/interactions/useTypeahead.d.ts +3 -0
  175. package/types/interactions/useTypeahead.d.ts.map +1 -0
  176. package/types/interactions/utils.d.ts +46 -0
  177. package/types/interactions/utils.d.ts.map +1 -0
  178. package/types/middleware/inner.d.ts +14 -0
  179. package/types/middleware/inner.d.ts.map +1 -0
  180. package/types/useFloating.d.ts +7 -1
  181. package/types/useFloating.d.ts.map +1 -1
  182. package/dist/cjs/Floating.js +0 -15
  183. package/dist/cjs/Floating.js.map +0 -6
  184. package/dist/cjs/index.js +0 -34
  185. package/dist/cjs/index.js.map +0 -6
  186. package/dist/cjs/useFloating.js +0 -46
  187. package/dist/cjs/useFloating.js.map +0 -6
  188. package/dist/esm/Floating.js +0 -2
  189. package/dist/esm/Floating.js.map +0 -6
  190. package/dist/esm/useFloating.js +0 -23
  191. package/dist/esm/useFloating.js.map +0 -6
@@ -0,0 +1,578 @@
1
+ import * as React from 'react'
2
+ import { useLayoutEffect, useMemo, useRef, useState } from 'react'
3
+ import { useEvent } from '@tamagui/use-event'
4
+ import type {
5
+ ElementProps,
6
+ FloatingInteractionContext,
7
+ UseListNavigationProps,
8
+ } from './types'
9
+ import {
10
+ activeElement,
11
+ enqueueFocus,
12
+ findNonDisabledListIndex,
13
+ getMinListIndex,
14
+ getMaxListIndex,
15
+ isHTMLElement,
16
+ isIndexOutOfListBounds,
17
+ isTypeableCombobox,
18
+ isVirtualClick,
19
+ isVirtualPointerEvent,
20
+ stopEvent,
21
+ } from './utils'
22
+
23
+ const ARROW_UP = 'ArrowUp'
24
+ const ARROW_DOWN = 'ArrowDown'
25
+ const ARROW_LEFT = 'ArrowLeft'
26
+ const ARROW_RIGHT = 'ArrowRight'
27
+
28
+ function doSwitch(
29
+ orientation: UseListNavigationProps['orientation'],
30
+ vertical: boolean,
31
+ horizontal: boolean
32
+ ) {
33
+ switch (orientation) {
34
+ case 'vertical':
35
+ return vertical
36
+ case 'horizontal':
37
+ return horizontal
38
+ default:
39
+ return vertical || horizontal
40
+ }
41
+ }
42
+
43
+ function isMainOrientationKey(
44
+ key: string,
45
+ orientation: UseListNavigationProps['orientation']
46
+ ) {
47
+ const vertical = key === ARROW_UP || key === ARROW_DOWN
48
+ const horizontal = key === ARROW_LEFT || key === ARROW_RIGHT
49
+ return doSwitch(orientation, vertical, horizontal)
50
+ }
51
+
52
+ function isMainOrientationToEndKey(
53
+ key: string,
54
+ orientation: UseListNavigationProps['orientation'],
55
+ rtl: boolean
56
+ ) {
57
+ const vertical = key === ARROW_DOWN
58
+ const horizontal = rtl ? key === ARROW_LEFT : key === ARROW_RIGHT
59
+ return (
60
+ doSwitch(orientation, vertical, horizontal) ||
61
+ key === 'Enter' ||
62
+ key === ' ' ||
63
+ key === ''
64
+ )
65
+ }
66
+
67
+ function isCrossOrientationOpenKey(
68
+ key: string,
69
+ orientation: UseListNavigationProps['orientation'],
70
+ rtl: boolean
71
+ ) {
72
+ const vertical = rtl ? key === ARROW_LEFT : key === ARROW_RIGHT
73
+ const horizontal = key === ARROW_DOWN
74
+ return doSwitch(orientation, vertical, horizontal)
75
+ }
76
+
77
+ function isCrossOrientationCloseKey(
78
+ key: string,
79
+ orientation: UseListNavigationProps['orientation'],
80
+ rtl: boolean
81
+ ) {
82
+ const vertical = rtl ? key === ARROW_RIGHT : key === ARROW_LEFT
83
+ const horizontal = key === ARROW_UP
84
+ return doSwitch(orientation, vertical, horizontal)
85
+ }
86
+
87
+ // arrow key-based navigation of a list of items, with real or virtual (aria-activedescendant) focus
88
+ export function useListNavigation(
89
+ context: FloatingInteractionContext,
90
+ props: UseListNavigationProps
91
+ ): ElementProps {
92
+ const { open, onOpenChange, elements } = context
93
+ const {
94
+ listRef,
95
+ activeIndex,
96
+ onNavigate: unstable_onNavigate = () => {},
97
+ enabled = true,
98
+ selectedIndex = null,
99
+ allowEscape = false,
100
+ loop = false,
101
+ nested = false,
102
+ rtl = false,
103
+ virtual = false,
104
+ focusItemOnOpen = 'auto',
105
+ focusItemOnHover = true,
106
+ openOnArrowKeyDown = true,
107
+ disabledIndices = undefined,
108
+ orientation = 'vertical',
109
+ scrollItemIntoView = true,
110
+ } = props
111
+
112
+ const typeableComboboxReference = isTypeableCombobox(elements.domReference)
113
+
114
+ const focusItemOnOpenRef = useRef(focusItemOnOpen)
115
+ const indexRef = useRef(selectedIndex ?? -1)
116
+ const keyRef = useRef<null | string>(null)
117
+ const isPointerModalityRef = useRef(true)
118
+ const previousMountedRef = useRef(!!elements.floating)
119
+ const previousOpenRef = useRef(open)
120
+ const forceSyncFocusRef = useRef(false)
121
+ const forceScrollIntoViewRef = useRef(false)
122
+
123
+ // latest-value refs
124
+ const disabledIndicesRef = useRef(disabledIndices)
125
+ disabledIndicesRef.current = disabledIndices
126
+ const latestOpenRef = useRef(open)
127
+ latestOpenRef.current = open
128
+ const scrollItemIntoViewRef = useRef(scrollItemIntoView)
129
+ scrollItemIntoViewRef.current = scrollItemIntoView
130
+ const selectedIndexRef = useRef(selectedIndex)
131
+ selectedIndexRef.current = selectedIndex
132
+
133
+ // stable callback via useEvent
134
+ const stableOnNavigate = useEvent(unstable_onNavigate)
135
+
136
+ const [activeId, setActiveId] = useState<string | undefined>()
137
+
138
+ // stable onNavigate that reads from indexRef
139
+ const onNavigate = useEvent(() => {
140
+ stableOnNavigate(indexRef.current === -1 ? null : indexRef.current)
141
+ })
142
+
143
+ // store previous onNavigate for cleanup
144
+ const previousOnNavigateRef = useRef(onNavigate)
145
+
146
+ const focusItem = useEvent(() => {
147
+ function runFocus(item: HTMLElement) {
148
+ if (virtual) {
149
+ setActiveId(item.id)
150
+ } else {
151
+ enqueueFocus(item, {
152
+ sync: forceSyncFocusRef.current,
153
+ preventScroll: true,
154
+ })
155
+ }
156
+ }
157
+
158
+ const initialItem = listRef.current[indexRef.current]
159
+ const forceScrollIntoView = forceScrollIntoViewRef.current
160
+
161
+ if (initialItem) {
162
+ runFocus(initialItem)
163
+ }
164
+
165
+ const scheduler = forceSyncFocusRef.current
166
+ ? (v: () => void) => v()
167
+ : requestAnimationFrame
168
+
169
+ scheduler(() => {
170
+ const waitedItem = listRef.current[indexRef.current] || initialItem
171
+
172
+ if (!waitedItem) return
173
+
174
+ if (!initialItem) {
175
+ runFocus(waitedItem)
176
+ }
177
+
178
+ const scrollIntoViewOptions = scrollItemIntoViewRef.current
179
+ const shouldScrollIntoView =
180
+ scrollIntoViewOptions &&
181
+ waitedItem &&
182
+ (forceScrollIntoView || !isPointerModalityRef.current)
183
+
184
+ if (shouldScrollIntoView) {
185
+ waitedItem.scrollIntoView?.(
186
+ typeof scrollIntoViewOptions === 'boolean'
187
+ ? { block: 'nearest', inline: 'nearest' }
188
+ : scrollIntoViewOptions
189
+ )
190
+ }
191
+ })
192
+ })
193
+
194
+ // sync selectedIndex to activeIndex on open, reset on close
195
+ useLayoutEffect(() => {
196
+ if (!enabled) return
197
+
198
+ if (open && elements.floating) {
199
+ if (focusItemOnOpenRef.current && selectedIndex != null) {
200
+ forceScrollIntoViewRef.current = true
201
+ indexRef.current = selectedIndex
202
+ onNavigate()
203
+ }
204
+ } else if (previousMountedRef.current) {
205
+ indexRef.current = -1
206
+ previousOnNavigateRef.current()
207
+ }
208
+ }, [enabled, open, elements.floating, selectedIndex, onNavigate])
209
+
210
+ // sync activeIndex to focused item while open
211
+ useLayoutEffect(() => {
212
+ if (!enabled) return
213
+ if (!open) return
214
+ if (!elements.floating) return
215
+
216
+ if (activeIndex == null) {
217
+ forceSyncFocusRef.current = false
218
+
219
+ if (selectedIndexRef.current != null) {
220
+ return
221
+ }
222
+
223
+ // reset while the floating element was open (e.g. the list changed)
224
+ if (previousMountedRef.current) {
225
+ indexRef.current = -1
226
+ focusItem()
227
+ }
228
+
229
+ // initial sync
230
+ if (
231
+ (!previousOpenRef.current || !previousMountedRef.current) &&
232
+ focusItemOnOpenRef.current &&
233
+ (keyRef.current != null ||
234
+ (focusItemOnOpenRef.current === true && keyRef.current == null))
235
+ ) {
236
+ let runs = 0
237
+ const waitForListPopulated = () => {
238
+ if (listRef.current[0] == null) {
239
+ if (runs < 2) {
240
+ const scheduler = runs ? requestAnimationFrame : queueMicrotask
241
+ scheduler(waitForListPopulated)
242
+ }
243
+ runs++
244
+ } else {
245
+ indexRef.current =
246
+ keyRef.current == null ||
247
+ isMainOrientationToEndKey(keyRef.current, orientation, rtl) ||
248
+ nested
249
+ ? getMinListIndex(listRef, disabledIndicesRef.current)
250
+ : getMaxListIndex(listRef, disabledIndicesRef.current)
251
+ keyRef.current = null
252
+ onNavigate()
253
+ }
254
+ }
255
+
256
+ waitForListPopulated()
257
+ }
258
+ } else if (!isIndexOutOfListBounds(listRef, activeIndex)) {
259
+ indexRef.current = activeIndex
260
+ focusItem()
261
+ forceScrollIntoViewRef.current = false
262
+ }
263
+ }, [
264
+ enabled,
265
+ open,
266
+ elements.floating,
267
+ activeIndex,
268
+ selectedIndexRef,
269
+ nested,
270
+ listRef,
271
+ orientation,
272
+ rtl,
273
+ onNavigate,
274
+ focusItem,
275
+ disabledIndicesRef,
276
+ ])
277
+
278
+ // track previous state
279
+ useLayoutEffect(() => {
280
+ previousOnNavigateRef.current = onNavigate
281
+ previousOpenRef.current = open
282
+ previousMountedRef.current = !!elements.floating
283
+ })
284
+
285
+ useLayoutEffect(() => {
286
+ if (!open) {
287
+ keyRef.current = null
288
+ focusItemOnOpenRef.current = focusItemOnOpen
289
+ }
290
+ }, [open, focusItemOnOpen])
291
+
292
+ const hasActiveIndex = activeIndex != null
293
+
294
+ const commonOnKeyDown = useEvent((event: React.KeyboardEvent) => {
295
+ isPointerModalityRef.current = false
296
+ forceSyncFocusRef.current = true
297
+
298
+ // composing character check (chrome fires ArrowDown twice)
299
+ if (event.which === 229) {
300
+ return
301
+ }
302
+
303
+ // if floating element is animating out, ignore navigation
304
+ if (!latestOpenRef.current && event.currentTarget === elements.floating) {
305
+ return
306
+ }
307
+
308
+ if (nested && isCrossOrientationCloseKey(event.key, orientation, rtl)) {
309
+ stopEvent(event)
310
+ onOpenChange(false, event.nativeEvent, 'list-navigation')
311
+
312
+ if (isHTMLElement(elements.domReference)) {
313
+ elements.domReference.focus()
314
+ }
315
+
316
+ return
317
+ }
318
+
319
+ const currentIndex = indexRef.current
320
+ const minIndex = getMinListIndex(listRef, disabledIndices)
321
+ const maxIndex = getMaxListIndex(listRef, disabledIndices)
322
+
323
+ if (!typeableComboboxReference) {
324
+ if (event.key === 'Home') {
325
+ stopEvent(event)
326
+ indexRef.current = minIndex
327
+ onNavigate()
328
+ }
329
+
330
+ if (event.key === 'End') {
331
+ stopEvent(event)
332
+ indexRef.current = maxIndex
333
+ onNavigate()
334
+ }
335
+ }
336
+
337
+ if (isMainOrientationKey(event.key, orientation)) {
338
+ stopEvent(event)
339
+
340
+ // reset the index if no item is focused
341
+ if (
342
+ open &&
343
+ !virtual &&
344
+ activeElement(event.currentTarget.ownerDocument) === event.currentTarget
345
+ ) {
346
+ indexRef.current = isMainOrientationToEndKey(event.key, orientation, rtl)
347
+ ? minIndex
348
+ : maxIndex
349
+ onNavigate()
350
+ return
351
+ }
352
+
353
+ if (isMainOrientationToEndKey(event.key, orientation, rtl)) {
354
+ if (loop) {
355
+ indexRef.current =
356
+ currentIndex >= maxIndex
357
+ ? allowEscape && currentIndex !== listRef.current.length
358
+ ? -1
359
+ : minIndex
360
+ : findNonDisabledListIndex(listRef, {
361
+ startingIndex: currentIndex,
362
+ disabledIndices,
363
+ })
364
+ } else {
365
+ indexRef.current = Math.min(
366
+ maxIndex,
367
+ findNonDisabledListIndex(listRef, {
368
+ startingIndex: currentIndex,
369
+ disabledIndices,
370
+ })
371
+ )
372
+ }
373
+ } else {
374
+ if (loop) {
375
+ indexRef.current =
376
+ currentIndex <= minIndex
377
+ ? allowEscape && currentIndex !== -1
378
+ ? listRef.current.length
379
+ : maxIndex
380
+ : findNonDisabledListIndex(listRef, {
381
+ startingIndex: currentIndex,
382
+ decrement: true,
383
+ disabledIndices,
384
+ })
385
+ } else {
386
+ indexRef.current = Math.max(
387
+ minIndex,
388
+ findNonDisabledListIndex(listRef, {
389
+ startingIndex: currentIndex,
390
+ decrement: true,
391
+ disabledIndices,
392
+ })
393
+ )
394
+ }
395
+ }
396
+
397
+ if (isIndexOutOfListBounds(listRef, indexRef.current)) {
398
+ indexRef.current = -1
399
+ }
400
+
401
+ onNavigate()
402
+ }
403
+ })
404
+
405
+ const ariaActiveDescendantProp = useMemo(() => {
406
+ return (
407
+ virtual &&
408
+ open &&
409
+ hasActiveIndex && {
410
+ 'aria-activedescendant': activeId,
411
+ }
412
+ )
413
+ }, [virtual, open, hasActiveIndex, activeId])
414
+
415
+ const floating: ElementProps['floating'] = useMemo(() => {
416
+ return {
417
+ 'aria-orientation': orientation === 'both' ? undefined : orientation,
418
+ ...(!typeableComboboxReference ? ariaActiveDescendantProp : {}),
419
+ onKeyDown: commonOnKeyDown,
420
+ onPointerMove() {
421
+ isPointerModalityRef.current = true
422
+ },
423
+ }
424
+ }, [ariaActiveDescendantProp, commonOnKeyDown, orientation, typeableComboboxReference])
425
+
426
+ const reference: ElementProps['reference'] = useMemo(() => {
427
+ function checkVirtualMouse(event: React.PointerEvent) {
428
+ if (focusItemOnOpen === 'auto' && isVirtualClick(event.nativeEvent)) {
429
+ focusItemOnOpenRef.current = true
430
+ }
431
+ }
432
+
433
+ function checkVirtualPointer(event: React.PointerEvent) {
434
+ focusItemOnOpenRef.current = focusItemOnOpen
435
+ if (focusItemOnOpen === 'auto' && isVirtualPointerEvent(event.nativeEvent)) {
436
+ focusItemOnOpenRef.current = true
437
+ }
438
+ }
439
+
440
+ return {
441
+ ...ariaActiveDescendantProp,
442
+ onKeyDown(event: any) {
443
+ isPointerModalityRef.current = false
444
+
445
+ const isArrowKey = event.key.startsWith('Arrow')
446
+ const isCrossOpenKey = isCrossOrientationOpenKey(event.key, orientation, rtl)
447
+ const isMainKey = isMainOrientationKey(event.key, orientation)
448
+ const isNavigationKey =
449
+ (nested ? isCrossOpenKey : isMainKey) ||
450
+ event.key === 'Enter' ||
451
+ event.key.trim() === ''
452
+
453
+ if (virtual && open) {
454
+ return commonOnKeyDown(event)
455
+ }
456
+
457
+ // if a floating element should not open on arrow key down, avoid
458
+ // setting activeIndex while it's closed
459
+ if (!open && !openOnArrowKeyDown && isArrowKey) {
460
+ return
461
+ }
462
+
463
+ if (isNavigationKey) {
464
+ keyRef.current = event.key
465
+ }
466
+
467
+ if (nested) {
468
+ if (isCrossOpenKey) {
469
+ stopEvent(event)
470
+
471
+ if (open) {
472
+ indexRef.current = getMinListIndex(listRef, disabledIndicesRef.current)
473
+ onNavigate()
474
+ } else {
475
+ onOpenChange(true, event.nativeEvent, 'list-navigation')
476
+ }
477
+ }
478
+
479
+ return
480
+ }
481
+
482
+ if (isMainKey) {
483
+ if (selectedIndex != null) {
484
+ indexRef.current = selectedIndex
485
+ }
486
+
487
+ stopEvent(event)
488
+
489
+ if (!open && openOnArrowKeyDown) {
490
+ onOpenChange(true, event.nativeEvent, 'list-navigation')
491
+ } else {
492
+ commonOnKeyDown(event)
493
+ }
494
+
495
+ if (open) {
496
+ onNavigate()
497
+ }
498
+ }
499
+ },
500
+ onFocus() {
501
+ if (open && !virtual) {
502
+ indexRef.current = -1
503
+ onNavigate()
504
+ }
505
+ },
506
+ onPointerDown: checkVirtualPointer,
507
+ onPointerEnter: checkVirtualPointer,
508
+ onMouseDown: checkVirtualMouse,
509
+ onClick: checkVirtualMouse,
510
+ }
511
+ }, [
512
+ ariaActiveDescendantProp,
513
+ commonOnKeyDown,
514
+ disabledIndicesRef,
515
+ focusItemOnOpen,
516
+ listRef,
517
+ nested,
518
+ onNavigate,
519
+ onOpenChange,
520
+ open,
521
+ openOnArrowKeyDown,
522
+ orientation,
523
+ rtl,
524
+ selectedIndex,
525
+ virtual,
526
+ ])
527
+
528
+ const item = useMemo(() => {
529
+ function syncCurrentTarget(currentTarget: HTMLElement | null) {
530
+ if (!latestOpenRef.current) return
531
+ const index = listRef.current.indexOf(currentTarget)
532
+ if (index !== -1 && indexRef.current !== index) {
533
+ indexRef.current = index
534
+ onNavigate()
535
+ }
536
+ }
537
+
538
+ const itemProps: ElementProps['item'] = {
539
+ onFocus({ currentTarget }: any) {
540
+ forceSyncFocusRef.current = true
541
+ syncCurrentTarget(currentTarget)
542
+ },
543
+ onClick: ({ currentTarget }: any) => currentTarget.focus({ preventScroll: true }), // safari
544
+ onMouseMove({ currentTarget }: any) {
545
+ forceSyncFocusRef.current = true
546
+ forceScrollIntoViewRef.current = false
547
+ if (focusItemOnHover) {
548
+ syncCurrentTarget(currentTarget)
549
+ }
550
+ },
551
+ onPointerLeave({ pointerType }: any) {
552
+ if (!isPointerModalityRef.current || pointerType === 'touch') {
553
+ return
554
+ }
555
+
556
+ forceSyncFocusRef.current = true
557
+
558
+ if (!focusItemOnHover) {
559
+ return
560
+ }
561
+
562
+ indexRef.current = -1
563
+ onNavigate()
564
+
565
+ if (!virtual) {
566
+ elements.floating?.focus({ preventScroll: true })
567
+ }
568
+ },
569
+ }
570
+
571
+ return itemProps
572
+ }, [latestOpenRef, focusItemOnHover, listRef, onNavigate, virtual, elements.floating])
573
+
574
+ return useMemo(
575
+ () => (enabled ? { reference, floating, item } : {}),
576
+ [enabled, reference, floating, item]
577
+ )
578
+ }
@@ -0,0 +1,103 @@
1
+ import * as React from 'react'
2
+
3
+ import type { ElementProps, FloatingInteractionContext, UseRoleProps } from './types'
4
+
5
+ type RoleValue = NonNullable<UseRoleProps['role']>
6
+
7
+ const componentRoleToAriaRoleMap = new Map<RoleValue, RoleValue | false>([
8
+ ['select', 'listbox'],
9
+ ['combobox', 'listbox'],
10
+ ['label', false],
11
+ ])
12
+
13
+ let idCounter = 0
14
+
15
+ // sets ARIA attributes based on role
16
+ export function useRole(
17
+ context: FloatingInteractionContext,
18
+ props: UseRoleProps = {}
19
+ ): ElementProps {
20
+ const { open, elements } = context
21
+ const { enabled = true, role = 'dialog' } = props
22
+
23
+ const defaultReferenceId = React.useId()
24
+ const referenceId = elements.domReference?.id || defaultReferenceId
25
+
26
+ const defaultFloatingId = React.useMemo(() => `floating-${idCounter++}`, [])
27
+ const floatingId = React.useMemo(
28
+ () => elements.floating?.id || defaultFloatingId,
29
+ [elements.floating, defaultFloatingId]
30
+ )
31
+
32
+ const ariaRole = (componentRoleToAriaRoleMap.get(role) ?? role) as
33
+ | 'tooltip'
34
+ | 'dialog'
35
+ | 'alertdialog'
36
+ | 'menu'
37
+ | 'listbox'
38
+ | 'grid'
39
+ | 'tree'
40
+ | false
41
+
42
+ const reference: ElementProps['reference'] = React.useMemo(() => {
43
+ if (ariaRole === 'tooltip' || role === 'label') {
44
+ return {
45
+ [`aria-${role === 'label' ? 'labelledby' : 'describedby'}`]: open
46
+ ? floatingId
47
+ : undefined,
48
+ }
49
+ }
50
+
51
+ return {
52
+ 'aria-expanded': open ? 'true' : 'false',
53
+ 'aria-haspopup': ariaRole === 'alertdialog' ? 'dialog' : ariaRole,
54
+ 'aria-controls': open ? floatingId : undefined,
55
+ ...(ariaRole === 'listbox' && { role: 'combobox' }),
56
+ ...(ariaRole === 'menu' && { id: referenceId }),
57
+ ...(role === 'select' && { 'aria-autocomplete': 'none' }),
58
+ ...(role === 'combobox' && { 'aria-autocomplete': 'list' }),
59
+ }
60
+ }, [ariaRole, floatingId, open, referenceId, role])
61
+
62
+ const floating: ElementProps['floating'] = React.useMemo(() => {
63
+ const floatingProps = {
64
+ id: floatingId,
65
+ ...(ariaRole && { role: ariaRole }),
66
+ }
67
+
68
+ if (ariaRole === 'tooltip' || role === 'label') {
69
+ return floatingProps
70
+ }
71
+
72
+ return {
73
+ ...floatingProps,
74
+ ...(ariaRole === 'menu' && { 'aria-labelledby': referenceId }),
75
+ }
76
+ }, [ariaRole, floatingId, referenceId, role])
77
+
78
+ const item: ElementProps['item'] = React.useCallback(
79
+ ({ active, selected }: { active?: boolean; selected?: boolean }) => {
80
+ const commonProps = {
81
+ role: 'option',
82
+ ...(active && { id: `${floatingId}-fui-option` }),
83
+ }
84
+
85
+ switch (role) {
86
+ case 'select':
87
+ case 'combobox':
88
+ return {
89
+ ...commonProps,
90
+ 'aria-selected': selected,
91
+ }
92
+ }
93
+
94
+ return {}
95
+ },
96
+ [floatingId, role]
97
+ )
98
+
99
+ return React.useMemo(
100
+ () => (enabled ? { reference, floating, item } : {}),
101
+ [enabled, reference, floating, item]
102
+ )
103
+ }