@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,379 @@
|
|
|
1
|
+
// Ported from @floating-ui/react useDismiss — closes on escape / outside-press /
|
|
2
|
+
// ancestor-scroll. octane events are NATIVE (`event.nativeEvent` → `event`). The
|
|
3
|
+
// upstream `floating` free-variable in the scrollbar check is the intended
|
|
4
|
+
// `elements.floating`. NOTE: the `*Capture` handler keys are emitted as-is; octane
|
|
5
|
+
// has no capture-phase prop, so the inside-press optimisation degrades gracefully —
|
|
6
|
+
// the document-level `contains` check still prevents inside clicks from dismissing.
|
|
7
|
+
import {
|
|
8
|
+
getComputedStyle,
|
|
9
|
+
getParentNode,
|
|
10
|
+
isElement,
|
|
11
|
+
isHTMLElement,
|
|
12
|
+
isLastTraversableNode,
|
|
13
|
+
isWebKit,
|
|
14
|
+
} from '@floating-ui/utils/dom';
|
|
15
|
+
import { getOverflowAncestors } from '@floating-ui/dom';
|
|
16
|
+
import { useEffect, useMemo, useRef } from 'octane';
|
|
17
|
+
|
|
18
|
+
import { splitSlot, subSlot } from './internal';
|
|
19
|
+
import { useFloatingTree } from './tree';
|
|
20
|
+
import {
|
|
21
|
+
contains,
|
|
22
|
+
createAttribute,
|
|
23
|
+
getDocument,
|
|
24
|
+
getNodeChildren,
|
|
25
|
+
getTarget,
|
|
26
|
+
isEventTargetWithin,
|
|
27
|
+
isReactEvent,
|
|
28
|
+
isRootElement,
|
|
29
|
+
useEffectEvent,
|
|
30
|
+
} from './utils';
|
|
31
|
+
|
|
32
|
+
const bubbleHandlerKeys: any = {
|
|
33
|
+
pointerdown: 'onPointerDown',
|
|
34
|
+
mousedown: 'onMouseDown',
|
|
35
|
+
click: 'onClick',
|
|
36
|
+
};
|
|
37
|
+
const captureHandlerKeys: any = {
|
|
38
|
+
pointerdown: 'onPointerDownCapture',
|
|
39
|
+
mousedown: 'onMouseDownCapture',
|
|
40
|
+
click: 'onClickCapture',
|
|
41
|
+
};
|
|
42
|
+
const normalizeProp = (normalizable: any) => ({
|
|
43
|
+
escapeKey: typeof normalizable === 'boolean' ? normalizable : (normalizable?.escapeKey ?? false),
|
|
44
|
+
outsidePress:
|
|
45
|
+
typeof normalizable === 'boolean' ? normalizable : (normalizable?.outsidePress ?? true),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export function useDismiss(...args: any[]): any {
|
|
49
|
+
const [user, slot] = splitSlot(args);
|
|
50
|
+
const context = user[0];
|
|
51
|
+
const props = (user[1] as any) ?? {};
|
|
52
|
+
|
|
53
|
+
const open = context.open;
|
|
54
|
+
const onOpenChange = context.onOpenChange;
|
|
55
|
+
const elements = context.elements;
|
|
56
|
+
const dataRef = context.dataRef;
|
|
57
|
+
|
|
58
|
+
const enabled = props.enabled ?? true;
|
|
59
|
+
const escapeKey = props.escapeKey ?? true;
|
|
60
|
+
const unstableOutsidePress = props.outsidePress ?? true;
|
|
61
|
+
const outsidePressEvent = props.outsidePressEvent ?? 'pointerdown';
|
|
62
|
+
const referencePress = props.referencePress ?? false;
|
|
63
|
+
const referencePressEvent = props.referencePressEvent ?? 'pointerdown';
|
|
64
|
+
const ancestorScroll = props.ancestorScroll ?? false;
|
|
65
|
+
const bubbles = props.bubbles;
|
|
66
|
+
const capture = props.capture;
|
|
67
|
+
|
|
68
|
+
const tree = useFloatingTree();
|
|
69
|
+
const outsidePressFn = useEffectEvent(
|
|
70
|
+
typeof unstableOutsidePress === 'function' ? unstableOutsidePress : () => false,
|
|
71
|
+
subSlot(slot, 'opfn'),
|
|
72
|
+
);
|
|
73
|
+
const outsidePress =
|
|
74
|
+
typeof unstableOutsidePress === 'function' ? outsidePressFn : unstableOutsidePress;
|
|
75
|
+
const endedOrStartedInsideRef = useRef(false, subSlot(slot, 'eosi'));
|
|
76
|
+
const { escapeKey: escapeKeyBubbles, outsidePress: outsidePressBubbles } = normalizeProp(bubbles);
|
|
77
|
+
const { escapeKey: escapeKeyCapture, outsidePress: outsidePressCapture } = normalizeProp(capture);
|
|
78
|
+
const isComposingRef = useRef(false, subSlot(slot, 'comp'));
|
|
79
|
+
|
|
80
|
+
const closeOnEscapeKeyDown = useEffectEvent(
|
|
81
|
+
(event: any) => {
|
|
82
|
+
if (!open || !enabled || !escapeKey || event.key !== 'Escape') {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (isComposingRef.current) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const nodeId = dataRef.current.floatingContext?.nodeId;
|
|
89
|
+
const children = tree ? getNodeChildren(tree.nodesRef.current, nodeId) : [];
|
|
90
|
+
if (!escapeKeyBubbles) {
|
|
91
|
+
event.stopPropagation();
|
|
92
|
+
if (children.length > 0) {
|
|
93
|
+
let shouldDismiss = true;
|
|
94
|
+
children.forEach((child: any) => {
|
|
95
|
+
if (child.context?.open && !child.context.dataRef.current.__escapeKeyBubbles) {
|
|
96
|
+
shouldDismiss = false;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
if (!shouldDismiss) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
onOpenChange(false, isReactEvent(event) ? event.nativeEvent : event, 'escape-key');
|
|
106
|
+
},
|
|
107
|
+
subSlot(slot, 'esc'),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const closeOnEscapeKeyDownCapture = useEffectEvent(
|
|
111
|
+
(event: any) => {
|
|
112
|
+
const callback = () => {
|
|
113
|
+
closeOnEscapeKeyDown(event);
|
|
114
|
+
getTarget(event)?.removeEventListener('keydown', callback);
|
|
115
|
+
};
|
|
116
|
+
getTarget(event)?.addEventListener('keydown', callback);
|
|
117
|
+
},
|
|
118
|
+
subSlot(slot, 'escc'),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const closeOnPressOutside = useEffectEvent(
|
|
122
|
+
(event: any) => {
|
|
123
|
+
const insideReactTree = dataRef.current.insideReactTree;
|
|
124
|
+
dataRef.current.insideReactTree = false;
|
|
125
|
+
|
|
126
|
+
const endedOrStartedInside = endedOrStartedInsideRef.current;
|
|
127
|
+
endedOrStartedInsideRef.current = false;
|
|
128
|
+
if (outsidePressEvent === 'click' && endedOrStartedInside) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (insideReactTree) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (typeof outsidePress === 'function' && !outsidePress(event)) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const target = getTarget(event);
|
|
138
|
+
const inertSelector = '[' + createAttribute('inert') + ']';
|
|
139
|
+
const markers = getDocument(elements.floating).querySelectorAll(inertSelector);
|
|
140
|
+
let targetRootAncestor = isElement(target) ? target : null;
|
|
141
|
+
while (targetRootAncestor && !isLastTraversableNode(targetRootAncestor)) {
|
|
142
|
+
const nextParent = getParentNode(targetRootAncestor);
|
|
143
|
+
if (isLastTraversableNode(nextParent) || !isElement(nextParent)) {
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
targetRootAncestor = nextParent;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (
|
|
150
|
+
markers.length &&
|
|
151
|
+
isElement(target) &&
|
|
152
|
+
!isRootElement(target as Element) &&
|
|
153
|
+
!contains(target as any, elements.floating) &&
|
|
154
|
+
Array.from(markers).every((marker) => !contains(targetRootAncestor as any, marker as any))
|
|
155
|
+
) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (isHTMLElement(target) && elements.floating) {
|
|
160
|
+
const lastTraversableNode = isLastTraversableNode(target);
|
|
161
|
+
const style = getComputedStyle(target);
|
|
162
|
+
const scrollRe = /auto|scroll/;
|
|
163
|
+
const isScrollableX = lastTraversableNode || scrollRe.test(style.overflowX);
|
|
164
|
+
const isScrollableY = lastTraversableNode || scrollRe.test(style.overflowY);
|
|
165
|
+
const canScrollX =
|
|
166
|
+
isScrollableX && target.clientWidth > 0 && target.scrollWidth > target.clientWidth;
|
|
167
|
+
const canScrollY =
|
|
168
|
+
isScrollableY && target.clientHeight > 0 && target.scrollHeight > target.clientHeight;
|
|
169
|
+
const isRTL = style.direction === 'rtl';
|
|
170
|
+
const pressedVerticalScrollbar =
|
|
171
|
+
canScrollY &&
|
|
172
|
+
(isRTL
|
|
173
|
+
? (event as any).offsetX <= target.offsetWidth - target.clientWidth
|
|
174
|
+
: (event as any).offsetX > target.clientWidth);
|
|
175
|
+
const pressedHorizontalScrollbar =
|
|
176
|
+
canScrollX && (event as any).offsetY > target.clientHeight;
|
|
177
|
+
if (pressedVerticalScrollbar || pressedHorizontalScrollbar) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const nodeId = dataRef.current.floatingContext?.nodeId;
|
|
183
|
+
const targetIsInsideChildren =
|
|
184
|
+
tree &&
|
|
185
|
+
getNodeChildren(tree.nodesRef.current, nodeId).some((node: any) =>
|
|
186
|
+
isEventTargetWithin(event, node.context?.elements.floating),
|
|
187
|
+
);
|
|
188
|
+
if (
|
|
189
|
+
isEventTargetWithin(event, elements.floating) ||
|
|
190
|
+
isEventTargetWithin(event, elements.domReference) ||
|
|
191
|
+
targetIsInsideChildren
|
|
192
|
+
) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const children = tree ? getNodeChildren(tree.nodesRef.current, nodeId) : [];
|
|
196
|
+
if (children.length > 0) {
|
|
197
|
+
let shouldDismiss = true;
|
|
198
|
+
children.forEach((child: any) => {
|
|
199
|
+
if (child.context?.open && !child.context.dataRef.current.__outsidePressBubbles) {
|
|
200
|
+
shouldDismiss = false;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
if (!shouldDismiss) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
onOpenChange(false, event, 'outside-press');
|
|
209
|
+
},
|
|
210
|
+
subSlot(slot, 'press'),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const closeOnPressOutsideCapture = useEffectEvent(
|
|
214
|
+
(event: any) => {
|
|
215
|
+
const callback = () => {
|
|
216
|
+
closeOnPressOutside(event);
|
|
217
|
+
getTarget(event)?.removeEventListener(outsidePressEvent, callback);
|
|
218
|
+
};
|
|
219
|
+
getTarget(event)?.addEventListener(outsidePressEvent, callback);
|
|
220
|
+
},
|
|
221
|
+
subSlot(slot, 'pressc'),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
useEffect(
|
|
225
|
+
() => {
|
|
226
|
+
if (!open || !enabled) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
dataRef.current.__escapeKeyBubbles = escapeKeyBubbles;
|
|
230
|
+
dataRef.current.__outsidePressBubbles = outsidePressBubbles;
|
|
231
|
+
let compositionTimeout = -1;
|
|
232
|
+
function onScroll(event: any) {
|
|
233
|
+
onOpenChange(false, event, 'ancestor-scroll');
|
|
234
|
+
}
|
|
235
|
+
function handleCompositionStart() {
|
|
236
|
+
window.clearTimeout(compositionTimeout);
|
|
237
|
+
isComposingRef.current = true;
|
|
238
|
+
}
|
|
239
|
+
function handleCompositionEnd() {
|
|
240
|
+
compositionTimeout = window.setTimeout(
|
|
241
|
+
() => {
|
|
242
|
+
isComposingRef.current = false;
|
|
243
|
+
},
|
|
244
|
+
isWebKit() ? 5 : 0,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
const doc = getDocument(elements.floating);
|
|
248
|
+
if (escapeKey) {
|
|
249
|
+
doc.addEventListener(
|
|
250
|
+
'keydown',
|
|
251
|
+
escapeKeyCapture ? closeOnEscapeKeyDownCapture : closeOnEscapeKeyDown,
|
|
252
|
+
escapeKeyCapture,
|
|
253
|
+
);
|
|
254
|
+
doc.addEventListener('compositionstart', handleCompositionStart);
|
|
255
|
+
doc.addEventListener('compositionend', handleCompositionEnd);
|
|
256
|
+
}
|
|
257
|
+
outsidePress &&
|
|
258
|
+
doc.addEventListener(
|
|
259
|
+
outsidePressEvent,
|
|
260
|
+
outsidePressCapture ? closeOnPressOutsideCapture : closeOnPressOutside,
|
|
261
|
+
outsidePressCapture,
|
|
262
|
+
);
|
|
263
|
+
let ancestors: any[] = [];
|
|
264
|
+
if (ancestorScroll) {
|
|
265
|
+
if (isElement(elements.domReference)) {
|
|
266
|
+
ancestors = getOverflowAncestors(elements.domReference);
|
|
267
|
+
}
|
|
268
|
+
if (isElement(elements.floating)) {
|
|
269
|
+
ancestors = ancestors.concat(getOverflowAncestors(elements.floating));
|
|
270
|
+
}
|
|
271
|
+
if (
|
|
272
|
+
!isElement(elements.reference) &&
|
|
273
|
+
elements.reference &&
|
|
274
|
+
elements.reference.contextElement
|
|
275
|
+
) {
|
|
276
|
+
ancestors = ancestors.concat(getOverflowAncestors(elements.reference.contextElement));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
ancestors = ancestors.filter((ancestor) => ancestor !== doc.defaultView?.visualViewport);
|
|
280
|
+
ancestors.forEach((ancestor) => {
|
|
281
|
+
ancestor.addEventListener('scroll', onScroll, { passive: true });
|
|
282
|
+
});
|
|
283
|
+
return () => {
|
|
284
|
+
if (escapeKey) {
|
|
285
|
+
doc.removeEventListener(
|
|
286
|
+
'keydown',
|
|
287
|
+
escapeKeyCapture ? closeOnEscapeKeyDownCapture : closeOnEscapeKeyDown,
|
|
288
|
+
escapeKeyCapture,
|
|
289
|
+
);
|
|
290
|
+
doc.removeEventListener('compositionstart', handleCompositionStart);
|
|
291
|
+
doc.removeEventListener('compositionend', handleCompositionEnd);
|
|
292
|
+
}
|
|
293
|
+
outsidePress &&
|
|
294
|
+
doc.removeEventListener(
|
|
295
|
+
outsidePressEvent,
|
|
296
|
+
outsidePressCapture ? closeOnPressOutsideCapture : closeOnPressOutside,
|
|
297
|
+
outsidePressCapture,
|
|
298
|
+
);
|
|
299
|
+
ancestors.forEach((ancestor) => {
|
|
300
|
+
ancestor.removeEventListener('scroll', onScroll);
|
|
301
|
+
});
|
|
302
|
+
window.clearTimeout(compositionTimeout);
|
|
303
|
+
};
|
|
304
|
+
},
|
|
305
|
+
[
|
|
306
|
+
dataRef,
|
|
307
|
+
elements,
|
|
308
|
+
escapeKey,
|
|
309
|
+
outsidePress,
|
|
310
|
+
outsidePressEvent,
|
|
311
|
+
open,
|
|
312
|
+
onOpenChange,
|
|
313
|
+
ancestorScroll,
|
|
314
|
+
enabled,
|
|
315
|
+
escapeKeyBubbles,
|
|
316
|
+
outsidePressBubbles,
|
|
317
|
+
closeOnEscapeKeyDown,
|
|
318
|
+
escapeKeyCapture,
|
|
319
|
+
closeOnEscapeKeyDownCapture,
|
|
320
|
+
closeOnPressOutside,
|
|
321
|
+
outsidePressCapture,
|
|
322
|
+
closeOnPressOutsideCapture,
|
|
323
|
+
],
|
|
324
|
+
subSlot(slot, 'e:listeners'),
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
useEffect(
|
|
328
|
+
() => {
|
|
329
|
+
dataRef.current.insideReactTree = false;
|
|
330
|
+
},
|
|
331
|
+
[dataRef, outsidePress, outsidePressEvent],
|
|
332
|
+
subSlot(slot, 'e:reset'),
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
const reference = useMemo(
|
|
336
|
+
() => ({
|
|
337
|
+
onKeyDown: closeOnEscapeKeyDown,
|
|
338
|
+
...(referencePress && {
|
|
339
|
+
[bubbleHandlerKeys[referencePressEvent]]: (event: any) => {
|
|
340
|
+
onOpenChange(false, event, 'reference-press');
|
|
341
|
+
},
|
|
342
|
+
...(referencePressEvent !== 'click' && {
|
|
343
|
+
onClick(event: any) {
|
|
344
|
+
onOpenChange(false, event, 'reference-press');
|
|
345
|
+
},
|
|
346
|
+
}),
|
|
347
|
+
}),
|
|
348
|
+
}),
|
|
349
|
+
[closeOnEscapeKeyDown, onOpenChange, referencePress, referencePressEvent],
|
|
350
|
+
subSlot(slot, 'm:ref'),
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
const floating = useMemo(
|
|
354
|
+
() => {
|
|
355
|
+
function setMouseDownOrUpInside(event: any) {
|
|
356
|
+
if (event.button !== 0) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
endedOrStartedInsideRef.current = true;
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
onKeyDown: closeOnEscapeKeyDown,
|
|
363
|
+
onMouseDown: setMouseDownOrUpInside,
|
|
364
|
+
onMouseUp: setMouseDownOrUpInside,
|
|
365
|
+
[captureHandlerKeys[outsidePressEvent]]: () => {
|
|
366
|
+
dataRef.current.insideReactTree = true;
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
},
|
|
370
|
+
[closeOnEscapeKeyDown, outsidePressEvent, dataRef],
|
|
371
|
+
subSlot(slot, 'm:flo'),
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
return useMemo(
|
|
375
|
+
() => (enabled ? { reference, floating } : {}),
|
|
376
|
+
[enabled, reference, floating],
|
|
377
|
+
subSlot(slot, 'm:ret'),
|
|
378
|
+
);
|
|
379
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// Ported from @floating-ui/react-dom (the positioning useFloating + the ref-aware
|
|
2
|
+
// `arrow` wrapper). React hooks → octane hooks; ReactDOM.flushSync → octane flushSync.
|
|
3
|
+
// Every internal hook gets a distinct sub-slot derived from the caller's slot (see
|
|
4
|
+
// ./internal). The returned `context`/refs carry the root slot so the interaction
|
|
5
|
+
// hooks (later phases) can compose without their own slot.
|
|
6
|
+
import { arrow as arrowCore, computePosition } from '@floating-ui/dom';
|
|
7
|
+
import { flushSync, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'octane';
|
|
8
|
+
|
|
9
|
+
import { splitSlot, subSlot } from './internal';
|
|
10
|
+
import { deepEqual, getDPR, roundByDPR } from './utils';
|
|
11
|
+
|
|
12
|
+
// Keep a ref pointed at the latest `value` without retriggering effects.
|
|
13
|
+
function useLatestRef<T>(value: T, slot: symbol | undefined): { current: T } {
|
|
14
|
+
const ref = useRef(value, subSlot(slot, 'lr:ref'));
|
|
15
|
+
useLayoutEffect(
|
|
16
|
+
() => {
|
|
17
|
+
ref.current = value;
|
|
18
|
+
},
|
|
19
|
+
undefined,
|
|
20
|
+
subSlot(slot, 'lr:eff'),
|
|
21
|
+
);
|
|
22
|
+
return ref;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The positioning core (ported from @floating-ui/react-dom's useFloating). The
|
|
26
|
+
// PUBLIC useFloating (in ./context) wraps this and adds the interaction context.
|
|
27
|
+
export function usePositionFloating(args: any[]): any {
|
|
28
|
+
const [user, slot] = splitSlot(args);
|
|
29
|
+
const options = (user[0] as any) ?? {};
|
|
30
|
+
|
|
31
|
+
const placement = options.placement ?? 'bottom';
|
|
32
|
+
const strategy = options.strategy ?? 'absolute';
|
|
33
|
+
const middleware = options.middleware ?? [];
|
|
34
|
+
const platform = options.platform;
|
|
35
|
+
const externalReference = options.elements ? options.elements.reference : undefined;
|
|
36
|
+
const externalFloating = options.elements ? options.elements.floating : undefined;
|
|
37
|
+
const transform = options.transform ?? true;
|
|
38
|
+
const whileElementsMounted = options.whileElementsMounted;
|
|
39
|
+
const open = options.open;
|
|
40
|
+
|
|
41
|
+
const [data, setData] = useState(
|
|
42
|
+
{
|
|
43
|
+
x: 0,
|
|
44
|
+
y: 0,
|
|
45
|
+
strategy,
|
|
46
|
+
placement,
|
|
47
|
+
middlewareData: {},
|
|
48
|
+
isPositioned: false,
|
|
49
|
+
},
|
|
50
|
+
subSlot(slot, 'data'),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const [latestMiddleware, setLatestMiddleware] = useState(middleware, subSlot(slot, 'mw'));
|
|
54
|
+
if (!deepEqual(latestMiddleware, middleware)) {
|
|
55
|
+
setLatestMiddleware(middleware);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const [_reference, _setReference] = useState(null, subSlot(slot, 'ref'));
|
|
59
|
+
const [_floating, _setFloating] = useState(null, subSlot(slot, 'flo'));
|
|
60
|
+
|
|
61
|
+
const referenceRef = useRef<any>(null, subSlot(slot, 'rref'));
|
|
62
|
+
const floatingRef = useRef<any>(null, subSlot(slot, 'rflo'));
|
|
63
|
+
|
|
64
|
+
const setReference = useCallback(
|
|
65
|
+
(node: any) => {
|
|
66
|
+
if (node !== referenceRef.current) {
|
|
67
|
+
referenceRef.current = node;
|
|
68
|
+
_setReference(node);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
[],
|
|
72
|
+
subSlot(slot, 'sref'),
|
|
73
|
+
);
|
|
74
|
+
const setFloating = useCallback(
|
|
75
|
+
(node: any) => {
|
|
76
|
+
if (node !== floatingRef.current) {
|
|
77
|
+
floatingRef.current = node;
|
|
78
|
+
_setFloating(node);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
[],
|
|
82
|
+
subSlot(slot, 'sflo'),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const referenceEl = externalReference || _reference;
|
|
86
|
+
const floatingEl = externalFloating || _floating;
|
|
87
|
+
|
|
88
|
+
const dataRef = useRef(data, subSlot(slot, 'dref'));
|
|
89
|
+
const hasWhileElementsMounted = whileElementsMounted != null;
|
|
90
|
+
const whileElementsMountedRef = useLatestRef(whileElementsMounted, subSlot(slot, 'wem'));
|
|
91
|
+
const platformRef = useLatestRef(platform, subSlot(slot, 'plat'));
|
|
92
|
+
const openRef = useLatestRef(open, subSlot(slot, 'open'));
|
|
93
|
+
const isMountedRef = useRef(false, subSlot(slot, 'mnt'));
|
|
94
|
+
|
|
95
|
+
const update = useCallback(
|
|
96
|
+
() => {
|
|
97
|
+
if (!referenceRef.current || !floatingRef.current) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const config: any = { placement, strategy, middleware: latestMiddleware };
|
|
101
|
+
if (platformRef.current) {
|
|
102
|
+
config.platform = platformRef.current;
|
|
103
|
+
}
|
|
104
|
+
computePosition(referenceRef.current, floatingRef.current, config).then((computed) => {
|
|
105
|
+
const fullData = {
|
|
106
|
+
...computed,
|
|
107
|
+
isPositioned: openRef.current !== false,
|
|
108
|
+
};
|
|
109
|
+
if (isMountedRef.current && !deepEqual(dataRef.current, fullData)) {
|
|
110
|
+
dataRef.current = fullData;
|
|
111
|
+
flushSync(() => {
|
|
112
|
+
setData(fullData);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
[latestMiddleware, placement, strategy, platformRef, openRef],
|
|
118
|
+
subSlot(slot, 'upd'),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
useLayoutEffect(
|
|
122
|
+
() => {
|
|
123
|
+
if (open === false && dataRef.current.isPositioned) {
|
|
124
|
+
dataRef.current.isPositioned = false;
|
|
125
|
+
setData((d: any) => ({ ...d, isPositioned: false }));
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
[open],
|
|
129
|
+
subSlot(slot, 'e:open'),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
useLayoutEffect(
|
|
133
|
+
() => {
|
|
134
|
+
isMountedRef.current = true;
|
|
135
|
+
return () => {
|
|
136
|
+
isMountedRef.current = false;
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
[],
|
|
140
|
+
subSlot(slot, 'e:mnt'),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
useLayoutEffect(
|
|
144
|
+
() => {
|
|
145
|
+
if (referenceEl) referenceRef.current = referenceEl;
|
|
146
|
+
if (floatingEl) floatingRef.current = floatingEl;
|
|
147
|
+
if (referenceEl && floatingEl) {
|
|
148
|
+
if (whileElementsMountedRef.current) {
|
|
149
|
+
return whileElementsMountedRef.current(referenceEl, floatingEl, update);
|
|
150
|
+
}
|
|
151
|
+
update();
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
[referenceEl, floatingEl, update, whileElementsMountedRef, hasWhileElementsMounted],
|
|
155
|
+
subSlot(slot, 'e:el'),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const refs = useMemo(
|
|
159
|
+
() => ({ reference: referenceRef, floating: floatingRef, setReference, setFloating }),
|
|
160
|
+
[setReference, setFloating],
|
|
161
|
+
subSlot(slot, 'm:refs'),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const elements = useMemo(
|
|
165
|
+
() => ({ reference: referenceEl, floating: floatingEl }),
|
|
166
|
+
[referenceEl, floatingEl],
|
|
167
|
+
subSlot(slot, 'm:el'),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const floatingStyles = useMemo(
|
|
171
|
+
() => {
|
|
172
|
+
const initialStyles = { position: strategy, left: 0, top: 0 };
|
|
173
|
+
if (!elements.floating) {
|
|
174
|
+
return initialStyles;
|
|
175
|
+
}
|
|
176
|
+
const x = roundByDPR(elements.floating, data.x);
|
|
177
|
+
const y = roundByDPR(elements.floating, data.y);
|
|
178
|
+
if (transform) {
|
|
179
|
+
return {
|
|
180
|
+
...initialStyles,
|
|
181
|
+
transform: 'translate(' + x + 'px, ' + y + 'px)',
|
|
182
|
+
...(getDPR(elements.floating) >= 1.5 && { willChange: 'transform' }),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return { position: strategy, left: x, top: y };
|
|
186
|
+
},
|
|
187
|
+
[strategy, transform, elements.floating, data.x, data.y],
|
|
188
|
+
subSlot(slot, 'm:fs'),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
return useMemo(
|
|
192
|
+
() => ({ ...data, update, refs, elements, floatingStyles }),
|
|
193
|
+
[data, update, refs, elements, floatingStyles],
|
|
194
|
+
subSlot(slot, 'm:ret'),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Ref-aware `arrow` middleware: accepts an octane ref ({current}) or an element.
|
|
199
|
+
export const arrow = (options: any) => {
|
|
200
|
+
function isRef(value: any) {
|
|
201
|
+
return {}.hasOwnProperty.call(value, 'current');
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
name: 'arrow',
|
|
205
|
+
options,
|
|
206
|
+
fn(state: any) {
|
|
207
|
+
const { element, padding } = typeof options === 'function' ? options(state) : options;
|
|
208
|
+
if (element && isRef(element)) {
|
|
209
|
+
if (element.current != null) {
|
|
210
|
+
return arrowCore({ element: element.current, padding }).fn(state);
|
|
211
|
+
}
|
|
212
|
+
return {};
|
|
213
|
+
}
|
|
214
|
+
if (element) {
|
|
215
|
+
return arrowCore({ element, padding }).fn(state);
|
|
216
|
+
}
|
|
217
|
+
return {};
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
};
|