@octanejs/base-ui 0.1.1 → 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.
- package/README.md +7 -1
- package/package.json +12 -4
- package/src/alert-dialog.ts +47 -0
- package/src/checkbox.ts +7 -16
- package/src/dialog.ts +859 -0
- package/src/field.ts +4 -30
- package/src/index.ts +3 -0
- package/src/number-field.ts +8 -45
- package/src/popover.ts +1205 -0
- package/src/radio.ts +3 -24
- package/src/slider.ts +4 -5
- package/src/switch.ts +4 -30
- package/src/utils/InternalBackdrop.ts +28 -0
- package/src/utils/adaptiveOriginMiddleware.ts +82 -0
- package/src/utils/closePart.ts +61 -0
- package/src/utils/constants.ts +9 -0
- package/src/utils/createChangeEventDetails.ts +7 -0
- package/src/utils/dom.ts +9 -0
- package/src/utils/empty.ts +7 -0
- package/src/utils/floating/FloatingFocusManager.ts +833 -0
- package/src/utils/floating/FloatingPortal.ts +268 -0
- package/src/utils/floating/FloatingRootStore.ts +127 -0
- package/src/utils/floating/FloatingTree.ts +70 -0
- package/src/utils/floating/FloatingTreeStore.ts +22 -0
- package/src/utils/floating/FocusGuard.ts +34 -0
- package/src/utils/floating/composite.ts +20 -0
- package/src/utils/floating/constants.ts +8 -0
- package/src/utils/floating/createAttribute.ts +4 -0
- package/src/utils/floating/createEventEmitter.ts +20 -0
- package/src/utils/floating/element.ts +54 -0
- package/src/utils/floating/enqueueFocus.ts +38 -0
- package/src/utils/floating/event.ts +53 -0
- package/src/utils/floating/getEmptyRootContext.ts +19 -0
- package/src/utils/floating/markOthers.ts +197 -0
- package/src/utils/floating/nodes.ts +31 -0
- package/src/utils/floating/tabbable.ts +260 -0
- package/src/utils/floating/types.ts +57 -0
- package/src/utils/floating/useClick.ts +174 -0
- package/src/utils/floating/useDismiss.ts +641 -0
- package/src/utils/floating/useFloating.ts +198 -0
- package/src/utils/floating/useFloatingRootContext.ts +81 -0
- package/src/utils/floating/useHoverFloatingInteraction.ts +12 -0
- package/src/utils/floating/useHoverReferenceInteraction.ts +17 -0
- package/src/utils/floating/useSyncedFloatingRootContext.ts +93 -0
- package/src/utils/getDisabledMountTransitionStyles.ts +12 -0
- package/src/utils/hideMiddleware.ts +22 -0
- package/src/utils/inertValue.ts +5 -0
- package/src/utils/mergeCleanups.ts +13 -0
- package/src/utils/platform.ts +46 -2
- package/src/utils/popupStateMapping.ts +48 -0
- package/src/utils/popups/index.ts +4 -0
- package/src/utils/popups/popupStoreUtils.ts +484 -0
- package/src/utils/popups/popupTriggerMap.ts +62 -0
- package/src/utils/popups/store.ts +147 -0
- package/src/utils/popups/useTriggerFocusGuards.ts +73 -0
- package/src/utils/store/ReactStore.ts +168 -0
- package/src/utils/store/Store.ts +84 -0
- package/src/utils/store/createSelector.ts +66 -0
- package/src/utils/store/useStore.ts +49 -0
- package/src/utils/useAnchorPositioning.ts +581 -0
- package/src/utils/useAnchoredPopupScrollLock.ts +50 -0
- package/src/utils/useAnimationFrame.ts +28 -5
- package/src/utils/useEnhancedClickHandler.ts +47 -0
- package/src/utils/useOnFirstRender.ts +11 -0
- package/src/utils/useOpenInteractionType.ts +32 -0
- package/src/utils/usePositioner.ts +48 -0
- package/src/utils/useScrollLock.ts +253 -0
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/floating-ui-react/components/FloatingFocusManager.tsx
|
|
2
|
+
// (v1.6.0), octane-adapted: reads the FloatingRootStore (`store.useState`/`context`/`setOpen`);
|
|
3
|
+
// native events; `useMergedRefs` → `useComposedRefs`; `useIsoLayoutEffect` → `useLayoutEffect`;
|
|
4
|
+
// every hook threads an explicit slot; returns `[beforeGuard?, children, afterGuard?]`. Traps focus
|
|
5
|
+
// inside the popup, hides the rest of the page from assistive tech (`markOthers`), restores focus on
|
|
6
|
+
// close. Emits `data-base-ui-*` attributes for parity with Base UI.
|
|
7
|
+
import { createElement, useState, useRef, useEffect, useLayoutEffect } from 'octane';
|
|
8
|
+
import { getNodeName, isHTMLElement } from '@floating-ui/utils/dom';
|
|
9
|
+
|
|
10
|
+
import { S, subSlot } from '../../internal';
|
|
11
|
+
import { addEventListener } from '../addEventListener';
|
|
12
|
+
import { mergeCleanups } from '../mergeCleanups';
|
|
13
|
+
import { useComposedRefs } from '../composeRefs';
|
|
14
|
+
import { useValueAsRef } from '../useValueAsRef';
|
|
15
|
+
import { useStableCallback } from '../useStableCallback';
|
|
16
|
+
import { useTimeout } from '../useTimeout';
|
|
17
|
+
import { useAnimationFrame } from '../useAnimationFrame';
|
|
18
|
+
import { platform } from '../platform';
|
|
19
|
+
import { ownerDocument, ownerWindow } from '../owner';
|
|
20
|
+
import { resolveRef } from '../resolveRef';
|
|
21
|
+
import type { InteractionType } from '../useEnhancedClickHandler';
|
|
22
|
+
import { FocusGuard } from './FocusGuard';
|
|
23
|
+
import {
|
|
24
|
+
activeElement,
|
|
25
|
+
contains,
|
|
26
|
+
getTarget,
|
|
27
|
+
isTypeableCombobox,
|
|
28
|
+
getFloatingFocusElement,
|
|
29
|
+
isTypeableElement,
|
|
30
|
+
} from './element';
|
|
31
|
+
import { isVirtualClick, isVirtualPointerEvent } from './event';
|
|
32
|
+
import { stopEvent } from '../composite/list-utils';
|
|
33
|
+
import {
|
|
34
|
+
tabbable,
|
|
35
|
+
focusable,
|
|
36
|
+
isOutsideEvent,
|
|
37
|
+
isTabbable,
|
|
38
|
+
getNextTabbable,
|
|
39
|
+
getPreviousTabbable,
|
|
40
|
+
type FocusableElement,
|
|
41
|
+
} from './tabbable';
|
|
42
|
+
import { getNodeAncestors, getNodeChildren } from './nodes';
|
|
43
|
+
import { isElementVisible } from './composite';
|
|
44
|
+
import type { FloatingContext, FloatingRootContext } from './types';
|
|
45
|
+
import { createChangeEventDetails, REASONS } from '../createChangeEventDetails';
|
|
46
|
+
import { createAttribute } from './createAttribute';
|
|
47
|
+
import { CLICK_TRIGGER_IDENTIFIER } from './constants';
|
|
48
|
+
import { enqueueFocus } from './enqueueFocus';
|
|
49
|
+
import { markOthers } from './markOthers';
|
|
50
|
+
import { usePortalContext } from './FloatingPortal';
|
|
51
|
+
import { useFloatingTree } from './FloatingTree';
|
|
52
|
+
import type { FloatingTreeStore } from './FloatingTreeStore';
|
|
53
|
+
|
|
54
|
+
interface FloatingUIOpenChangeDetails {
|
|
55
|
+
open: boolean;
|
|
56
|
+
reason: string;
|
|
57
|
+
nativeEvent: Event;
|
|
58
|
+
nested: boolean;
|
|
59
|
+
triggerElement?: Element | undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getEventType(event: Event, lastInteractionType?: InteractionType): InteractionType {
|
|
63
|
+
const win = ownerWindow(getTarget(event) as any);
|
|
64
|
+
if (event instanceof win.KeyboardEvent) {
|
|
65
|
+
return 'keyboard';
|
|
66
|
+
}
|
|
67
|
+
if (event instanceof win.FocusEvent) {
|
|
68
|
+
return lastInteractionType || 'keyboard';
|
|
69
|
+
}
|
|
70
|
+
if ('pointerType' in event) {
|
|
71
|
+
return ((event as PointerEvent).pointerType as InteractionType) || 'keyboard';
|
|
72
|
+
}
|
|
73
|
+
if ('touches' in event) {
|
|
74
|
+
return 'touch';
|
|
75
|
+
}
|
|
76
|
+
if (event instanceof win.MouseEvent) {
|
|
77
|
+
return lastInteractionType || ((event as MouseEvent).detail === 0 ? 'keyboard' : 'mouse');
|
|
78
|
+
}
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const LIST_LIMIT = 20;
|
|
83
|
+
let previouslyFocusedElements: WeakRef<Element>[] = [];
|
|
84
|
+
|
|
85
|
+
function clearDisconnectedPreviouslyFocusedElements() {
|
|
86
|
+
previouslyFocusedElements = previouslyFocusedElements.filter(
|
|
87
|
+
(entry) => entry.deref()?.isConnected,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function addPreviouslyFocusedElement(element: Element | null | undefined) {
|
|
92
|
+
clearDisconnectedPreviouslyFocusedElements();
|
|
93
|
+
if (element && getNodeName(element) !== 'body') {
|
|
94
|
+
previouslyFocusedElements.push(new WeakRef(element));
|
|
95
|
+
if (previouslyFocusedElements.length > LIST_LIMIT) {
|
|
96
|
+
previouslyFocusedElements = previouslyFocusedElements.slice(-LIST_LIMIT);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getPreviouslyFocusedElement() {
|
|
102
|
+
clearDisconnectedPreviouslyFocusedElements();
|
|
103
|
+
return previouslyFocusedElements[previouslyFocusedElements.length - 1]?.deref();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getFirstTabbableElement(container: Element | null) {
|
|
107
|
+
if (!container) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
if (isTabbable(container)) {
|
|
111
|
+
return container;
|
|
112
|
+
}
|
|
113
|
+
return tabbable(container)[0] || container;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function handleTabIndex(floatingFocusElement: HTMLElement) {
|
|
117
|
+
if (
|
|
118
|
+
floatingFocusElement.hasAttribute('tabindex') &&
|
|
119
|
+
!floatingFocusElement.hasAttribute('data-tabindex')
|
|
120
|
+
) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (!floatingFocusElement.getAttribute('role')?.includes('dialog')) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const focusableElements = focusable(floatingFocusElement);
|
|
127
|
+
const tabbableContent = focusableElements.filter((element) => {
|
|
128
|
+
const dataTabIndex = element.getAttribute('data-tabindex') || '';
|
|
129
|
+
return (
|
|
130
|
+
isTabbable(element) ||
|
|
131
|
+
(element.hasAttribute('data-tabindex') && !dataTabIndex.startsWith('-'))
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
const tabIndex = floatingFocusElement.getAttribute('tabindex');
|
|
135
|
+
if (tabbableContent.length === 0) {
|
|
136
|
+
if (tabIndex !== '0') {
|
|
137
|
+
floatingFocusElement.setAttribute('tabindex', '0');
|
|
138
|
+
floatingFocusElement.setAttribute('data-tabindex', '0');
|
|
139
|
+
}
|
|
140
|
+
} else if (
|
|
141
|
+
tabIndex !== '-1' ||
|
|
142
|
+
(floatingFocusElement.hasAttribute('data-tabindex') &&
|
|
143
|
+
floatingFocusElement.getAttribute('data-tabindex') !== '-1')
|
|
144
|
+
) {
|
|
145
|
+
floatingFocusElement.setAttribute('tabindex', '-1');
|
|
146
|
+
floatingFocusElement.setAttribute('data-tabindex', '-1');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function FloatingFocusManager(props: {
|
|
151
|
+
context: FloatingRootContext | FloatingContext;
|
|
152
|
+
children: any;
|
|
153
|
+
disabled?: boolean;
|
|
154
|
+
initialFocus?: any;
|
|
155
|
+
returnFocus?: any;
|
|
156
|
+
restoreFocus?: boolean | 'popup';
|
|
157
|
+
modal?: boolean;
|
|
158
|
+
closeOnFocusOut?: boolean;
|
|
159
|
+
openInteractionType?: InteractionType | null;
|
|
160
|
+
nextFocusableElement?: any;
|
|
161
|
+
previousFocusableElement?: any;
|
|
162
|
+
beforeContentFocusGuardRef?: any;
|
|
163
|
+
externalTree?: FloatingTreeStore;
|
|
164
|
+
getInsideElements?: () => Array<Element | null | undefined>;
|
|
165
|
+
}): any {
|
|
166
|
+
const slot = S('FloatingFocusManager');
|
|
167
|
+
const {
|
|
168
|
+
context,
|
|
169
|
+
children,
|
|
170
|
+
disabled = false,
|
|
171
|
+
initialFocus = true,
|
|
172
|
+
returnFocus = true,
|
|
173
|
+
restoreFocus = false,
|
|
174
|
+
modal = true,
|
|
175
|
+
closeOnFocusOut = true,
|
|
176
|
+
openInteractionType = '',
|
|
177
|
+
nextFocusableElement,
|
|
178
|
+
previousFocusableElement,
|
|
179
|
+
beforeContentFocusGuardRef,
|
|
180
|
+
externalTree,
|
|
181
|
+
getInsideElements,
|
|
182
|
+
} = props;
|
|
183
|
+
|
|
184
|
+
const store = (context && 'rootStore' in context ? context.rootStore : context) as any;
|
|
185
|
+
|
|
186
|
+
const open = store.useState('open', subSlot(slot, 'open'));
|
|
187
|
+
const domReference = store.useState('domReferenceElement', subSlot(slot, 'dom'));
|
|
188
|
+
const floating = store.useState('floatingElement', subSlot(slot, 'fel'));
|
|
189
|
+
const { events, dataRef } = store.context;
|
|
190
|
+
|
|
191
|
+
const getNodeId = useStableCallback(
|
|
192
|
+
() => dataRef.current.floatingContext?.nodeId,
|
|
193
|
+
subSlot(slot, 'getNodeId'),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const ignoreInitialFocus = initialFocus === false;
|
|
197
|
+
const isUntrappedTypeableCombobox = isTypeableCombobox(domReference) && ignoreInitialFocus;
|
|
198
|
+
|
|
199
|
+
const initialFocusRef = useValueAsRef(initialFocus, subSlot(slot, 'ifr'));
|
|
200
|
+
const returnFocusRef = useValueAsRef(returnFocus, subSlot(slot, 'rfr'));
|
|
201
|
+
const openInteractionTypeRef = useValueAsRef(openInteractionType, subSlot(slot, 'oitr'));
|
|
202
|
+
const openRef = useValueAsRef(open, subSlot(slot, 'openRef'));
|
|
203
|
+
|
|
204
|
+
const tree = useFloatingTree(externalTree);
|
|
205
|
+
const portalContext = usePortalContext();
|
|
206
|
+
|
|
207
|
+
const preventReturnFocusRef = useRef(false, subSlot(slot, 'prf'));
|
|
208
|
+
const isPointerDownRef = useRef(false, subSlot(slot, 'ipd'));
|
|
209
|
+
const pointerDownOutsideRef = useRef(false, subSlot(slot, 'pdo'));
|
|
210
|
+
const lastFocusedTabbableRef = useRef<FocusableElement | null>(null, subSlot(slot, 'lft'));
|
|
211
|
+
const closeTypeRef = useRef<InteractionType>('', subSlot(slot, 'ct'));
|
|
212
|
+
const lastInteractionTypeRef = useRef<InteractionType>('', subSlot(slot, 'lit'));
|
|
213
|
+
|
|
214
|
+
const beforeGuardRef = useRef<HTMLSpanElement | null>(null, subSlot(slot, 'bg'));
|
|
215
|
+
const afterGuardRef = useRef<HTMLSpanElement | null>(null, subSlot(slot, 'ag'));
|
|
216
|
+
|
|
217
|
+
const mergedBeforeGuardRef = useComposedRefs(
|
|
218
|
+
beforeGuardRef,
|
|
219
|
+
beforeContentFocusGuardRef,
|
|
220
|
+
portalContext?.beforeInsideRef,
|
|
221
|
+
subSlot(slot, 'mbg'),
|
|
222
|
+
);
|
|
223
|
+
const mergedAfterGuardRef = useComposedRefs(
|
|
224
|
+
afterGuardRef,
|
|
225
|
+
portalContext?.afterInsideRef,
|
|
226
|
+
subSlot(slot, 'mag'),
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const blurTimeout = useTimeout(subSlot(slot, 'bt'));
|
|
230
|
+
const pointerDownTimeout = useTimeout(subSlot(slot, 'pdt'));
|
|
231
|
+
const restoreFocusFrame = useAnimationFrame(subSlot(slot, 'rff'));
|
|
232
|
+
|
|
233
|
+
const isInsidePortal = portalContext != null;
|
|
234
|
+
const floatingFocusElement = getFloatingFocusElement(floating);
|
|
235
|
+
|
|
236
|
+
const getTabbableContent = useStableCallback(
|
|
237
|
+
(container: Element | null = floatingFocusElement) => {
|
|
238
|
+
return container ? tabbable(container) : [];
|
|
239
|
+
},
|
|
240
|
+
subSlot(slot, 'gtc'),
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const getResolvedInsideElements = useStableCallback(
|
|
244
|
+
() => getInsideElements?.().filter((element): element is Element => element != null) ?? [],
|
|
245
|
+
subSlot(slot, 'grie'),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
// Prevent Tab from escaping the modal when there are no tabbable elements.
|
|
249
|
+
useEffect(
|
|
250
|
+
() => {
|
|
251
|
+
if (disabled || !modal) {
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
function onKeyDown(event: KeyboardEvent) {
|
|
255
|
+
if (event.key === 'Tab') {
|
|
256
|
+
if (
|
|
257
|
+
contains(floatingFocusElement, activeElement(ownerDocument(floatingFocusElement))) &&
|
|
258
|
+
getTabbableContent().length === 0 &&
|
|
259
|
+
!isUntrappedTypeableCombobox
|
|
260
|
+
) {
|
|
261
|
+
stopEvent(event);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const doc = ownerDocument(floatingFocusElement);
|
|
266
|
+
return addEventListener(doc, 'keydown', onKeyDown as EventListener);
|
|
267
|
+
},
|
|
268
|
+
[disabled, floatingFocusElement, modal, isUntrappedTypeableCombobox, getTabbableContent],
|
|
269
|
+
subSlot(slot, 'e:tab'),
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
// Track pointer/keyboard interactions.
|
|
273
|
+
useEffect(
|
|
274
|
+
() => {
|
|
275
|
+
if (disabled || !open) {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
const doc = ownerDocument(floatingFocusElement);
|
|
279
|
+
function clearPointerDownOutside() {
|
|
280
|
+
pointerDownOutsideRef.current = false;
|
|
281
|
+
}
|
|
282
|
+
function onPointerDown(event: PointerEvent) {
|
|
283
|
+
const target = getTarget(event) as Element | null;
|
|
284
|
+
const insideElements = getResolvedInsideElements();
|
|
285
|
+
const pointerTargetInside =
|
|
286
|
+
contains(floating, target) ||
|
|
287
|
+
contains(domReference, target) ||
|
|
288
|
+
contains(portalContext?.portalNode, target) ||
|
|
289
|
+
insideElements.some(
|
|
290
|
+
(element: Element) => element === target || contains(element, target),
|
|
291
|
+
);
|
|
292
|
+
pointerDownOutsideRef.current = !pointerTargetInside;
|
|
293
|
+
lastInteractionTypeRef.current = (event.pointerType as InteractionType) || 'keyboard';
|
|
294
|
+
if (target?.closest(`[${CLICK_TRIGGER_IDENTIFIER}]`)) {
|
|
295
|
+
isPointerDownRef.current = true;
|
|
296
|
+
pointerDownTimeout.start(0, () => {
|
|
297
|
+
isPointerDownRef.current = false;
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function onKeyDown() {
|
|
302
|
+
lastInteractionTypeRef.current = 'keyboard';
|
|
303
|
+
}
|
|
304
|
+
return mergeCleanups(
|
|
305
|
+
addEventListener(doc, 'pointerdown', onPointerDown as EventListener, true),
|
|
306
|
+
addEventListener(doc, 'pointerup', clearPointerDownOutside, true),
|
|
307
|
+
addEventListener(doc, 'pointercancel', clearPointerDownOutside, true),
|
|
308
|
+
addEventListener(doc, 'keydown', onKeyDown, true),
|
|
309
|
+
clearPointerDownOutside,
|
|
310
|
+
);
|
|
311
|
+
},
|
|
312
|
+
[
|
|
313
|
+
disabled,
|
|
314
|
+
floating,
|
|
315
|
+
domReference,
|
|
316
|
+
floatingFocusElement,
|
|
317
|
+
open,
|
|
318
|
+
portalContext,
|
|
319
|
+
pointerDownTimeout,
|
|
320
|
+
getResolvedInsideElements,
|
|
321
|
+
],
|
|
322
|
+
subSlot(slot, 'e:track'),
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
// Close on focus out + restore focus within the floating tree.
|
|
326
|
+
useEffect(
|
|
327
|
+
() => {
|
|
328
|
+
if (disabled || !closeOnFocusOut) {
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
const doc = ownerDocument(floatingFocusElement);
|
|
332
|
+
function handlePointerDown() {
|
|
333
|
+
isPointerDownRef.current = true;
|
|
334
|
+
pointerDownTimeout.start(0, () => {
|
|
335
|
+
isPointerDownRef.current = false;
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
function handleFocusIn(event: FocusEvent) {
|
|
339
|
+
const target = getTarget(event) as FocusableElement | null;
|
|
340
|
+
if (isTabbable(target)) {
|
|
341
|
+
lastFocusedTabbableRef.current = target;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function handleFocusOutside(event: FocusEvent) {
|
|
345
|
+
const relatedTarget = event.relatedTarget as HTMLElement | null;
|
|
346
|
+
const currentTarget = event.currentTarget;
|
|
347
|
+
const target = getTarget(event) as HTMLElement | null;
|
|
348
|
+
if (modal && relatedTarget == null && target != null && contains(floating, target)) {
|
|
349
|
+
addPreviouslyFocusedElement(target);
|
|
350
|
+
}
|
|
351
|
+
queueMicrotask(() => {
|
|
352
|
+
const nodeId = getNodeId();
|
|
353
|
+
const triggers = store.context.triggerElements;
|
|
354
|
+
const insideElements = getResolvedInsideElements();
|
|
355
|
+
const isRelatedFocusGuard =
|
|
356
|
+
relatedTarget?.hasAttribute(createAttribute('focus-guard')) &&
|
|
357
|
+
[
|
|
358
|
+
beforeGuardRef.current,
|
|
359
|
+
afterGuardRef.current,
|
|
360
|
+
portalContext?.beforeInsideRef.current,
|
|
361
|
+
portalContext?.afterInsideRef.current,
|
|
362
|
+
portalContext?.beforeOutsideRef.current,
|
|
363
|
+
portalContext?.afterOutsideRef.current,
|
|
364
|
+
resolveRef(previousFocusableElement),
|
|
365
|
+
resolveRef(nextFocusableElement),
|
|
366
|
+
].includes(relatedTarget);
|
|
367
|
+
const movedToUnrelatedNode = !(
|
|
368
|
+
contains(domReference, relatedTarget) ||
|
|
369
|
+
contains(floating, relatedTarget) ||
|
|
370
|
+
contains(relatedTarget, floating) ||
|
|
371
|
+
contains(portalContext?.portalNode, relatedTarget) ||
|
|
372
|
+
insideElements.some(
|
|
373
|
+
(element: Element) => element === relatedTarget || contains(element, relatedTarget),
|
|
374
|
+
) ||
|
|
375
|
+
(relatedTarget != null && triggers.hasElement(relatedTarget)) ||
|
|
376
|
+
triggers.hasMatchingElement((trigger: Element) => contains(trigger, relatedTarget)) ||
|
|
377
|
+
isRelatedFocusGuard ||
|
|
378
|
+
(tree &&
|
|
379
|
+
(getNodeChildren(tree.nodesRef.current, nodeId).find(
|
|
380
|
+
(node) =>
|
|
381
|
+
contains(node.context?.elements.floating, relatedTarget) ||
|
|
382
|
+
contains(node.context?.elements.domReference, relatedTarget),
|
|
383
|
+
) ||
|
|
384
|
+
getNodeAncestors(tree.nodesRef.current, nodeId).find(
|
|
385
|
+
(node) =>
|
|
386
|
+
[
|
|
387
|
+
node.context?.elements.floating,
|
|
388
|
+
getFloatingFocusElement(node.context?.elements.floating),
|
|
389
|
+
].includes(relatedTarget) ||
|
|
390
|
+
node.context?.elements.domReference === relatedTarget,
|
|
391
|
+
)))
|
|
392
|
+
);
|
|
393
|
+
if (currentTarget === domReference && floatingFocusElement) {
|
|
394
|
+
handleTabIndex(floatingFocusElement);
|
|
395
|
+
}
|
|
396
|
+
if (
|
|
397
|
+
restoreFocus &&
|
|
398
|
+
currentTarget !== domReference &&
|
|
399
|
+
!isElementVisible(target) &&
|
|
400
|
+
activeElement(doc) === doc.body
|
|
401
|
+
) {
|
|
402
|
+
if (isHTMLElement(floatingFocusElement)) {
|
|
403
|
+
floatingFocusElement.focus();
|
|
404
|
+
if (restoreFocus === 'popup') {
|
|
405
|
+
restoreFocusFrame.request(() => {
|
|
406
|
+
floatingFocusElement.focus();
|
|
407
|
+
});
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const tabbableContent = getTabbableContent() as Array<Element | null>;
|
|
412
|
+
const prevTabbable = lastFocusedTabbableRef.current;
|
|
413
|
+
const nodeToFocus =
|
|
414
|
+
(prevTabbable && tabbableContent.includes(prevTabbable) ? prevTabbable : null) ||
|
|
415
|
+
tabbableContent[tabbableContent.length - 1] ||
|
|
416
|
+
floatingFocusElement;
|
|
417
|
+
if (isHTMLElement(nodeToFocus)) {
|
|
418
|
+
nodeToFocus.focus();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (dataRef.current.insideReactTree) {
|
|
422
|
+
dataRef.current.insideReactTree = false;
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (
|
|
426
|
+
(isUntrappedTypeableCombobox ? true : !modal) &&
|
|
427
|
+
relatedTarget &&
|
|
428
|
+
movedToUnrelatedNode &&
|
|
429
|
+
!isPointerDownRef.current &&
|
|
430
|
+
(isUntrappedTypeableCombobox || relatedTarget !== getPreviouslyFocusedElement())
|
|
431
|
+
) {
|
|
432
|
+
preventReturnFocusRef.current = true;
|
|
433
|
+
store.setOpen(false, createChangeEventDetails(REASONS.focusOut, event));
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
function markInsideReactTree() {
|
|
438
|
+
if (pointerDownOutsideRef.current) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
dataRef.current.insideReactTree = true;
|
|
442
|
+
blurTimeout.start(0, () => {
|
|
443
|
+
dataRef.current.insideReactTree = false;
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
const domReferenceElement = isHTMLElement(domReference) ? domReference : null;
|
|
447
|
+
if (!floating && !domReferenceElement) {
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
return mergeCleanups(
|
|
451
|
+
domReferenceElement &&
|
|
452
|
+
addEventListener(domReferenceElement, 'focusout', handleFocusOutside as EventListener),
|
|
453
|
+
domReferenceElement &&
|
|
454
|
+
addEventListener(domReferenceElement, 'pointerdown', handlePointerDown),
|
|
455
|
+
floating && addEventListener(floating, 'focusin', handleFocusIn as EventListener),
|
|
456
|
+
floating && addEventListener(floating, 'focusout', handleFocusOutside as EventListener),
|
|
457
|
+
floating &&
|
|
458
|
+
portalContext &&
|
|
459
|
+
addEventListener(floating, 'focusout', markInsideReactTree as EventListener, true),
|
|
460
|
+
);
|
|
461
|
+
},
|
|
462
|
+
[
|
|
463
|
+
disabled,
|
|
464
|
+
domReference,
|
|
465
|
+
floating,
|
|
466
|
+
floatingFocusElement,
|
|
467
|
+
modal,
|
|
468
|
+
tree,
|
|
469
|
+
portalContext,
|
|
470
|
+
store,
|
|
471
|
+
closeOnFocusOut,
|
|
472
|
+
restoreFocus,
|
|
473
|
+
getTabbableContent,
|
|
474
|
+
isUntrappedTypeableCombobox,
|
|
475
|
+
getNodeId,
|
|
476
|
+
dataRef,
|
|
477
|
+
blurTimeout,
|
|
478
|
+
pointerDownTimeout,
|
|
479
|
+
restoreFocusFrame,
|
|
480
|
+
nextFocusableElement,
|
|
481
|
+
previousFocusableElement,
|
|
482
|
+
getResolvedInsideElements,
|
|
483
|
+
],
|
|
484
|
+
subSlot(slot, 'e:focusout'),
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
// Hide everything outside the floating tree from assistive tech while open.
|
|
488
|
+
useEffect(
|
|
489
|
+
() => {
|
|
490
|
+
if (disabled || !floating || !open) {
|
|
491
|
+
return undefined;
|
|
492
|
+
}
|
|
493
|
+
const portalNodes = Array.from(
|
|
494
|
+
portalContext?.portalNode?.querySelectorAll(`[${createAttribute('portal')}]`) || [],
|
|
495
|
+
);
|
|
496
|
+
const ancestors = tree ? getNodeAncestors(tree.nodesRef.current, getNodeId()) : [];
|
|
497
|
+
const rootAncestorComboboxDomReference = ancestors.find((node) =>
|
|
498
|
+
isTypeableCombobox(node.context?.elements.domReference || null),
|
|
499
|
+
)?.context?.elements.domReference;
|
|
500
|
+
const controlInsideElements = [
|
|
501
|
+
floating,
|
|
502
|
+
...portalNodes,
|
|
503
|
+
beforeGuardRef.current,
|
|
504
|
+
afterGuardRef.current,
|
|
505
|
+
portalContext?.beforeOutsideRef.current,
|
|
506
|
+
portalContext?.afterOutsideRef.current,
|
|
507
|
+
...getResolvedInsideElements(),
|
|
508
|
+
];
|
|
509
|
+
const insideElements = [
|
|
510
|
+
...controlInsideElements,
|
|
511
|
+
rootAncestorComboboxDomReference,
|
|
512
|
+
resolveRef(previousFocusableElement),
|
|
513
|
+
resolveRef(nextFocusableElement),
|
|
514
|
+
isUntrappedTypeableCombobox ? domReference : null,
|
|
515
|
+
].filter((x): x is Element => x != null);
|
|
516
|
+
const ariaHiddenCleanup = markOthers(insideElements, {
|
|
517
|
+
ariaHidden: modal || isUntrappedTypeableCombobox,
|
|
518
|
+
mark: false,
|
|
519
|
+
});
|
|
520
|
+
const markerInsideElements = [floating, ...portalNodes].filter(
|
|
521
|
+
(x): x is Element => x != null,
|
|
522
|
+
);
|
|
523
|
+
const markerCleanup = markOthers(markerInsideElements);
|
|
524
|
+
return () => {
|
|
525
|
+
markerCleanup();
|
|
526
|
+
ariaHiddenCleanup();
|
|
527
|
+
};
|
|
528
|
+
},
|
|
529
|
+
[
|
|
530
|
+
open,
|
|
531
|
+
disabled,
|
|
532
|
+
domReference,
|
|
533
|
+
floating,
|
|
534
|
+
modal,
|
|
535
|
+
portalContext,
|
|
536
|
+
isUntrappedTypeableCombobox,
|
|
537
|
+
tree,
|
|
538
|
+
getNodeId,
|
|
539
|
+
nextFocusableElement,
|
|
540
|
+
previousFocusableElement,
|
|
541
|
+
getResolvedInsideElements,
|
|
542
|
+
],
|
|
543
|
+
subSlot(slot, 'e:hide'),
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
// Focus the initial element when the floating element opens.
|
|
547
|
+
useLayoutEffect(
|
|
548
|
+
() => {
|
|
549
|
+
if (!open || disabled || !isHTMLElement(floatingFocusElement)) {
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
const doc = ownerDocument(floatingFocusElement);
|
|
553
|
+
const previouslyFocused = activeElement(doc);
|
|
554
|
+
queueMicrotask(() => {
|
|
555
|
+
const initialFocusValueOrFn = initialFocusRef.current;
|
|
556
|
+
const resolvedInitialFocus =
|
|
557
|
+
typeof initialFocusValueOrFn === 'function'
|
|
558
|
+
? initialFocusValueOrFn(openInteractionTypeRef.current || '')
|
|
559
|
+
: initialFocusValueOrFn;
|
|
560
|
+
if (resolvedInitialFocus === undefined || resolvedInitialFocus === false) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const focusAlreadyInsideFloatingEl = contains(floatingFocusElement, previouslyFocused);
|
|
564
|
+
if (focusAlreadyInsideFloatingEl) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
let focusableElements: Array<FocusableElement> | null = null;
|
|
568
|
+
const getDefaultFocusElement = () => {
|
|
569
|
+
if (focusableElements == null) {
|
|
570
|
+
focusableElements = getTabbableContent(floatingFocusElement) as FocusableElement[];
|
|
571
|
+
}
|
|
572
|
+
return focusableElements[0] || floatingFocusElement;
|
|
573
|
+
};
|
|
574
|
+
let elToFocus: FocusableElement | null | undefined;
|
|
575
|
+
if (resolvedInitialFocus === true || resolvedInitialFocus === null) {
|
|
576
|
+
elToFocus = getDefaultFocusElement();
|
|
577
|
+
} else {
|
|
578
|
+
elToFocus = resolveRef(resolvedInitialFocus);
|
|
579
|
+
}
|
|
580
|
+
elToFocus = elToFocus || getDefaultFocusElement();
|
|
581
|
+
const hadFocusInside = contains(floatingFocusElement, activeElement(doc));
|
|
582
|
+
enqueueFocus(elToFocus, {
|
|
583
|
+
preventScroll: elToFocus === floatingFocusElement,
|
|
584
|
+
shouldFocus() {
|
|
585
|
+
if (!openRef.current) {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
if (hadFocusInside) {
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
const currentActiveElement = activeElement(doc);
|
|
592
|
+
const focusMovedInside =
|
|
593
|
+
currentActiveElement !== elToFocus &&
|
|
594
|
+
contains(floatingFocusElement, currentActiveElement);
|
|
595
|
+
return !focusMovedInside;
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
},
|
|
600
|
+
[
|
|
601
|
+
disabled,
|
|
602
|
+
open,
|
|
603
|
+
floatingFocusElement,
|
|
604
|
+
getTabbableContent,
|
|
605
|
+
initialFocusRef,
|
|
606
|
+
openInteractionTypeRef,
|
|
607
|
+
openRef,
|
|
608
|
+
],
|
|
609
|
+
subSlot(slot, 'e:initfocus'),
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
// Track return-focus targets and restore focus on unmount/close.
|
|
613
|
+
useLayoutEffect(
|
|
614
|
+
() => {
|
|
615
|
+
if (disabled || !floatingFocusElement) {
|
|
616
|
+
return undefined;
|
|
617
|
+
}
|
|
618
|
+
const doc = ownerDocument(floatingFocusElement);
|
|
619
|
+
const elementFocusedBeforeOpen = activeElement(doc);
|
|
620
|
+
const preferPreviousFocus = openInteractionTypeRef.current == null;
|
|
621
|
+
addPreviouslyFocusedElement(elementFocusedBeforeOpen);
|
|
622
|
+
function onOpenChangeLocal(details: FloatingUIOpenChangeDetails) {
|
|
623
|
+
if (!details.open) {
|
|
624
|
+
closeTypeRef.current = getEventType(details.nativeEvent, lastInteractionTypeRef.current);
|
|
625
|
+
}
|
|
626
|
+
if (details.reason === REASONS.triggerHover && details.nativeEvent.type === 'mouseleave') {
|
|
627
|
+
preventReturnFocusRef.current = true;
|
|
628
|
+
}
|
|
629
|
+
if (details.reason !== REASONS.outsidePress) {
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (details.nested) {
|
|
633
|
+
preventReturnFocusRef.current = false;
|
|
634
|
+
} else if (
|
|
635
|
+
isVirtualClick(details.nativeEvent as MouseEvent) ||
|
|
636
|
+
isVirtualPointerEvent(details.nativeEvent as PointerEvent)
|
|
637
|
+
) {
|
|
638
|
+
preventReturnFocusRef.current = false;
|
|
639
|
+
} else {
|
|
640
|
+
let isPreventScrollSupported = false;
|
|
641
|
+
ownerDocument(floatingFocusElement)
|
|
642
|
+
.createElement('div')
|
|
643
|
+
.focus({
|
|
644
|
+
get preventScroll() {
|
|
645
|
+
isPreventScrollSupported = true;
|
|
646
|
+
return false;
|
|
647
|
+
},
|
|
648
|
+
} as any);
|
|
649
|
+
if (isPreventScrollSupported) {
|
|
650
|
+
preventReturnFocusRef.current = false;
|
|
651
|
+
} else {
|
|
652
|
+
preventReturnFocusRef.current = true;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
events.on('openchange', onOpenChangeLocal);
|
|
657
|
+
function getReturnElement() {
|
|
658
|
+
const returnFocusValueOrFn = returnFocusRef.current;
|
|
659
|
+
let resolvedReturnFocusValue =
|
|
660
|
+
typeof returnFocusValueOrFn === 'function'
|
|
661
|
+
? returnFocusValueOrFn(closeTypeRef.current)
|
|
662
|
+
: returnFocusValueOrFn;
|
|
663
|
+
if (resolvedReturnFocusValue === undefined || resolvedReturnFocusValue === false) {
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
if (resolvedReturnFocusValue === null) {
|
|
667
|
+
resolvedReturnFocusValue = true;
|
|
668
|
+
}
|
|
669
|
+
const referenceReturnElement = domReference?.isConnected ? domReference : null;
|
|
670
|
+
const previousReturnElement =
|
|
671
|
+
elementFocusedBeforeOpen?.isConnected && getNodeName(elementFocusedBeforeOpen) !== 'body'
|
|
672
|
+
? elementFocusedBeforeOpen
|
|
673
|
+
: null;
|
|
674
|
+
let defaultReturnElement = preferPreviousFocus
|
|
675
|
+
? previousReturnElement || referenceReturnElement
|
|
676
|
+
: referenceReturnElement || previousReturnElement;
|
|
677
|
+
if (!defaultReturnElement) {
|
|
678
|
+
defaultReturnElement = getPreviouslyFocusedElement() || null;
|
|
679
|
+
}
|
|
680
|
+
if (typeof resolvedReturnFocusValue === 'boolean') {
|
|
681
|
+
return defaultReturnElement;
|
|
682
|
+
}
|
|
683
|
+
return resolveRef(resolvedReturnFocusValue) || defaultReturnElement || null;
|
|
684
|
+
}
|
|
685
|
+
return () => {
|
|
686
|
+
events.off('openchange', onOpenChangeLocal);
|
|
687
|
+
const activeEl = activeElement(doc);
|
|
688
|
+
const insideElements = getResolvedInsideElements();
|
|
689
|
+
const isFocusInsideFloatingTree =
|
|
690
|
+
contains(floating, activeEl) ||
|
|
691
|
+
insideElements.some(
|
|
692
|
+
(element: Element) => element === activeEl || contains(element, activeEl),
|
|
693
|
+
) ||
|
|
694
|
+
(tree &&
|
|
695
|
+
getNodeChildren(tree.nodesRef.current, getNodeId(), false).some((node) =>
|
|
696
|
+
contains(node.context?.elements.floating, activeEl),
|
|
697
|
+
));
|
|
698
|
+
const returnFocusValueOrFn = returnFocusRef.current;
|
|
699
|
+
const returnElement = getReturnElement();
|
|
700
|
+
queueMicrotask(() => {
|
|
701
|
+
const tabbableReturnElement = getFirstTabbableElement(returnElement);
|
|
702
|
+
const hasExplicitReturnFocus = typeof returnFocusValueOrFn !== 'boolean';
|
|
703
|
+
if (
|
|
704
|
+
returnFocusValueOrFn &&
|
|
705
|
+
!preventReturnFocusRef.current &&
|
|
706
|
+
isHTMLElement(tabbableReturnElement) &&
|
|
707
|
+
(!hasExplicitReturnFocus && tabbableReturnElement !== activeEl && activeEl !== doc.body
|
|
708
|
+
? isFocusInsideFloatingTree
|
|
709
|
+
: true)
|
|
710
|
+
) {
|
|
711
|
+
tabbableReturnElement.focus({ preventScroll: true });
|
|
712
|
+
}
|
|
713
|
+
preventReturnFocusRef.current = false;
|
|
714
|
+
});
|
|
715
|
+
};
|
|
716
|
+
},
|
|
717
|
+
[
|
|
718
|
+
disabled,
|
|
719
|
+
floating,
|
|
720
|
+
floatingFocusElement,
|
|
721
|
+
returnFocusRef,
|
|
722
|
+
openInteractionTypeRef,
|
|
723
|
+
events,
|
|
724
|
+
tree,
|
|
725
|
+
domReference,
|
|
726
|
+
getNodeId,
|
|
727
|
+
getResolvedInsideElements,
|
|
728
|
+
],
|
|
729
|
+
subSlot(slot, 'e:returnfocus'),
|
|
730
|
+
);
|
|
731
|
+
|
|
732
|
+
// Safari: blur a typeable input before the popup unmounts (avoids a scroll jump).
|
|
733
|
+
useLayoutEffect(
|
|
734
|
+
() => {
|
|
735
|
+
if (!platform.engine.webkit || open || !floating) {
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
const activeEl = activeElement(ownerDocument(floating));
|
|
739
|
+
if (!isHTMLElement(activeEl) || !isTypeableElement(activeEl)) {
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
if (contains(floating, activeEl)) {
|
|
743
|
+
activeEl.blur();
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
[open, floating],
|
|
747
|
+
subSlot(slot, 'e:webkit'),
|
|
748
|
+
);
|
|
749
|
+
|
|
750
|
+
// Sync `modal`/`open` into the FloatingPortal context (it decides whether to render its guards).
|
|
751
|
+
useLayoutEffect(
|
|
752
|
+
() => {
|
|
753
|
+
if (disabled || !portalContext) {
|
|
754
|
+
return undefined;
|
|
755
|
+
}
|
|
756
|
+
portalContext.setFocusManagerState({
|
|
757
|
+
modal,
|
|
758
|
+
closeOnFocusOut,
|
|
759
|
+
open,
|
|
760
|
+
onOpenChange: store.setOpen,
|
|
761
|
+
domReference,
|
|
762
|
+
} as any);
|
|
763
|
+
return () => {
|
|
764
|
+
portalContext.setFocusManagerState(null);
|
|
765
|
+
};
|
|
766
|
+
},
|
|
767
|
+
[disabled, portalContext, modal, open, store, closeOnFocusOut, domReference],
|
|
768
|
+
subSlot(slot, 'e:portalsync'),
|
|
769
|
+
);
|
|
770
|
+
|
|
771
|
+
// Keep the floating element tabIndex in sync.
|
|
772
|
+
useLayoutEffect(
|
|
773
|
+
() => {
|
|
774
|
+
if (disabled || !floatingFocusElement) {
|
|
775
|
+
return undefined;
|
|
776
|
+
}
|
|
777
|
+
handleTabIndex(floatingFocusElement);
|
|
778
|
+
return () => {
|
|
779
|
+
queueMicrotask(clearDisconnectedPreviouslyFocusedElements);
|
|
780
|
+
};
|
|
781
|
+
},
|
|
782
|
+
[disabled, floatingFocusElement],
|
|
783
|
+
subSlot(slot, 'e:tabindex'),
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
const shouldRenderGuards =
|
|
787
|
+
!disabled && (modal ? !isUntrappedTypeableCombobox : true) && (isInsidePortal || modal);
|
|
788
|
+
|
|
789
|
+
return [
|
|
790
|
+
shouldRenderGuards
|
|
791
|
+
? createElement(FocusGuard, {
|
|
792
|
+
'data-type': 'inside',
|
|
793
|
+
ref: mergedBeforeGuardRef,
|
|
794
|
+
onFocus(event: any) {
|
|
795
|
+
if (modal) {
|
|
796
|
+
const els = getTabbableContent();
|
|
797
|
+
enqueueFocus(els[els.length - 1]);
|
|
798
|
+
} else if (portalContext?.portalNode) {
|
|
799
|
+
preventReturnFocusRef.current = false;
|
|
800
|
+
if (isOutsideEvent(event, portalContext.portalNode)) {
|
|
801
|
+
const nextTabbable = getNextTabbable(domReference);
|
|
802
|
+
nextTabbable?.focus();
|
|
803
|
+
} else {
|
|
804
|
+
resolveRef(previousFocusableElement ?? portalContext.beforeOutsideRef)?.focus();
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
},
|
|
808
|
+
})
|
|
809
|
+
: null,
|
|
810
|
+
children,
|
|
811
|
+
shouldRenderGuards
|
|
812
|
+
? createElement(FocusGuard, {
|
|
813
|
+
'data-type': 'inside',
|
|
814
|
+
ref: mergedAfterGuardRef,
|
|
815
|
+
onFocus(event: any) {
|
|
816
|
+
if (modal) {
|
|
817
|
+
enqueueFocus(getTabbableContent()[0]);
|
|
818
|
+
} else if (portalContext?.portalNode) {
|
|
819
|
+
if (closeOnFocusOut) {
|
|
820
|
+
preventReturnFocusRef.current = true;
|
|
821
|
+
}
|
|
822
|
+
if (isOutsideEvent(event, portalContext.portalNode)) {
|
|
823
|
+
const prevTabbable = getPreviousTabbable(domReference);
|
|
824
|
+
prevTabbable?.focus();
|
|
825
|
+
} else {
|
|
826
|
+
resolveRef(nextFocusableElement ?? portalContext.afterOutsideRef)?.focus();
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
},
|
|
830
|
+
})
|
|
831
|
+
: null,
|
|
832
|
+
];
|
|
833
|
+
}
|