@octanejs/base-ui 0.1.14 → 0.1.16
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/package.json +5 -5
- package/src/alert-dialog.ts +2 -0
- package/src/button.ts +63 -0
- package/src/context-menu.ts +317 -0
- package/src/csp-provider.ts +37 -0
- package/src/dialog.ts +89 -22
- package/src/direction-provider.ts +32 -0
- package/src/index.ts +10 -0
- package/src/menu.ts +3387 -0
- package/src/menubar.ts +160 -0
- package/src/popover.ts +99 -25
- package/src/preview-card.ts +812 -0
- package/src/tooltip.ts +1053 -0
- package/src/unstable-use-media-query.ts +101 -0
- package/src/utils/CSPContext.ts +21 -0
- package/src/utils/ContextMenuRootContext.ts +44 -0
- package/src/utils/FloatingPortalLite.ts +29 -0
- package/src/utils/MenubarContext.ts +38 -0
- package/src/utils/RequestQueue.ts +132 -0
- package/src/utils/TimeoutManager.ts +32 -0
- package/src/utils/composite/grid-utils.ts +437 -0
- package/src/utils/composite/gridNavigation.ts +127 -0
- package/src/utils/composite/list-utils.ts +56 -28
- package/src/utils/composite/useCompositeRoot.ts +11 -13
- package/src/utils/constants.ts +22 -0
- package/src/utils/createChangeEventDetails.ts +4 -0
- package/src/utils/findRootOwnerId.ts +19 -0
- package/src/utils/floating/FloatingDelayGroup.ts +280 -0
- package/src/utils/floating/element.ts +36 -2
- package/src/utils/floating/gridNavigation.ts +54 -0
- package/src/utils/floating/safePolygon.ts +448 -0
- package/src/utils/floating/useClientPoint.ts +276 -0
- package/src/utils/floating/useFocus.ts +248 -0
- package/src/utils/floating/useHoverFloatingInteraction.ts +301 -11
- package/src/utils/floating/useHoverInteractionSharedState.ts +139 -0
- package/src/utils/floating/useHoverReferenceInteraction.ts +507 -14
- package/src/utils/floating/useHoverShared.ts +78 -0
- package/src/utils/floating/useListNavigation.ts +1018 -0
- package/src/utils/floating/useTypeahead.ts +287 -0
- package/src/utils/getCssDimensions.ts +28 -0
- package/src/utils/getPseudoElementBounds.ts +54 -0
- package/src/utils/platform.ts +10 -0
- package/src/utils/popups/index.ts +6 -0
- package/src/utils/popups/inlineRect.ts +289 -0
- package/src/utils/useMixedToggleClickHandler.ts +77 -0
- package/src/utils/useOpenInteractionType.ts +37 -1
- package/src/utils/usePopupAutoResize.ts +250 -0
- package/src/utils/usePopupViewport.ts +375 -0
- package/src/utils/usePreviousValue.ts +25 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
// Ported verbatim from .base-ui/packages/react/src/floating-ui-react/safePolygon.ts (v1.6.0).
|
|
2
|
+
// Builds the "safe" triangle between the cursor's exit point and the popup so the user can move
|
|
3
|
+
// diagonally toward the popup without it closing under the pointer. Pure geometry plus a mousemove
|
|
4
|
+
// handler; no React/octane API is involved, so this is a straight transcription.
|
|
5
|
+
import { isElement } from '../dom';
|
|
6
|
+
import { Timeout } from '../useTimeout';
|
|
7
|
+
import { contains, getTarget } from './element';
|
|
8
|
+
import { getNodeChildren } from './nodes';
|
|
9
|
+
import type { HandleClose, HandleCloseOptions } from './useHoverShared';
|
|
10
|
+
|
|
11
|
+
const CURSOR_SPEED_THRESHOLD = 0.1;
|
|
12
|
+
const CURSOR_SPEED_THRESHOLD_SQUARED = CURSOR_SPEED_THRESHOLD * CURSOR_SPEED_THRESHOLD;
|
|
13
|
+
const POLYGON_BUFFER = 0.5;
|
|
14
|
+
|
|
15
|
+
interface Rect {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type Side = 'top' | 'right' | 'bottom' | 'left';
|
|
23
|
+
|
|
24
|
+
function hasIntersectingEdge(
|
|
25
|
+
pointX: number,
|
|
26
|
+
pointY: number,
|
|
27
|
+
xi: number,
|
|
28
|
+
yi: number,
|
|
29
|
+
xj: number,
|
|
30
|
+
yj: number,
|
|
31
|
+
): boolean {
|
|
32
|
+
return yi >= pointY !== yj >= pointY && pointX <= ((xj - xi) * (pointY - yi)) / (yj - yi) + xi;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isPointInQuadrilateral(
|
|
36
|
+
pointX: number,
|
|
37
|
+
pointY: number,
|
|
38
|
+
x1: number,
|
|
39
|
+
y1: number,
|
|
40
|
+
x2: number,
|
|
41
|
+
y2: number,
|
|
42
|
+
x3: number,
|
|
43
|
+
y3: number,
|
|
44
|
+
x4: number,
|
|
45
|
+
y4: number,
|
|
46
|
+
): boolean {
|
|
47
|
+
let isInsideValue = false;
|
|
48
|
+
|
|
49
|
+
if (hasIntersectingEdge(pointX, pointY, x1, y1, x2, y2)) {
|
|
50
|
+
isInsideValue = !isInsideValue;
|
|
51
|
+
}
|
|
52
|
+
if (hasIntersectingEdge(pointX, pointY, x2, y2, x3, y3)) {
|
|
53
|
+
isInsideValue = !isInsideValue;
|
|
54
|
+
}
|
|
55
|
+
if (hasIntersectingEdge(pointX, pointY, x3, y3, x4, y4)) {
|
|
56
|
+
isInsideValue = !isInsideValue;
|
|
57
|
+
}
|
|
58
|
+
if (hasIntersectingEdge(pointX, pointY, x4, y4, x1, y1)) {
|
|
59
|
+
isInsideValue = !isInsideValue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return isInsideValue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isInsideRect(pointX: number, pointY: number, rect: Rect): boolean {
|
|
66
|
+
return (
|
|
67
|
+
pointX >= rect.x &&
|
|
68
|
+
pointX <= rect.x + rect.width &&
|
|
69
|
+
pointY >= rect.y &&
|
|
70
|
+
pointY <= rect.y + rect.height
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isInsideAxisAlignedRect(
|
|
75
|
+
pointX: number,
|
|
76
|
+
pointY: number,
|
|
77
|
+
x1: number,
|
|
78
|
+
y1: number,
|
|
79
|
+
x2: number,
|
|
80
|
+
y2: number,
|
|
81
|
+
): boolean {
|
|
82
|
+
const minX = Math.min(x1, x2);
|
|
83
|
+
const maxX = Math.max(x1, x2);
|
|
84
|
+
const minY = Math.min(y1, y2);
|
|
85
|
+
const maxY = Math.max(y1, y2);
|
|
86
|
+
|
|
87
|
+
return pointX >= minX && pointX <= maxX && pointY >= minY && pointY <= maxY;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface SafePolygonOptions extends HandleCloseOptions {}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Generates a safe polygon area that the user can traverse without closing the floating element
|
|
94
|
+
* once leaving the reference element.
|
|
95
|
+
*/
|
|
96
|
+
export function safePolygon(options: SafePolygonOptions = {}): HandleClose {
|
|
97
|
+
const { blockPointerEvents = false } = options;
|
|
98
|
+
const timeout = new Timeout();
|
|
99
|
+
|
|
100
|
+
const fn: HandleClose = ({ x, y, placement, elements, onClose, nodeId, tree }) => {
|
|
101
|
+
const side = placement?.split('-')[0] as Side | undefined;
|
|
102
|
+
let hasLanded = false;
|
|
103
|
+
let lastX: number | null = null;
|
|
104
|
+
let lastY: number | null = null;
|
|
105
|
+
let lastCursorTime = typeof performance !== 'undefined' ? performance.now() : 0;
|
|
106
|
+
|
|
107
|
+
function isCursorMovingSlowly(nextX: number, nextY: number): boolean {
|
|
108
|
+
const currentTime = performance.now();
|
|
109
|
+
const elapsedTime = currentTime - lastCursorTime;
|
|
110
|
+
|
|
111
|
+
if (lastX === null || lastY === null || elapsedTime === 0) {
|
|
112
|
+
lastX = nextX;
|
|
113
|
+
lastY = nextY;
|
|
114
|
+
lastCursorTime = currentTime;
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const deltaX = nextX - lastX;
|
|
119
|
+
const deltaY = nextY - lastY;
|
|
120
|
+
const distanceSquared = deltaX * deltaX + deltaY * deltaY;
|
|
121
|
+
const thresholdSquared = elapsedTime * elapsedTime * CURSOR_SPEED_THRESHOLD_SQUARED;
|
|
122
|
+
|
|
123
|
+
lastX = nextX;
|
|
124
|
+
lastY = nextY;
|
|
125
|
+
lastCursorTime = currentTime;
|
|
126
|
+
|
|
127
|
+
return distanceSquared < thresholdSquared;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function close(): void {
|
|
131
|
+
timeout.clear();
|
|
132
|
+
onClose();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return function onMouseMove(event: MouseEvent): undefined {
|
|
136
|
+
timeout.clear();
|
|
137
|
+
|
|
138
|
+
const domReference = elements.domReference;
|
|
139
|
+
const floating = elements.floating;
|
|
140
|
+
if (!domReference || !floating || side == null || x == null || y == null) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const { clientX, clientY } = event;
|
|
145
|
+
const target = getTarget(event) as Element | null;
|
|
146
|
+
const isLeave = event.type === 'mouseleave';
|
|
147
|
+
const isOverFloatingEl = contains(floating, target);
|
|
148
|
+
const isOverReferenceEl = contains(domReference, target);
|
|
149
|
+
|
|
150
|
+
if (isOverFloatingEl) {
|
|
151
|
+
hasLanded = true;
|
|
152
|
+
if (!isLeave) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (isOverReferenceEl) {
|
|
158
|
+
hasLanded = false;
|
|
159
|
+
if (!isLeave) {
|
|
160
|
+
hasLanded = true;
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Prevent an overlapping floating element from getting stuck in an open-close loop.
|
|
166
|
+
if (isLeave && isElement(event.relatedTarget) && contains(floating, event.relatedTarget)) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function hasOpenChildNode(): boolean {
|
|
171
|
+
return Boolean(tree && getNodeChildren(tree.nodesRef.current, nodeId).length > 0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function closeIfNoOpenChild(): void {
|
|
175
|
+
if (!hasOpenChildNode()) {
|
|
176
|
+
close();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// If any nested child is open, abort.
|
|
181
|
+
if (hasOpenChildNode()) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const refRect = domReference.getBoundingClientRect();
|
|
186
|
+
const rect = floating.getBoundingClientRect();
|
|
187
|
+
const cursorLeaveFromRight = x > rect.right - rect.width / 2;
|
|
188
|
+
const cursorLeaveFromBottom = y > rect.bottom - rect.height / 2;
|
|
189
|
+
const isFloatingWider = rect.width > refRect.width;
|
|
190
|
+
const isFloatingTaller = rect.height > refRect.height;
|
|
191
|
+
const left = (isFloatingWider ? refRect : rect).left;
|
|
192
|
+
const right = (isFloatingWider ? refRect : rect).right;
|
|
193
|
+
const top = (isFloatingTaller ? refRect : rect).top;
|
|
194
|
+
const bottom = (isFloatingTaller ? refRect : rect).bottom;
|
|
195
|
+
|
|
196
|
+
// If the pointer is leaving from the opposite side, the "buffer" logic creates a point
|
|
197
|
+
// where the floating element remains open, but should be ignored. A constant of 1
|
|
198
|
+
// handles floating point rounding errors.
|
|
199
|
+
if (
|
|
200
|
+
(side === 'top' && y >= refRect.bottom - 1) ||
|
|
201
|
+
(side === 'bottom' && y <= refRect.top + 1) ||
|
|
202
|
+
(side === 'left' && x >= refRect.right - 1) ||
|
|
203
|
+
(side === 'right' && x <= refRect.left + 1)
|
|
204
|
+
) {
|
|
205
|
+
closeIfNoOpenChild();
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Ignore when the cursor is within the rectangular trough between the two elements.
|
|
210
|
+
// Since the triangle is created from the cursor point, which can start beyond the ref
|
|
211
|
+
// element's edge, traversing back and forth from the ref to the floating element can
|
|
212
|
+
// cause it to close. This ensures it always remains open in that case.
|
|
213
|
+
let isInsideTroughRect = false;
|
|
214
|
+
|
|
215
|
+
switch (side) {
|
|
216
|
+
case 'top':
|
|
217
|
+
isInsideTroughRect = isInsideAxisAlignedRect(
|
|
218
|
+
clientX,
|
|
219
|
+
clientY,
|
|
220
|
+
left,
|
|
221
|
+
refRect.top + 1,
|
|
222
|
+
right,
|
|
223
|
+
rect.bottom - 1,
|
|
224
|
+
);
|
|
225
|
+
break;
|
|
226
|
+
case 'bottom':
|
|
227
|
+
isInsideTroughRect = isInsideAxisAlignedRect(
|
|
228
|
+
clientX,
|
|
229
|
+
clientY,
|
|
230
|
+
left,
|
|
231
|
+
rect.top + 1,
|
|
232
|
+
right,
|
|
233
|
+
refRect.bottom - 1,
|
|
234
|
+
);
|
|
235
|
+
break;
|
|
236
|
+
case 'left':
|
|
237
|
+
isInsideTroughRect = isInsideAxisAlignedRect(
|
|
238
|
+
clientX,
|
|
239
|
+
clientY,
|
|
240
|
+
rect.right - 1,
|
|
241
|
+
bottom,
|
|
242
|
+
refRect.left + 1,
|
|
243
|
+
top,
|
|
244
|
+
);
|
|
245
|
+
break;
|
|
246
|
+
case 'right':
|
|
247
|
+
isInsideTroughRect = isInsideAxisAlignedRect(
|
|
248
|
+
clientX,
|
|
249
|
+
clientY,
|
|
250
|
+
refRect.right - 1,
|
|
251
|
+
bottom,
|
|
252
|
+
rect.left + 1,
|
|
253
|
+
top,
|
|
254
|
+
);
|
|
255
|
+
break;
|
|
256
|
+
default:
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (isInsideTroughRect) {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (hasLanded && !isInsideRect(clientX, clientY, refRect)) {
|
|
264
|
+
closeIfNoOpenChild();
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!isLeave && isCursorMovingSlowly(clientX, clientY)) {
|
|
269
|
+
closeIfNoOpenChild();
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let isInsidePolygon = false;
|
|
274
|
+
|
|
275
|
+
switch (side) {
|
|
276
|
+
case 'top': {
|
|
277
|
+
const cursorXOffset = isFloatingWider ? POLYGON_BUFFER / 2 : POLYGON_BUFFER * 4;
|
|
278
|
+
const cursorPointOneX = isFloatingWider
|
|
279
|
+
? x + cursorXOffset
|
|
280
|
+
: cursorLeaveFromRight
|
|
281
|
+
? x + cursorXOffset
|
|
282
|
+
: x - cursorXOffset;
|
|
283
|
+
const cursorPointTwoX = isFloatingWider
|
|
284
|
+
? x - cursorXOffset
|
|
285
|
+
: cursorLeaveFromRight
|
|
286
|
+
? x + cursorXOffset
|
|
287
|
+
: x - cursorXOffset;
|
|
288
|
+
const cursorPointY = y + POLYGON_BUFFER + 1;
|
|
289
|
+
|
|
290
|
+
const commonYLeft = cursorLeaveFromRight
|
|
291
|
+
? rect.bottom - POLYGON_BUFFER
|
|
292
|
+
: isFloatingWider
|
|
293
|
+
? rect.bottom - POLYGON_BUFFER
|
|
294
|
+
: rect.top;
|
|
295
|
+
const commonYRight = cursorLeaveFromRight
|
|
296
|
+
? isFloatingWider
|
|
297
|
+
? rect.bottom - POLYGON_BUFFER
|
|
298
|
+
: rect.top
|
|
299
|
+
: rect.bottom - POLYGON_BUFFER;
|
|
300
|
+
|
|
301
|
+
isInsidePolygon = isPointInQuadrilateral(
|
|
302
|
+
clientX,
|
|
303
|
+
clientY,
|
|
304
|
+
cursorPointOneX,
|
|
305
|
+
cursorPointY,
|
|
306
|
+
cursorPointTwoX,
|
|
307
|
+
cursorPointY,
|
|
308
|
+
rect.left,
|
|
309
|
+
commonYLeft,
|
|
310
|
+
rect.right,
|
|
311
|
+
commonYRight,
|
|
312
|
+
);
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
case 'bottom': {
|
|
316
|
+
const cursorXOffset = isFloatingWider ? POLYGON_BUFFER / 2 : POLYGON_BUFFER * 4;
|
|
317
|
+
const cursorPointOneX = isFloatingWider
|
|
318
|
+
? x + cursorXOffset
|
|
319
|
+
: cursorLeaveFromRight
|
|
320
|
+
? x + cursorXOffset
|
|
321
|
+
: x - cursorXOffset;
|
|
322
|
+
const cursorPointTwoX = isFloatingWider
|
|
323
|
+
? x - cursorXOffset
|
|
324
|
+
: cursorLeaveFromRight
|
|
325
|
+
? x + cursorXOffset
|
|
326
|
+
: x - cursorXOffset;
|
|
327
|
+
const cursorPointY = y - POLYGON_BUFFER;
|
|
328
|
+
|
|
329
|
+
const commonYLeft = cursorLeaveFromRight
|
|
330
|
+
? rect.top + POLYGON_BUFFER
|
|
331
|
+
: isFloatingWider
|
|
332
|
+
? rect.top + POLYGON_BUFFER
|
|
333
|
+
: rect.bottom;
|
|
334
|
+
const commonYRight = cursorLeaveFromRight
|
|
335
|
+
? isFloatingWider
|
|
336
|
+
? rect.top + POLYGON_BUFFER
|
|
337
|
+
: rect.bottom
|
|
338
|
+
: rect.top + POLYGON_BUFFER;
|
|
339
|
+
|
|
340
|
+
isInsidePolygon = isPointInQuadrilateral(
|
|
341
|
+
clientX,
|
|
342
|
+
clientY,
|
|
343
|
+
cursorPointOneX,
|
|
344
|
+
cursorPointY,
|
|
345
|
+
cursorPointTwoX,
|
|
346
|
+
cursorPointY,
|
|
347
|
+
rect.left,
|
|
348
|
+
commonYLeft,
|
|
349
|
+
rect.right,
|
|
350
|
+
commonYRight,
|
|
351
|
+
);
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
case 'left': {
|
|
355
|
+
const cursorYOffset = isFloatingTaller ? POLYGON_BUFFER / 2 : POLYGON_BUFFER * 4;
|
|
356
|
+
const cursorPointOneY = isFloatingTaller
|
|
357
|
+
? y + cursorYOffset
|
|
358
|
+
: cursorLeaveFromBottom
|
|
359
|
+
? y + cursorYOffset
|
|
360
|
+
: y - cursorYOffset;
|
|
361
|
+
const cursorPointTwoY = isFloatingTaller
|
|
362
|
+
? y - cursorYOffset
|
|
363
|
+
: cursorLeaveFromBottom
|
|
364
|
+
? y + cursorYOffset
|
|
365
|
+
: y - cursorYOffset;
|
|
366
|
+
const cursorPointX = x + POLYGON_BUFFER + 1;
|
|
367
|
+
|
|
368
|
+
const commonXTop = cursorLeaveFromBottom
|
|
369
|
+
? rect.right - POLYGON_BUFFER
|
|
370
|
+
: isFloatingTaller
|
|
371
|
+
? rect.right - POLYGON_BUFFER
|
|
372
|
+
: rect.left;
|
|
373
|
+
const commonXBottom = cursorLeaveFromBottom
|
|
374
|
+
? isFloatingTaller
|
|
375
|
+
? rect.right - POLYGON_BUFFER
|
|
376
|
+
: rect.left
|
|
377
|
+
: rect.right - POLYGON_BUFFER;
|
|
378
|
+
|
|
379
|
+
isInsidePolygon = isPointInQuadrilateral(
|
|
380
|
+
clientX,
|
|
381
|
+
clientY,
|
|
382
|
+
commonXTop,
|
|
383
|
+
rect.top,
|
|
384
|
+
commonXBottom,
|
|
385
|
+
rect.bottom,
|
|
386
|
+
cursorPointX,
|
|
387
|
+
cursorPointOneY,
|
|
388
|
+
cursorPointX,
|
|
389
|
+
cursorPointTwoY,
|
|
390
|
+
);
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
case 'right': {
|
|
394
|
+
const cursorYOffset = isFloatingTaller ? POLYGON_BUFFER / 2 : POLYGON_BUFFER * 4;
|
|
395
|
+
const cursorPointOneY = isFloatingTaller
|
|
396
|
+
? y + cursorYOffset
|
|
397
|
+
: cursorLeaveFromBottom
|
|
398
|
+
? y + cursorYOffset
|
|
399
|
+
: y - cursorYOffset;
|
|
400
|
+
const cursorPointTwoY = isFloatingTaller
|
|
401
|
+
? y - cursorYOffset
|
|
402
|
+
: cursorLeaveFromBottom
|
|
403
|
+
? y + cursorYOffset
|
|
404
|
+
: y - cursorYOffset;
|
|
405
|
+
const cursorPointX = x - POLYGON_BUFFER;
|
|
406
|
+
|
|
407
|
+
const commonXTop = cursorLeaveFromBottom
|
|
408
|
+
? rect.left + POLYGON_BUFFER
|
|
409
|
+
: isFloatingTaller
|
|
410
|
+
? rect.left + POLYGON_BUFFER
|
|
411
|
+
: rect.right;
|
|
412
|
+
const commonXBottom = cursorLeaveFromBottom
|
|
413
|
+
? isFloatingTaller
|
|
414
|
+
? rect.left + POLYGON_BUFFER
|
|
415
|
+
: rect.right
|
|
416
|
+
: rect.left + POLYGON_BUFFER;
|
|
417
|
+
|
|
418
|
+
isInsidePolygon = isPointInQuadrilateral(
|
|
419
|
+
clientX,
|
|
420
|
+
clientY,
|
|
421
|
+
cursorPointX,
|
|
422
|
+
cursorPointOneY,
|
|
423
|
+
cursorPointX,
|
|
424
|
+
cursorPointTwoY,
|
|
425
|
+
commonXTop,
|
|
426
|
+
rect.top,
|
|
427
|
+
commonXBottom,
|
|
428
|
+
rect.bottom,
|
|
429
|
+
);
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
default:
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (!isInsidePolygon) {
|
|
436
|
+
closeIfNoOpenChild();
|
|
437
|
+
} else if (!hasLanded) {
|
|
438
|
+
timeout.start(40, closeIfNoOpenChild);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return undefined;
|
|
442
|
+
};
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
fn.__options = { ...options, blockPointerEvents };
|
|
446
|
+
|
|
447
|
+
return fn;
|
|
448
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/floating-ui-react/hooks/useClientPoint.ts (v1.6.0).
|
|
2
|
+
// Positions the popup relative to a client point (the cursor) instead of the trigger's box, by
|
|
3
|
+
// installing a virtual reference element whose rect follows the pointer on the chosen axis.
|
|
4
|
+
//
|
|
5
|
+
// octane adaptations: handlers receive the NATIVE event; every composed hook threads an explicit
|
|
6
|
+
// slot.
|
|
7
|
+
//
|
|
8
|
+
// SLOT: plain-`.ts` hook; the trailing arg is the caller's slot.
|
|
9
|
+
import { useEffect, useMemo, useRef, useState } from 'octane';
|
|
10
|
+
|
|
11
|
+
import { S, splitSlot, subSlot } from '../../internal';
|
|
12
|
+
import { addEventListener } from '../addEventListener';
|
|
13
|
+
import { ownerWindow } from '../owner';
|
|
14
|
+
import { useStableCallback } from '../useStableCallback';
|
|
15
|
+
import { contains, getTarget } from './element';
|
|
16
|
+
import { isMouseLikePointerType } from './event';
|
|
17
|
+
|
|
18
|
+
function createVirtualElement(
|
|
19
|
+
domElement: Element | null | undefined,
|
|
20
|
+
data: {
|
|
21
|
+
axis: 'x' | 'y' | 'both';
|
|
22
|
+
dataRef: { current: any };
|
|
23
|
+
pointerType: string | undefined;
|
|
24
|
+
x: number | null;
|
|
25
|
+
y: number | null;
|
|
26
|
+
},
|
|
27
|
+
): any {
|
|
28
|
+
let offsetX: number | null = null;
|
|
29
|
+
let offsetY: number | null = null;
|
|
30
|
+
let isAutoUpdateEvent = false;
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
contextElement: domElement || undefined,
|
|
34
|
+
getBoundingClientRect() {
|
|
35
|
+
const domRect = domElement?.getBoundingClientRect() || { width: 0, height: 0, x: 0, y: 0 };
|
|
36
|
+
|
|
37
|
+
const isXAxis = data.axis === 'x' || data.axis === 'both';
|
|
38
|
+
const isYAxis = data.axis === 'y' || data.axis === 'both';
|
|
39
|
+
const canTrackCursorOnAutoUpdate =
|
|
40
|
+
['mouseenter', 'mousemove'].includes(data.dataRef.current.openEvent?.type || '') &&
|
|
41
|
+
data.pointerType !== 'touch';
|
|
42
|
+
|
|
43
|
+
let width = domRect.width;
|
|
44
|
+
let height = domRect.height;
|
|
45
|
+
let x = domRect.x;
|
|
46
|
+
let y = domRect.y;
|
|
47
|
+
|
|
48
|
+
if (offsetX == null && data.x && isXAxis) {
|
|
49
|
+
offsetX = domRect.x - data.x;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (offsetY == null && data.y && isYAxis) {
|
|
53
|
+
offsetY = domRect.y - data.y;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
x -= offsetX || 0;
|
|
57
|
+
y -= offsetY || 0;
|
|
58
|
+
width = 0;
|
|
59
|
+
height = 0;
|
|
60
|
+
|
|
61
|
+
if (!isAutoUpdateEvent || canTrackCursorOnAutoUpdate) {
|
|
62
|
+
width = data.axis === 'y' ? domRect.width : 0;
|
|
63
|
+
height = data.axis === 'x' ? domRect.height : 0;
|
|
64
|
+
x = isXAxis && data.x != null ? data.x : x;
|
|
65
|
+
y = isYAxis && data.y != null ? data.y : y;
|
|
66
|
+
} else if (isAutoUpdateEvent && !canTrackCursorOnAutoUpdate) {
|
|
67
|
+
height = data.axis === 'x' ? domRect.height : height;
|
|
68
|
+
width = data.axis === 'y' ? domRect.width : width;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
isAutoUpdateEvent = true;
|
|
72
|
+
|
|
73
|
+
return { width, height, x, y, top: y, right: x + width, bottom: y + height, left: x };
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isMouseBasedEvent(event: Event | undefined): event is MouseEvent {
|
|
79
|
+
return event != null && (event as MouseEvent).clientX != null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface UseClientPointProps {
|
|
83
|
+
/**
|
|
84
|
+
* Whether the hook is enabled, including all internal effects and event handlers.
|
|
85
|
+
* @default true
|
|
86
|
+
*/
|
|
87
|
+
enabled?: boolean | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* Whether to restrict the client point to an axis and use the reference element (if it exists)
|
|
90
|
+
* as the other axis. Useful when the floating element is also interactive.
|
|
91
|
+
* @default 'both'
|
|
92
|
+
*/
|
|
93
|
+
axis?: 'x' | 'y' | 'both' | undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function useClientPoint(...args: any[]): Record<string, any> {
|
|
97
|
+
const [user, slotArg] = splitSlot(args);
|
|
98
|
+
const slot = slotArg ?? S('useClientPoint');
|
|
99
|
+
const context = user[0] as any;
|
|
100
|
+
const props = (user[1] as UseClientPointProps) ?? {};
|
|
101
|
+
|
|
102
|
+
const { enabled = true, axis = 'both' } = props;
|
|
103
|
+
|
|
104
|
+
const store = context != null && 'rootStore' in context ? context.rootStore : context;
|
|
105
|
+
|
|
106
|
+
const open = store.useState('open', subSlot(slot, 's:open'));
|
|
107
|
+
const floating = store.useState('floatingElement', subSlot(slot, 's:floating'));
|
|
108
|
+
const domReference = store.useState('domReferenceElement', subSlot(slot, 's:ref'));
|
|
109
|
+
|
|
110
|
+
const dataRef = store.context.dataRef;
|
|
111
|
+
|
|
112
|
+
const initialRef = useRef(false, subSlot(slot, 'initial'));
|
|
113
|
+
const cleanupListenerRef = useRef<null | (() => void)>(null, subSlot(slot, 'cleanup'));
|
|
114
|
+
|
|
115
|
+
const [pointerType, setPointerType] = useState<string | undefined>(
|
|
116
|
+
undefined,
|
|
117
|
+
subSlot(slot, 'ptype'),
|
|
118
|
+
);
|
|
119
|
+
const [reactive, setReactive] = useState<never[]>([], subSlot(slot, 'reactive'));
|
|
120
|
+
|
|
121
|
+
const resetReference = useStableCallback(
|
|
122
|
+
(reference: Element | null) => {
|
|
123
|
+
store.set('positionReference', reference);
|
|
124
|
+
},
|
|
125
|
+
subSlot(slot, 'reset'),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const setReference = useStableCallback(
|
|
129
|
+
(newX: number | null, newY: number | null, referenceElement?: Element | null) => {
|
|
130
|
+
if (initialRef.current) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Don't set if the open event wasn't mouse-like (e.g. focus to open, then hover the
|
|
135
|
+
// reference). Only applies when the event exists.
|
|
136
|
+
if (dataRef.current.openEvent && !isMouseBasedEvent(dataRef.current.openEvent)) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
store.set(
|
|
141
|
+
'positionReference',
|
|
142
|
+
createVirtualElement(referenceElement ?? domReference, {
|
|
143
|
+
x: newX,
|
|
144
|
+
y: newY,
|
|
145
|
+
axis,
|
|
146
|
+
dataRef,
|
|
147
|
+
pointerType,
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
150
|
+
},
|
|
151
|
+
subSlot(slot, 'setRef'),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const handleReferenceEnterOrMove = useStableCallback(
|
|
155
|
+
(event: MouseEvent) => {
|
|
156
|
+
if (!open) {
|
|
157
|
+
setReference(event.clientX, event.clientY, event.currentTarget as Element);
|
|
158
|
+
} else if (!cleanupListenerRef.current) {
|
|
159
|
+
// With no cleanup there's no listener; re-add it for the case where the cursor landed on
|
|
160
|
+
// the floating element and then came back to the reference (an interactive popup).
|
|
161
|
+
setReference(event.clientX, event.clientY, event.currentTarget as Element);
|
|
162
|
+
setReactive([]);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
subSlot(slot, 'enterMove'),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// For a mouse-like pointer keep following the cursor even while the popup transitions out. On
|
|
169
|
+
// touch that's undesirable — the popup would jump to the dismissal touch point.
|
|
170
|
+
const openCheck = isMouseLikePointerType(pointerType) ? floating : open;
|
|
171
|
+
|
|
172
|
+
useEffect(
|
|
173
|
+
() => {
|
|
174
|
+
if (!enabled) {
|
|
175
|
+
resetReference(domReference);
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!openCheck) {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function cleanupListener() {
|
|
184
|
+
cleanupListenerRef.current?.();
|
|
185
|
+
cleanupListenerRef.current = null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const win = ownerWindow(floating);
|
|
189
|
+
|
|
190
|
+
function handleMouseMove(event: MouseEvent) {
|
|
191
|
+
const target = getTarget(event) as Element | null;
|
|
192
|
+
if (!contains(floating, target)) {
|
|
193
|
+
setReference(event.clientX, event.clientY);
|
|
194
|
+
} else {
|
|
195
|
+
cleanupListener();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!dataRef.current.openEvent || isMouseBasedEvent(dataRef.current.openEvent)) {
|
|
200
|
+
cleanupListenerRef.current = addEventListener(
|
|
201
|
+
win,
|
|
202
|
+
'mousemove',
|
|
203
|
+
handleMouseMove as EventListener,
|
|
204
|
+
);
|
|
205
|
+
} else {
|
|
206
|
+
resetReference(domReference);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return cleanupListener;
|
|
210
|
+
},
|
|
211
|
+
[
|
|
212
|
+
openCheck,
|
|
213
|
+
enabled,
|
|
214
|
+
floating,
|
|
215
|
+
dataRef,
|
|
216
|
+
domReference,
|
|
217
|
+
store,
|
|
218
|
+
setReference,
|
|
219
|
+
resetReference,
|
|
220
|
+
reactive,
|
|
221
|
+
],
|
|
222
|
+
subSlot(slot, 'e:track'),
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// Clear virtual cursor references when the hook unmounts. Enabled flips are handled above.
|
|
226
|
+
useEffect(
|
|
227
|
+
() => () => {
|
|
228
|
+
store.set('positionReference', null);
|
|
229
|
+
},
|
|
230
|
+
[store],
|
|
231
|
+
subSlot(slot, 'e:unmount'),
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
useEffect(
|
|
235
|
+
() => {
|
|
236
|
+
if (enabled && !floating) {
|
|
237
|
+
initialRef.current = false;
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
[enabled, floating],
|
|
241
|
+
subSlot(slot, 'e:reset'),
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
useEffect(
|
|
245
|
+
() => {
|
|
246
|
+
if (!enabled && open) {
|
|
247
|
+
initialRef.current = true;
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
[enabled, open],
|
|
251
|
+
subSlot(slot, 'e:freeze'),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const reference = useMemo(
|
|
255
|
+
() => {
|
|
256
|
+
function setPointerTypeRef(event: PointerEvent) {
|
|
257
|
+
setPointerType(event.pointerType);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
onPointerDown: setPointerTypeRef,
|
|
262
|
+
onPointerEnter: setPointerTypeRef,
|
|
263
|
+
onMouseMove: handleReferenceEnterOrMove,
|
|
264
|
+
onMouseEnter: handleReferenceEnterOrMove,
|
|
265
|
+
};
|
|
266
|
+
},
|
|
267
|
+
[handleReferenceEnterOrMove],
|
|
268
|
+
subSlot(slot, 'ref'),
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
return useMemo(
|
|
272
|
+
() => (enabled ? { reference, trigger: reference } : {}),
|
|
273
|
+
[enabled, reference],
|
|
274
|
+
subSlot(slot, 'out'),
|
|
275
|
+
);
|
|
276
|
+
}
|