@octanejs/floating-ui 0.1.2

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.
@@ -0,0 +1,813 @@
1
+ // Ported from @floating-ui/react FloatingFocusManager (+ markOthers, the focus-trap
2
+ // helpers, VisuallyHiddenDismiss). `.ts` component via createElement; React forwardRef
3
+ // → props.ref; `event.nativeEvent` → `event`. The trap works through direct DOM
4
+ // listeners (keydown/focusin/focusout) + markOthers (aria-hidden/inert) + return-focus,
5
+ // and the FocusGuards rely on octane's capture-phase focus delegation. Multiple roots
6
+ // are returned as an ARRAY (octane renders it like a React fragment).
7
+ import { getNodeName, isHTMLElement, isShadowRoot } from '@floating-ui/utils/dom';
8
+ import { focusable, isTabbable, tabbable } from 'tabbable';
9
+ import { createElement, useEffect, useMemo, useRef } from 'octane';
10
+
11
+ import { FocusGuard, usePortalContext } from './FloatingPortal';
12
+ import { S } from './internal';
13
+ import { useFloatingTree } from './tree';
14
+ import {
15
+ activeElement,
16
+ clearTimeoutIfSet,
17
+ contains,
18
+ createAttribute,
19
+ enqueueFocus,
20
+ getDocument,
21
+ getFloatingFocusElement,
22
+ getNextTabbable,
23
+ getNodeAncestors,
24
+ getNodeChildren,
25
+ getPreviousTabbable,
26
+ getTabbableOptions,
27
+ getTarget,
28
+ isOutsideEvent,
29
+ isTypeableCombobox,
30
+ isVirtualClick,
31
+ isVirtualPointerEvent,
32
+ stopEvent,
33
+ useEffectEvent,
34
+ useLatestRef,
35
+ useModernLayoutEffect,
36
+ } from './utils';
37
+
38
+ const HIDDEN_STYLES: any = {
39
+ border: 0,
40
+ clip: 'rect(0 0 0 0)',
41
+ height: '1px',
42
+ margin: '-1px',
43
+ overflow: 'hidden',
44
+ padding: 0,
45
+ position: 'fixed',
46
+ whiteSpace: 'nowrap',
47
+ width: '1px',
48
+ top: 0,
49
+ left: 0,
50
+ };
51
+
52
+ // ── markOthers (aria-hidden / inert the rest of the document) ────────────────
53
+ const counters: any = {
54
+ inert: new WeakMap(),
55
+ 'aria-hidden': new WeakMap(),
56
+ none: new WeakMap(),
57
+ };
58
+ function getCounterMap(control: any) {
59
+ if (control === 'inert') return counters.inert;
60
+ if (control === 'aria-hidden') return counters['aria-hidden'];
61
+ return counters.none;
62
+ }
63
+ let uncontrolledElementsSet = new WeakSet<any>();
64
+ let markerMap: any = {};
65
+ let lockCount = 0;
66
+ export const supportsInert = () =>
67
+ typeof HTMLElement !== 'undefined' && 'inert' in HTMLElement.prototype;
68
+ function unwrapHost(node: any): any {
69
+ if (!node) {
70
+ return null;
71
+ }
72
+ return isShadowRoot(node) ? (node as any).host : unwrapHost(node.parentNode);
73
+ }
74
+ const correctElements = (parent: any, targets: any[]) =>
75
+ targets
76
+ .map((target) => {
77
+ if (parent.contains(target)) {
78
+ return target;
79
+ }
80
+ const correctedTarget = unwrapHost(target);
81
+ if (parent.contains(correctedTarget)) {
82
+ return correctedTarget;
83
+ }
84
+ return null;
85
+ })
86
+ .filter((x) => x != null);
87
+ function applyAttributeToOthers(
88
+ uncorrectedAvoidElements: any[],
89
+ body: any,
90
+ ariaHidden: boolean,
91
+ inert: boolean,
92
+ ): () => void {
93
+ const markerName = 'data-floating-ui-inert';
94
+ const controlAttribute = inert ? 'inert' : ariaHidden ? 'aria-hidden' : null;
95
+ const avoidElements = correctElements(body, uncorrectedAvoidElements);
96
+ const elementsToKeep = new Set<any>();
97
+ const elementsToStop = new Set<any>(avoidElements);
98
+ const hiddenElements: any[] = [];
99
+ if (!markerMap[markerName]) {
100
+ markerMap[markerName] = new WeakMap();
101
+ }
102
+ const markerCounter = markerMap[markerName];
103
+ avoidElements.forEach(keep);
104
+ deep(body);
105
+ elementsToKeep.clear();
106
+ function keep(el: any) {
107
+ if (!el || elementsToKeep.has(el)) {
108
+ return;
109
+ }
110
+ elementsToKeep.add(el);
111
+ el.parentNode && keep(el.parentNode);
112
+ }
113
+ function deep(parent: any) {
114
+ if (!parent || elementsToStop.has(parent)) {
115
+ return;
116
+ }
117
+ [].forEach.call(parent.children, (node: any) => {
118
+ if (getNodeName(node) === 'script') return;
119
+ if (elementsToKeep.has(node)) {
120
+ deep(node);
121
+ } else {
122
+ const attr = controlAttribute ? node.getAttribute(controlAttribute) : null;
123
+ const alreadyHidden = attr !== null && attr !== 'false';
124
+ const counterMap = getCounterMap(controlAttribute);
125
+ const counterValue = (counterMap.get(node) || 0) + 1;
126
+ const markerValue = (markerCounter.get(node) || 0) + 1;
127
+ counterMap.set(node, counterValue);
128
+ markerCounter.set(node, markerValue);
129
+ hiddenElements.push(node);
130
+ if (counterValue === 1 && alreadyHidden) {
131
+ uncontrolledElementsSet.add(node);
132
+ }
133
+ if (markerValue === 1) {
134
+ node.setAttribute(markerName, '');
135
+ }
136
+ if (!alreadyHidden && controlAttribute) {
137
+ node.setAttribute(controlAttribute, controlAttribute === 'inert' ? '' : 'true');
138
+ }
139
+ }
140
+ });
141
+ }
142
+ lockCount++;
143
+ return () => {
144
+ hiddenElements.forEach((element) => {
145
+ const counterMap = getCounterMap(controlAttribute);
146
+ const currentCounterValue = counterMap.get(element) || 0;
147
+ const counterValue = currentCounterValue - 1;
148
+ const markerValue = (markerCounter.get(element) || 0) - 1;
149
+ counterMap.set(element, counterValue);
150
+ markerCounter.set(element, markerValue);
151
+ if (!counterValue) {
152
+ if (!uncontrolledElementsSet.has(element) && controlAttribute) {
153
+ element.removeAttribute(controlAttribute);
154
+ }
155
+ uncontrolledElementsSet.delete(element);
156
+ }
157
+ if (!markerValue) {
158
+ element.removeAttribute(markerName);
159
+ }
160
+ });
161
+ lockCount--;
162
+ if (!lockCount) {
163
+ counters.inert = new WeakMap();
164
+ counters['aria-hidden'] = new WeakMap();
165
+ counters.none = new WeakMap();
166
+ uncontrolledElementsSet = new WeakSet();
167
+ markerMap = {};
168
+ }
169
+ };
170
+ }
171
+ function markOthers(avoidElements: any[], ariaHidden = false, inert = false): () => void {
172
+ const body = getDocument(avoidElements[0]).body;
173
+ return applyAttributeToOthers(
174
+ avoidElements.concat(Array.from(body.querySelectorAll('[aria-live],[role="status"],output'))),
175
+ body,
176
+ ariaHidden,
177
+ inert,
178
+ );
179
+ }
180
+
181
+ // ── previously-focused element tracking (for return focus) ───────────────────
182
+ const LIST_LIMIT = 20;
183
+ let previouslyFocusedElements: any[] = [];
184
+ function clearDisconnectedPreviouslyFocusedElements() {
185
+ previouslyFocusedElements = previouslyFocusedElements.filter(
186
+ (elementRef) => elementRef.deref()?.isConnected,
187
+ );
188
+ }
189
+ function addPreviouslyFocusedElement(element: any) {
190
+ clearDisconnectedPreviouslyFocusedElements();
191
+ if (element && getNodeName(element) !== 'body') {
192
+ previouslyFocusedElements.push(new WeakRef(element));
193
+ if (previouslyFocusedElements.length > LIST_LIMIT) {
194
+ previouslyFocusedElements = previouslyFocusedElements.slice(-LIST_LIMIT);
195
+ }
196
+ }
197
+ }
198
+ function getPreviouslyFocusedElement() {
199
+ clearDisconnectedPreviouslyFocusedElements();
200
+ return previouslyFocusedElements[previouslyFocusedElements.length - 1]?.deref();
201
+ }
202
+ function getFirstTabbableElement(container: any) {
203
+ const tabbableOptions = getTabbableOptions();
204
+ if (isTabbable(container, tabbableOptions)) {
205
+ return container;
206
+ }
207
+ return tabbable(container, tabbableOptions)[0] || container;
208
+ }
209
+ function handleTabIndex(floatingFocusElement: any, orderRef: any) {
210
+ if (
211
+ !orderRef.current.includes('floating') &&
212
+ !floatingFocusElement.getAttribute('role')?.includes('dialog')
213
+ ) {
214
+ return;
215
+ }
216
+ const options = getTabbableOptions();
217
+ const focusableElements = focusable(floatingFocusElement, options);
218
+ const tabbableContent = focusableElements.filter((element: any) => {
219
+ const dataTabIndex = element.getAttribute('data-tabindex') || '';
220
+ return (
221
+ isTabbable(element, options) ||
222
+ (element.hasAttribute('data-tabindex') && !dataTabIndex.startsWith('-'))
223
+ );
224
+ });
225
+ const tabIndex = floatingFocusElement.getAttribute('tabindex');
226
+ if (orderRef.current.includes('floating') || tabbableContent.length === 0) {
227
+ if (tabIndex !== '0') {
228
+ floatingFocusElement.setAttribute('tabindex', '0');
229
+ }
230
+ } else if (
231
+ tabIndex !== '-1' ||
232
+ (floatingFocusElement.hasAttribute('data-tabindex') &&
233
+ floatingFocusElement.getAttribute('data-tabindex') !== '-1')
234
+ ) {
235
+ floatingFocusElement.setAttribute('tabindex', '-1');
236
+ floatingFocusElement.setAttribute('data-tabindex', '-1');
237
+ }
238
+ }
239
+
240
+ function useLiteMergeRefs(refs: any[], slot: symbol): any {
241
+ return useMemo(
242
+ () => {
243
+ return (value: any) => {
244
+ refs.forEach((ref) => {
245
+ if (ref) {
246
+ ref.current = value;
247
+ }
248
+ });
249
+ };
250
+ },
251
+ refs,
252
+ slot,
253
+ );
254
+ }
255
+
256
+ export function VisuallyHiddenDismiss(props: any): any {
257
+ return createElement('button', {
258
+ ...props,
259
+ type: 'button',
260
+ ref: props.ref,
261
+ tabIndex: -1,
262
+ style: HIDDEN_STYLES,
263
+ });
264
+ }
265
+
266
+ export function FloatingFocusManager(props: any): any {
267
+ const context = props.context;
268
+ const children = props.children;
269
+ const disabled = props.disabled ?? false;
270
+ const order = props.order ?? ['content'];
271
+ const _guards = props.guards ?? true;
272
+ const initialFocus = props.initialFocus ?? 0;
273
+ const returnFocus = props.returnFocus ?? true;
274
+ const restoreFocus = props.restoreFocus ?? false;
275
+ const modal = props.modal ?? true;
276
+ const visuallyHiddenDismiss = props.visuallyHiddenDismiss ?? false;
277
+ const closeOnFocusOut = props.closeOnFocusOut ?? true;
278
+ const outsideElementsInert = props.outsideElementsInert ?? false;
279
+ const _getInsideElements = props.getInsideElements ?? (() => []);
280
+
281
+ const open = context.open;
282
+ const onOpenChange = context.onOpenChange;
283
+ const events = context.events;
284
+ const dataRef = context.dataRef;
285
+ const domReference = context.elements.domReference;
286
+ const floating = context.elements.floating;
287
+
288
+ const getNodeId = useEffectEvent(
289
+ () => dataRef.current.floatingContext?.nodeId,
290
+ S('FFM:getNodeId'),
291
+ );
292
+ const getInsideElements = useEffectEvent(_getInsideElements, S('FFM:getInside'));
293
+ const ignoreInitialFocus = typeof initialFocus === 'number' && initialFocus < 0;
294
+ const isUntrappedTypeableCombobox = isTypeableCombobox(domReference) && ignoreInitialFocus;
295
+
296
+ const inertSupported = supportsInert();
297
+ const guards = inertSupported ? _guards : true;
298
+ const useInert = !guards || (inertSupported && outsideElementsInert);
299
+ const orderRef = useLatestRef(order, S('FFM:order'));
300
+ const initialFocusRef = useLatestRef(initialFocus, S('FFM:initial'));
301
+ const returnFocusRef = useLatestRef(returnFocus, S('FFM:return'));
302
+ const tree = useFloatingTree();
303
+ const portalContext = usePortalContext();
304
+ const startDismissButtonRef = useRef<any>(null, S('FFM:startDismiss'));
305
+ const endDismissButtonRef = useRef<any>(null, S('FFM:endDismiss'));
306
+ const preventReturnFocusRef = useRef(false, S('FFM:preventReturn'));
307
+ const isPointerDownRef = useRef(false, S('FFM:pointerDown'));
308
+ const tabbableIndexRef = useRef(-1, S('FFM:tabbableIndex'));
309
+ const blurTimeoutRef = useRef(-1, S('FFM:blurTimeout'));
310
+ const isInsidePortal = portalContext != null;
311
+ const floatingFocusElement = getFloatingFocusElement(floating);
312
+
313
+ const getTabbableContent = useEffectEvent((container = floatingFocusElement) => {
314
+ return container ? tabbable(container, getTabbableOptions()) : [];
315
+ }, S('FFM:getContent'));
316
+ const getTabbableElements = useEffectEvent((container?: any) => {
317
+ const content = getTabbableContent(container);
318
+ return orderRef.current
319
+ .map((type: string) => {
320
+ if (domReference && type === 'reference') {
321
+ return domReference;
322
+ }
323
+ if (floatingFocusElement && type === 'floating') {
324
+ return floatingFocusElement;
325
+ }
326
+ return content;
327
+ })
328
+ .filter(Boolean)
329
+ .flat();
330
+ }, S('FFM:getEls'));
331
+
332
+ useEffect(
333
+ () => {
334
+ if (disabled) return;
335
+ if (!modal) return;
336
+ function onKeyDown(event: any) {
337
+ if (event.key === 'Tab') {
338
+ if (
339
+ contains(
340
+ floatingFocusElement,
341
+ activeElement(getDocument(floatingFocusElement)) as any,
342
+ ) &&
343
+ getTabbableContent().length === 0 &&
344
+ !isUntrappedTypeableCombobox
345
+ ) {
346
+ stopEvent(event);
347
+ }
348
+ const els = getTabbableElements();
349
+ const target = getTarget(event);
350
+ if (orderRef.current[0] === 'reference' && target === domReference) {
351
+ stopEvent(event);
352
+ if (event.shiftKey) {
353
+ enqueueFocus(els[els.length - 1]);
354
+ } else {
355
+ enqueueFocus(els[1]);
356
+ }
357
+ }
358
+ if (
359
+ orderRef.current[1] === 'floating' &&
360
+ target === floatingFocusElement &&
361
+ event.shiftKey
362
+ ) {
363
+ stopEvent(event);
364
+ enqueueFocus(els[0]);
365
+ }
366
+ }
367
+ }
368
+ const doc = getDocument(floatingFocusElement);
369
+ doc.addEventListener('keydown', onKeyDown);
370
+ return () => {
371
+ doc.removeEventListener('keydown', onKeyDown);
372
+ };
373
+ },
374
+ [
375
+ disabled,
376
+ domReference,
377
+ floatingFocusElement,
378
+ modal,
379
+ orderRef,
380
+ isUntrappedTypeableCombobox,
381
+ getTabbableContent,
382
+ getTabbableElements,
383
+ ],
384
+ S('FFM:e:keydown'),
385
+ );
386
+
387
+ useEffect(
388
+ () => {
389
+ if (disabled) return;
390
+ if (!floating) return;
391
+ function handleFocusIn(event: any) {
392
+ const target = getTarget(event);
393
+ const tabbableContent = getTabbableContent();
394
+ const tabbableIndex = tabbableContent.indexOf(target as any);
395
+ if (tabbableIndex !== -1) {
396
+ tabbableIndexRef.current = tabbableIndex;
397
+ }
398
+ }
399
+ floating.addEventListener('focusin', handleFocusIn);
400
+ return () => {
401
+ floating.removeEventListener('focusin', handleFocusIn);
402
+ };
403
+ },
404
+ [disabled, floating, getTabbableContent],
405
+ S('FFM:e:focusin'),
406
+ );
407
+
408
+ useEffect(
409
+ () => {
410
+ if (disabled) return;
411
+ if (!closeOnFocusOut) return;
412
+ function handlePointerDown() {
413
+ isPointerDownRef.current = true;
414
+ setTimeout(() => {
415
+ isPointerDownRef.current = false;
416
+ });
417
+ }
418
+ function handleFocusOutside(event: any) {
419
+ const relatedTarget = event.relatedTarget;
420
+ const currentTarget = event.currentTarget;
421
+ const target = getTarget(event);
422
+ queueMicrotask(() => {
423
+ const nodeId = getNodeId();
424
+ const movedToUnrelatedNode = !(
425
+ contains(domReference, relatedTarget) ||
426
+ contains(floating, relatedTarget) ||
427
+ contains(relatedTarget, floating) ||
428
+ contains(portalContext?.portalNode, relatedTarget) ||
429
+ (relatedTarget != null && relatedTarget.hasAttribute(createAttribute('focus-guard'))) ||
430
+ (tree &&
431
+ (getNodeChildren(tree.nodesRef.current, nodeId).find(
432
+ (node: any) =>
433
+ contains(node.context?.elements.floating, relatedTarget) ||
434
+ contains(node.context?.elements.domReference, relatedTarget),
435
+ ) ||
436
+ getNodeAncestors(tree.nodesRef.current, nodeId).find(
437
+ (node: any) =>
438
+ [
439
+ node.context?.elements.floating,
440
+ getFloatingFocusElement(node.context?.elements.floating),
441
+ ].includes(relatedTarget) ||
442
+ node.context?.elements.domReference === relatedTarget,
443
+ )))
444
+ );
445
+ if (currentTarget === domReference && floatingFocusElement) {
446
+ handleTabIndex(floatingFocusElement, orderRef);
447
+ }
448
+ if (
449
+ restoreFocus &&
450
+ currentTarget !== domReference &&
451
+ !(target as any)?.isConnected &&
452
+ activeElement(getDocument(floatingFocusElement)) ===
453
+ getDocument(floatingFocusElement).body
454
+ ) {
455
+ if (isHTMLElement(floatingFocusElement)) {
456
+ floatingFocusElement.focus();
457
+ }
458
+ const prevTabbableIndex = tabbableIndexRef.current;
459
+ const tabbableContent = getTabbableContent();
460
+ const nodeToFocus =
461
+ tabbableContent[prevTabbableIndex] ||
462
+ tabbableContent[tabbableContent.length - 1] ||
463
+ floatingFocusElement;
464
+ if (isHTMLElement(nodeToFocus)) {
465
+ nodeToFocus.focus();
466
+ }
467
+ }
468
+ if (dataRef.current.insideReactTree) {
469
+ dataRef.current.insideReactTree = false;
470
+ return;
471
+ }
472
+ if (
473
+ (isUntrappedTypeableCombobox ? true : !modal) &&
474
+ relatedTarget &&
475
+ movedToUnrelatedNode &&
476
+ !isPointerDownRef.current &&
477
+ relatedTarget !== getPreviouslyFocusedElement()
478
+ ) {
479
+ preventReturnFocusRef.current = true;
480
+ onOpenChange(false, event, 'focus-out');
481
+ }
482
+ });
483
+ }
484
+ const shouldHandleBlurCapture = Boolean(!tree && portalContext);
485
+ function markInsideReactTree() {
486
+ clearTimeoutIfSet(blurTimeoutRef);
487
+ dataRef.current.insideReactTree = true;
488
+ blurTimeoutRef.current = window.setTimeout(() => {
489
+ dataRef.current.insideReactTree = false;
490
+ });
491
+ }
492
+ if (floating && isHTMLElement(domReference)) {
493
+ domReference.addEventListener('focusout', handleFocusOutside);
494
+ domReference.addEventListener('pointerdown', handlePointerDown);
495
+ floating.addEventListener('focusout', handleFocusOutside);
496
+ if (shouldHandleBlurCapture) {
497
+ floating.addEventListener('focusout', markInsideReactTree, true);
498
+ }
499
+ return () => {
500
+ domReference.removeEventListener('focusout', handleFocusOutside);
501
+ domReference.removeEventListener('pointerdown', handlePointerDown);
502
+ floating.removeEventListener('focusout', handleFocusOutside);
503
+ if (shouldHandleBlurCapture) {
504
+ floating.removeEventListener('focusout', markInsideReactTree, true);
505
+ }
506
+ };
507
+ }
508
+ },
509
+ [
510
+ disabled,
511
+ domReference,
512
+ floating,
513
+ floatingFocusElement,
514
+ modal,
515
+ tree,
516
+ portalContext,
517
+ onOpenChange,
518
+ closeOnFocusOut,
519
+ restoreFocus,
520
+ getTabbableContent,
521
+ isUntrappedTypeableCombobox,
522
+ getNodeId,
523
+ orderRef,
524
+ dataRef,
525
+ ],
526
+ S('FFM:e:focusout'),
527
+ );
528
+
529
+ const beforeGuardRef = useRef<any>(null, S('FFM:beforeGuard'));
530
+ const afterGuardRef = useRef<any>(null, S('FFM:afterGuard'));
531
+ const mergedBeforeGuardRef = useLiteMergeRefs(
532
+ [beforeGuardRef, portalContext?.beforeInsideRef],
533
+ S('FFM:mergeBefore'),
534
+ );
535
+ const mergedAfterGuardRef = useLiteMergeRefs(
536
+ [afterGuardRef, portalContext?.afterInsideRef],
537
+ S('FFM:mergeAfter'),
538
+ );
539
+
540
+ useEffect(
541
+ () => {
542
+ if (disabled) return;
543
+ if (!floating) return;
544
+ const portalNodes = Array.from(
545
+ portalContext?.portalNode?.querySelectorAll('[' + createAttribute('portal') + ']') || [],
546
+ );
547
+ const ancestors = tree ? getNodeAncestors(tree.nodesRef.current, getNodeId()) : [];
548
+ const rootAncestorComboboxDomReference = ancestors.find((node: any) =>
549
+ isTypeableCombobox(node.context?.elements.domReference || null),
550
+ )?.context?.elements.domReference;
551
+ const insideElements = [
552
+ floating,
553
+ rootAncestorComboboxDomReference,
554
+ ...portalNodes,
555
+ ...getInsideElements(),
556
+ startDismissButtonRef.current,
557
+ endDismissButtonRef.current,
558
+ beforeGuardRef.current,
559
+ afterGuardRef.current,
560
+ portalContext?.beforeOutsideRef.current,
561
+ portalContext?.afterOutsideRef.current,
562
+ orderRef.current.includes('reference') || isUntrappedTypeableCombobox ? domReference : null,
563
+ ].filter((x) => x != null);
564
+ const cleanup =
565
+ modal || isUntrappedTypeableCombobox
566
+ ? markOthers(insideElements, !useInert, useInert)
567
+ : markOthers(insideElements);
568
+ return () => {
569
+ cleanup();
570
+ };
571
+ },
572
+ [
573
+ disabled,
574
+ domReference,
575
+ floating,
576
+ modal,
577
+ orderRef,
578
+ portalContext,
579
+ isUntrappedTypeableCombobox,
580
+ guards,
581
+ useInert,
582
+ tree,
583
+ getNodeId,
584
+ getInsideElements,
585
+ ],
586
+ S('FFM:e:markOthers'),
587
+ );
588
+
589
+ useModernLayoutEffect(
590
+ () => {
591
+ if (disabled || !isHTMLElement(floatingFocusElement)) return;
592
+ const previouslyFocusedElement = activeElement(getDocument(floatingFocusElement));
593
+ queueMicrotask(() => {
594
+ const focusableElements = getTabbableElements(floatingFocusElement);
595
+ const initialFocusValue = initialFocusRef.current;
596
+ const elToFocus =
597
+ (typeof initialFocusValue === 'number'
598
+ ? focusableElements[initialFocusValue]
599
+ : initialFocusValue.current) || floatingFocusElement;
600
+ const focusAlreadyInsideFloatingEl = contains(
601
+ floatingFocusElement,
602
+ previouslyFocusedElement as any,
603
+ );
604
+ if (!ignoreInitialFocus && !focusAlreadyInsideFloatingEl && open) {
605
+ enqueueFocus(elToFocus, { preventScroll: elToFocus === floatingFocusElement });
606
+ }
607
+ });
608
+ },
609
+ [
610
+ disabled,
611
+ open,
612
+ floatingFocusElement,
613
+ ignoreInitialFocus,
614
+ getTabbableElements,
615
+ initialFocusRef,
616
+ ],
617
+ S('FFM:e:initialFocus'),
618
+ );
619
+
620
+ useModernLayoutEffect(
621
+ () => {
622
+ if (disabled || !floatingFocusElement) return;
623
+ const doc = getDocument(floatingFocusElement);
624
+ const previouslyFocusedElement = activeElement(doc);
625
+ addPreviouslyFocusedElement(previouslyFocusedElement);
626
+
627
+ function onOpenChangeLocal(_ref: any) {
628
+ const { reason, event, nested } = _ref;
629
+ if (['hover', 'safe-polygon'].includes(reason) && event.type === 'mouseleave') {
630
+ preventReturnFocusRef.current = true;
631
+ }
632
+ if (reason !== 'outside-press') return;
633
+ if (nested) {
634
+ preventReturnFocusRef.current = false;
635
+ } else if (isVirtualClick(event) || isVirtualPointerEvent(event)) {
636
+ preventReturnFocusRef.current = false;
637
+ } else {
638
+ let isPreventScrollSupported = false;
639
+ document.createElement('div').focus({
640
+ get preventScroll() {
641
+ isPreventScrollSupported = true;
642
+ return false;
643
+ },
644
+ } as any);
645
+ preventReturnFocusRef.current = !isPreventScrollSupported;
646
+ }
647
+ }
648
+ events.on('openchange', onOpenChangeLocal);
649
+ const fallbackEl = doc.createElement('span');
650
+ fallbackEl.setAttribute('tabindex', '-1');
651
+ fallbackEl.setAttribute('aria-hidden', 'true');
652
+ Object.assign(fallbackEl.style, HIDDEN_STYLES);
653
+ if (isInsidePortal && domReference) {
654
+ domReference.insertAdjacentElement('afterend', fallbackEl);
655
+ }
656
+ function getReturnElement() {
657
+ if (typeof returnFocusRef.current === 'boolean') {
658
+ const el = domReference || getPreviouslyFocusedElement();
659
+ return el && el.isConnected ? el : fallbackEl;
660
+ }
661
+ return returnFocusRef.current.current || fallbackEl;
662
+ }
663
+ return () => {
664
+ events.off('openchange', onOpenChangeLocal);
665
+ const activeEl = activeElement(doc);
666
+ const isFocusInsideFloatingTree =
667
+ contains(floating, activeEl as any) ||
668
+ (tree &&
669
+ getNodeChildren(tree.nodesRef.current, getNodeId(), false).some((node: any) =>
670
+ contains(node.context?.elements.floating, activeEl as any),
671
+ ));
672
+ const returnElement = getReturnElement();
673
+ queueMicrotask(() => {
674
+ const tabbableReturnElement = getFirstTabbableElement(returnElement);
675
+ if (
676
+ returnFocusRef.current &&
677
+ !preventReturnFocusRef.current &&
678
+ isHTMLElement(tabbableReturnElement) &&
679
+ (tabbableReturnElement !== activeEl && activeEl !== doc.body
680
+ ? isFocusInsideFloatingTree
681
+ : true)
682
+ ) {
683
+ tabbableReturnElement.focus({ preventScroll: true });
684
+ }
685
+ fallbackEl.remove();
686
+ });
687
+ };
688
+ },
689
+ [
690
+ disabled,
691
+ floating,
692
+ floatingFocusElement,
693
+ returnFocusRef,
694
+ dataRef,
695
+ events,
696
+ tree,
697
+ isInsidePortal,
698
+ domReference,
699
+ getNodeId,
700
+ ],
701
+ S('FFM:e:returnFocus'),
702
+ );
703
+
704
+ useEffect(
705
+ () => {
706
+ queueMicrotask(() => {
707
+ preventReturnFocusRef.current = false;
708
+ });
709
+ return () => {
710
+ queueMicrotask(clearDisconnectedPreviouslyFocusedElements);
711
+ };
712
+ },
713
+ [disabled],
714
+ S('FFM:e:resetReturn'),
715
+ );
716
+
717
+ useModernLayoutEffect(
718
+ () => {
719
+ if (disabled) return;
720
+ if (!portalContext) return;
721
+ portalContext.setFocusManagerState({
722
+ modal,
723
+ closeOnFocusOut,
724
+ open,
725
+ onOpenChange,
726
+ domReference,
727
+ });
728
+ return () => {
729
+ portalContext.setFocusManagerState(null);
730
+ };
731
+ },
732
+ [disabled, portalContext, modal, open, onOpenChange, closeOnFocusOut, domReference],
733
+ S('FFM:e:portalState'),
734
+ );
735
+
736
+ useModernLayoutEffect(
737
+ () => {
738
+ if (disabled) return;
739
+ if (!floatingFocusElement) return;
740
+ handleTabIndex(floatingFocusElement, orderRef);
741
+ },
742
+ [disabled, floatingFocusElement, orderRef],
743
+ S('FFM:e:tabIndex'),
744
+ );
745
+
746
+ function renderDismissButton(location: string) {
747
+ if (disabled || !visuallyHiddenDismiss || !modal) {
748
+ return null;
749
+ }
750
+ return createElement(VisuallyHiddenDismiss, {
751
+ ref: location === 'start' ? startDismissButtonRef : endDismissButtonRef,
752
+ onClick: (event: any) => onOpenChange(false, event),
753
+ children: typeof visuallyHiddenDismiss === 'string' ? visuallyHiddenDismiss : 'Dismiss',
754
+ });
755
+ }
756
+
757
+ const shouldRenderGuards =
758
+ !disabled &&
759
+ guards &&
760
+ (modal ? !isUntrappedTypeableCombobox : true) &&
761
+ (isInsidePortal || modal);
762
+
763
+ return [
764
+ shouldRenderGuards &&
765
+ createElement(FocusGuard, {
766
+ 'data-type': 'inside',
767
+ ref: mergedBeforeGuardRef,
768
+ onFocus: (event: any) => {
769
+ if (modal) {
770
+ const els = getTabbableElements();
771
+ enqueueFocus(order[0] === 'reference' ? els[0] : els[els.length - 1]);
772
+ } else if (
773
+ portalContext != null &&
774
+ portalContext.preserveTabOrder &&
775
+ portalContext.portalNode
776
+ ) {
777
+ preventReturnFocusRef.current = false;
778
+ if (isOutsideEvent(event, portalContext.portalNode)) {
779
+ getNextTabbable(domReference)?.focus();
780
+ } else {
781
+ portalContext.beforeOutsideRef.current?.focus();
782
+ }
783
+ }
784
+ },
785
+ }),
786
+ !isUntrappedTypeableCombobox && renderDismissButton('start'),
787
+ children,
788
+ renderDismissButton('end'),
789
+ shouldRenderGuards &&
790
+ createElement(FocusGuard, {
791
+ 'data-type': 'inside',
792
+ ref: mergedAfterGuardRef,
793
+ onFocus: (event: any) => {
794
+ if (modal) {
795
+ enqueueFocus(getTabbableElements()[0]);
796
+ } else if (
797
+ portalContext != null &&
798
+ portalContext.preserveTabOrder &&
799
+ portalContext.portalNode
800
+ ) {
801
+ if (closeOnFocusOut) {
802
+ preventReturnFocusRef.current = true;
803
+ }
804
+ if (isOutsideEvent(event, portalContext.portalNode)) {
805
+ getPreviousTabbable(domReference)?.focus();
806
+ } else {
807
+ portalContext.afterOutsideRef.current?.focus();
808
+ }
809
+ }
810
+ },
811
+ }),
812
+ ];
813
+ }