@hanzogui/popper 8.3.0 → 8.3.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.
Files changed (40) hide show
  1. package/dist/cjs/Popper.js +644 -0
  2. package/dist/cjs/Popper.js.map +1 -0
  3. package/dist/cjs/index.js +5 -0
  4. package/dist/cjs/index.js.map +1 -0
  5. package/dist/cjs/package.json +1 -0
  6. package/dist/esm/Popper.js +637 -0
  7. package/dist/esm/Popper.js.map +1 -0
  8. package/dist/esm/index.js +2 -2
  9. package/dist/esm/index.js.map +1 -1
  10. package/package.json +23 -23
  11. package/src/Popper.tsx +16 -18
  12. package/src/index.ts +1 -1
  13. package/types/Popper.d.ts +27 -27
  14. package/types/Popper.d.ts.map +1 -1
  15. package/types/index.d.ts +1 -1
  16. package/types/index.d.ts.map +1 -1
  17. package/dist/cjs/Popper.cjs +0 -627
  18. package/dist/cjs/Popper.native.js +0 -631
  19. package/dist/cjs/Popper.native.js.map +0 -1
  20. package/dist/cjs/index.cjs +0 -20
  21. package/dist/cjs/index.native.js +0 -23
  22. package/dist/cjs/index.native.js.map +0 -1
  23. package/dist/esm/Popper.mjs +0 -566
  24. package/dist/esm/Popper.mjs.map +0 -1
  25. package/dist/esm/Popper.native.js +0 -563
  26. package/dist/esm/Popper.native.js.map +0 -1
  27. package/dist/esm/index.mjs +0 -2
  28. package/dist/esm/index.mjs.map +0 -1
  29. package/dist/esm/index.native.js +0 -2
  30. package/dist/esm/index.native.js.map +0 -1
  31. package/dist/jsx/Popper.mjs +0 -566
  32. package/dist/jsx/Popper.mjs.map +0 -1
  33. package/dist/jsx/Popper.native.js +0 -631
  34. package/dist/jsx/Popper.native.js.map +0 -1
  35. package/dist/jsx/index.js +0 -2
  36. package/dist/jsx/index.js.map +0 -1
  37. package/dist/jsx/index.mjs +0 -2
  38. package/dist/jsx/index.mjs.map +0 -1
  39. package/dist/jsx/index.native.js +0 -23
  40. package/dist/jsx/index.native.js.map +0 -1
@@ -0,0 +1,637 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // adapted from radix-ui popper
3
+ import { flushSync } from 'react-dom';
4
+ import { useComposedRefs } from '@hanzogui/compose-refs';
5
+ import { isWeb, useIsomorphicLayoutEffect } from '@hanzogui/constants';
6
+ import { LayoutMeasurementController, View as GuiView, createStyledContext, getVariableValue, registerLayoutNode, styled, } from '@hanzogui/core';
7
+ import { FloatingOverrideContext } from '@hanzogui/floating';
8
+ import { arrow, flip, getOverflowAncestors, offset as offsetFn, platform, shift, size as sizeMiddleware, useFloating, } from '@hanzogui/floating';
9
+ import { getSpace } from '@hanzogui/get-token';
10
+ import { YStack } from '@hanzogui/stacks';
11
+ import { startTransition } from '@hanzogui/start-transition';
12
+ import * as React from 'react';
13
+ import { Keyboard, useWindowDimensions } from 'react-native';
14
+ export const PopperContextFast = createStyledContext(
15
+ // since we always provide this we can avoid setting here
16
+ {}, 'Popper__');
17
+ export const PopperPositionContext = createStyledContext;
18
+ export const { useStyledContext: usePopperContext, Provider: PopperProviderFast } = PopperContextFast;
19
+ export const PopperContextSlow = createStyledContext(
20
+ // since we always provide this we can avoid setting here
21
+ {}, 'PopperSlow__');
22
+ export const { useStyledContext: usePopperContextSlow, Provider: PopperProviderSlow } = PopperContextSlow;
23
+ // handles both slow and fast:
24
+ export const PopperProvider = ({ scope, children, ...context }) => {
25
+ // single ref holds all unstable functions — updated every render so the
26
+ // stable wrappers below always forward to the latest version
27
+ const fns = React.useRef(context);
28
+ fns.current = context;
29
+ // stable wrappers that never change identity — objectIdentityKey in
30
+ // createStyledContext produces the same key across renders, so PopperAnchor
31
+ // instances never re-render from context changes (only from parent re-renders)
32
+ const [slowContext] = React.useState(() => ({
33
+ refs: context.refs,
34
+ triggerElements: context.triggerElements,
35
+ update(...a) {
36
+ fns.current.update(...a);
37
+ },
38
+ getReferenceProps(p) {
39
+ return fns.current.getReferenceProps?.(p);
40
+ },
41
+ onHoverReference(e) {
42
+ ;
43
+ fns.current.onHoverReference?.(e);
44
+ },
45
+ onLeaveReference() {
46
+ ;
47
+ fns.current.onLeaveReference?.();
48
+ },
49
+ }));
50
+ return (_jsx(PopperProviderFast, { scope: scope, ...context, children: _jsx(PopperProviderSlow, { scope: scope, ...slowContext, children: children }) }));
51
+ };
52
+ const checkFloating = "web" === 'native'
53
+ ? {
54
+ name: 'checkFloating',
55
+ fn(data) {
56
+ return {
57
+ data: {
58
+ hasFloating: !!data.rects.floating.width,
59
+ },
60
+ };
61
+ },
62
+ }
63
+ : undefined;
64
+ const setupOptions = {};
65
+ export function setupPopper(options) {
66
+ Object.assign(setupOptions, options);
67
+ }
68
+ // forked from radix-ui 👇
69
+ // https://github.com/radix-ui/primitives/blob/1910a8c91c5927e58b8fca3aeaa31411f32fee7c/packages/react/popper/src/Popper.tsx#L359-L399
70
+ function getSideAndAlignFromPlacement(placement) {
71
+ const [side, align = 'center'] = placement.split('-');
72
+ return [side, align];
73
+ }
74
+ const transformOriginMiddleware = (options) => ({
75
+ name: 'transformOrigin',
76
+ options,
77
+ fn(data) {
78
+ const { placement, rects, middlewareData } = data;
79
+ const cannotCenterArrow = middlewareData.arrow?.centerOffset !== 0;
80
+ const isArrowHidden = cannotCenterArrow;
81
+ const arrowWidth = isArrowHidden ? 0 : options.arrowWidth;
82
+ const arrowHeight = isArrowHidden ? 0 : options.arrowHeight;
83
+ const [placedSide, placedAlign] = getSideAndAlignFromPlacement(placement);
84
+ const noArrowAlign = { start: '0%', center: '50%', end: '100%' }[placedAlign];
85
+ const arrowXCenter = (middlewareData.arrow?.x ?? 0) + arrowWidth / 2;
86
+ const arrowYCenter = (middlewareData.arrow?.y ?? 0) + arrowHeight / 2;
87
+ let x = '';
88
+ let y = '';
89
+ if (placedSide === 'bottom') {
90
+ x = isArrowHidden ? noArrowAlign : `${arrowXCenter}px`;
91
+ y = `${-arrowHeight}px`;
92
+ }
93
+ else if (placedSide === 'top') {
94
+ x = isArrowHidden ? noArrowAlign : `${arrowXCenter}px`;
95
+ y = `${rects.floating.height + arrowHeight}px`;
96
+ }
97
+ else if (placedSide === 'right') {
98
+ x = `${-arrowHeight}px`;
99
+ y = isArrowHidden ? noArrowAlign : `${arrowYCenter}px`;
100
+ }
101
+ else if (placedSide === 'left') {
102
+ x = `${rects.floating.width + arrowHeight}px`;
103
+ y = isArrowHidden ? noArrowAlign : `${arrowYCenter}px`;
104
+ }
105
+ return { data: { x, y } };
106
+ },
107
+ });
108
+ // replaces floating-ui's autoUpdate with hanzogui's batched IO measurement loop
109
+ // keeps scroll/resize listeners for immediate response, but replaces per-element
110
+ // ResizeObserver + IntersectionObserver with the shared layoutOnAnimationFrame loop
111
+ function hanzoguiAutoUpdate(reference, floating, update) {
112
+ // initial position
113
+ update();
114
+ // schedule a second update after layout/scroll events settle (e.g. focus-
115
+ // triggered scrolls that cause flip corrections)
116
+ let rafId = requestAnimationFrame(() => {
117
+ update();
118
+ rafId = 0;
119
+ });
120
+ const cleanups = [
121
+ () => {
122
+ if (rafId)
123
+ cancelAnimationFrame(rafId);
124
+ },
125
+ ];
126
+ // watch reference element via hanzogui's IO measurement loop
127
+ // only watch reference, NOT floating — watching floating causes loops
128
+ // (computePosition sets position → rect changes → update → repeat)
129
+ if (reference instanceof HTMLElement) {
130
+ cleanups.push(registerLayoutNode(reference, update));
131
+ }
132
+ // scroll listeners for immediate response (only for real DOM elements)
133
+ const refAncestors = reference instanceof Element ? getOverflowAncestors(reference) : [];
134
+ const ancestors = [...refAncestors, ...getOverflowAncestors(floating)];
135
+ const uniqueAncestors = [...new Set(ancestors)];
136
+ for (const ancestor of uniqueAncestors) {
137
+ ancestor.addEventListener('scroll', update, { passive: true });
138
+ }
139
+ // window resize
140
+ window.addEventListener('resize', update);
141
+ cleanups.push(() => {
142
+ for (const ancestor of uniqueAncestors) {
143
+ ancestor.removeEventListener('scroll', update);
144
+ }
145
+ window.removeEventListener('resize', update);
146
+ });
147
+ return () => cleanups.forEach((fn) => fn());
148
+ }
149
+ export function Popper(props) {
150
+ const { children, size, strategy = 'absolute', placement = 'bottom', stayInFrame, allowFlip, offset, disableRTL, resize, passThrough, open, scope, } = props;
151
+ const [arrowEl, setArrow] = React.useState(null);
152
+ const [arrowSize, setArrowSize] = React.useState(0);
153
+ const offsetOptions = offset ?? arrowSize;
154
+ const floatingStyle = React.useRef({});
155
+ const isOpen = passThrough ? false : (open ?? true);
156
+ // freeze middleware reference when closed so floating-ui's deepEqual trivially
157
+ // passes (same object) and skips computePosition on re-renders while closed.
158
+ // unlike swapping to [], this retains the last good middleware so cached
159
+ // position data (offset, arrow, transformOrigin) stays correct for reopen.
160
+ const middlewareRef = React.useRef([]);
161
+ if (isOpen) {
162
+ middlewareRef.current = [
163
+ // order matters: offset first, then flip, then shift, then arrow
164
+ typeof offsetOptions !== 'undefined' ? offsetFn(offsetOptions) : null,
165
+ allowFlip ? flip(typeof allowFlip === 'boolean' ? {} : allowFlip) : null,
166
+ // NOTE: shift's axis terminology is reversed vs flip/offset:
167
+ // for top/bottom placements: mainAxis = horizontal, crossAxis = vertical
168
+ // for left/right placements: mainAxis = vertical, crossAxis = horizontal
169
+ // default to horizontal shift only (mainAxis: true, crossAxis: false)
170
+ stayInFrame
171
+ ? shift({
172
+ padding: 10,
173
+ mainAxis: true,
174
+ crossAxis: false,
175
+ ...(typeof stayInFrame === 'object' ? stayInFrame : null),
176
+ })
177
+ : null,
178
+ arrowEl ? arrow({ element: arrowEl }) : null,
179
+ checkFloating,
180
+ "web" !== 'native' && resize
181
+ ? sizeMiddleware({
182
+ padding: typeof stayInFrame === 'object' ? stayInFrame.padding : 10,
183
+ apply({ availableHeight, availableWidth }) {
184
+ if (passThrough) {
185
+ return;
186
+ }
187
+ Object.assign(floatingStyle.current, {
188
+ maxHeight: `${availableHeight}px`,
189
+ maxWidth: `${availableWidth}px`,
190
+ });
191
+ // we wrap PopperContent with one container stack so we need to account for it
192
+ const floatingChild = floating.refs.floating.current?.firstChild;
193
+ if (floatingChild && floatingChild instanceof HTMLElement) {
194
+ Object.assign(floatingChild.style, floatingStyle.current);
195
+ }
196
+ },
197
+ ...(typeof resize === 'object' && resize),
198
+ })
199
+ : null,
200
+ // Add size middleware for CSS custom properties (web only)
201
+ "web" !== 'native'
202
+ ? sizeMiddleware({
203
+ apply({ elements, rects, availableWidth, availableHeight }) {
204
+ const { width: anchorWidth, height: anchorHeight } = rects.reference;
205
+ const contentStyle = elements.floating.style;
206
+ contentStyle.setProperty('--hanzogui-popper-available-width', `${availableWidth}px`);
207
+ contentStyle.setProperty('--hanzogui-popper-available-height', `${availableHeight}px`);
208
+ contentStyle.setProperty('--hanzogui-popper-anchor-width', `${anchorWidth}px`);
209
+ contentStyle.setProperty('--hanzogui-popper-anchor-height', `${anchorHeight}px`);
210
+ },
211
+ })
212
+ : null,
213
+ // Transform origin middleware (web only)
214
+ "web" !== 'native'
215
+ ? transformOriginMiddleware({
216
+ arrowHeight: arrowSize,
217
+ arrowWidth: arrowSize,
218
+ })
219
+ : null,
220
+ ].filter(Boolean);
221
+ }
222
+ let floating = useFloating({
223
+ open: isOpen,
224
+ strategy,
225
+ placement,
226
+ sameScrollView: false, // this only takes effect on native
227
+ whileElementsMounted: !isOpen ? undefined : hanzoguiAutoUpdate,
228
+ platform: (disableRTL ?? setupOptions.disableRTL)
229
+ ? {
230
+ ...platform,
231
+ isRTL(element) {
232
+ return false;
233
+ },
234
+ }
235
+ : platform,
236
+ middleware: middlewareRef.current,
237
+ });
238
+ if ("web" !== 'native') {
239
+ // add our size middleware here
240
+ floating = React.useMemo(() => {
241
+ const og = floating.getFloatingProps;
242
+ if (resize && og) {
243
+ floating.getFloatingProps = (props) => {
244
+ return og({
245
+ ...props,
246
+ style: {
247
+ ...props.style,
248
+ ...floatingStyle.current,
249
+ },
250
+ });
251
+ };
252
+ }
253
+ return floating;
254
+ }, [floating, resize ? JSON.stringify(resize) : null]);
255
+ }
256
+ const { middlewareData } = floating;
257
+ if ("web" === 'native') {
258
+ // On Native there's no autoupdate so we call update() when necessary
259
+ // Subscribe to window dimensions (orientation, scale, etc...)
260
+ const dimensions = useWindowDimensions();
261
+ // Subscribe to keyboard state
262
+ const [keyboardOpen, setKeyboardOpen] = React.useState(false);
263
+ React.useEffect(() => {
264
+ const showSubscription = Keyboard.addListener('keyboardDidShow', () => {
265
+ startTransition(() => {
266
+ setKeyboardOpen(true);
267
+ });
268
+ });
269
+ const hideSubscription = Keyboard.addListener('keyboardDidHide', () => {
270
+ startTransition(() => {
271
+ setKeyboardOpen(false);
272
+ });
273
+ });
274
+ return () => {
275
+ showSubscription.remove();
276
+ hideSubscription.remove();
277
+ };
278
+ }, []);
279
+ useIsomorphicLayoutEffect(() => {
280
+ if (passThrough)
281
+ return;
282
+ floating.update();
283
+ }, [passThrough, dimensions, keyboardOpen]);
284
+ }
285
+ const popperContext = React.useMemo(() => {
286
+ return {
287
+ size,
288
+ arrowRef: setArrow,
289
+ arrowStyle: middlewareData.arrow,
290
+ onArrowSize: setArrowSize,
291
+ hasFloating: middlewareData.checkFloating?.hasFloating,
292
+ transformOrigin: middlewareData.transformOrigin,
293
+ open: !!open,
294
+ ...floating,
295
+ };
296
+ }, [
297
+ open,
298
+ size,
299
+ floating,
300
+ JSON.stringify(middlewareData.arrow || null),
301
+ JSON.stringify(middlewareData.transformOrigin || null),
302
+ ]);
303
+ return (_jsx(PopperProvider, { scope: scope, ...popperContext, children: _jsx(FloatingOverrideContext.Provider, { value: null, children: children }) }));
304
+ }
305
+ export const PopperAnchor = YStack.styleable(function PopperAnchor(props, forwardedRef) {
306
+ const { virtualRef, scope, ...rest } = props;
307
+ const context = usePopperContextSlow(scope);
308
+ const { getReferenceProps, refs, update } = context;
309
+ const ref = React.useRef(null);
310
+ const triggerId = React.useId();
311
+ // register this trigger element with the shared trigger map
312
+ // so useHover can detect cursor moves between sibling triggers
313
+ React.useEffect(() => {
314
+ if (!scope || !context.triggerElements || !ref.current)
315
+ return;
316
+ if (!(ref.current instanceof Element))
317
+ return;
318
+ const el = ref.current;
319
+ context.triggerElements.add(triggerId, el);
320
+ return () => {
321
+ context.triggerElements?.delete(triggerId);
322
+ };
323
+ }, [scope, triggerId, context.triggerElements]);
324
+ React.useEffect(() => {
325
+ if (virtualRef) {
326
+ refs.setReference(virtualRef.current);
327
+ // recompute position after setting virtual reference
328
+ update();
329
+ }
330
+ }, [virtualRef]);
331
+ const refProps = getReferenceProps?.({
332
+ ...rest,
333
+ ref,
334
+ }) || null;
335
+ // Wrap setReference in startTransition to avoid React #185 (setState during render)
336
+ const safeSetReference = React.useCallback((node) => {
337
+ startTransition(() => {
338
+ refs.setReference(node);
339
+ });
340
+ },
341
+ // it was refs.setRefernce but its stable and refs is undefined on server
342
+ [refs]);
343
+ const shouldHandleInHover = isWeb && scope;
344
+ const composedRefs = useComposedRefs(forwardedRef, ref,
345
+ // web handles this onMouseEnter below so it can support multiple targets + hovering
346
+ shouldHandleInHover ? undefined : safeSetReference);
347
+ return (_jsx(GuiView, { ...rest, ...refProps, ref: composedRefs, ...(shouldHandleInHover && {
348
+ // scoped poppers with multiple triggers: set the reference on
349
+ // mouseEnter so floating-ui positions relative to the hovered
350
+ // trigger, not the last one rendered.
351
+ //
352
+ // flushSync is critical here: without it, setReference batches
353
+ // with React's async state updates and the arrow/content position
354
+ // computes against the OLD reference element. this causes the
355
+ // arrow to flash at x=0 (top-left) during trigger switches.
356
+ // flushSync forces synchronous commit so update() below reads
357
+ // the correct reference element immediately.
358
+ onMouseEnter: (e) => {
359
+ const el = (e.currentTarget ?? ref.current);
360
+ if (el instanceof HTMLElement) {
361
+ flushSync(() => refs.setReference(el));
362
+ update();
363
+ if (!refProps)
364
+ return;
365
+ refProps.onPointerEnter?.(e);
366
+ context.onHoverReference?.(e.nativeEvent);
367
+ }
368
+ },
369
+ onMouseLeave: (e) => {
370
+ context.onLeaveReference?.();
371
+ refProps?.onMouseLeave?.(e);
372
+ },
373
+ }) }));
374
+ });
375
+ export const PopperContentFrame = styled(YStack, {
376
+ name: 'PopperContent',
377
+ variants: {
378
+ unstyled: {
379
+ true: {},
380
+ },
381
+ size: {
382
+ '...size': (val, { tokens }) => {
383
+ return {
384
+ padding: tokens.space[val],
385
+ borderRadius: tokens.radius[val],
386
+ };
387
+ },
388
+ },
389
+ },
390
+ });
391
+ export const PopperContent = React.forwardRef(function PopperContent(props, forwardedRef) {
392
+ // detect controlled animatePosition before destructuring. when the user passes
393
+ // animatePosition (even with a currently-falsy value like undefined or false),
394
+ // toggling it later must not flip 'transition' presence on the inner View - that
395
+ // would change useComponentState's hasAnimationProp mid-life, conditionally calling
396
+ // useAnimations/usePresence and tripping React's "Should have a queue" invariant.
397
+ const isAnimatePosControlled = 'animatePosition' in props || 'enableAnimationForPositionChange' in props;
398
+ const { scope, animatePosition, enableAnimationForPositionChange, children, passThrough, unstyled, ...rest } = props;
399
+ const animatePos = animatePosition ?? enableAnimationForPositionChange;
400
+ const context = usePopperContext(scope);
401
+ const { strategy, placement, refs, x, y, getFloatingProps, size, isPositioned, transformOrigin, update, } = context;
402
+ // keep update() accessible inside safeSetFloating without adding it as a dep
403
+ const updateRef = React.useRef(update);
404
+ updateRef.current = update;
405
+ // ref callback: call refs.setFloating directly (no startTransition) so floating-ui's
406
+ // state update runs synchronously and position is computed on mount.
407
+ // note: ref callbacks fire during the commit phase, not render, so calling setState
408
+ // here is safe - React batches it for the next commit.
409
+ //
410
+ // when animatePosition=true, disableAnimation state changes cycle the DOM node
411
+ // (null then re-mount). we block all null calls here to prevent floating-ui from
412
+ // losing its reference mid-cycle; genuine unmount is handled by the useEffect below.
413
+ // for same-node cycling (animateOnly prop change without remount), refs.setFloating
414
+ // is a no-op in floating-ui (same-node guard), so we call update() to force recompute.
415
+ const lastNodeRef = React.useRef(null);
416
+ const safeSetFloating = React.useCallback((node) => {
417
+ const isNewNode = node !== lastNodeRef.current;
418
+ if (node) {
419
+ lastNodeRef.current = node;
420
+ refs.setFloating(node);
421
+ if (!isNewNode) {
422
+ // same node re-appeared (prop cycling without remount):
423
+ // refs.setFloating is a no-op, so force position recompute
424
+ updateRef.current?.();
425
+ }
426
+ }
427
+ // null calls are blocked: cycling nulls are transient, genuine unmount
428
+ // is handled by the useEffect cleanup below
429
+ }, [refs.setFloating]);
430
+ // clear floating-ui's reference when the component genuinely unmounts.
431
+ // IMPORTANT: useEffect cleanup is deferred — when PopperContent remounts
432
+ // (e.g. animation prop cycling), the new instance's ref callback fires
433
+ // BEFORE this cleanup runs. without the guard, we'd null out the ref that
434
+ // the new instance just set, causing all subsequent update() calls to
435
+ // early-return (the "stuck tooltip" bug).
436
+ React.useEffect(() => {
437
+ return () => {
438
+ const ourNode = lastNodeRef.current;
439
+ // only clear if floating-ui still points to OUR node — if a new
440
+ // instance already set a different node, don't touch it
441
+ if (ourNode && refs.floating.current === ourNode) {
442
+ refs.setFloating(null);
443
+ }
444
+ lastNodeRef.current = null;
445
+ };
446
+ }, []);
447
+ const contentRefs = useComposedRefs(safeSetFloating, forwardedRef);
448
+ const [needsMeasure, setNeedsMeasure] = React.useState(animatePos);
449
+ useIsomorphicLayoutEffect(() => {
450
+ if (needsMeasure && x && y) {
451
+ setNeedsMeasure(false);
452
+ }
453
+ }, [needsMeasure, animatePos, x, y]);
454
+ // track whether we've ever been positioned. floating-ui resets isPositioned
455
+ // to false when open changes to false (e.g. hoverable safePolygon briefly
456
+ // closing). without this, the brief close disables animation and causes
457
+ // position jumps when the popover reopens at the new trigger.
458
+ const hasBeenPositioned = React.useRef(false);
459
+ const lastGoodPosition = React.useRef({ x: 0, y: 0 });
460
+ if (x !== 0 || y !== 0) {
461
+ // always track the latest computed position so that when a new reference
462
+ // is set while closed (e.g. content → gap → different trigger), the
463
+ // effectiveX/Y fallback uses the fresh position, not the stale one
464
+ lastGoodPosition.current = { x, y };
465
+ if (isPositioned) {
466
+ hasBeenPositioned.current = true;
467
+ }
468
+ }
469
+ // use the last known good position when floating-ui provides 0,0.
470
+ // this happens in two cases:
471
+ // 1. close/reopen cycle: isPositioned resets to false
472
+ // 2. trigger switch: reference element changes, floating-ui briefly
473
+ // provides x=0,y=0 while isPositioned is still true, causing the
474
+ // animation driver to animate toward (0,0) for 2-3 frames
475
+ const brieflyZero = hasBeenPositioned.current && x === 0 && y === 0;
476
+ const effectiveX = brieflyZero ? lastGoodPosition.current.x : x;
477
+ const effectiveY = brieflyZero ? lastGoodPosition.current.y : y;
478
+ // only hide before the very first positioning
479
+ const hide = !hasBeenPositioned.current && effectiveX === 0 && effectiveY === 0;
480
+ const disableAnimationProp =
481
+ // if they want to animate also when re-positioning allow it
482
+ animatePos === 'even-when-repositioning'
483
+ ? needsMeasure
484
+ : (!hasBeenPositioned.current && !isPositioned) || needsMeasure;
485
+ const [disableAnimation, setDisableAnimation] = React.useState(disableAnimationProp);
486
+ // set in an effect so we apply the css transition only after the element is positioned,
487
+ // not on the first render (which would animate from y=0 to the actual position)
488
+ React.useEffect(() => {
489
+ setDisableAnimation(disableAnimationProp);
490
+ }, [disableAnimationProp]);
491
+ const positionProps = hide
492
+ ? {} // omit x/y when hiding - prevents motion driver from animating from origin
493
+ : { x: effectiveX || 0, y: effectiveY || 0 };
494
+ const frameProps = {
495
+ ref: contentRefs,
496
+ ...positionProps,
497
+ top: 0,
498
+ left: 0,
499
+ position: strategy,
500
+ opacity: hide ? 0 : 1,
501
+ // when animatePosition is controlled by the user, always emit these keys with
502
+ // safe no-op values (transition: undefined, animateOnly: []) so the inner
503
+ // View's hook count stays stable across animatePos toggles. animatePresence
504
+ // must always be false here too, to short-circuit usePresence consistently.
505
+ ...(isAnimatePosControlled && {
506
+ transition: animatePos ? rest.transition : undefined,
507
+ // animateOnly: [] turns off transitions while keeping styles applied,
508
+ // letting the element move to its position silently before animations start
509
+ animateOnly: animatePos && !disableAnimation ? rest.animateOnly : [],
510
+ animatePresence: false,
511
+ }),
512
+ };
513
+ // outer frame because we explicitly don't want animation to apply to this
514
+ const { style, ...floatingProps } = getFloatingProps
515
+ ? getFloatingProps(frameProps)
516
+ : frameProps;
517
+ // Compute the CSS transform-origin value from middleware data
518
+ const transformOriginStyle = isWeb && transformOrigin
519
+ ? { transformOrigin: `${transformOrigin.x} ${transformOrigin.y}` }
520
+ : undefined;
521
+ return (_jsx(LayoutMeasurementController, { disable: !context.open, children: _jsx(GuiView, { passThrough: passThrough, ref: contentRefs, direction: rest.direction, ...(passThrough ? null : floatingProps), ...(!passThrough &&
522
+ animatePos && {
523
+ 'data-popper-animate-position': 'true',
524
+ }), children: _jsx(PopperContentFrame, { passThrough: passThrough, unstyled: unstyled, ...(!passThrough && {
525
+ 'data-placement': placement,
526
+ 'data-strategy': strategy,
527
+ size,
528
+ ...style,
529
+ ...transformOriginStyle,
530
+ ...rest,
531
+ }), children: children }, "popper-content-frame") }) }));
532
+ });
533
+ export const PopperArrowFrame = styled(YStack, {
534
+ name: 'PopperArrow',
535
+ variants: {
536
+ unstyled: {
537
+ false: {
538
+ borderColor: '$borderColor',
539
+ backgroundColor: '$background',
540
+ position: 'relative',
541
+ },
542
+ },
543
+ },
544
+ defaultVariants: {
545
+ unstyled: process.env.GUI_HEADLESS === '1',
546
+ },
547
+ });
548
+ const PopperArrowOuterFrame = styled(YStack, {
549
+ name: 'PopperArrowOuter',
550
+ variants: {
551
+ unstyled: {
552
+ false: {
553
+ position: 'absolute',
554
+ zIndex: 1000000,
555
+ pointerEvents: 'none',
556
+ overflow: 'hidden',
557
+ alignItems: 'center',
558
+ justifyContent: 'center',
559
+ },
560
+ },
561
+ },
562
+ defaultVariants: {
563
+ unstyled: process.env.GUI_HEADLESS === '1',
564
+ },
565
+ });
566
+ const opposites = {
567
+ top: 'bottom',
568
+ right: 'left',
569
+ bottom: 'top',
570
+ left: 'right',
571
+ };
572
+ export const PopperArrow = React.forwardRef(function PopperArrow(propsIn, forwardedRef) {
573
+ // see PopperContent for why we detect controlled animatePosition before destructuring
574
+ const isAnimatePosControlled = 'animatePosition' in propsIn;
575
+ const { scope, animatePosition, transition, ...rest } = propsIn;
576
+ const { offset, size: sizeProp, borderWidth = 0, ...arrowProps } = rest;
577
+ const context = usePopperContext(scope);
578
+ // TODO: get rid! at the very least move up to Popover and simplify
579
+ const sizeVal = typeof sizeProp === 'number'
580
+ ? sizeProp
581
+ : getVariableValue(getSpace(sizeProp ?? context.size, {
582
+ shift: -2,
583
+ bounds: [2],
584
+ }));
585
+ const size = Math.max(0, +sizeVal);
586
+ const { placement } = context;
587
+ const refs = useComposedRefs(context.arrowRef, forwardedRef);
588
+ // Sometimes floating-ui can return NaN during orientation or screen size changes on native
589
+ // so we explicitly force the x,y position types as a number
590
+ const x = context.arrowStyle?.x || 0;
591
+ const y = context.arrowStyle?.y || 0;
592
+ // hide arrow until floating-ui has computed its position to prevent
593
+ // flash at x=0 during initial render or trigger switches in hydration
594
+ const arrowPositioned = context.arrowStyle != null;
595
+ const primaryPlacement = (placement ? placement.split('-')[0] : 'top');
596
+ const arrowStyle = { x, y, width: size, height: size };
597
+ const innerArrowStyle = {};
598
+ const isVertical = primaryPlacement === 'bottom' || primaryPlacement === 'top';
599
+ if (primaryPlacement) {
600
+ // allows for extra diagonal size
601
+ arrowStyle[isVertical ? 'width' : 'height'] = size * 2;
602
+ const oppSide = opposites[primaryPlacement];
603
+ if (oppSide) {
604
+ arrowStyle[oppSide] = -size;
605
+ innerArrowStyle[oppSide] = size / 2;
606
+ }
607
+ if (oppSide === 'top' || oppSide === 'bottom') {
608
+ arrowStyle.left = 0;
609
+ }
610
+ if (oppSide === 'left' || oppSide === 'right') {
611
+ arrowStyle.top = 0;
612
+ }
613
+ // send the Arrow's offset up to Popper
614
+ useIsomorphicLayoutEffect(() => {
615
+ context.onArrowSize?.(size);
616
+ }, [size, context.onArrowSize]);
617
+ }
618
+ // outer frame to cut off for ability to have nicer shadows/borders
619
+ return (_jsx(PopperArrowOuterFrame, { ref: refs, ...arrowStyle, ...(!arrowPositioned && { opacity: 0 }), ...(isAnimatePosControlled && {
620
+ transition: animatePosition ? transition : undefined,
621
+ animateOnly: animatePosition ? ['transform'] : [],
622
+ animatePresence: false,
623
+ }), children: _jsx(PopperArrowFrame, { width: size, height: size, ...arrowProps, ...innerArrowStyle, rotate: "45deg", ...(primaryPlacement === 'bottom' && {
624
+ borderLeftWidth: borderWidth,
625
+ borderTopWidth: borderWidth,
626
+ }), ...(primaryPlacement === 'top' && {
627
+ borderBottomWidth: borderWidth,
628
+ borderRightWidth: borderWidth,
629
+ }), ...(primaryPlacement === 'right' && {
630
+ borderLeftWidth: borderWidth,
631
+ borderBottomWidth: borderWidth,
632
+ }), ...(primaryPlacement === 'left' && {
633
+ borderTopWidth: borderWidth,
634
+ borderRightWidth: borderWidth,
635
+ }) }) }));
636
+ });
637
+ //# sourceMappingURL=Popper.js.map