@jsenv/navi 0.30.26 → 0.30.28
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/dist/dev/jsenv_navi.js +222 -25
- package/dist/dev/jsenv_navi.js.map +9 -7
- package/dist/dev/jsenv_navi_side_effects.js +20 -11
- package/dist/dev/jsenv_navi_side_effects.js.map +3 -3
- package/dist/jsenv_navi.js +219 -25
- package/dist/jsenv_navi.js.map +9 -7
- package/dist/jsenv_navi_side_effects.js +20 -11
- package/dist/jsenv_navi_side_effects.js.map +3 -3
- package/docs/AI_INSTRUCTIONS.md +3 -2
- package/docs/data_states.md +4 -2
- package/docs/safe_area.md +25 -0
- package/package.json +2 -2
package/dist/dev/jsenv_navi.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
10323
|
-
*
|
|
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);
|
|
@@ -33403,6 +33521,16 @@ const useActionAsyncData = (action, {
|
|
|
33403
33521
|
if (runningState === COMPLETED) {
|
|
33404
33522
|
return [action.dataSignal.peek(), false, undefined];
|
|
33405
33523
|
}
|
|
33524
|
+
// Called off (abort(), a route left with nothing else asking for it): nothing
|
|
33525
|
+
// is on its way until something runs it again. What it held stays drawn and
|
|
33526
|
+
// no wait is claimed. A delegated wait holding nothing has nothing to draw,
|
|
33527
|
+
// and suspends below until the next run.
|
|
33528
|
+
if (runningState === ABORTED) {
|
|
33529
|
+
const dataHeld = action.dataSignal.peek();
|
|
33530
|
+
if (dataHeld !== undefined || loadingEffect === "use") {
|
|
33531
|
+
return [dataHeld, false, undefined];
|
|
33532
|
+
}
|
|
33533
|
+
}
|
|
33406
33534
|
if (runningState === FAILED) {
|
|
33407
33535
|
if (prerun) {
|
|
33408
33536
|
// Neither shown nor reported from a head start: the failure is the
|
|
@@ -33478,8 +33606,7 @@ const useActionAsyncData = (action, {
|
|
|
33478
33606
|
|
|
33479
33607
|
// `loading: true` says the component draws its own wait, so this hook never
|
|
33480
33608
|
// suspends: running, or holding params nobody has started yet — a debounced
|
|
33481
|
-
// binding that just retargeted,
|
|
33482
|
-
// about to issue. Either way what is on screen is not the current answer,
|
|
33609
|
+
// binding that just retargeted, a run this very render is about to issue. Either way what is on screen is not the current answer,
|
|
33483
33610
|
// which is what `loading` says. Suspending would not draw a spinner: it swaps
|
|
33484
33611
|
// the subtree for the boundary, and everything under it remounts — a dialog
|
|
33485
33612
|
// open inside it is then closed, by a hook the caller told to stay out of it.
|
|
@@ -33487,7 +33614,8 @@ const useActionAsyncData = (action, {
|
|
|
33487
33614
|
return [action.dataSignal.peek(), true, undefined];
|
|
33488
33615
|
}
|
|
33489
33616
|
|
|
33490
|
-
// IDLE or
|
|
33617
|
+
// IDLE, RUNNING, or called off holding nothing, with loadingEffect:
|
|
33618
|
+
// "delegate" — suspend
|
|
33491
33619
|
const reason = runningState === RUNNING ? "loading" : "idle";
|
|
33492
33620
|
loadingRef.current = {
|
|
33493
33621
|
reason,
|
|
@@ -33506,6 +33634,13 @@ const useActionAsyncData = (action, {
|
|
|
33506
33634
|
actionPendingPromiseWeakMap.delete(action);
|
|
33507
33635
|
unsubscribe();
|
|
33508
33636
|
resolve();
|
|
33637
|
+
} else if (reason === "loading" && state === ABORTED) {
|
|
33638
|
+
// The run waited on was called off: drawn again, this reader asks
|
|
33639
|
+
// for it again when nothing else will (calledOffBeforeThisReader),
|
|
33640
|
+
// draws what the instance held, or waits for the next run as idle.
|
|
33641
|
+
actionPendingPromiseWeakMap.delete(action);
|
|
33642
|
+
unsubscribe();
|
|
33643
|
+
resolve();
|
|
33509
33644
|
}
|
|
33510
33645
|
});
|
|
33511
33646
|
});
|
|
@@ -61120,6 +61255,7 @@ const measureDrawing = ({
|
|
|
61120
61255
|
* never seen under a bar, and after every read of the commit that queued them.
|
|
61121
61256
|
*/
|
|
61122
61257
|
|
|
61258
|
+
|
|
61123
61259
|
// Several bars can share an edge — during a page transition the outgoing and
|
|
61124
61260
|
// the incoming one are both mounted. They are all pinned to that same edge, so
|
|
61125
61261
|
// they overlap: the room to give back is the largest of them, not their sum,
|
|
@@ -61137,6 +61273,12 @@ const pendingSizeMapByArea = new Map();
|
|
|
61137
61273
|
let flushMicrotaskQueued = false;
|
|
61138
61274
|
let flushFrame = null;
|
|
61139
61275
|
|
|
61276
|
+
// The safe area moves with these writes and no resize comes with them: what is
|
|
61277
|
+
// placed in it (a Popover with container="safe-area") is told here.
|
|
61278
|
+
const [publishFixedBarSpaceChange, subscribeFixedBarSpaceChange] =
|
|
61279
|
+
createPubSub();
|
|
61280
|
+
const onFixedBarSpaceChange = subscribeFixedBarSpaceChange;
|
|
61281
|
+
|
|
61140
61282
|
// A size a render produced: a prop, the children or a theme variable changed
|
|
61141
61283
|
// the bar, and the room it takes is given back before that commit is painted.
|
|
61142
61284
|
/**
|
|
@@ -61187,13 +61329,19 @@ const queueSize = (area, barElement, size) => {
|
|
|
61187
61329
|
};
|
|
61188
61330
|
|
|
61189
61331
|
const flushPendingSizes = () => {
|
|
61332
|
+
let written = false;
|
|
61190
61333
|
for (const [area, pendingSizeMap] of pendingSizeMapByArea) {
|
|
61191
61334
|
for (const [barElement, size] of pendingSizeMap) {
|
|
61192
61335
|
storeSize(area, barElement, size);
|
|
61193
61336
|
}
|
|
61194
|
-
writeSpace(area)
|
|
61337
|
+
if (writeSpace(area)) {
|
|
61338
|
+
written = true;
|
|
61339
|
+
}
|
|
61195
61340
|
}
|
|
61196
61341
|
pendingSizeMapByArea.clear();
|
|
61342
|
+
if (written) {
|
|
61343
|
+
publishFixedBarSpaceChange();
|
|
61344
|
+
}
|
|
61197
61345
|
};
|
|
61198
61346
|
|
|
61199
61347
|
const storeSize = (area, barElement, size) => {
|
|
@@ -61220,18 +61368,20 @@ const writeSpace = (area) => {
|
|
|
61220
61368
|
const property = `--navi-fixed-bar-space-${area}`;
|
|
61221
61369
|
const { style } = document.documentElement;
|
|
61222
61370
|
if (sizeMap.size === 0) {
|
|
61223
|
-
if (writtenValueByArea.has(area)) {
|
|
61224
|
-
|
|
61225
|
-
style.removeProperty(property);
|
|
61371
|
+
if (!writtenValueByArea.has(area)) {
|
|
61372
|
+
return false;
|
|
61226
61373
|
}
|
|
61227
|
-
|
|
61374
|
+
writtenValueByArea.delete(area);
|
|
61375
|
+
style.removeProperty(property);
|
|
61376
|
+
return true;
|
|
61228
61377
|
}
|
|
61229
61378
|
const value = `${largestSize}px`;
|
|
61230
61379
|
if (writtenValueByArea.get(area) === value) {
|
|
61231
|
-
return;
|
|
61380
|
+
return false;
|
|
61232
61381
|
}
|
|
61233
61382
|
writtenValueByArea.set(area, value);
|
|
61234
61383
|
style.setProperty(property, value);
|
|
61384
|
+
return true;
|
|
61235
61385
|
};
|
|
61236
61386
|
|
|
61237
61387
|
installImportMetaCssBuild(import.meta);
|
|
@@ -76143,8 +76293,9 @@ const needsPopupFromMount = (props) => {
|
|
|
76143
76293
|
* (measured by wematch, 2026-09-06: no panel at all for 120ms, then a panel
|
|
76144
76294
|
* with its head missing, then the head arriving from below). So the cut is
|
|
76145
76295
|
* 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
|
|
76147
|
-
*
|
|
76296
|
+
* band of glass beside an app narrowed with --navi-app-max-width, the fixed
|
|
76297
|
+
* bars beside a popover placed in the safe area — and is a constant
|
|
76298
|
+
* everywhere else: at layer="top" the viewport's own edge cuts the
|
|
76148
76299
|
* travel on the compositor for free, and at layer="local" the clip wrapper
|
|
76149
76300
|
* (a static ancestor, overflow: hidden) does. Where a band exists the cut
|
|
76150
76301
|
* still animates and still can drift under load; the full answer there is a
|
|
@@ -76276,22 +76427,34 @@ const popupCss = /* css */ `
|
|
|
76276
76427
|
0 for a 0 band and 100vmax for any other, a step written without a
|
|
76277
76428
|
branch. See this file's top comment for why an animated cut has to be
|
|
76278
76429
|
that rare. */
|
|
76430
|
+
--x-popup-area-inset-top: var(--navi-app-inset-top, 0px);
|
|
76431
|
+
--x-popup-area-inset-right: var(--navi-app-inset-right, 0px);
|
|
76432
|
+
--x-popup-area-inset-bottom: var(--navi-app-inset-bottom, 0px);
|
|
76433
|
+
--x-popup-area-inset-left: var(--navi-app-inset-left, 0px);
|
|
76279
76434
|
--x-popup-cut-top-at-area: calc(
|
|
76280
76435
|
-1 * var(--container-position-room-top, 100vmax) - 100vmax +
|
|
76281
|
-
min(100vmax, var(--
|
|
76436
|
+
min(100vmax, var(--x-popup-area-inset-top) * 1000000)
|
|
76282
76437
|
);
|
|
76283
76438
|
--x-popup-cut-right-at-area: calc(
|
|
76284
76439
|
-1 * var(--container-position-room-right, 100vmax) - 100vmax +
|
|
76285
|
-
min(100vmax, var(--
|
|
76440
|
+
min(100vmax, var(--x-popup-area-inset-right) * 1000000)
|
|
76286
76441
|
);
|
|
76287
76442
|
--x-popup-cut-bottom-at-area: calc(
|
|
76288
76443
|
-1 * var(--container-position-room-bottom, 100vmax) - 100vmax +
|
|
76289
|
-
min(100vmax, var(--
|
|
76444
|
+
min(100vmax, var(--x-popup-area-inset-bottom) * 1000000)
|
|
76290
76445
|
);
|
|
76291
76446
|
--x-popup-cut-left-at-area: calc(
|
|
76292
76447
|
-1 * var(--container-position-room-left, 100vmax) - 100vmax +
|
|
76293
|
-
min(100vmax, var(--
|
|
76448
|
+
min(100vmax, var(--x-popup-area-inset-left) * 1000000)
|
|
76294
76449
|
);
|
|
76450
|
+
/* Placed in the safe area (Popover's container="safe-area"): past its edge
|
|
76451
|
+
lie the fixed bars, and a popup sliding in comes out from behind them. */
|
|
76452
|
+
&[data-container="safe-area"] {
|
|
76453
|
+
--x-popup-area-inset-top: var(--navi-safe-area-inset-top);
|
|
76454
|
+
--x-popup-area-inset-right: var(--navi-safe-area-inset-right);
|
|
76455
|
+
--x-popup-area-inset-bottom: var(--navi-safe-area-inset-bottom);
|
|
76456
|
+
--x-popup-area-inset-left: var(--navi-safe-area-inset-left);
|
|
76457
|
+
}
|
|
76295
76458
|
/* The translate property is deliberately absent from this list — it
|
|
76296
76459
|
carries where the popup stands, and applyNewPosition (visible_rect.js)
|
|
76297
76460
|
owns it and drives its own transition through the Web Animations API
|
|
@@ -80183,6 +80346,14 @@ const css$C = /* css */`
|
|
|
80183
80346
|
* (`"3appw"`) or a container length (`"3cqw"`).
|
|
80184
80347
|
* @param {string|number} [props.marginWithAnchor=0] - Extra spacing kept
|
|
80185
80348
|
* between the popover and the edges of its anchor, written the same way.
|
|
80349
|
+
* @param {"app"|"safe-area"} [props.container="app"] - The rectangle a
|
|
80350
|
+
* `layer="top"` popover is placed in: docked against with no anchor,
|
|
80351
|
+
* flipped and clamped within with one. `"app"`: the app's own screen, its
|
|
80352
|
+
* fixed bars included — the popover covers them. `"safe-area"`: that screen
|
|
80353
|
+
* less the bars and the device's own bands (`--navi-safe-area-inset-*`), for
|
|
80354
|
+
* something docked over the page that must leave the bars uncovered, a
|
|
80355
|
+
* toast. Followed live as a bar comes or goes. Still in the top layer, so
|
|
80356
|
+
* above what the page holds. See docs/safe_area.md.
|
|
80186
80357
|
* @param {boolean} [props.backdrop=true] - Whether anything is laid between
|
|
80187
80358
|
* the popover and the page at all — asked before what an outside press does
|
|
80188
80359
|
* and before how the backdrop is painted. `false` leaves the page
|
|
@@ -80614,6 +80785,7 @@ const usePopoverProps = props => {
|
|
|
80614
80785
|
// "center" default.
|
|
80615
80786
|
positionAreaWhenAnchorIsInvalid,
|
|
80616
80787
|
marginWithContainer = 0,
|
|
80788
|
+
container = "app",
|
|
80617
80789
|
// "close" → pointer press outside closes (a plain close = commit)
|
|
80618
80790
|
// "cancel" → pointer press outside closes AS A CANCEL (revert)
|
|
80619
80791
|
// "capture"→ absorb the press, stay open
|
|
@@ -80705,6 +80877,10 @@ const usePopoverProps = props => {
|
|
|
80705
80877
|
})
|
|
80706
80878
|
}) : null;
|
|
80707
80879
|
const isTopLayer = layer === "top";
|
|
80880
|
+
if (!isTopLayer && container !== "app") {
|
|
80881
|
+
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.`);
|
|
80882
|
+
}
|
|
80883
|
+
const placesInSafeArea = isTopLayer && container === "safe-area";
|
|
80708
80884
|
const ref = props.ref;
|
|
80709
80885
|
const backdropRef = useRef();
|
|
80710
80886
|
// Disarms a still-pending backdrop hide from a previous close (see
|
|
@@ -80756,14 +80932,15 @@ const usePopoverProps = props => {
|
|
|
80756
80932
|
positionAreaFixed,
|
|
80757
80933
|
positionAreaWhenAnchorIsInvalid,
|
|
80758
80934
|
marginWithAnchor,
|
|
80759
|
-
marginWithContainer
|
|
80935
|
+
marginWithContainer,
|
|
80936
|
+
placesInSafeArea
|
|
80760
80937
|
};
|
|
80761
80938
|
const repositionRef = useRef(null);
|
|
80762
80939
|
useEffect(() => {
|
|
80763
80940
|
repositionRef.current?.(new CustomEvent("position_props_change", {
|
|
80764
80941
|
detail: {}
|
|
80765
80942
|
}));
|
|
80766
|
-
}, [positionArea, positionAreaFixed, positionAreaWhenAnchorIsInvalid, marginWithAnchor, marginWithContainer]);
|
|
80943
|
+
}, [positionArea, positionAreaFixed, positionAreaWhenAnchorIsInvalid, marginWithAnchor, marginWithContainer, placesInSafeArea]);
|
|
80767
80944
|
// The freeze is taken where the value changes, not only at open time: a
|
|
80768
80945
|
// popover showing skeletons first says sizing="auto" until its content is
|
|
80769
80946
|
// there, and would otherwise be held at the size of the waiting state.
|
|
@@ -81022,8 +81199,11 @@ const usePopoverProps = props => {
|
|
|
81022
81199
|
positionAreaFixed,
|
|
81023
81200
|
positionAreaWhenAnchorIsInvalid,
|
|
81024
81201
|
marginWithAnchor,
|
|
81025
|
-
marginWithContainer
|
|
81202
|
+
marginWithContainer,
|
|
81203
|
+
placesInSafeArea
|
|
81026
81204
|
} = positionPropsRef.current;
|
|
81205
|
+
// undefined keeps the app's own screen (setPlacementViewportInsets).
|
|
81206
|
+
const viewportInsets = placesInSafeArea ? getSafeAreaInsets() : undefined;
|
|
81027
81207
|
let position;
|
|
81028
81208
|
if (hasAnchorElement) {
|
|
81029
81209
|
const {
|
|
@@ -81062,6 +81242,7 @@ const usePopoverProps = props => {
|
|
|
81062
81242
|
// renderer, same as omitting `container` entirely would resolve
|
|
81063
81243
|
// to on its own.
|
|
81064
81244
|
container: positionedAncestor,
|
|
81245
|
+
viewportInsets,
|
|
81065
81246
|
minLeft,
|
|
81066
81247
|
event: positionEvent
|
|
81067
81248
|
};
|
|
@@ -81108,6 +81289,7 @@ const usePopoverProps = props => {
|
|
|
81108
81289
|
const pickOptions = {
|
|
81109
81290
|
positionArea,
|
|
81110
81291
|
container: positionedAncestor,
|
|
81292
|
+
viewportInsets,
|
|
81111
81293
|
marginWithContainer: resolveMarginInPixels(marginWithContainer, "marginWithContainer"),
|
|
81112
81294
|
event: positionEvent
|
|
81113
81295
|
};
|
|
@@ -81229,6 +81411,20 @@ const usePopoverProps = props => {
|
|
|
81229
81411
|
// while open (e.g. an expand/collapse toggle inside it) — not just when
|
|
81230
81412
|
// the anchor itself moves/resizes/re-anchors.
|
|
81231
81413
|
rectEffect.observeSize(popoverEl);
|
|
81414
|
+
// A bar mounting or leaving moves the safe area and leaves the viewport as
|
|
81415
|
+
// it was, so rectEffect.check() would find nothing changed: placed
|
|
81416
|
+
// directly. "resize", so the popover glides to its new place.
|
|
81417
|
+
if (isTopLayer) {
|
|
81418
|
+
addCleanup(onFixedBarSpaceChange(() => {
|
|
81419
|
+
if (!positionPropsRef.current.placesInSafeArea) {
|
|
81420
|
+
return;
|
|
81421
|
+
}
|
|
81422
|
+
if (deferPlacementWhileCovered(popoverEl)) {
|
|
81423
|
+
return;
|
|
81424
|
+
}
|
|
81425
|
+
placeUnlessAnchorOutOfView(new CustomEvent("resize"));
|
|
81426
|
+
}));
|
|
81427
|
+
}
|
|
81232
81428
|
// Whatever its own backdrop: a modal dialog above leaves all of the page
|
|
81233
81429
|
// inert, a popover without a wall included. layer="local" is left out, its
|
|
81234
81430
|
// ceiling answers to its container.
|
|
@@ -81570,6 +81766,7 @@ const usePopoverProps = props => {
|
|
|
81570
81766
|
id,
|
|
81571
81767
|
tabIndex,
|
|
81572
81768
|
"data-layer": layer,
|
|
81769
|
+
"data-container": placesInSafeArea ? "safe-area" : undefined,
|
|
81573
81770
|
"data-expand-x": expand || expandX ? "" : undefined,
|
|
81574
81771
|
"data-expand-y": expand || expandY ? "" : undefined,
|
|
81575
81772
|
"navi-animation": isAutoAnimation ? undefined : animation,
|