@jsenv/navi 0.30.35 → 0.30.37
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 +231 -69
- package/dist/dev/jsenv_navi.js.map +8 -8
- package/dist/jsenv_navi.js +206 -69
- package/dist/jsenv_navi.js.map +8 -8
- package/docs/AI_INSTRUCTIONS.md +3 -2
- package/docs/drag_to_travel.md +5 -3
- package/docs/interactions.md +19 -0
- package/docs/popup_open.md +7 -6
- package/docs/resource.md +4 -0
- package/docs/safe_area.md +27 -0
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -29381,8 +29381,9 @@ const debug$2 = (...args) => {
|
|
|
29381
29381
|
* policy as that verb, and is a write of its own for a dependent:
|
|
29382
29382
|
* `{ resource: ORDER, on: ["cancel"] }`. Its `rerunOn` says which reads of
|
|
29383
29383
|
* this resource it reruns (`{ GET_RANGE: true }`); a read it leaves out
|
|
29384
|
-
* follows `rerunOn` for its verb. A
|
|
29385
|
-
* already exposes (`withParams`, `store`…),
|
|
29384
|
+
* follows `rerunOn` for its verb. A verb's name in any case (`post`,
|
|
29385
|
+
* `get_many`…), or one the resource already exposes (`withParams`, `store`…),
|
|
29386
|
+
* throws. A `.scopedOne()` or
|
|
29386
29387
|
* `.scopedMany()` takes `writes` too, `.scopedMany()` with the plural verbs.
|
|
29387
29388
|
* @param {{ GET?: { ms?: number, max?: number }, GET_MANY?: { ms?: number, max?: number } }} [restCallbacks.keep] -
|
|
29388
29389
|
* how long an answer of a read is trusted, coming back to the same params
|
|
@@ -30938,12 +30939,15 @@ ${name}.many("${propertyName}") source location: ${declarationSite}`,
|
|
|
30938
30939
|
|
|
30939
30940
|
// expose one action per write named in `writes`
|
|
30940
30941
|
if (writes) {
|
|
30942
|
+
const membersExposed = Object.keys(stateFacade).filter(
|
|
30943
|
+
(key) => !VERB_KEYS.includes(key),
|
|
30944
|
+
);
|
|
30941
30945
|
for (const [writeName, write] of Object.entries(writes)) {
|
|
30942
30946
|
assertWriteCanBeNamed(
|
|
30943
30947
|
name,
|
|
30944
30948
|
writeName,
|
|
30945
30949
|
write,
|
|
30946
|
-
|
|
30950
|
+
membersExposed,
|
|
30947
30951
|
namedWriteVerbs,
|
|
30948
30952
|
);
|
|
30949
30953
|
const { verb: writeVerb, callback, rerunOn: writeRerunOn } = write;
|
|
@@ -31256,7 +31260,7 @@ const assertWriteCanBeNamed = (
|
|
|
31256
31260
|
name,
|
|
31257
31261
|
writeName,
|
|
31258
31262
|
write,
|
|
31259
|
-
|
|
31263
|
+
membersExposed,
|
|
31260
31264
|
namedWriteVerbs,
|
|
31261
31265
|
) => {
|
|
31262
31266
|
const where = `resource(${name}) writes.${writeName}`;
|
|
@@ -31265,9 +31269,9 @@ const assertWriteCanBeNamed = (
|
|
|
31265
31269
|
`${where}: "${writeName}" is a verb's name — a write named in "writes" takes a name of its own (cancel, archive…)`,
|
|
31266
31270
|
);
|
|
31267
31271
|
}
|
|
31268
|
-
if (
|
|
31272
|
+
if (membersExposed.includes(writeName)) {
|
|
31269
31273
|
throw new Error(
|
|
31270
|
-
`${where}: the resource already exposes "${writeName}" —
|
|
31274
|
+
`${where}: the resource already exposes "${writeName}" — a write takes a name none of its members has: ${membersExposed.join(", ")}`,
|
|
31271
31275
|
);
|
|
31272
31276
|
}
|
|
31273
31277
|
if (!write || typeof write !== "object") {
|
|
@@ -75472,18 +75476,19 @@ const readRoom = () =>
|
|
|
75472
75476
|
visualViewportHeightSignal.peek() - getVirtualKeyboardOverlayHeight();
|
|
75473
75477
|
|
|
75474
75478
|
/**
|
|
75475
|
-
* Pushing a popup docked to an edge (a side panel, a bottom sheet
|
|
75476
|
-
* it came in closes it: it follows the finger while it travels,
|
|
75477
|
-
* either finishes the departure or brings it back to rest —
|
|
75478
|
-
* travel was already heading to.
|
|
75479
|
+
* Pushing a popup docked to an edge (a side panel, a bottom sheet, a toast)
|
|
75480
|
+
* back the way it came in closes it: it follows the finger while it travels,
|
|
75481
|
+
* and letting go either finishes the departure or brings it back to rest —
|
|
75482
|
+
* whichever the travel was already heading to. A popup may be given other ways
|
|
75483
|
+
* out than that one: a toast is also thrown sideways.
|
|
75479
75484
|
*
|
|
75480
75485
|
* Reading the pointer is not done here: when a press becomes a gesture, which
|
|
75481
75486
|
* axis it leans on, how fast it was going when it was let go, who else has a
|
|
75482
75487
|
* claim on it (a field, a scroller with room left, a box inside the panel that
|
|
75483
75488
|
* travels the same way) is one gesture system for the whole codebase
|
|
75484
75489
|
* (@jsenv/dom's startDragToTravel). A panel closing is a travel of exactly one
|
|
75485
|
-
* box,
|
|
75486
|
-
* file has to say.
|
|
75490
|
+
* box, out by one of the edges it may leave by and nowhere else — which is all
|
|
75491
|
+
* this file has to say.
|
|
75487
75492
|
*
|
|
75488
75493
|
* The release travel is driven here (Web Animations) rather than handed to the
|
|
75489
75494
|
* exit transition the `animation` prop may install: its pace is what is left to
|
|
@@ -75498,7 +75503,7 @@ const readRoom = () =>
|
|
|
75498
75503
|
// less, so the panel keeps the same speed whenever the finger let go.
|
|
75499
75504
|
const TRAVEL_DURATION = 220;
|
|
75500
75505
|
|
|
75501
|
-
// Which way a popup
|
|
75506
|
+
// Which way a popup leaving by each side travels — read by the panel itself to
|
|
75502
75507
|
// say so in the DOM, which is how a box travelling INSIDE it (a row of slides,
|
|
75503
75508
|
// a route travel) knows that axis is already walked.
|
|
75504
75509
|
const SWIPE_AXIS_BY_SIDE = {
|
|
@@ -75507,12 +75512,27 @@ const SWIPE_AXIS_BY_SIDE = {
|
|
|
75507
75512
|
top: "y",
|
|
75508
75513
|
bottom: "y",
|
|
75509
75514
|
};
|
|
75510
|
-
// Which way the finger pushes to send the panel
|
|
75511
|
-
//
|
|
75515
|
+
// Which way the finger pushes to send the panel out by each side, in screen
|
|
75516
|
+
// coordinates: positive is right/down, the same sign the gesture reports.
|
|
75512
75517
|
const CLOSE_DIRECTION_BY_SIDE = { left: -1, right: 1, top: -1, bottom: 1 };
|
|
75513
75518
|
|
|
75514
75519
|
/**
|
|
75515
|
-
*
|
|
75520
|
+
* The axes a popup leaving by `sides` travels on, as the gesture and the DOM
|
|
75521
|
+
* say them: "x", "y" or "xy".
|
|
75522
|
+
*/
|
|
75523
|
+
const swipeAxesOf = (sides) => {
|
|
75524
|
+
const hasX = sides.some((side) => SWIPE_AXIS_BY_SIDE[side] === "x");
|
|
75525
|
+
const hasY = sides.some((side) => SWIPE_AXIS_BY_SIDE[side] === "y");
|
|
75526
|
+
if (hasX && hasY) {
|
|
75527
|
+
return "xy";
|
|
75528
|
+
}
|
|
75529
|
+
return hasX ? "x" : "y";
|
|
75530
|
+
};
|
|
75531
|
+
|
|
75532
|
+
/**
|
|
75533
|
+
* Builds the `pointerdown` handler of a popup that leaves by any of `sides`:
|
|
75534
|
+
* usually the one edge it is docked to, the way it came in; a toast is also
|
|
75535
|
+
* thrown sideways, and leaves the way it was thrown.
|
|
75516
75536
|
*
|
|
75517
75537
|
* `grip` says where the gesture may start, as a selector the pressed element is
|
|
75518
75538
|
* matched against with `closest`. A popup that names one is held THERE and
|
|
@@ -75520,11 +75540,17 @@ const CLOSE_DIRECTION_BY_SIDE = { left: -1, right: 1, top: -1, bottom: 1 };
|
|
|
75520
75540
|
* and a press on it is that content's — including gestures of its own, which
|
|
75521
75541
|
* this file has no way of knowing about. A popup that names no grip is pushed
|
|
75522
75542
|
* from its whole surface, which only suits one made of nothing else (a side
|
|
75523
|
-
* panel showing a page).
|
|
75524
|
-
*/
|
|
75525
|
-
const createSwipeToClose = (
|
|
75526
|
-
const
|
|
75527
|
-
|
|
75543
|
+
* panel showing a page, a toast).
|
|
75544
|
+
*/
|
|
75545
|
+
const createSwipeToClose = (sides, { grip } = {}) => {
|
|
75546
|
+
const axes = swipeAxesOf(sides);
|
|
75547
|
+
// The side a push along `axis` towards `sign` leaves by, if it leaves at all.
|
|
75548
|
+
const sideTowards = (axis, sign) =>
|
|
75549
|
+
sides.find(
|
|
75550
|
+
(side) =>
|
|
75551
|
+
SWIPE_AXIS_BY_SIDE[side] === axis &&
|
|
75552
|
+
CLOSE_DIRECTION_BY_SIDE[side] === sign,
|
|
75553
|
+
);
|
|
75528
75554
|
|
|
75529
75555
|
return (pointerDownEvent) => {
|
|
75530
75556
|
const panelEl = pointerDownEvent.currentTarget;
|
|
@@ -75541,6 +75567,11 @@ const createSwipeToClose = (side, { grip } = {}) => {
|
|
|
75541
75567
|
}
|
|
75542
75568
|
}
|
|
75543
75569
|
|
|
75570
|
+
// Decided when the finger picks its axis (onStart), once for the gesture.
|
|
75571
|
+
let axis;
|
|
75572
|
+
// How far the panel travels to be out, for each way it can go on that axis.
|
|
75573
|
+
const distanceOutBySign = new Map();
|
|
75574
|
+
|
|
75544
75575
|
// How far the panel has been pulled, as the style showing it — written on
|
|
75545
75576
|
// the panel while the finger drives it, handed to the release travel as
|
|
75546
75577
|
// its two keyframes. The gesture reports a distance in screen coordinates,
|
|
@@ -75550,7 +75581,7 @@ const createSwipeToClose = (side, { grip } = {}) => {
|
|
|
75550
75581
|
// composes under the placement instead of replacing it.
|
|
75551
75582
|
const styleAt = (distance) => ({
|
|
75552
75583
|
transform: translateOf(axis, distance),
|
|
75553
|
-
clipPath: clipOf(
|
|
75584
|
+
clipPath: clipOf(sides, axis, distance),
|
|
75554
75585
|
});
|
|
75555
75586
|
const paint = (distance) => {
|
|
75556
75587
|
Object.assign(panelEl.style, styleAt(distance));
|
|
@@ -75562,21 +75593,22 @@ const createSwipeToClose = (side, { grip } = {}) => {
|
|
|
75562
75593
|
};
|
|
75563
75594
|
// The resting value is written first and the animation only covers the way
|
|
75564
75595
|
// to it: both end on the same number, so nothing is seen changing when the
|
|
75565
|
-
// animation hands the panel back to its own style.
|
|
75566
|
-
|
|
75596
|
+
// animation hands the panel back to its own style. `span` is the full travel
|
|
75597
|
+
// on that side, which the duration is a fraction of.
|
|
75598
|
+
const travelTo = (from, to, span, onArrival) => {
|
|
75567
75599
|
const covered = from > to ? from - to : to - from;
|
|
75568
75600
|
paint(to);
|
|
75569
75601
|
const animation = panelEl.animate([styleAt(from), styleAt(to)], {
|
|
75570
|
-
duration: (covered /
|
|
75602
|
+
duration: (covered / span) * TRAVEL_DURATION,
|
|
75571
75603
|
easing: "ease-out",
|
|
75572
75604
|
});
|
|
75573
75605
|
animation.finished.then(onArrival, () => {});
|
|
75574
75606
|
};
|
|
75575
|
-
const close = (event) => {
|
|
75607
|
+
const close = (event, out, span) => {
|
|
75576
75608
|
triggerNaviCommand(panelEl, "--navi-close", event);
|
|
75577
75609
|
if (panelEl.getAttribute("aria-expanded") === "true") {
|
|
75578
75610
|
// Refused: the panel is staying, so it goes back where it was.
|
|
75579
|
-
travelTo(
|
|
75611
|
+
travelTo(out, 0, span, restore);
|
|
75580
75612
|
return;
|
|
75581
75613
|
}
|
|
75582
75614
|
restore();
|
|
@@ -75584,79 +75616,94 @@ const createSwipeToClose = (side, { grip } = {}) => {
|
|
|
75584
75616
|
|
|
75585
75617
|
startDragToTravel(pointerDownEvent, {
|
|
75586
75618
|
element: panelEl,
|
|
75587
|
-
axes
|
|
75588
|
-
onStart: ({ sign, target }) => {
|
|
75589
|
-
|
|
75619
|
+
axes,
|
|
75620
|
+
onStart: ({ axis: gestureAxis, sign, target }) => {
|
|
75621
|
+
const side = sideTowards(gestureAxis, sign);
|
|
75622
|
+
if (!side) {
|
|
75590
75623
|
// Pushing the panel further in is not a travel: there is nowhere that
|
|
75591
75624
|
// way, and reading it as one would make the panel lean at every
|
|
75592
75625
|
// gesture that starts by going the wrong way.
|
|
75593
75626
|
return false;
|
|
75594
75627
|
}
|
|
75595
|
-
const size =
|
|
75628
|
+
const size = distanceOut(panelEl, side);
|
|
75596
75629
|
if (!size) {
|
|
75597
75630
|
return false;
|
|
75598
75631
|
}
|
|
75599
75632
|
// Something else with a better claim on the gesture: a scroller between
|
|
75600
75633
|
// the finger and the panel, with room left that way.
|
|
75601
|
-
if (scrollRoomTowards(target, panelEl,
|
|
75634
|
+
if (scrollRoomTowards(target, panelEl, gestureAxis, sign)) {
|
|
75602
75635
|
return false;
|
|
75603
75636
|
}
|
|
75637
|
+
axis = gestureAxis;
|
|
75638
|
+
distanceOutBySign.set(sign, size);
|
|
75639
|
+
const otherSide = sideTowards(gestureAxis, -sign);
|
|
75640
|
+
if (otherSide) {
|
|
75641
|
+
distanceOutBySign.set(-sign, distanceOut(panelEl, otherSide));
|
|
75642
|
+
}
|
|
75604
75643
|
panelEl.style.transitionProperty = "none";
|
|
75605
|
-
//
|
|
75606
|
-
// leans against a wall and comes back.
|
|
75644
|
+
// Only the ways out lead anywhere: pulling towards a side the panel
|
|
75645
|
+
// does not leave by leans against a wall and comes back.
|
|
75607
75646
|
return {
|
|
75608
75647
|
size,
|
|
75609
|
-
travelBack:
|
|
75610
|
-
travelOn:
|
|
75648
|
+
travelBack: Boolean(sideTowards(gestureAxis, 1)),
|
|
75649
|
+
travelOn: Boolean(sideTowards(gestureAxis, -1)),
|
|
75611
75650
|
};
|
|
75612
75651
|
},
|
|
75613
75652
|
onPull: ({ pulled }) => {
|
|
75614
75653
|
paint(pulled);
|
|
75615
75654
|
},
|
|
75616
|
-
onEnd: ({ pulled, size, travels, event }) => {
|
|
75655
|
+
onEnd: ({ pulled, size, sign, travels, event }) => {
|
|
75617
75656
|
if (travels) {
|
|
75618
|
-
|
|
75657
|
+
const span = distanceOutBySign.get(sign);
|
|
75658
|
+
travelTo(pulled, span * sign, span, () =>
|
|
75659
|
+
close(event, span * sign, span),
|
|
75660
|
+
);
|
|
75619
75661
|
return;
|
|
75620
75662
|
}
|
|
75621
|
-
travelTo(pulled, 0, restore);
|
|
75663
|
+
travelTo(pulled, 0, size, restore);
|
|
75622
75664
|
},
|
|
75623
75665
|
});
|
|
75624
75666
|
};
|
|
75625
75667
|
};
|
|
75626
75668
|
|
|
75627
|
-
|
|
75669
|
+
// What takes the popup out of sight: its own size, plus the gap a margin keeps
|
|
75670
|
+
// between it and the edge it is pushed through — the room applyNewPosition
|
|
75671
|
+
// publishes on that side. Travelling its size alone, a popup kept off its edge
|
|
75672
|
+
// (a toast 12px above a bar) would stop with that much of itself still showing.
|
|
75673
|
+
const distanceOut = (element, side) => {
|
|
75628
75674
|
const rect = element.getBoundingClientRect();
|
|
75629
|
-
|
|
75675
|
+
const size = SWIPE_AXIS_BY_SIDE[side] === "x" ? rect.width : rect.height;
|
|
75676
|
+
const room = parseFloat(
|
|
75677
|
+
element.style.getPropertyValue(`--container-position-room-${side}`),
|
|
75678
|
+
);
|
|
75679
|
+
return Number.isNaN(room) ? size : size + room;
|
|
75630
75680
|
};
|
|
75631
75681
|
const translateOf = (axis, distance) =>
|
|
75632
75682
|
axis === "x"
|
|
75633
75683
|
? `translate(${distance}px, 0px)`
|
|
75634
75684
|
: `translate(0px, ${distance}px)`;
|
|
75635
75685
|
// The same cut the entry/exit animation makes (popup_css.js), at a distance the
|
|
75636
|
-
// finger decides instead of a transition:
|
|
75637
|
-
//
|
|
75638
|
-
// edge where a band of glass lies past it, 100vmax out where the
|
|
75639
|
-
// edge does the cutting),
|
|
75640
|
-
//
|
|
75641
|
-
// the box — the pull never takes the popup
|
|
75642
|
-
// only shave what it legitimately paints
|
|
75643
|
-
// policy from popup_css.js rather than the
|
|
75644
|
-
// (a Web Animation on transform AND clip-path,
|
|
75645
|
-
// cut the entrance would not.
|
|
75686
|
+
// finger decides instead of a transition: each edge the popup may leave by on
|
|
75687
|
+
// the axis it travels, as popup_css.js placed it (--x-popup-cut-*-at-area — on
|
|
75688
|
+
// the area's edge where a band of glass lies past it, 100vmax out where the
|
|
75689
|
+
// window's own edge does the cutting), moved by what has been pulled towards
|
|
75690
|
+
// it, so the cut holds that line while the box travels under it, whichever way.
|
|
75691
|
+
// Every other side is left far outside the box — the pull never takes the popup
|
|
75692
|
+
// past them, and cutting there would only shave what it legitimately paints
|
|
75693
|
+
// outside its own box. Reading the policy from popup_css.js rather than the
|
|
75694
|
+
// rooms keeps the release travel (a Web Animation on transform AND clip-path,
|
|
75695
|
+
// two clocks) from animating a cut the entrance would not.
|
|
75646
75696
|
const UNCUT = "-100vmax";
|
|
75647
|
-
const
|
|
75648
|
-
|
|
75649
|
-
const
|
|
75650
|
-
|
|
75651
|
-
|
|
75652
|
-
|
|
75653
|
-
|
|
75654
|
-
return `
|
|
75655
|
-
}
|
|
75656
|
-
|
|
75657
|
-
return `inset(${UNCUT} ${UNCUT} ${cut} ${UNCUT})`;
|
|
75658
|
-
}
|
|
75659
|
-
return `inset(${UNCUT} ${UNCUT} ${UNCUT} ${cut})`;
|
|
75697
|
+
const INSET_ORDER = ["top", "right", "bottom", "left"];
|
|
75698
|
+
const clipOf = (sides, axis, distance) => {
|
|
75699
|
+
const insets = INSET_ORDER.map((side) => {
|
|
75700
|
+
if (!sides.includes(side) || SWIPE_AXIS_BY_SIDE[side] !== axis) {
|
|
75701
|
+
return UNCUT;
|
|
75702
|
+
}
|
|
75703
|
+
const towards = distance * CLOSE_DIRECTION_BY_SIDE[side];
|
|
75704
|
+
return `calc(var(--x-popup-cut-${side}-at-area, ${UNCUT}) + ${towards}px)`;
|
|
75705
|
+
});
|
|
75706
|
+
return `inset(${insets.join(" ")})`;
|
|
75660
75707
|
};
|
|
75661
75708
|
|
|
75662
75709
|
/**
|
|
@@ -77438,7 +77485,7 @@ const useDialogProps = props => {
|
|
|
77438
77485
|
// actually resting on an edge: anywhere else the gesture would send the
|
|
77439
77486
|
// dialog somewhere it never came from.
|
|
77440
77487
|
const swipeToCloseSide = isDocked && (parsedPositionArea.y === "top" || parsedPositionArea.y === "bottom") ? parsedPositionArea.y : null;
|
|
77441
|
-
const onSwipePointerDown = swipeToCloseSide ? createSwipeToClose(swipeToCloseSide, {
|
|
77488
|
+
const onSwipePointerDown = swipeToCloseSide ? createSwipeToClose([swipeToCloseSide], {
|
|
77442
77489
|
grip: DOCKED_SWIPE_GRIP
|
|
77443
77490
|
}) : null;
|
|
77444
77491
|
// A corner sitting exactly on the container's own corner must not be
|
|
@@ -78531,6 +78578,16 @@ const css$C = /* css */`
|
|
|
78531
78578
|
to filter). An entrance animation moves the popover through scale and
|
|
78532
78579
|
transform instead, which compose under it: see popup_css.js. */
|
|
78533
78580
|
|
|
78581
|
+
/* What a touch may do on a popover pushed sideways to close it (see
|
|
78582
|
+
swipe_to_close.js): the push takes the sideways axis, the page keeps the
|
|
78583
|
+
vertical scroll. A push up or down travels the way the page scrolls and
|
|
78584
|
+
cannot give that axis away: the browser keeps it, and the gesture is
|
|
78585
|
+
settled by keepTouchRefusable. */
|
|
78586
|
+
&[data-swipe-to-close~="left"],
|
|
78587
|
+
&[data-swipe-to-close~="right"] {
|
|
78588
|
+
touch-action: pan-y;
|
|
78589
|
+
}
|
|
78590
|
+
|
|
78534
78591
|
/* The clamped max, not --popover-maxmax-*: that one ignores what the
|
|
78535
78592
|
placement left available, which reaches here through
|
|
78536
78593
|
--container-position-remaining-* (see applyNewPosition) — the min()
|
|
@@ -78761,6 +78818,20 @@ const css$C = /* css */`
|
|
|
78761
78818
|
* overlap with the anchor; wrap it in `inset(...)` (e.g. `inset(top)`,
|
|
78762
78819
|
* `inset(top-left)`) to overlap the anchor instead (edges touching or
|
|
78763
78820
|
* inside it).
|
|
78821
|
+
* @param {boolean|Array<"top"|"right"|"bottom"|"left">} [props.closeByDrag=false] -
|
|
78822
|
+
* Pushing the popover out of its container closes it: it follows the pointer
|
|
78823
|
+
* and, when released, finishes leaving or comes back to rest. `true` pushes
|
|
78824
|
+
* it back through the edge it is docked to, the word `positionArea` leads
|
|
78825
|
+
* with (`"bottom"`, `"bottom-start"`: pushed down; `"left-end"`: pushed
|
|
78826
|
+
* left); a centred popover rests on no edge and is not pushed (warned in
|
|
78827
|
+
* dev). A list names the sides it may leave by, and it leaves the way it was
|
|
78828
|
+
* pushed: `["bottom", "left", "right"]` for a toast pushed back down behind
|
|
78829
|
+
* the bar or thrown sideways. A popover placed against an anchor at its
|
|
78830
|
+
* opening is not pushed either way (warned in dev). It is pushed from its
|
|
78831
|
+
* whole surface, so it suits a popover holding little else: a button inside
|
|
78832
|
+
* keeps its tap, and a press that turns into a push swallows the click it
|
|
78833
|
+
* would have left. The close keeps, as a cross's does. See docs/interactions.md
|
|
78834
|
+
* for a swipe on the popover's own element, which does not move it.
|
|
78764
78835
|
* @param {string} [props.positionAreaFixed] - Overrides `positionArea` once
|
|
78765
78836
|
* the popover has actually been positioned once, so a live reposition
|
|
78766
78837
|
* (e.g. anchor moved) doesn't jump to a different side.
|
|
@@ -79212,6 +79283,7 @@ const usePopoverProps = props => {
|
|
|
79212
79283
|
positionAreaWhenAnchorIsInvalid,
|
|
79213
79284
|
marginWithContainer = 0,
|
|
79214
79285
|
container = "app",
|
|
79286
|
+
closeByDrag = false,
|
|
79215
79287
|
// "close" → pointer press outside closes (a plain close = commit)
|
|
79216
79288
|
// "cancel" → pointer press outside closes AS A CANCEL (revert)
|
|
79217
79289
|
// "capture"→ absorb the press, stay open
|
|
@@ -79339,6 +79411,7 @@ const usePopoverProps = props => {
|
|
|
79339
79411
|
return popupGroup.register(openController);
|
|
79340
79412
|
}, [popupGroup, openController]);
|
|
79341
79413
|
const hasBackdrop = pressOutside !== "ignore" && backdrop && !popupGroup;
|
|
79414
|
+
const swipeToCloseSides = resolveSwipeToCloseSides(closeByDrag, positionArea);
|
|
79342
79415
|
if (!popupGroup && !backdrop && pressOutside === "capture") {
|
|
79343
79416
|
console.warn(`Popover: pressOutside="capture" needs a backdrop. Absorbing a press is what a wall does, and backdrop={false} takes it away — every press outside reaches the page, exactly as with "ignore".`);
|
|
79344
79417
|
}
|
|
@@ -80172,6 +80245,7 @@ const usePopoverProps = props => {
|
|
|
80172
80245
|
}
|
|
80173
80246
|
}
|
|
80174
80247
|
});
|
|
80248
|
+
const swipeToClose = swipeToCloseSides ? createSwipeToClose(swipeToCloseSides) : null;
|
|
80175
80249
|
Object.assign(contentProps, {
|
|
80176
80250
|
id,
|
|
80177
80251
|
tabIndex,
|
|
@@ -80250,6 +80324,26 @@ const usePopoverProps = props => {
|
|
|
80250
80324
|
onKeyDown?.(e);
|
|
80251
80325
|
onKeyDownShortcuts(e);
|
|
80252
80326
|
},
|
|
80327
|
+
...(swipeToClose ? {
|
|
80328
|
+
"data-swipe-to-close": swipeToCloseSides.join(" "),
|
|
80329
|
+
// The axes it travels on when pushed out, said to the shared gesture
|
|
80330
|
+
// layer before any finger lands (see side_panel.jsx, which says the
|
|
80331
|
+
// same for the same reasons).
|
|
80332
|
+
"data-drag-travel": swipeAxesOf(swipeToCloseSides),
|
|
80333
|
+
"data-travel-by-drag": swipeAxesOf(swipeToCloseSides),
|
|
80334
|
+
"onTouchMove": e => {
|
|
80335
|
+
rest.onTouchMove?.(e);
|
|
80336
|
+
keepTouchRefusable(e);
|
|
80337
|
+
},
|
|
80338
|
+
"onPointerDown": e => {
|
|
80339
|
+
rest.onPointerDown?.(e);
|
|
80340
|
+
// Placed against an anchor at this opening: it is not in its
|
|
80341
|
+
// container to be pushed out of (warned at the opening).
|
|
80342
|
+
if (!anchorElementRef.current) {
|
|
80343
|
+
swipeToClose(e);
|
|
80344
|
+
}
|
|
80345
|
+
}
|
|
80346
|
+
} : null),
|
|
80253
80347
|
// A popup opened from inside this one is in front of it, not a peer of
|
|
80254
80348
|
// it: the group stops here.
|
|
80255
80349
|
"children": popupGroup ? jsx(PopupGroupContext.Provider, {
|
|
@@ -80323,6 +80417,31 @@ const resolvePositionAreaAndAnimationKind = ({
|
|
|
80323
80417
|
Popover.Close = PopupClose;
|
|
80324
80418
|
Popover.Title = PopupTitle;
|
|
80325
80419
|
|
|
80420
|
+
// The sides a popover pushed away leaves by, or null when it is not pushed.
|
|
80421
|
+
const resolveSwipeToCloseSides = (closeByDrag, positionArea) => {
|
|
80422
|
+
if (!closeByDrag) {
|
|
80423
|
+
return null;
|
|
80424
|
+
}
|
|
80425
|
+
if (Array.isArray(closeByDrag)) {
|
|
80426
|
+
const sides = closeByDrag.filter(side => side in SWIPE_AXIS_BY_SIDE);
|
|
80427
|
+
return sides.length ? sides : null;
|
|
80428
|
+
}
|
|
80429
|
+
const side = dockedSideOf(positionArea);
|
|
80430
|
+
if (!side) {
|
|
80431
|
+
return null;
|
|
80432
|
+
}
|
|
80433
|
+
return [side];
|
|
80434
|
+
};
|
|
80435
|
+
const INSET_TOKEN_RE = /^inset\(\s*([a-z-]+)\s*\)$/;
|
|
80436
|
+
// The edge a popover docked to its container rests on: the word its
|
|
80437
|
+
// positionArea leads with, "bottom-start" resting on the bottom and "left-end"
|
|
80438
|
+
// on the left. "center" rests on none.
|
|
80439
|
+
const dockedSideOf = positionArea => {
|
|
80440
|
+
const token = INSET_TOKEN_RE.exec(positionArea)?.[1] ?? positionArea;
|
|
80441
|
+
const side = token.split("-")[0];
|
|
80442
|
+
return side in SWIPE_AXIS_BY_SIDE ? side : null;
|
|
80443
|
+
};
|
|
80444
|
+
|
|
80326
80445
|
// The placement needs pixels: a value only CSS could evaluate (a percentage, a
|
|
80327
80446
|
// calc()…) would put the popover at NaN, which is the top of the page.
|
|
80328
80447
|
const resolveMarginInPixels = (margin, name) => {
|
|
@@ -90629,8 +90748,19 @@ const useItemStore = ({
|
|
|
90629
90748
|
// was asked for, and the window keeps growing meanwhile: dropping what the
|
|
90630
90749
|
// page did not cover empties items on screen.
|
|
90631
90750
|
const staleRanksRef = useRef(null);
|
|
90751
|
+
// A run standing for no item has no rank to mark: its count is what is from
|
|
90752
|
+
// before, and no window frames anything that would ask for it again.
|
|
90753
|
+
const countStaleRef = useRef(false);
|
|
90754
|
+
const markHeldStale = () => {
|
|
90755
|
+
staleRanksRef.current = new Set(pages.byIndex.keys());
|
|
90756
|
+
countStaleRef.current = pages.count === 0;
|
|
90757
|
+
};
|
|
90632
90758
|
if (staleRanksRef.current === null) {
|
|
90633
|
-
|
|
90759
|
+
if (restored) {
|
|
90760
|
+
markHeldStale();
|
|
90761
|
+
} else {
|
|
90762
|
+
staleRanksRef.current = new Set();
|
|
90763
|
+
}
|
|
90634
90764
|
}
|
|
90635
90765
|
const takeInPage = ({
|
|
90636
90766
|
pageItems,
|
|
@@ -90669,6 +90799,7 @@ const useItemStore = ({
|
|
|
90669
90799
|
i++;
|
|
90670
90800
|
}
|
|
90671
90801
|
pages.count = pageCount;
|
|
90802
|
+
countStaleRef.current = false;
|
|
90672
90803
|
// Which rank holds which id, kept by the source so a list drawing
|
|
90673
90804
|
// this collection again finds it drawn (see readComposition above).
|
|
90674
90805
|
if (itemsAction.writeComposition) {
|
|
@@ -90704,7 +90835,7 @@ const useItemStore = ({
|
|
|
90704
90835
|
const invalidationRef = useRef(invalidation);
|
|
90705
90836
|
if (invalidationRef.current !== invalidation) {
|
|
90706
90837
|
invalidationRef.current = invalidation;
|
|
90707
|
-
|
|
90838
|
+
markHeldStale();
|
|
90708
90839
|
}
|
|
90709
90840
|
// The one request in flight, with the means to call it off: a page asked for
|
|
90710
90841
|
// a window the list has left is work the server and the browser are doing for
|
|
@@ -90902,7 +91033,8 @@ const useItemStore = ({
|
|
|
90902
91033
|
rank++;
|
|
90903
91034
|
}
|
|
90904
91035
|
}
|
|
90905
|
-
const
|
|
91036
|
+
const countStale = countStaleRef.current;
|
|
91037
|
+
const revalidating = staleFrom !== -1 || countStale;
|
|
90906
91038
|
// The list is held on an item nothing on screen leads to: the items it holds
|
|
90907
91039
|
// do not contain it, so no window it could draw will ever bring it. Only
|
|
90908
91040
|
// asking for it by name does.
|
|
@@ -90918,6 +91050,10 @@ const useItemStore = ({
|
|
|
90918
91050
|
const from = typeof wanted.index === "number" ? rankOfIndex(wanted.index) - Math.floor(budget / 2) : 0;
|
|
90919
91051
|
start = from < 0 ? 0 : from;
|
|
90920
91052
|
end = start + budget - 1;
|
|
91053
|
+
} else if (countStale) {
|
|
91054
|
+
// Nothing drawn to anchor on: the first page, like the very first ask.
|
|
91055
|
+
start = 0;
|
|
91056
|
+
end = budget - 1;
|
|
90921
91057
|
} else if (revalidating) {
|
|
90922
91058
|
// The items from before that the window draws, and the missing ones
|
|
90923
91059
|
// with them: one ask.
|
|
@@ -91119,6 +91255,7 @@ const useItemStore = ({
|
|
|
91119
91255
|
staleRanksRef.current.delete(rank);
|
|
91120
91256
|
rank++;
|
|
91121
91257
|
}
|
|
91258
|
+
countStaleRef.current = false;
|
|
91122
91259
|
setRefreshing(false);
|
|
91123
91260
|
return;
|
|
91124
91261
|
}
|
|
@@ -108030,7 +108167,7 @@ const SidePanel = ({
|
|
|
108030
108167
|
}) => {
|
|
108031
108168
|
import.meta.css = [css, "@jsenv/navi/src/layout/side_panel.jsx"];
|
|
108032
108169
|
const rest = dropPickerPopupProps(restGiven);
|
|
108033
|
-
const onSwipePointerDown = closeByDrag ? createSwipeToClose(side) : null;
|
|
108170
|
+
const onSwipePointerDown = closeByDrag ? createSwipeToClose([side]) : null;
|
|
108034
108171
|
return jsx(Popup, {
|
|
108035
108172
|
mode: mode
|
|
108036
108173
|
// Spread rather than written: the collision warning between `signal` and
|