@hanzogui/popper 8.3.1 → 8.3.3

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