@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.
- package/LICENSE +21 -0
- package/package.json +44 -0
- package/src/Composite.ts +241 -0
- package/src/FloatingArrow.ts +145 -0
- package/src/FloatingFocusManager.ts +813 -0
- package/src/FloatingList.ts +137 -0
- package/src/FloatingOverlay.ts +85 -0
- package/src/FloatingPortal.ts +257 -0
- package/src/context.ts +181 -0
- package/src/delayGroup.ts +142 -0
- package/src/index.ts +79 -0
- package/src/internal.ts +46 -0
- package/src/pubsub.ts +19 -0
- package/src/safePolygon.ts +339 -0
- package/src/transitions.ts +146 -0
- package/src/tree.ts +85 -0
- package/src/useClick.ts +139 -0
- package/src/useClientPoint.ts +193 -0
- package/src/useDismiss.ts +379 -0
- package/src/useFloating.ts +220 -0
- package/src/useFocus.ts +154 -0
- package/src/useHover.ts +406 -0
- package/src/useId.ts +6 -0
- package/src/useInteractions.ts +94 -0
- package/src/useListNavigation.ts +721 -0
- package/src/useMergeRefs.ts +59 -0
- package/src/useRole.ts +106 -0
- package/src/useTypeahead.ts +168 -0
- package/src/utils/index.ts +656 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Ported from @floating-ui/react FloatingList + useListItem — registers list items
|
|
2
|
+
// and their DOM order so useListNavigation/useTypeahead can index them. `.ts`
|
|
3
|
+
// component via createElement; useListItem is a hook (resolves its own slot).
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
createElement,
|
|
7
|
+
useCallback,
|
|
8
|
+
useContext,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
useState,
|
|
12
|
+
} from 'octane';
|
|
13
|
+
|
|
14
|
+
import { S, splitSlot, subSlot } from './internal';
|
|
15
|
+
import { useModernLayoutEffect } from './utils';
|
|
16
|
+
|
|
17
|
+
function sortByDocumentPosition(a: Node, b: Node): number {
|
|
18
|
+
const position = a.compareDocumentPosition(b);
|
|
19
|
+
if (
|
|
20
|
+
position & Node.DOCUMENT_POSITION_FOLLOWING ||
|
|
21
|
+
position & Node.DOCUMENT_POSITION_CONTAINED_BY
|
|
22
|
+
) {
|
|
23
|
+
return -1;
|
|
24
|
+
}
|
|
25
|
+
if (position & Node.DOCUMENT_POSITION_PRECEDING || position & Node.DOCUMENT_POSITION_CONTAINS) {
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const FloatingListContext = createContext<any>({
|
|
32
|
+
register: () => {},
|
|
33
|
+
unregister: () => {},
|
|
34
|
+
map: new Map(),
|
|
35
|
+
elementsRef: { current: [] },
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export function FloatingList(props: any): any {
|
|
39
|
+
const children = props.children;
|
|
40
|
+
const elementsRef = props.elementsRef;
|
|
41
|
+
const labelsRef = props.labelsRef;
|
|
42
|
+
|
|
43
|
+
const [nodes, setNodes] = useState(() => new Set<any>(), S('FloatingList:nodes'));
|
|
44
|
+
const register = useCallback(
|
|
45
|
+
(node: any) => {
|
|
46
|
+
setNodes((prevSet: Set<any>) => new Set(prevSet).add(node));
|
|
47
|
+
},
|
|
48
|
+
[],
|
|
49
|
+
S('FloatingList:register'),
|
|
50
|
+
);
|
|
51
|
+
const unregister = useCallback(
|
|
52
|
+
(node: any) => {
|
|
53
|
+
setNodes((prevSet: Set<any>) => {
|
|
54
|
+
const set = new Set(prevSet);
|
|
55
|
+
set.delete(node);
|
|
56
|
+
return set;
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
[],
|
|
60
|
+
S('FloatingList:unregister'),
|
|
61
|
+
);
|
|
62
|
+
const map = useMemo(
|
|
63
|
+
() => {
|
|
64
|
+
const newMap = new Map();
|
|
65
|
+
const sortedNodes = Array.from(nodes.keys()).sort(sortByDocumentPosition);
|
|
66
|
+
sortedNodes.forEach((node, index) => {
|
|
67
|
+
newMap.set(node, index);
|
|
68
|
+
});
|
|
69
|
+
return newMap;
|
|
70
|
+
},
|
|
71
|
+
[nodes],
|
|
72
|
+
S('FloatingList:map'),
|
|
73
|
+
);
|
|
74
|
+
const value = useMemo(
|
|
75
|
+
() => ({ register, unregister, map, elementsRef, labelsRef }),
|
|
76
|
+
[register, unregister, map, elementsRef, labelsRef],
|
|
77
|
+
S('FloatingList:value'),
|
|
78
|
+
);
|
|
79
|
+
return createElement(FloatingListContext.Provider, { value, children });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function useListItem(...args: any[]): any {
|
|
83
|
+
const [user, slotArg] = splitSlot(args);
|
|
84
|
+
const slot = slotArg ?? S('useListItem');
|
|
85
|
+
const props = (user[0] as any) ?? {};
|
|
86
|
+
const label = props.label;
|
|
87
|
+
|
|
88
|
+
const { register, unregister, map, elementsRef, labelsRef } = useContext(FloatingListContext);
|
|
89
|
+
const [index, setIndex] = useState<any>(null, subSlot(slot, 'index'));
|
|
90
|
+
const componentRef = useRef<any>(null, subSlot(slot, 'ref'));
|
|
91
|
+
|
|
92
|
+
const ref = useCallback(
|
|
93
|
+
(node: any) => {
|
|
94
|
+
componentRef.current = node;
|
|
95
|
+
if (index !== null) {
|
|
96
|
+
elementsRef.current[index] = node;
|
|
97
|
+
if (labelsRef) {
|
|
98
|
+
const isLabelDefined = label !== undefined;
|
|
99
|
+
labelsRef.current[index] = isLabelDefined ? label : (node?.textContent ?? null);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
[index, elementsRef, labelsRef, label],
|
|
104
|
+
subSlot(slot, 'cb'),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
useModernLayoutEffect(
|
|
108
|
+
() => {
|
|
109
|
+
const node = componentRef.current;
|
|
110
|
+
if (node) {
|
|
111
|
+
register(node);
|
|
112
|
+
return () => {
|
|
113
|
+
unregister(node);
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
[register, unregister],
|
|
118
|
+
subSlot(slot, 'e:reg'),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
useModernLayoutEffect(
|
|
122
|
+
() => {
|
|
123
|
+
const idx = componentRef.current ? map.get(componentRef.current) : null;
|
|
124
|
+
if (idx != null) {
|
|
125
|
+
setIndex(idx);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
[map],
|
|
129
|
+
subSlot(slot, 'e:idx'),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return useMemo(
|
|
133
|
+
() => ({ ref, index: index == null ? -1 : index }),
|
|
134
|
+
[index, ref],
|
|
135
|
+
subSlot(slot, 'ret'),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Ported from @floating-ui/react FloatingOverlay — a fixed <div> overlay with an
|
|
2
|
+
// optional body scroll-lock. A `.ts` component (createElement, ref-as-prop). React
|
|
3
|
+
// forwardRef → `props.ref`.
|
|
4
|
+
import { createElement } from 'octane';
|
|
5
|
+
|
|
6
|
+
import { S } from './internal';
|
|
7
|
+
import { getPlatform, useModernLayoutEffect } from './utils';
|
|
8
|
+
|
|
9
|
+
const scrollbarProperty = '--floating-ui-scrollbar-width';
|
|
10
|
+
let lockCount = 0;
|
|
11
|
+
let cleanup = () => {};
|
|
12
|
+
|
|
13
|
+
function enableScrollLock() {
|
|
14
|
+
const platform = getPlatform();
|
|
15
|
+
const isIOS =
|
|
16
|
+
/iP(hone|ad|od)|iOS/.test(platform) ||
|
|
17
|
+
(platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
|
18
|
+
const bodyStyle = document.body.style as any;
|
|
19
|
+
const scrollbarX =
|
|
20
|
+
Math.round(document.documentElement.getBoundingClientRect().left) +
|
|
21
|
+
document.documentElement.scrollLeft;
|
|
22
|
+
const paddingProp = scrollbarX ? 'paddingLeft' : 'paddingRight';
|
|
23
|
+
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
24
|
+
const scrollX = bodyStyle.left ? parseFloat(bodyStyle.left) : window.scrollX;
|
|
25
|
+
const scrollY = bodyStyle.top ? parseFloat(bodyStyle.top) : window.scrollY;
|
|
26
|
+
bodyStyle.overflow = 'hidden';
|
|
27
|
+
bodyStyle.setProperty(scrollbarProperty, scrollbarWidth + 'px');
|
|
28
|
+
if (scrollbarWidth) {
|
|
29
|
+
bodyStyle[paddingProp] = scrollbarWidth + 'px';
|
|
30
|
+
}
|
|
31
|
+
if (isIOS) {
|
|
32
|
+
const offsetLeft = window.visualViewport?.offsetLeft || 0;
|
|
33
|
+
const offsetTop = window.visualViewport?.offsetTop || 0;
|
|
34
|
+
Object.assign(bodyStyle, {
|
|
35
|
+
position: 'fixed',
|
|
36
|
+
top: -(scrollY - Math.floor(offsetTop)) + 'px',
|
|
37
|
+
left: -(scrollX - Math.floor(offsetLeft)) + 'px',
|
|
38
|
+
right: '0',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return () => {
|
|
42
|
+
Object.assign(bodyStyle, { overflow: '', [paddingProp]: '' });
|
|
43
|
+
bodyStyle.removeProperty(scrollbarProperty);
|
|
44
|
+
if (isIOS) {
|
|
45
|
+
Object.assign(bodyStyle, { position: '', top: '', left: '', right: '' });
|
|
46
|
+
window.scrollTo(scrollX, scrollY);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function FloatingOverlay(props: any): any {
|
|
52
|
+
const { lockScroll = false, ref, ...rest } = props;
|
|
53
|
+
|
|
54
|
+
useModernLayoutEffect(
|
|
55
|
+
() => {
|
|
56
|
+
if (!lockScroll) return;
|
|
57
|
+
lockCount++;
|
|
58
|
+
if (lockCount === 1) {
|
|
59
|
+
cleanup = enableScrollLock();
|
|
60
|
+
}
|
|
61
|
+
return () => {
|
|
62
|
+
lockCount--;
|
|
63
|
+
if (lockCount === 0) {
|
|
64
|
+
cleanup();
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
[lockScroll],
|
|
69
|
+
S('FloatingOverlay:lock'),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return createElement('div', {
|
|
73
|
+
ref,
|
|
74
|
+
...rest,
|
|
75
|
+
style: {
|
|
76
|
+
position: 'fixed',
|
|
77
|
+
overflow: 'auto',
|
|
78
|
+
top: 0,
|
|
79
|
+
right: 0,
|
|
80
|
+
bottom: 0,
|
|
81
|
+
left: 0,
|
|
82
|
+
...rest.style,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// Ported from @floating-ui/react FloatingPortal (+ FocusGuard, useFloatingPortalNode,
|
|
2
|
+
// PortalContext). `.ts` components via createElement; React forwardRef → props.ref;
|
|
3
|
+
// ReactDOM.createPortal(children, node) → octane createPortal (which renders a value
|
|
4
|
+
// anywhere). Focus guards only render when a non-modal FloatingFocusManager registers
|
|
5
|
+
// its state, so a standalone portal just renders its children into the portal node.
|
|
6
|
+
import { isNode } from '@floating-ui/utils/dom';
|
|
7
|
+
import {
|
|
8
|
+
createContext,
|
|
9
|
+
createElement,
|
|
10
|
+
createPortal,
|
|
11
|
+
useContext,
|
|
12
|
+
useEffect,
|
|
13
|
+
useMemo,
|
|
14
|
+
useRef,
|
|
15
|
+
useState,
|
|
16
|
+
} from 'octane';
|
|
17
|
+
|
|
18
|
+
import { S, splitSlot, subSlot } from './internal';
|
|
19
|
+
import { useId } from './useId';
|
|
20
|
+
import {
|
|
21
|
+
createAttribute,
|
|
22
|
+
disableFocusInside,
|
|
23
|
+
enableFocusInside,
|
|
24
|
+
getNextTabbable,
|
|
25
|
+
getPreviousTabbable,
|
|
26
|
+
isOutsideEvent,
|
|
27
|
+
isSafari,
|
|
28
|
+
useModernLayoutEffect,
|
|
29
|
+
} from './utils';
|
|
30
|
+
|
|
31
|
+
const HIDDEN_STYLES: any = {
|
|
32
|
+
border: 0,
|
|
33
|
+
clip: 'rect(0 0 0 0)',
|
|
34
|
+
height: '1px',
|
|
35
|
+
margin: '-1px',
|
|
36
|
+
overflow: 'hidden',
|
|
37
|
+
padding: 0,
|
|
38
|
+
position: 'fixed',
|
|
39
|
+
whiteSpace: 'nowrap',
|
|
40
|
+
width: '1px',
|
|
41
|
+
top: 0,
|
|
42
|
+
left: 0,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function FocusGuard(props: any): any {
|
|
46
|
+
const [role, setRole] = useState<any>(undefined, S('FocusGuard:role'));
|
|
47
|
+
useModernLayoutEffect(
|
|
48
|
+
() => {
|
|
49
|
+
if (isSafari()) {
|
|
50
|
+
setRole('button');
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
[],
|
|
54
|
+
S('FocusGuard:eff'),
|
|
55
|
+
);
|
|
56
|
+
return createElement('span', {
|
|
57
|
+
...props,
|
|
58
|
+
tabIndex: 0,
|
|
59
|
+
role,
|
|
60
|
+
'aria-hidden': role ? undefined : true,
|
|
61
|
+
[createAttribute('focus-guard')]: '',
|
|
62
|
+
style: HIDDEN_STYLES,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const HIDDEN_OWNER_STYLES: any = {
|
|
67
|
+
clipPath: 'inset(50%)',
|
|
68
|
+
position: 'fixed',
|
|
69
|
+
top: 0,
|
|
70
|
+
left: 0,
|
|
71
|
+
};
|
|
72
|
+
export const PortalContext = createContext<any>(null);
|
|
73
|
+
const attr = createAttribute('portal');
|
|
74
|
+
|
|
75
|
+
export function useFloatingPortalNode(...args: any[]): any {
|
|
76
|
+
// Exported hook → may be called directly by consumers (compiler injects the
|
|
77
|
+
// slot) or by FloatingPortal (passes an S() slot); fall back to S() otherwise.
|
|
78
|
+
const [user, slotArg] = splitSlot(args);
|
|
79
|
+
const slot = slotArg ?? S('useFloatingPortalNode');
|
|
80
|
+
const props = (user[0] as any) ?? {};
|
|
81
|
+
const id = props.id;
|
|
82
|
+
const root = props.root;
|
|
83
|
+
|
|
84
|
+
const uniqueId = useId(subSlot(slot, 'id'));
|
|
85
|
+
const portalContext = usePortalContext();
|
|
86
|
+
const [portalNode, setPortalNode] = useState<any>(null, subSlot(slot, 'node'));
|
|
87
|
+
const portalNodeRef = useRef<any>(null, subSlot(slot, 'noderef'));
|
|
88
|
+
|
|
89
|
+
useModernLayoutEffect(
|
|
90
|
+
() => {
|
|
91
|
+
return () => {
|
|
92
|
+
portalNode?.remove();
|
|
93
|
+
queueMicrotask(() => {
|
|
94
|
+
portalNodeRef.current = null;
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
[portalNode],
|
|
99
|
+
subSlot(slot, 'e:cleanup'),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
useModernLayoutEffect(
|
|
103
|
+
() => {
|
|
104
|
+
if (!uniqueId) return;
|
|
105
|
+
if (portalNodeRef.current) return;
|
|
106
|
+
const existingIdRoot = id ? document.getElementById(id) : null;
|
|
107
|
+
if (!existingIdRoot) return;
|
|
108
|
+
const subRoot = document.createElement('div');
|
|
109
|
+
subRoot.id = uniqueId;
|
|
110
|
+
subRoot.setAttribute(attr, '');
|
|
111
|
+
existingIdRoot.appendChild(subRoot);
|
|
112
|
+
portalNodeRef.current = subRoot;
|
|
113
|
+
setPortalNode(subRoot);
|
|
114
|
+
},
|
|
115
|
+
[id, uniqueId],
|
|
116
|
+
subSlot(slot, 'e:id'),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
useModernLayoutEffect(
|
|
120
|
+
() => {
|
|
121
|
+
if (root === null) return;
|
|
122
|
+
if (!uniqueId) return;
|
|
123
|
+
if (portalNodeRef.current) return;
|
|
124
|
+
let container = root || portalContext?.portalNode;
|
|
125
|
+
if (container && !isNode(container)) container = container.current;
|
|
126
|
+
container = container || document.body;
|
|
127
|
+
let idWrapper = null;
|
|
128
|
+
if (id) {
|
|
129
|
+
idWrapper = document.createElement('div');
|
|
130
|
+
idWrapper.id = id;
|
|
131
|
+
container.appendChild(idWrapper);
|
|
132
|
+
}
|
|
133
|
+
const subRoot = document.createElement('div');
|
|
134
|
+
subRoot.id = uniqueId;
|
|
135
|
+
subRoot.setAttribute(attr, '');
|
|
136
|
+
container = idWrapper || container;
|
|
137
|
+
container.appendChild(subRoot);
|
|
138
|
+
portalNodeRef.current = subRoot;
|
|
139
|
+
setPortalNode(subRoot);
|
|
140
|
+
},
|
|
141
|
+
[id, root, uniqueId, portalContext],
|
|
142
|
+
subSlot(slot, 'e:root'),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return portalNode;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function FloatingPortal(props: any): any {
|
|
149
|
+
const children = props.children;
|
|
150
|
+
const id = props.id;
|
|
151
|
+
const root = props.root;
|
|
152
|
+
const preserveTabOrder = props.preserveTabOrder ?? true;
|
|
153
|
+
|
|
154
|
+
const portalNode = useFloatingPortalNode([{ id, root }, S('FloatingPortal:node')]);
|
|
155
|
+
const [focusManagerState, setFocusManagerState] = useState<any>(null, S('FloatingPortal:fms'));
|
|
156
|
+
const beforeOutsideRef = useRef<any>(null, S('FloatingPortal:bo'));
|
|
157
|
+
const afterOutsideRef = useRef<any>(null, S('FloatingPortal:ao'));
|
|
158
|
+
const beforeInsideRef = useRef<any>(null, S('FloatingPortal:bi'));
|
|
159
|
+
const afterInsideRef = useRef<any>(null, S('FloatingPortal:ai'));
|
|
160
|
+
const modal = focusManagerState?.modal;
|
|
161
|
+
const open = focusManagerState?.open;
|
|
162
|
+
const shouldRenderGuards =
|
|
163
|
+
!!focusManagerState &&
|
|
164
|
+
!focusManagerState.modal &&
|
|
165
|
+
focusManagerState.open &&
|
|
166
|
+
preserveTabOrder &&
|
|
167
|
+
!!(root || portalNode);
|
|
168
|
+
|
|
169
|
+
useEffect(
|
|
170
|
+
() => {
|
|
171
|
+
if (!portalNode || !preserveTabOrder || modal) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
function onFocus(event: any) {
|
|
175
|
+
if (portalNode && isOutsideEvent(event)) {
|
|
176
|
+
const focusing = event.type === 'focusin';
|
|
177
|
+
const manageFocus = focusing ? enableFocusInside : disableFocusInside;
|
|
178
|
+
manageFocus(portalNode);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
portalNode.addEventListener('focusin', onFocus, true);
|
|
182
|
+
portalNode.addEventListener('focusout', onFocus, true);
|
|
183
|
+
return () => {
|
|
184
|
+
portalNode.removeEventListener('focusin', onFocus, true);
|
|
185
|
+
portalNode.removeEventListener('focusout', onFocus, true);
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
[portalNode, preserveTabOrder, modal],
|
|
189
|
+
S('FloatingPortal:e:tab'),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
useEffect(
|
|
193
|
+
() => {
|
|
194
|
+
if (!portalNode) return;
|
|
195
|
+
if (open) return;
|
|
196
|
+
enableFocusInside(portalNode);
|
|
197
|
+
},
|
|
198
|
+
[open, portalNode],
|
|
199
|
+
S('FloatingPortal:e:enable'),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const value = useMemo(
|
|
203
|
+
() => ({
|
|
204
|
+
preserveTabOrder,
|
|
205
|
+
beforeOutsideRef,
|
|
206
|
+
afterOutsideRef,
|
|
207
|
+
beforeInsideRef,
|
|
208
|
+
afterInsideRef,
|
|
209
|
+
portalNode,
|
|
210
|
+
setFocusManagerState,
|
|
211
|
+
}),
|
|
212
|
+
[preserveTabOrder, portalNode],
|
|
213
|
+
S('FloatingPortal:value'),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
return createElement(PortalContext.Provider, {
|
|
217
|
+
value,
|
|
218
|
+
children: [
|
|
219
|
+
shouldRenderGuards &&
|
|
220
|
+
portalNode &&
|
|
221
|
+
createElement(FocusGuard, {
|
|
222
|
+
'data-type': 'outside',
|
|
223
|
+
ref: beforeOutsideRef,
|
|
224
|
+
onFocus: (event: any) => {
|
|
225
|
+
if (isOutsideEvent(event, portalNode)) {
|
|
226
|
+
beforeInsideRef.current?.focus();
|
|
227
|
+
} else {
|
|
228
|
+
const domReference = focusManagerState ? focusManagerState.domReference : null;
|
|
229
|
+
getPreviousTabbable(domReference)?.focus();
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
}),
|
|
233
|
+
shouldRenderGuards &&
|
|
234
|
+
portalNode &&
|
|
235
|
+
createElement('span', { 'aria-owns': portalNode.id, style: HIDDEN_OWNER_STYLES }),
|
|
236
|
+
portalNode && createPortal(children, portalNode),
|
|
237
|
+
shouldRenderGuards &&
|
|
238
|
+
portalNode &&
|
|
239
|
+
createElement(FocusGuard, {
|
|
240
|
+
'data-type': 'outside',
|
|
241
|
+
ref: afterOutsideRef,
|
|
242
|
+
onFocus: (event: any) => {
|
|
243
|
+
if (isOutsideEvent(event, portalNode)) {
|
|
244
|
+
afterInsideRef.current?.focus();
|
|
245
|
+
} else {
|
|
246
|
+
const domReference = focusManagerState ? focusManagerState.domReference : null;
|
|
247
|
+
getNextTabbable(domReference)?.focus();
|
|
248
|
+
focusManagerState?.closeOnFocusOut &&
|
|
249
|
+
focusManagerState?.onOpenChange(false, event, 'focus-out');
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
}),
|
|
253
|
+
],
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export const usePortalContext = () => useContext(PortalContext);
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// Ported from @floating-ui/react: the event emitter, useFloatingRootContext, and
|
|
2
|
+
// the PUBLIC useFloating (which wraps the positioning core with the interaction
|
|
3
|
+
// context that the interaction hooks consume). React hooks → octane hooks; every
|
|
4
|
+
// internal hook gets a sub-slot derived from the caller's slot.
|
|
5
|
+
import { isElement } from '@floating-ui/utils/dom';
|
|
6
|
+
import { useCallback, useMemo, useRef, useState } from 'octane';
|
|
7
|
+
|
|
8
|
+
import { splitSlot, subSlot } from './internal';
|
|
9
|
+
import { createPubSub } from './pubsub';
|
|
10
|
+
import { useFloatingParentNodeId, useFloatingTree } from './tree';
|
|
11
|
+
import { useId } from './useId';
|
|
12
|
+
import { usePositionFloating } from './useFloating';
|
|
13
|
+
import { useEffectEvent, useModernLayoutEffect } from './utils';
|
|
14
|
+
|
|
15
|
+
export { createPubSub } from './pubsub';
|
|
16
|
+
|
|
17
|
+
export function useFloatingRootContext(options: any, slot: symbol | undefined): any {
|
|
18
|
+
const open = options.open ?? false;
|
|
19
|
+
const onOpenChangeProp = options.onOpenChange;
|
|
20
|
+
const elementsProp = options.elements;
|
|
21
|
+
|
|
22
|
+
const floatingId = useId(subSlot(slot, 'id'));
|
|
23
|
+
const dataRef = useRef<any>({}, subSlot(slot, 'data'));
|
|
24
|
+
const [events] = useState(() => createPubSub(), subSlot(slot, 'events'));
|
|
25
|
+
const nested = useFloatingParentNodeId() != null;
|
|
26
|
+
|
|
27
|
+
const [positionReference, setPositionReference] = useState(
|
|
28
|
+
elementsProp.reference,
|
|
29
|
+
subSlot(slot, 'posref'),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const onOpenChange = useEffectEvent(
|
|
33
|
+
(openVal: boolean, event?: Event, reason?: string) => {
|
|
34
|
+
dataRef.current.openEvent = openVal ? event : undefined;
|
|
35
|
+
events.emit('openchange', { open: openVal, event, reason, nested });
|
|
36
|
+
onOpenChangeProp?.(openVal, event, reason);
|
|
37
|
+
},
|
|
38
|
+
subSlot(slot, 'ooc'),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const refs = useMemo(() => ({ setPositionReference }), [], subSlot(slot, 'm:refs'));
|
|
42
|
+
|
|
43
|
+
const elements = useMemo(
|
|
44
|
+
() => ({
|
|
45
|
+
reference: positionReference || elementsProp.reference || null,
|
|
46
|
+
floating: elementsProp.floating || null,
|
|
47
|
+
domReference: elementsProp.reference,
|
|
48
|
+
}),
|
|
49
|
+
[positionReference, elementsProp.reference, elementsProp.floating],
|
|
50
|
+
subSlot(slot, 'm:el'),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return useMemo(
|
|
54
|
+
() => ({ dataRef, open, onOpenChange, elements, events, floatingId, refs }),
|
|
55
|
+
[open, onOpenChange, elements, events, floatingId, refs],
|
|
56
|
+
subSlot(slot, 'm:ret'),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function useFloating(...args: any[]): any {
|
|
61
|
+
const [user, slot] = splitSlot(args);
|
|
62
|
+
const options = (user[0] as any) ?? {};
|
|
63
|
+
const nodeId = options.nodeId;
|
|
64
|
+
|
|
65
|
+
const internalRootContext = useFloatingRootContext(
|
|
66
|
+
{
|
|
67
|
+
...options,
|
|
68
|
+
elements: {
|
|
69
|
+
reference: null,
|
|
70
|
+
floating: null,
|
|
71
|
+
...options.elements,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
subSlot(slot, 'root'),
|
|
75
|
+
);
|
|
76
|
+
const rootContext = options.rootContext || internalRootContext;
|
|
77
|
+
const computedElements = rootContext.elements;
|
|
78
|
+
|
|
79
|
+
const [_domReference, setDomReference] = useState(null, subSlot(slot, 'domref'));
|
|
80
|
+
const [positionReference, _setPositionReference] = useState(null, subSlot(slot, 'posref'));
|
|
81
|
+
const optionDomReference = computedElements?.domReference;
|
|
82
|
+
const domReference = optionDomReference || _domReference;
|
|
83
|
+
const domReferenceRef = useRef<any>(null, subSlot(slot, 'domrefref'));
|
|
84
|
+
const tree = useFloatingTree();
|
|
85
|
+
|
|
86
|
+
useModernLayoutEffect(
|
|
87
|
+
() => {
|
|
88
|
+
if (domReference) {
|
|
89
|
+
domReferenceRef.current = domReference;
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
[domReference],
|
|
93
|
+
subSlot(slot, 'e:domref'),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const position = usePositionFloating([
|
|
97
|
+
{
|
|
98
|
+
...options,
|
|
99
|
+
elements: {
|
|
100
|
+
...computedElements,
|
|
101
|
+
...(positionReference ? { reference: positionReference } : {}),
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
subSlot(slot, 'pos'),
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
const setPositionReference = useCallback(
|
|
108
|
+
(node: any) => {
|
|
109
|
+
const computedPositionReference = isElement(node)
|
|
110
|
+
? {
|
|
111
|
+
getBoundingClientRect: () => node.getBoundingClientRect(),
|
|
112
|
+
getClientRects: () => node.getClientRects(),
|
|
113
|
+
contextElement: node,
|
|
114
|
+
}
|
|
115
|
+
: node;
|
|
116
|
+
_setPositionReference(computedPositionReference);
|
|
117
|
+
position.refs.setReference(computedPositionReference);
|
|
118
|
+
},
|
|
119
|
+
[position.refs],
|
|
120
|
+
subSlot(slot, 'spr'),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const setReference = useCallback(
|
|
124
|
+
(node: any) => {
|
|
125
|
+
if (isElement(node) || node === null) {
|
|
126
|
+
domReferenceRef.current = node;
|
|
127
|
+
setDomReference(node);
|
|
128
|
+
}
|
|
129
|
+
if (
|
|
130
|
+
isElement(position.refs.reference.current) ||
|
|
131
|
+
position.refs.reference.current === null ||
|
|
132
|
+
(node !== null && !isElement(node))
|
|
133
|
+
) {
|
|
134
|
+
position.refs.setReference(node);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
[position.refs],
|
|
138
|
+
subSlot(slot, 'sr'),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const refs = useMemo(
|
|
142
|
+
() => ({
|
|
143
|
+
...position.refs,
|
|
144
|
+
setReference,
|
|
145
|
+
setPositionReference,
|
|
146
|
+
domReference: domReferenceRef,
|
|
147
|
+
}),
|
|
148
|
+
[position.refs, setReference, setPositionReference],
|
|
149
|
+
subSlot(slot, 'm:refs'),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const elements = useMemo(
|
|
153
|
+
() => ({ ...position.elements, domReference }),
|
|
154
|
+
[position.elements, domReference],
|
|
155
|
+
subSlot(slot, 'm:el'),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const context = useMemo(
|
|
159
|
+
() => ({ ...position, ...rootContext, refs, elements, nodeId }),
|
|
160
|
+
[position, refs, elements, nodeId, rootContext],
|
|
161
|
+
subSlot(slot, 'm:ctx'),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
useModernLayoutEffect(
|
|
165
|
+
() => {
|
|
166
|
+
rootContext.dataRef.current.floatingContext = context;
|
|
167
|
+
const node = tree?.nodesRef.current.find((n: any) => n.id === nodeId);
|
|
168
|
+
if (node) {
|
|
169
|
+
node.context = context;
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
undefined,
|
|
173
|
+
subSlot(slot, 'e:ctx'),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
return useMemo(
|
|
177
|
+
() => ({ ...position, context, refs, elements }),
|
|
178
|
+
[position, refs, elements, context],
|
|
179
|
+
subSlot(slot, 'm:ret'),
|
|
180
|
+
);
|
|
181
|
+
}
|