@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/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, 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";
|
|
@@ -783,6 +783,91 @@ const stringifyForDisplay = (
|
|
|
783
783
|
return String(value);
|
|
784
784
|
};
|
|
785
785
|
|
|
786
|
+
/*
|
|
787
|
+
* Who asks for an action instance right now, and what a run called off for
|
|
788
|
+
* want of askers owes to whoever comes next.
|
|
789
|
+
*
|
|
790
|
+
* Instances are shared by params: two route actions reading the same thing
|
|
791
|
+
* with the same params, or a route action and a component reading it with
|
|
792
|
+
* `run: true`, ask ONE instance. So a binding whose params turn falsy (its
|
|
793
|
+
* route left) does not own the run in flight: it calls it off only when
|
|
794
|
+
* nothing else asks for that instance at that moment. What asks is an asker:
|
|
795
|
+
* a binding of actionRunEffect while its params are truthy, a mounted
|
|
796
|
+
* `useAsyncData` reader with `run: true`. Each says which instance it asks for
|
|
797
|
+
* when the question is put, rather than registering one: a reader of a
|
|
798
|
+
* binding follows the binding's target the moment it moves, not at its next
|
|
799
|
+
* render.
|
|
800
|
+
*
|
|
801
|
+
* Calling a run off is a guess about who is coming: a reader mounting after
|
|
802
|
+
* it (the page arriving), or suspended before its first commit, finds an
|
|
803
|
+
* instance nobody will run again. So the instance is marked, and a `run: true`
|
|
804
|
+
* reader finding it marked asks again, as an arriving binding does when its
|
|
805
|
+
* run() finds the instance aborted. An abort decided by anyone else (abort(),
|
|
806
|
+
* a give-up on a busy popup) carries no mark: aborting does not undo, and the
|
|
807
|
+
* reader leaves it be.
|
|
808
|
+
*/
|
|
809
|
+
|
|
810
|
+
const askerSet = createIterableWeakSet("action_asker");
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* `readAskedInstance()` returns what is asked for now: an instance, a binding
|
|
814
|
+
* (its current target is what counts), or null. The asker is held weakly:
|
|
815
|
+
* whoever adds it keeps it, and removes it once it asks for nothing for good.
|
|
816
|
+
*/
|
|
817
|
+
const addActionAsker = (readAskedInstance) => {
|
|
818
|
+
const asker = { readAskedInstance };
|
|
819
|
+
askerSet.add(asker);
|
|
820
|
+
return asker;
|
|
821
|
+
};
|
|
822
|
+
const removeActionAsker = (asker) => {
|
|
823
|
+
askerSet.delete(asker);
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
const calledOffWeakSet = new WeakSet();
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Aborts the instance's run unless an asker asks for it, and marks a run
|
|
830
|
+
* called off that way (see isCalledOffForWantOfAskers).
|
|
831
|
+
*/
|
|
832
|
+
const callOffUnlessAsked = (action, reason) => {
|
|
833
|
+
const instance = resolveInstance(action);
|
|
834
|
+
for (const asker of askerSet) {
|
|
835
|
+
if (resolveInstance(asker.readAskedInstance()) === instance) {
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (!instance.abort(reason)) {
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
// A callback that ignores its abort signal completes all the same.
|
|
843
|
+
if (instance.runningStateSignal.peek() !== ABORTED) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
calledOffWeakSet.add(instance);
|
|
847
|
+
// The mark is for this abort only: once the instance moves, an abort that
|
|
848
|
+
// comes later is somebody else's.
|
|
849
|
+
const unsubscribe = instance.runningStateSignal.subscribe((state) => {
|
|
850
|
+
if (state !== ABORTED) {
|
|
851
|
+
calledOffWeakSet.delete(instance);
|
|
852
|
+
unsubscribe();
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
const isCalledOffForWantOfAskers = (action) => {
|
|
858
|
+
return calledOffWeakSet.has(resolveInstance(action));
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
// A binding counts as the instance it targets now. Read as it stands, never
|
|
862
|
+
// through getCurrentAction(), which would flush a debounced binding.
|
|
863
|
+
const resolveInstance = (action) => {
|
|
864
|
+
let instance = action;
|
|
865
|
+
while (instance && instance.isProxy) {
|
|
866
|
+
instance = getActionPrivateProperties(instance).currentAction;
|
|
867
|
+
}
|
|
868
|
+
return instance;
|
|
869
|
+
};
|
|
870
|
+
|
|
786
871
|
const SYMBOL_OBJECT_SIGNAL = Symbol.for("navi_object_signal");
|
|
787
872
|
|
|
788
873
|
/*
|
|
@@ -5055,6 +5140,8 @@ const writeActionState = (write) => {
|
|
|
5055
5140
|
}
|
|
5056
5141
|
};
|
|
5057
5142
|
|
|
5143
|
+
const askerByBindingWeakMap = new WeakMap();
|
|
5144
|
+
|
|
5058
5145
|
/**
|
|
5059
5146
|
* Reactively runs an action whenever the params derived from signals change.
|
|
5060
5147
|
*
|
|
@@ -5151,8 +5238,9 @@ const actionRunEffect = (
|
|
|
5151
5238
|
if (actionTargetPrevious && actionTarget) {
|
|
5152
5239
|
// params changed
|
|
5153
5240
|
if (!asking) {
|
|
5154
|
-
// falsy params, don't run
|
|
5155
|
-
|
|
5241
|
+
// falsy params, don't run; the instance is shared by params, and
|
|
5242
|
+
// something else may still ask for it (see action_askers.js)
|
|
5243
|
+
callOffUnlessAsked(actionTargetPrevious, "abortOnFalsyParams");
|
|
5156
5244
|
return;
|
|
5157
5245
|
}
|
|
5158
5246
|
if (!askingBefore) {
|
|
@@ -5178,6 +5266,11 @@ const actionRunEffect = (
|
|
|
5178
5266
|
},
|
|
5179
5267
|
...options,
|
|
5180
5268
|
});
|
|
5269
|
+
// Held by the binding, as long as it lives: askers are held weakly.
|
|
5270
|
+
askerByBindingWeakMap.set(
|
|
5271
|
+
actionRunnedByThisEffect,
|
|
5272
|
+
addActionAsker(() => (asking ? actionRunnedByThisEffect : null)),
|
|
5273
|
+
);
|
|
5181
5274
|
if (asking) {
|
|
5182
5275
|
runUnwatched(() =>
|
|
5183
5276
|
actionRunnedByThisEffect.run({ reason: "initial truthy params" }),
|
|
@@ -9909,8 +10002,10 @@ const activeRouteReadsSignal = computed(() => {
|
|
|
9909
10002
|
/**
|
|
9910
10003
|
* Binds an action to a route: it runs when the route matches, with the params
|
|
9911
10004
|
* the effect reads off the address, runs again on a `reload()`, and is aborted
|
|
9912
|
-
* when the route is left
|
|
9913
|
-
*
|
|
10005
|
+
* when the route is left, unless something else still asks for the same read
|
|
10006
|
+
* with the same params: another route action, a mounted `useAsyncData` with
|
|
10007
|
+
* `run: true`. Its data, its wait and its failure are read by the page through
|
|
10008
|
+
* `useAsyncData`.
|
|
9914
10009
|
*
|
|
9915
10010
|
* A page's CODE is declared the same way — the import is one more thing the
|
|
9916
10011
|
* address asks for, started with the data rather than after it:
|
|
@@ -31977,6 +32072,11 @@ const useForceRender = () => {
|
|
|
31977
32072
|
* failure a prerun left (a `prerun: true` head start, a press that preran
|
|
31978
32073
|
* it): nothing on screen asked for that run, so this asks again, once.
|
|
31979
32074
|
*
|
|
32075
|
+
* While mounted, it asks for that instance as a route action does: a route
|
|
32076
|
+
* action reading the same thing with the same params does not call the run
|
|
32077
|
+
* off under it when its route is left. A run called off that way before
|
|
32078
|
+
* this component was there to ask is asked again.
|
|
32079
|
+
*
|
|
31980
32080
|
* Prefer a `routeAction` wherever the parameter is one the address holds (a
|
|
31981
32081
|
* path param, a search param bound to a `stateSignal`), popup included: it is
|
|
31982
32082
|
* asked for when the address changes, with everything else that address
|
|
@@ -32126,6 +32226,20 @@ const useActionAsyncData = (action, {
|
|
|
32126
32226
|
}
|
|
32127
32227
|
useOnLoad(action, onLoad);
|
|
32128
32228
|
usePoll(action, poll);
|
|
32229
|
+
const routePage = useContext(RoutePageContext);
|
|
32230
|
+
|
|
32231
|
+
// `run: true` asks for the instance as long as this reader is mounted: a
|
|
32232
|
+
// binding sharing it whose route is left does not call the run off under it
|
|
32233
|
+
// (see action_askers.js). Asked through the binding itself, whose target
|
|
32234
|
+
// moves before this renders again.
|
|
32235
|
+
const askedRef = useRef(null);
|
|
32236
|
+
askedRef.current = run ? action : null;
|
|
32237
|
+
useLayoutEffect(() => {
|
|
32238
|
+
const asker = addActionAsker(() => askedRef.current);
|
|
32239
|
+
return () => {
|
|
32240
|
+
removeActionAsker(asker);
|
|
32241
|
+
};
|
|
32242
|
+
}, []);
|
|
32129
32243
|
|
|
32130
32244
|
// The instance the binding points at, remembered from one render to the
|
|
32131
32245
|
// next: a binding that comes back to params whose instance failed — a search
|
|
@@ -32138,6 +32252,11 @@ const useActionAsyncData = (action, {
|
|
|
32138
32252
|
const instanceRef = useRef(instance);
|
|
32139
32253
|
const backOnFailure = instanceRef.current !== instance && action.runningStateSignal.peek() === FAILED;
|
|
32140
32254
|
instanceRef.current = instance;
|
|
32255
|
+
// A run called off because nothing asked for it any more, before this reader
|
|
32256
|
+
// was there to ask (mounting after, or suspended before its first commit):
|
|
32257
|
+
// nobody else will run it again. Not from a page the address has left, whose
|
|
32258
|
+
// own route action is what called it off.
|
|
32259
|
+
const calledOffBeforeThisReader = isCalledOffForWantOfAskers(action) && !(routePage && !routePage.isShown());
|
|
32141
32260
|
// `run: true` — this component owns the request, so it is what starts it, and
|
|
32142
32261
|
// it starts it from the render rather than from an effect: a component that
|
|
32143
32262
|
// suspends has no effects, so a run written in one would be waiting for
|
|
@@ -32145,7 +32264,7 @@ const useActionAsyncData = (action, {
|
|
|
32145
32264
|
// no-op — which is what makes starting it while rendering sound. From there
|
|
32146
32265
|
// the wait is the ordinary one: suspended into <Loading>, or drawn by the
|
|
32147
32266
|
// component under `loading: true`, exactly as for data someone else ran.
|
|
32148
|
-
if (run && action.paramsSignal.peek() !== undefined && (runIsOwed(action) || backOnFailure)) {
|
|
32267
|
+
if (run && action.paramsSignal.peek() !== undefined && (runIsOwed(action) || backOnFailure || calledOffBeforeThisReader)) {
|
|
32149
32268
|
// Nothing waits on this run: the failure is held by the action, and this
|
|
32150
32269
|
// hook is what reads it back.
|
|
32151
32270
|
runUnwatched(() => action.run({
|
|
@@ -32168,7 +32287,6 @@ const useActionAsyncData = (action, {
|
|
|
32168
32287
|
// has already processed the settlement and the detached DOM is discarded.
|
|
32169
32288
|
const runningState = action.runningStateSignal.peek();
|
|
32170
32289
|
const [, setTick] = useState(0);
|
|
32171
|
-
const routePage = useContext(RoutePageContext);
|
|
32172
32290
|
// What this render read of the action, for the subscriptions below to tell
|
|
32173
32291
|
// a change that came between the render and them from none at all.
|
|
32174
32292
|
const readRef = useRef(null);
|
|
@@ -32273,6 +32391,16 @@ const useActionAsyncData = (action, {
|
|
|
32273
32391
|
if (runningState === COMPLETED) {
|
|
32274
32392
|
return [action.dataSignal.peek(), false, undefined];
|
|
32275
32393
|
}
|
|
32394
|
+
// Called off (abort(), a route left with nothing else asking for it): nothing
|
|
32395
|
+
// is on its way until something runs it again. What it held stays drawn and
|
|
32396
|
+
// no wait is claimed. A delegated wait holding nothing has nothing to draw,
|
|
32397
|
+
// and suspends below until the next run.
|
|
32398
|
+
if (runningState === ABORTED) {
|
|
32399
|
+
const dataHeld = action.dataSignal.peek();
|
|
32400
|
+
if (dataHeld !== undefined || loadingEffect === "use") {
|
|
32401
|
+
return [dataHeld, false, undefined];
|
|
32402
|
+
}
|
|
32403
|
+
}
|
|
32276
32404
|
if (runningState === FAILED) {
|
|
32277
32405
|
if (prerun) {
|
|
32278
32406
|
// Neither shown nor reported from a head start: the failure is the
|
|
@@ -32348,8 +32476,7 @@ const useActionAsyncData = (action, {
|
|
|
32348
32476
|
|
|
32349
32477
|
// `loading: true` says the component draws its own wait, so this hook never
|
|
32350
32478
|
// suspends: running, or holding params nobody has started yet — a debounced
|
|
32351
|
-
// binding that just retargeted,
|
|
32352
|
-
// about to issue. Either way what is on screen is not the current answer,
|
|
32479
|
+
// binding that just retargeted, a run this very render is about to issue. Either way what is on screen is not the current answer,
|
|
32353
32480
|
// which is what `loading` says. Suspending would not draw a spinner: it swaps
|
|
32354
32481
|
// the subtree for the boundary, and everything under it remounts — a dialog
|
|
32355
32482
|
// open inside it is then closed, by a hook the caller told to stay out of it.
|
|
@@ -32357,7 +32484,8 @@ const useActionAsyncData = (action, {
|
|
|
32357
32484
|
return [action.dataSignal.peek(), true, undefined];
|
|
32358
32485
|
}
|
|
32359
32486
|
|
|
32360
|
-
// IDLE or
|
|
32487
|
+
// IDLE, RUNNING, or called off holding nothing, with loadingEffect:
|
|
32488
|
+
// "delegate" — suspend
|
|
32361
32489
|
const reason = runningState === RUNNING ? "loading" : "idle";
|
|
32362
32490
|
loadingRef.current = {
|
|
32363
32491
|
reason,
|
|
@@ -32376,6 +32504,13 @@ const useActionAsyncData = (action, {
|
|
|
32376
32504
|
actionPendingPromiseWeakMap.delete(action);
|
|
32377
32505
|
unsubscribe();
|
|
32378
32506
|
resolve();
|
|
32507
|
+
} else if (reason === "loading" && state === ABORTED) {
|
|
32508
|
+
// The run waited on was called off: drawn again, this reader asks
|
|
32509
|
+
// for it again when nothing else will (calledOffBeforeThisReader),
|
|
32510
|
+
// draws what the instance held, or waits for the next run as idle.
|
|
32511
|
+
actionPendingPromiseWeakMap.delete(action);
|
|
32512
|
+
unsubscribe();
|
|
32513
|
+
resolve();
|
|
32379
32514
|
}
|
|
32380
32515
|
});
|
|
32381
32516
|
});
|
|
@@ -59565,6 +59700,7 @@ const measureDrawing = ({
|
|
|
59565
59700
|
* never seen under a bar, and after every read of the commit that queued them.
|
|
59566
59701
|
*/
|
|
59567
59702
|
|
|
59703
|
+
|
|
59568
59704
|
// Several bars can share an edge — during a page transition the outgoing and
|
|
59569
59705
|
// the incoming one are both mounted. They are all pinned to that same edge, so
|
|
59570
59706
|
// they overlap: the room to give back is the largest of them, not their sum,
|
|
@@ -59582,6 +59718,12 @@ const pendingSizeMapByArea = new Map();
|
|
|
59582
59718
|
let flushMicrotaskQueued = false;
|
|
59583
59719
|
let flushFrame = null;
|
|
59584
59720
|
|
|
59721
|
+
// The safe area moves with these writes and no resize comes with them: what is
|
|
59722
|
+
// placed in it (a Popover with container="safe-area") is told here.
|
|
59723
|
+
const [publishFixedBarSpaceChange, subscribeFixedBarSpaceChange] =
|
|
59724
|
+
createPubSub();
|
|
59725
|
+
const onFixedBarSpaceChange = subscribeFixedBarSpaceChange;
|
|
59726
|
+
|
|
59585
59727
|
// A size a render produced: a prop, the children or a theme variable changed
|
|
59586
59728
|
// the bar, and the room it takes is given back before that commit is painted.
|
|
59587
59729
|
/**
|
|
@@ -59632,13 +59774,19 @@ const queueSize = (area, barElement, size) => {
|
|
|
59632
59774
|
};
|
|
59633
59775
|
|
|
59634
59776
|
const flushPendingSizes = () => {
|
|
59777
|
+
let written = false;
|
|
59635
59778
|
for (const [area, pendingSizeMap] of pendingSizeMapByArea) {
|
|
59636
59779
|
for (const [barElement, size] of pendingSizeMap) {
|
|
59637
59780
|
storeSize(area, barElement, size);
|
|
59638
59781
|
}
|
|
59639
|
-
writeSpace(area)
|
|
59782
|
+
if (writeSpace(area)) {
|
|
59783
|
+
written = true;
|
|
59784
|
+
}
|
|
59640
59785
|
}
|
|
59641
59786
|
pendingSizeMapByArea.clear();
|
|
59787
|
+
if (written) {
|
|
59788
|
+
publishFixedBarSpaceChange();
|
|
59789
|
+
}
|
|
59642
59790
|
};
|
|
59643
59791
|
|
|
59644
59792
|
const storeSize = (area, barElement, size) => {
|
|
@@ -59665,18 +59813,20 @@ const writeSpace = (area) => {
|
|
|
59665
59813
|
const property = `--navi-fixed-bar-space-${area}`;
|
|
59666
59814
|
const { style } = document.documentElement;
|
|
59667
59815
|
if (sizeMap.size === 0) {
|
|
59668
|
-
if (writtenValueByArea.has(area)) {
|
|
59669
|
-
|
|
59670
|
-
style.removeProperty(property);
|
|
59816
|
+
if (!writtenValueByArea.has(area)) {
|
|
59817
|
+
return false;
|
|
59671
59818
|
}
|
|
59672
|
-
|
|
59819
|
+
writtenValueByArea.delete(area);
|
|
59820
|
+
style.removeProperty(property);
|
|
59821
|
+
return true;
|
|
59673
59822
|
}
|
|
59674
59823
|
const value = `${largestSize}px`;
|
|
59675
59824
|
if (writtenValueByArea.get(area) === value) {
|
|
59676
|
-
return;
|
|
59825
|
+
return false;
|
|
59677
59826
|
}
|
|
59678
59827
|
writtenValueByArea.set(area, value);
|
|
59679
59828
|
style.setProperty(property, value);
|
|
59829
|
+
return true;
|
|
59680
59830
|
};
|
|
59681
59831
|
|
|
59682
59832
|
installImportMetaCssBuild(import.meta);
|
|
@@ -74467,8 +74617,9 @@ const needsPopupFromMount = (props) => {
|
|
|
74467
74617
|
* (measured by wematch, 2026-09-06: no panel at all for 120ms, then a panel
|
|
74468
74618
|
* with its head missing, then the head arriving from below). So the cut is
|
|
74469
74619
|
* taken only where it hides something the screen would otherwise show — the
|
|
74470
|
-
* band of glass beside an app narrowed with --navi-app-max-width
|
|
74471
|
-
*
|
|
74620
|
+
* band of glass beside an app narrowed with --navi-app-max-width, the fixed
|
|
74621
|
+
* bars beside a popover placed in the safe area — and is a constant
|
|
74622
|
+
* everywhere else: at layer="top" the viewport's own edge cuts the
|
|
74472
74623
|
* travel on the compositor for free, and at layer="local" the clip wrapper
|
|
74473
74624
|
* (a static ancestor, overflow: hidden) does. Where a band exists the cut
|
|
74474
74625
|
* still animates and still can drift under load; the full answer there is a
|
|
@@ -74600,22 +74751,34 @@ const popupCss = /* css */ `
|
|
|
74600
74751
|
0 for a 0 band and 100vmax for any other, a step written without a
|
|
74601
74752
|
branch. See this file's top comment for why an animated cut has to be
|
|
74602
74753
|
that rare. */
|
|
74754
|
+
--x-popup-area-inset-top: var(--navi-app-inset-top, 0px);
|
|
74755
|
+
--x-popup-area-inset-right: var(--navi-app-inset-right, 0px);
|
|
74756
|
+
--x-popup-area-inset-bottom: var(--navi-app-inset-bottom, 0px);
|
|
74757
|
+
--x-popup-area-inset-left: var(--navi-app-inset-left, 0px);
|
|
74603
74758
|
--x-popup-cut-top-at-area: calc(
|
|
74604
74759
|
-1 * var(--container-position-room-top, 100vmax) - 100vmax +
|
|
74605
|
-
min(100vmax, var(--
|
|
74760
|
+
min(100vmax, var(--x-popup-area-inset-top) * 1000000)
|
|
74606
74761
|
);
|
|
74607
74762
|
--x-popup-cut-right-at-area: calc(
|
|
74608
74763
|
-1 * var(--container-position-room-right, 100vmax) - 100vmax +
|
|
74609
|
-
min(100vmax, var(--
|
|
74764
|
+
min(100vmax, var(--x-popup-area-inset-right) * 1000000)
|
|
74610
74765
|
);
|
|
74611
74766
|
--x-popup-cut-bottom-at-area: calc(
|
|
74612
74767
|
-1 * var(--container-position-room-bottom, 100vmax) - 100vmax +
|
|
74613
|
-
min(100vmax, var(--
|
|
74768
|
+
min(100vmax, var(--x-popup-area-inset-bottom) * 1000000)
|
|
74614
74769
|
);
|
|
74615
74770
|
--x-popup-cut-left-at-area: calc(
|
|
74616
74771
|
-1 * var(--container-position-room-left, 100vmax) - 100vmax +
|
|
74617
|
-
min(100vmax, var(--
|
|
74772
|
+
min(100vmax, var(--x-popup-area-inset-left) * 1000000)
|
|
74618
74773
|
);
|
|
74774
|
+
/* Placed in the safe area (Popover's container="safe-area"): past its edge
|
|
74775
|
+
lie the fixed bars, and a popup sliding in comes out from behind them. */
|
|
74776
|
+
&[data-container="safe-area"] {
|
|
74777
|
+
--x-popup-area-inset-top: var(--navi-safe-area-inset-top);
|
|
74778
|
+
--x-popup-area-inset-right: var(--navi-safe-area-inset-right);
|
|
74779
|
+
--x-popup-area-inset-bottom: var(--navi-safe-area-inset-bottom);
|
|
74780
|
+
--x-popup-area-inset-left: var(--navi-safe-area-inset-left);
|
|
74781
|
+
}
|
|
74619
74782
|
/* The translate property is deliberately absent from this list — it
|
|
74620
74783
|
carries where the popup stands, and applyNewPosition (visible_rect.js)
|
|
74621
74784
|
owns it and drives its own transition through the Web Animations API
|
|
@@ -78412,6 +78575,14 @@ const css$C = /* css */`
|
|
|
78412
78575
|
* (`"3appw"`) or a container length (`"3cqw"`).
|
|
78413
78576
|
* @param {string|number} [props.marginWithAnchor=0] - Extra spacing kept
|
|
78414
78577
|
* between the popover and the edges of its anchor, written the same way.
|
|
78578
|
+
* @param {"app"|"safe-area"} [props.container="app"] - The rectangle a
|
|
78579
|
+
* `layer="top"` popover is placed in: docked against with no anchor,
|
|
78580
|
+
* flipped and clamped within with one. `"app"`: the app's own screen, its
|
|
78581
|
+
* fixed bars included — the popover covers them. `"safe-area"`: that screen
|
|
78582
|
+
* less the bars and the device's own bands (`--navi-safe-area-inset-*`), for
|
|
78583
|
+
* something docked over the page that must leave the bars uncovered, a
|
|
78584
|
+
* toast. Followed live as a bar comes or goes. Still in the top layer, so
|
|
78585
|
+
* above what the page holds. See docs/safe_area.md.
|
|
78415
78586
|
* @param {boolean} [props.backdrop=true] - Whether anything is laid between
|
|
78416
78587
|
* the popover and the page at all — asked before what an outside press does
|
|
78417
78588
|
* and before how the backdrop is painted. `false` leaves the page
|
|
@@ -78841,6 +79012,7 @@ const usePopoverProps = props => {
|
|
|
78841
79012
|
// "center" default.
|
|
78842
79013
|
positionAreaWhenAnchorIsInvalid,
|
|
78843
79014
|
marginWithContainer = 0,
|
|
79015
|
+
container = "app",
|
|
78844
79016
|
// "close" → pointer press outside closes (a plain close = commit)
|
|
78845
79017
|
// "cancel" → pointer press outside closes AS A CANCEL (revert)
|
|
78846
79018
|
// "capture"→ absorb the press, stay open
|
|
@@ -78932,6 +79104,7 @@ const usePopoverProps = props => {
|
|
|
78932
79104
|
})
|
|
78933
79105
|
}) : null;
|
|
78934
79106
|
const isTopLayer = layer === "top";
|
|
79107
|
+
const placesInSafeArea = isTopLayer && container === "safe-area";
|
|
78935
79108
|
const ref = props.ref;
|
|
78936
79109
|
const backdropRef = useRef();
|
|
78937
79110
|
// Disarms a still-pending backdrop hide from a previous close (see
|
|
@@ -78980,14 +79153,15 @@ const usePopoverProps = props => {
|
|
|
78980
79153
|
positionAreaFixed,
|
|
78981
79154
|
positionAreaWhenAnchorIsInvalid,
|
|
78982
79155
|
marginWithAnchor,
|
|
78983
|
-
marginWithContainer
|
|
79156
|
+
marginWithContainer,
|
|
79157
|
+
placesInSafeArea
|
|
78984
79158
|
};
|
|
78985
79159
|
const repositionRef = useRef(null);
|
|
78986
79160
|
useEffect(() => {
|
|
78987
79161
|
repositionRef.current?.(new CustomEvent("position_props_change", {
|
|
78988
79162
|
detail: {}
|
|
78989
79163
|
}));
|
|
78990
|
-
}, [positionArea, positionAreaFixed, positionAreaWhenAnchorIsInvalid, marginWithAnchor, marginWithContainer]);
|
|
79164
|
+
}, [positionArea, positionAreaFixed, positionAreaWhenAnchorIsInvalid, marginWithAnchor, marginWithContainer, placesInSafeArea]);
|
|
78991
79165
|
// The freeze is taken where the value changes, not only at open time: a
|
|
78992
79166
|
// popover showing skeletons first says sizing="auto" until its content is
|
|
78993
79167
|
// there, and would otherwise be held at the size of the waiting state.
|
|
@@ -79237,8 +79411,11 @@ const usePopoverProps = props => {
|
|
|
79237
79411
|
positionAreaFixed,
|
|
79238
79412
|
positionAreaWhenAnchorIsInvalid,
|
|
79239
79413
|
marginWithAnchor,
|
|
79240
|
-
marginWithContainer
|
|
79414
|
+
marginWithContainer,
|
|
79415
|
+
placesInSafeArea
|
|
79241
79416
|
} = positionPropsRef.current;
|
|
79417
|
+
// undefined keeps the app's own screen (setPlacementViewportInsets).
|
|
79418
|
+
const viewportInsets = placesInSafeArea ? getSafeAreaInsets() : undefined;
|
|
79242
79419
|
let position;
|
|
79243
79420
|
if (hasAnchorElement) {
|
|
79244
79421
|
const {
|
|
@@ -79277,6 +79454,7 @@ const usePopoverProps = props => {
|
|
|
79277
79454
|
// renderer, same as omitting `container` entirely would resolve
|
|
79278
79455
|
// to on its own.
|
|
79279
79456
|
container: positionedAncestor,
|
|
79457
|
+
viewportInsets,
|
|
79280
79458
|
minLeft,
|
|
79281
79459
|
event: positionEvent
|
|
79282
79460
|
};
|
|
@@ -79323,6 +79501,7 @@ const usePopoverProps = props => {
|
|
|
79323
79501
|
const pickOptions = {
|
|
79324
79502
|
positionArea,
|
|
79325
79503
|
container: positionedAncestor,
|
|
79504
|
+
viewportInsets,
|
|
79326
79505
|
marginWithContainer: resolveMarginInPixels(marginWithContainer, "marginWithContainer"),
|
|
79327
79506
|
event: positionEvent
|
|
79328
79507
|
};
|
|
@@ -79444,6 +79623,20 @@ const usePopoverProps = props => {
|
|
|
79444
79623
|
// while open (e.g. an expand/collapse toggle inside it) — not just when
|
|
79445
79624
|
// the anchor itself moves/resizes/re-anchors.
|
|
79446
79625
|
rectEffect.observeSize(popoverEl);
|
|
79626
|
+
// A bar mounting or leaving moves the safe area and leaves the viewport as
|
|
79627
|
+
// it was, so rectEffect.check() would find nothing changed: placed
|
|
79628
|
+
// directly. "resize", so the popover glides to its new place.
|
|
79629
|
+
if (isTopLayer) {
|
|
79630
|
+
addCleanup(onFixedBarSpaceChange(() => {
|
|
79631
|
+
if (!positionPropsRef.current.placesInSafeArea) {
|
|
79632
|
+
return;
|
|
79633
|
+
}
|
|
79634
|
+
if (deferPlacementWhileCovered(popoverEl)) {
|
|
79635
|
+
return;
|
|
79636
|
+
}
|
|
79637
|
+
placeUnlessAnchorOutOfView(new CustomEvent("resize"));
|
|
79638
|
+
}));
|
|
79639
|
+
}
|
|
79447
79640
|
// Whatever its own backdrop: a modal dialog above leaves all of the page
|
|
79448
79641
|
// inert, a popover without a wall included. layer="local" is left out, its
|
|
79449
79642
|
// ceiling answers to its container.
|
|
@@ -79784,6 +79977,7 @@ const usePopoverProps = props => {
|
|
|
79784
79977
|
id,
|
|
79785
79978
|
tabIndex,
|
|
79786
79979
|
"data-layer": layer,
|
|
79980
|
+
"data-container": placesInSafeArea ? "safe-area" : undefined,
|
|
79787
79981
|
"data-expand-x": expand || expandX ? "" : undefined,
|
|
79788
79982
|
"data-expand-y": expand || expandY ? "" : undefined,
|
|
79789
79983
|
"navi-animation": isAutoAnimation ? undefined : animation,
|