@jsenv/navi 0.30.26 → 0.30.27

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.
@@ -2,7 +2,7 @@
2
2
  * AI reading this file: read ../docs/AI_INSTRUCTIONS.md for context on
3
3
  * using @jsenv/navi as intended.
4
4
  */
5
- import { installImportMetaCssBuild, coarsePointerSignal, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
5
+ import { installImportMetaCssBuild, coarsePointerSignal, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, smallTouchScreenSignal, getSafeAreaInsets } from "./jsenv_navi_side_effects.js";
6
6
  import { createPubSub, createIterableWeakSet, elementIsFocusable, getScrollContainer, dispatchInternalCustomEvent, dispatchCustomEvent, createEventGroupLogger, findEvent, mergeOneStyle, findFocusable, getElementSignature, chainEvent, isTouchDrivenEvent, getKeyboardEventDefaultAction, isPressDrivenClick, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, createValueEffect, findFocusDelegateTarget, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, getScrollContainerSet, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, keepTouchRefusable, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createInternalCustomEvent, activeElementSignal, normalizeStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, trapScrollInside, scrollIntoViewScoped, isPressDisputedByDrag, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, isScrollable, resolveOklchLightness, contrastColor, scrollRoomTowards, overflowGoesToViewport, getVirtualKeyboardOverlayHeight, subscribeVisualViewportResizeSettled, subscribeVirtualKeyboardGeometryChange, parsePositionArea, snapToPixel, trapFocusInside, onAncestorReopen, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, setVirtualKeyboardOverlaysContent } from "@jsenv/dom";
7
7
  export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
8
8
  import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
@@ -810,6 +810,91 @@ const stringifyForDisplay = (
810
810
  return String(value);
811
811
  };
812
812
 
813
+ /*
814
+ * Who asks for an action instance right now, and what a run called off for
815
+ * want of askers owes to whoever comes next.
816
+ *
817
+ * Instances are shared by params: two route actions reading the same thing
818
+ * with the same params, or a route action and a component reading it with
819
+ * `run: true`, ask ONE instance. So a binding whose params turn falsy (its
820
+ * route left) does not own the run in flight: it calls it off only when
821
+ * nothing else asks for that instance at that moment. What asks is an asker:
822
+ * a binding of actionRunEffect while its params are truthy, a mounted
823
+ * `useAsyncData` reader with `run: true`. Each says which instance it asks for
824
+ * when the question is put, rather than registering one: a reader of a
825
+ * binding follows the binding's target the moment it moves, not at its next
826
+ * render.
827
+ *
828
+ * Calling a run off is a guess about who is coming: a reader mounting after
829
+ * it (the page arriving), or suspended before its first commit, finds an
830
+ * instance nobody will run again. So the instance is marked, and a `run: true`
831
+ * reader finding it marked asks again, as an arriving binding does when its
832
+ * run() finds the instance aborted. An abort decided by anyone else (abort(),
833
+ * a give-up on a busy popup) carries no mark: aborting does not undo, and the
834
+ * reader leaves it be.
835
+ */
836
+
837
+ const askerSet = createIterableWeakSet("action_asker");
838
+
839
+ /**
840
+ * `readAskedInstance()` returns what is asked for now: an instance, a binding
841
+ * (its current target is what counts), or null. The asker is held weakly:
842
+ * whoever adds it keeps it, and removes it once it asks for nothing for good.
843
+ */
844
+ const addActionAsker = (readAskedInstance) => {
845
+ const asker = { readAskedInstance };
846
+ askerSet.add(asker);
847
+ return asker;
848
+ };
849
+ const removeActionAsker = (asker) => {
850
+ askerSet.delete(asker);
851
+ };
852
+
853
+ const calledOffWeakSet = new WeakSet();
854
+
855
+ /**
856
+ * Aborts the instance's run unless an asker asks for it, and marks a run
857
+ * called off that way (see isCalledOffForWantOfAskers).
858
+ */
859
+ const callOffUnlessAsked = (action, reason) => {
860
+ const instance = resolveInstance(action);
861
+ for (const asker of askerSet) {
862
+ if (resolveInstance(asker.readAskedInstance()) === instance) {
863
+ return;
864
+ }
865
+ }
866
+ if (!instance.abort(reason)) {
867
+ return;
868
+ }
869
+ // A callback that ignores its abort signal completes all the same.
870
+ if (instance.runningStateSignal.peek() !== ABORTED) {
871
+ return;
872
+ }
873
+ calledOffWeakSet.add(instance);
874
+ // The mark is for this abort only: once the instance moves, an abort that
875
+ // comes later is somebody else's.
876
+ const unsubscribe = instance.runningStateSignal.subscribe((state) => {
877
+ if (state !== ABORTED) {
878
+ calledOffWeakSet.delete(instance);
879
+ unsubscribe();
880
+ }
881
+ });
882
+ };
883
+
884
+ const isCalledOffForWantOfAskers = (action) => {
885
+ return calledOffWeakSet.has(resolveInstance(action));
886
+ };
887
+
888
+ // A binding counts as the instance it targets now. Read as it stands, never
889
+ // through getCurrentAction(), which would flush a debounced binding.
890
+ const resolveInstance = (action) => {
891
+ let instance = action;
892
+ while (instance && instance.isProxy) {
893
+ instance = getActionPrivateProperties(instance).currentAction;
894
+ }
895
+ return instance;
896
+ };
897
+
813
898
  const SYMBOL_OBJECT_SIGNAL = Symbol.for("navi_object_signal");
814
899
 
815
900
  /*
@@ -5324,6 +5409,8 @@ const writeActionState = (write) => {
5324
5409
  }
5325
5410
  };
5326
5411
 
5412
+ const askerByBindingWeakMap = new WeakMap();
5413
+
5327
5414
  /**
5328
5415
  * Reactively runs an action whenever the params derived from signals change.
5329
5416
  *
@@ -5420,8 +5507,9 @@ const actionRunEffect = (
5420
5507
  if (actionTargetPrevious && actionTarget) {
5421
5508
  // params changed
5422
5509
  if (!asking) {
5423
- // falsy params, don't run
5424
- actionTargetPrevious.abort("abortOnFalsyParams");
5510
+ // falsy params, don't run; the instance is shared by params, and
5511
+ // something else may still ask for it (see action_askers.js)
5512
+ callOffUnlessAsked(actionTargetPrevious, "abortOnFalsyParams");
5425
5513
  return;
5426
5514
  }
5427
5515
  if (!askingBefore) {
@@ -5447,6 +5535,11 @@ const actionRunEffect = (
5447
5535
  },
5448
5536
  ...options,
5449
5537
  });
5538
+ // Held by the binding, as long as it lives: askers are held weakly.
5539
+ askerByBindingWeakMap.set(
5540
+ actionRunnedByThisEffect,
5541
+ addActionAsker(() => (asking ? actionRunnedByThisEffect : null)),
5542
+ );
5450
5543
  if (asking) {
5451
5544
  runUnwatched(() =>
5452
5545
  actionRunnedByThisEffect.run({ reason: "initial truthy params" }),
@@ -10319,8 +10412,10 @@ const ROUTE_ACTION_BINDING_OPTIONS = new Set([
10319
10412
  /**
10320
10413
  * Binds an action to a route: it runs when the route matches, with the params
10321
10414
  * the effect reads off the address, runs again on a `reload()`, and is aborted
10322
- * when the route is left. Its data, its wait and its failure are read by the
10323
- * page through `useAsyncData`.
10415
+ * when the route is left, unless something else still asks for the same read
10416
+ * with the same params: another route action, a mounted `useAsyncData` with
10417
+ * `run: true`. Its data, its wait and its failure are read by the page through
10418
+ * `useAsyncData`.
10324
10419
  *
10325
10420
  * A page's CODE is declared the same way — the import is one more thing the
10326
10421
  * address asks for, started with the data rather than after it:
@@ -33107,6 +33202,11 @@ const useForceRender = () => {
33107
33202
  * failure a prerun left (a `prerun: true` head start, a press that preran
33108
33203
  * it): nothing on screen asked for that run, so this asks again, once.
33109
33204
  *
33205
+ * While mounted, it asks for that instance as a route action does: a route
33206
+ * action reading the same thing with the same params does not call the run
33207
+ * off under it when its route is left. A run called off that way before
33208
+ * this component was there to ask is asked again.
33209
+ *
33110
33210
  * Prefer a `routeAction` wherever the parameter is one the address holds (a
33111
33211
  * path param, a search param bound to a `stateSignal`), popup included: it is
33112
33212
  * asked for when the address changes, with everything else that address
@@ -33256,6 +33356,20 @@ const useActionAsyncData = (action, {
33256
33356
  }
33257
33357
  useOnLoad(action, onLoad);
33258
33358
  usePoll(action, poll);
33359
+ const routePage = useContext(RoutePageContext);
33360
+
33361
+ // `run: true` asks for the instance as long as this reader is mounted: a
33362
+ // binding sharing it whose route is left does not call the run off under it
33363
+ // (see action_askers.js). Asked through the binding itself, whose target
33364
+ // moves before this renders again.
33365
+ const askedRef = useRef(null);
33366
+ askedRef.current = run ? action : null;
33367
+ useLayoutEffect(() => {
33368
+ const asker = addActionAsker(() => askedRef.current);
33369
+ return () => {
33370
+ removeActionAsker(asker);
33371
+ };
33372
+ }, []);
33259
33373
 
33260
33374
  // The instance the binding points at, remembered from one render to the
33261
33375
  // next: a binding that comes back to params whose instance failed — a search
@@ -33268,6 +33382,11 @@ const useActionAsyncData = (action, {
33268
33382
  const instanceRef = useRef(instance);
33269
33383
  const backOnFailure = instanceRef.current !== instance && action.runningStateSignal.peek() === FAILED;
33270
33384
  instanceRef.current = instance;
33385
+ // A run called off because nothing asked for it any more, before this reader
33386
+ // was there to ask (mounting after, or suspended before its first commit):
33387
+ // nobody else will run it again. Not from a page the address has left, whose
33388
+ // own route action is what called it off.
33389
+ const calledOffBeforeThisReader = isCalledOffForWantOfAskers(action) && !(routePage && !routePage.isShown());
33271
33390
  // `run: true` — this component owns the request, so it is what starts it, and
33272
33391
  // it starts it from the render rather than from an effect: a component that
33273
33392
  // suspends has no effects, so a run written in one would be waiting for
@@ -33275,7 +33394,7 @@ const useActionAsyncData = (action, {
33275
33394
  // no-op — which is what makes starting it while rendering sound. From there
33276
33395
  // the wait is the ordinary one: suspended into <Loading>, or drawn by the
33277
33396
  // component under `loading: true`, exactly as for data someone else ran.
33278
- if (run && action.paramsSignal.peek() !== undefined && (runIsOwed(action) || backOnFailure)) {
33397
+ if (run && action.paramsSignal.peek() !== undefined && (runIsOwed(action) || backOnFailure || calledOffBeforeThisReader)) {
33279
33398
  // Nothing waits on this run: the failure is held by the action, and this
33280
33399
  // hook is what reads it back.
33281
33400
  runUnwatched(() => action.run({
@@ -33298,7 +33417,6 @@ const useActionAsyncData = (action, {
33298
33417
  // has already processed the settlement and the detached DOM is discarded.
33299
33418
  const runningState = action.runningStateSignal.peek();
33300
33419
  const [, setTick] = useState(0);
33301
- const routePage = useContext(RoutePageContext);
33302
33420
  // What this render read of the action, for the subscriptions below to tell
33303
33421
  // a change that came between the render and them from none at all.
33304
33422
  const readRef = useRef(null);
@@ -33506,6 +33624,13 @@ const useActionAsyncData = (action, {
33506
33624
  actionPendingPromiseWeakMap.delete(action);
33507
33625
  unsubscribe();
33508
33626
  resolve();
33627
+ } else if (reason === "loading" && state === ABORTED) {
33628
+ // The run waited on was called off: drawn again, this reader asks
33629
+ // for it again when nothing else will (calledOffBeforeThisReader),
33630
+ // or waits for the next run as idle.
33631
+ actionPendingPromiseWeakMap.delete(action);
33632
+ unsubscribe();
33633
+ resolve();
33509
33634
  }
33510
33635
  });
33511
33636
  });
@@ -61120,6 +61245,7 @@ const measureDrawing = ({
61120
61245
  * never seen under a bar, and after every read of the commit that queued them.
61121
61246
  */
61122
61247
 
61248
+
61123
61249
  // Several bars can share an edge — during a page transition the outgoing and
61124
61250
  // the incoming one are both mounted. They are all pinned to that same edge, so
61125
61251
  // they overlap: the room to give back is the largest of them, not their sum,
@@ -61137,6 +61263,12 @@ const pendingSizeMapByArea = new Map();
61137
61263
  let flushMicrotaskQueued = false;
61138
61264
  let flushFrame = null;
61139
61265
 
61266
+ // The safe area moves with these writes and no resize comes with them: what is
61267
+ // placed in it (a Popover with container="safe-area") is told here.
61268
+ const [publishFixedBarSpaceChange, subscribeFixedBarSpaceChange] =
61269
+ createPubSub();
61270
+ const onFixedBarSpaceChange = subscribeFixedBarSpaceChange;
61271
+
61140
61272
  // A size a render produced: a prop, the children or a theme variable changed
61141
61273
  // the bar, and the room it takes is given back before that commit is painted.
61142
61274
  /**
@@ -61187,13 +61319,19 @@ const queueSize = (area, barElement, size) => {
61187
61319
  };
61188
61320
 
61189
61321
  const flushPendingSizes = () => {
61322
+ let written = false;
61190
61323
  for (const [area, pendingSizeMap] of pendingSizeMapByArea) {
61191
61324
  for (const [barElement, size] of pendingSizeMap) {
61192
61325
  storeSize(area, barElement, size);
61193
61326
  }
61194
- writeSpace(area);
61327
+ if (writeSpace(area)) {
61328
+ written = true;
61329
+ }
61195
61330
  }
61196
61331
  pendingSizeMapByArea.clear();
61332
+ if (written) {
61333
+ publishFixedBarSpaceChange();
61334
+ }
61197
61335
  };
61198
61336
 
61199
61337
  const storeSize = (area, barElement, size) => {
@@ -61220,18 +61358,20 @@ const writeSpace = (area) => {
61220
61358
  const property = `--navi-fixed-bar-space-${area}`;
61221
61359
  const { style } = document.documentElement;
61222
61360
  if (sizeMap.size === 0) {
61223
- if (writtenValueByArea.has(area)) {
61224
- writtenValueByArea.delete(area);
61225
- style.removeProperty(property);
61361
+ if (!writtenValueByArea.has(area)) {
61362
+ return false;
61226
61363
  }
61227
- return;
61364
+ writtenValueByArea.delete(area);
61365
+ style.removeProperty(property);
61366
+ return true;
61228
61367
  }
61229
61368
  const value = `${largestSize}px`;
61230
61369
  if (writtenValueByArea.get(area) === value) {
61231
- return;
61370
+ return false;
61232
61371
  }
61233
61372
  writtenValueByArea.set(area, value);
61234
61373
  style.setProperty(property, value);
61374
+ return true;
61235
61375
  };
61236
61376
 
61237
61377
  installImportMetaCssBuild(import.meta);
@@ -76143,8 +76283,9 @@ const needsPopupFromMount = (props) => {
76143
76283
  * (measured by wematch, 2026-09-06: no panel at all for 120ms, then a panel
76144
76284
  * with its head missing, then the head arriving from below). So the cut is
76145
76285
  * taken only where it hides something the screen would otherwise show — the
76146
- * band of glass beside an app narrowed with --navi-app-max-width — and is a
76147
- * constant everywhere else: at layer="top" the viewport's own edge cuts the
76286
+ * band of glass beside an app narrowed with --navi-app-max-width, the fixed
76287
+ * bars beside a popover placed in the safe area — and is a constant
76288
+ * everywhere else: at layer="top" the viewport's own edge cuts the
76148
76289
  * travel on the compositor for free, and at layer="local" the clip wrapper
76149
76290
  * (a static ancestor, overflow: hidden) does. Where a band exists the cut
76150
76291
  * still animates and still can drift under load; the full answer there is a
@@ -76276,22 +76417,34 @@ const popupCss = /* css */ `
76276
76417
  0 for a 0 band and 100vmax for any other, a step written without a
76277
76418
  branch. See this file's top comment for why an animated cut has to be
76278
76419
  that rare. */
76420
+ --x-popup-area-inset-top: var(--navi-app-inset-top, 0px);
76421
+ --x-popup-area-inset-right: var(--navi-app-inset-right, 0px);
76422
+ --x-popup-area-inset-bottom: var(--navi-app-inset-bottom, 0px);
76423
+ --x-popup-area-inset-left: var(--navi-app-inset-left, 0px);
76279
76424
  --x-popup-cut-top-at-area: calc(
76280
76425
  -1 * var(--container-position-room-top, 100vmax) - 100vmax +
76281
- min(100vmax, var(--navi-app-inset-top, 0px) * 1000000)
76426
+ min(100vmax, var(--x-popup-area-inset-top) * 1000000)
76282
76427
  );
76283
76428
  --x-popup-cut-right-at-area: calc(
76284
76429
  -1 * var(--container-position-room-right, 100vmax) - 100vmax +
76285
- min(100vmax, var(--navi-app-inset-right, 0px) * 1000000)
76430
+ min(100vmax, var(--x-popup-area-inset-right) * 1000000)
76286
76431
  );
76287
76432
  --x-popup-cut-bottom-at-area: calc(
76288
76433
  -1 * var(--container-position-room-bottom, 100vmax) - 100vmax +
76289
- min(100vmax, var(--navi-app-inset-bottom, 0px) * 1000000)
76434
+ min(100vmax, var(--x-popup-area-inset-bottom) * 1000000)
76290
76435
  );
76291
76436
  --x-popup-cut-left-at-area: calc(
76292
76437
  -1 * var(--container-position-room-left, 100vmax) - 100vmax +
76293
- min(100vmax, var(--navi-app-inset-left, 0px) * 1000000)
76438
+ min(100vmax, var(--x-popup-area-inset-left) * 1000000)
76294
76439
  );
76440
+ /* Placed in the safe area (Popover's container="safe-area"): past its edge
76441
+ lie the fixed bars, and a popup sliding in comes out from behind them. */
76442
+ &[data-container="safe-area"] {
76443
+ --x-popup-area-inset-top: var(--navi-safe-area-inset-top);
76444
+ --x-popup-area-inset-right: var(--navi-safe-area-inset-right);
76445
+ --x-popup-area-inset-bottom: var(--navi-safe-area-inset-bottom);
76446
+ --x-popup-area-inset-left: var(--navi-safe-area-inset-left);
76447
+ }
76295
76448
  /* The translate property is deliberately absent from this list — it
76296
76449
  carries where the popup stands, and applyNewPosition (visible_rect.js)
76297
76450
  owns it and drives its own transition through the Web Animations API
@@ -80183,6 +80336,14 @@ const css$C = /* css */`
80183
80336
  * (`"3appw"`) or a container length (`"3cqw"`).
80184
80337
  * @param {string|number} [props.marginWithAnchor=0] - Extra spacing kept
80185
80338
  * between the popover and the edges of its anchor, written the same way.
80339
+ * @param {"app"|"safe-area"} [props.container="app"] - The rectangle a
80340
+ * `layer="top"` popover is placed in: docked against with no anchor,
80341
+ * flipped and clamped within with one. `"app"`: the app's own screen, its
80342
+ * fixed bars included — the popover covers them. `"safe-area"`: that screen
80343
+ * less the bars and the device's own bands (`--navi-safe-area-inset-*`), for
80344
+ * something docked over the page that must leave the bars uncovered, a
80345
+ * toast. Followed live as a bar comes or goes. Still in the top layer, so
80346
+ * above what the page holds. See docs/safe_area.md.
80186
80347
  * @param {boolean} [props.backdrop=true] - Whether anything is laid between
80187
80348
  * the popover and the page at all — asked before what an outside press does
80188
80349
  * and before how the backdrop is painted. `false` leaves the page
@@ -80614,6 +80775,7 @@ const usePopoverProps = props => {
80614
80775
  // "center" default.
80615
80776
  positionAreaWhenAnchorIsInvalid,
80616
80777
  marginWithContainer = 0,
80778
+ container = "app",
80617
80779
  // "close" → pointer press outside closes (a plain close = commit)
80618
80780
  // "cancel" → pointer press outside closes AS A CANCEL (revert)
80619
80781
  // "capture"→ absorb the press, stay open
@@ -80705,6 +80867,10 @@ const usePopoverProps = props => {
80705
80867
  })
80706
80868
  }) : null;
80707
80869
  const isTopLayer = layer === "top";
80870
+ if (!isTopLayer && container !== "app") {
80871
+ console.warn(`Popover: container="${container}" is for layer="top". A layer="local" popover is placed in its positioned ancestor; put that ancestor where it should be.`);
80872
+ }
80873
+ const placesInSafeArea = isTopLayer && container === "safe-area";
80708
80874
  const ref = props.ref;
80709
80875
  const backdropRef = useRef();
80710
80876
  // Disarms a still-pending backdrop hide from a previous close (see
@@ -80756,14 +80922,15 @@ const usePopoverProps = props => {
80756
80922
  positionAreaFixed,
80757
80923
  positionAreaWhenAnchorIsInvalid,
80758
80924
  marginWithAnchor,
80759
- marginWithContainer
80925
+ marginWithContainer,
80926
+ placesInSafeArea
80760
80927
  };
80761
80928
  const repositionRef = useRef(null);
80762
80929
  useEffect(() => {
80763
80930
  repositionRef.current?.(new CustomEvent("position_props_change", {
80764
80931
  detail: {}
80765
80932
  }));
80766
- }, [positionArea, positionAreaFixed, positionAreaWhenAnchorIsInvalid, marginWithAnchor, marginWithContainer]);
80933
+ }, [positionArea, positionAreaFixed, positionAreaWhenAnchorIsInvalid, marginWithAnchor, marginWithContainer, placesInSafeArea]);
80767
80934
  // The freeze is taken where the value changes, not only at open time: a
80768
80935
  // popover showing skeletons first says sizing="auto" until its content is
80769
80936
  // there, and would otherwise be held at the size of the waiting state.
@@ -81022,8 +81189,11 @@ const usePopoverProps = props => {
81022
81189
  positionAreaFixed,
81023
81190
  positionAreaWhenAnchorIsInvalid,
81024
81191
  marginWithAnchor,
81025
- marginWithContainer
81192
+ marginWithContainer,
81193
+ placesInSafeArea
81026
81194
  } = positionPropsRef.current;
81195
+ // undefined keeps the app's own screen (setPlacementViewportInsets).
81196
+ const viewportInsets = placesInSafeArea ? getSafeAreaInsets() : undefined;
81027
81197
  let position;
81028
81198
  if (hasAnchorElement) {
81029
81199
  const {
@@ -81062,6 +81232,7 @@ const usePopoverProps = props => {
81062
81232
  // renderer, same as omitting `container` entirely would resolve
81063
81233
  // to on its own.
81064
81234
  container: positionedAncestor,
81235
+ viewportInsets,
81065
81236
  minLeft,
81066
81237
  event: positionEvent
81067
81238
  };
@@ -81108,6 +81279,7 @@ const usePopoverProps = props => {
81108
81279
  const pickOptions = {
81109
81280
  positionArea,
81110
81281
  container: positionedAncestor,
81282
+ viewportInsets,
81111
81283
  marginWithContainer: resolveMarginInPixels(marginWithContainer, "marginWithContainer"),
81112
81284
  event: positionEvent
81113
81285
  };
@@ -81229,6 +81401,20 @@ const usePopoverProps = props => {
81229
81401
  // while open (e.g. an expand/collapse toggle inside it) — not just when
81230
81402
  // the anchor itself moves/resizes/re-anchors.
81231
81403
  rectEffect.observeSize(popoverEl);
81404
+ // A bar mounting or leaving moves the safe area and leaves the viewport as
81405
+ // it was, so rectEffect.check() would find nothing changed: placed
81406
+ // directly. "resize", so the popover glides to its new place.
81407
+ if (isTopLayer) {
81408
+ addCleanup(onFixedBarSpaceChange(() => {
81409
+ if (!positionPropsRef.current.placesInSafeArea) {
81410
+ return;
81411
+ }
81412
+ if (deferPlacementWhileCovered(popoverEl)) {
81413
+ return;
81414
+ }
81415
+ placeUnlessAnchorOutOfView(new CustomEvent("resize"));
81416
+ }));
81417
+ }
81232
81418
  // Whatever its own backdrop: a modal dialog above leaves all of the page
81233
81419
  // inert, a popover without a wall included. layer="local" is left out, its
81234
81420
  // ceiling answers to its container.
@@ -81570,6 +81756,7 @@ const usePopoverProps = props => {
81570
81756
  id,
81571
81757
  tabIndex,
81572
81758
  "data-layer": layer,
81759
+ "data-container": placesInSafeArea ? "safe-area" : undefined,
81573
81760
  "data-expand-x": expand || expandX ? "" : undefined,
81574
81761
  "data-expand-y": expand || expandY ? "" : undefined,
81575
81762
  "navi-animation": isAutoAnimation ? undefined : animation,