@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/dev/jsenv_navi.js
CHANGED
|
@@ -25136,6 +25136,12 @@ const DOUBLE_CLICK_SLOP_ATTRIBUTE = "data-double-click-slop";
|
|
|
25136
25136
|
// taps: said in the DOM at render time, for the CSS below and for the boxes above
|
|
25137
25137
|
// to read.
|
|
25138
25138
|
const SWIPE_AXES_ATTRIBUTE = "data-swipe";
|
|
25139
|
+
// A popup's `translate` is where it is placed, written inline by the placement
|
|
25140
|
+
// (applyNewPosition in @jsenv/dom), and an inline declaration wins over the
|
|
25141
|
+
// rule painting the pull: the swipe is answered with nothing following it.
|
|
25142
|
+
// Moving a popup by hand is pushing it away, which swipe_to_close.js does by
|
|
25143
|
+
// composing a transform under the placement.
|
|
25144
|
+
const POPUP_ELEMENT_SELECTOR = `[navi-control="popover"], [navi-control="dialog"]`;
|
|
25139
25145
|
const LONGPRESS_ATTRIBUTE = "data-longpress";
|
|
25140
25146
|
const DOUBLE_CLICK_ATTRIBUTE = "data-double-click";
|
|
25141
25147
|
|
|
@@ -25235,6 +25241,9 @@ defineInteractionDetector({
|
|
|
25235
25241
|
});
|
|
25236
25242
|
};
|
|
25237
25243
|
if (axes) {
|
|
25244
|
+
if (element.matches(POPUP_ELEMENT_SELECTOR)) {
|
|
25245
|
+
warnSwipeOnPopupElement(element);
|
|
25246
|
+
}
|
|
25238
25247
|
mark(SWIPE_AXES_ATTRIBUTE, axes);
|
|
25239
25248
|
// Read by the boxes ABOVE this one: an axis swiped here is not theirs to
|
|
25240
25249
|
// travel (see axesLeftBy).
|
|
@@ -25515,6 +25524,13 @@ const warnOnceAboutHoldTakenByInner = (element, innerElement) => {
|
|
|
25515
25524
|
);
|
|
25516
25525
|
};
|
|
25517
25526
|
|
|
25527
|
+
const warnSwipeOnPopupElement = (element) => {
|
|
25528
|
+
console.warn(
|
|
25529
|
+
`interactions: a swipe declared on a popup's own element does not move it — its translate is where the popup is placed. A popup pushed away closes with closeByDrag (Popover, SidePanel; a docked Dialog sheet does it on its own); a swipe on something it holds is declared on that element.`,
|
|
25530
|
+
element,
|
|
25531
|
+
);
|
|
25532
|
+
};
|
|
25533
|
+
|
|
25518
25534
|
const startSwipe = (
|
|
25519
25535
|
pointerDownEvent,
|
|
25520
25536
|
{
|
|
@@ -30502,8 +30518,9 @@ const SCOPED_MANY_OPTIONS = new Set([
|
|
|
30502
30518
|
* policy as that verb, and is a write of its own for a dependent:
|
|
30503
30519
|
* `{ resource: ORDER, on: ["cancel"] }`. Its `rerunOn` says which reads of
|
|
30504
30520
|
* this resource it reruns (`{ GET_RANGE: true }`); a read it leaves out
|
|
30505
|
-
* follows `rerunOn` for its verb. A
|
|
30506
|
-
* already exposes (`withParams`, `store`…),
|
|
30521
|
+
* follows `rerunOn` for its verb. A verb's name in any case (`post`,
|
|
30522
|
+
* `get_many`…), or one the resource already exposes (`withParams`, `store`…),
|
|
30523
|
+
* throws. A `.scopedOne()` or
|
|
30507
30524
|
* `.scopedMany()` takes `writes` too, `.scopedMany()` with the plural verbs.
|
|
30508
30525
|
* @param {{ GET?: { ms?: number, max?: number }, GET_MANY?: { ms?: number, max?: number } }} [restCallbacks.keep] -
|
|
30509
30526
|
* how long an answer of a read is trusted, coming back to the same params
|
|
@@ -32068,12 +32085,15 @@ ${name}.many("${propertyName}") source location: ${declarationSite}`,
|
|
|
32068
32085
|
|
|
32069
32086
|
// expose one action per write named in `writes`
|
|
32070
32087
|
if (writes) {
|
|
32088
|
+
const membersExposed = Object.keys(stateFacade).filter(
|
|
32089
|
+
(key) => !VERB_KEYS.includes(key),
|
|
32090
|
+
);
|
|
32071
32091
|
for (const [writeName, write] of Object.entries(writes)) {
|
|
32072
32092
|
assertWriteCanBeNamed(
|
|
32073
32093
|
name,
|
|
32074
32094
|
writeName,
|
|
32075
32095
|
write,
|
|
32076
|
-
|
|
32096
|
+
membersExposed,
|
|
32077
32097
|
namedWriteVerbs,
|
|
32078
32098
|
);
|
|
32079
32099
|
const { verb: writeVerb, callback, rerunOn: writeRerunOn } = write;
|
|
@@ -32386,7 +32406,7 @@ const assertWriteCanBeNamed = (
|
|
|
32386
32406
|
name,
|
|
32387
32407
|
writeName,
|
|
32388
32408
|
write,
|
|
32389
|
-
|
|
32409
|
+
membersExposed,
|
|
32390
32410
|
namedWriteVerbs,
|
|
32391
32411
|
) => {
|
|
32392
32412
|
const where = `resource(${name}) writes.${writeName}`;
|
|
@@ -32395,9 +32415,9 @@ const assertWriteCanBeNamed = (
|
|
|
32395
32415
|
`${where}: "${writeName}" is a verb's name — a write named in "writes" takes a name of its own (cancel, archive…)`,
|
|
32396
32416
|
);
|
|
32397
32417
|
}
|
|
32398
|
-
if (
|
|
32418
|
+
if (membersExposed.includes(writeName)) {
|
|
32399
32419
|
throw new Error(
|
|
32400
|
-
`${where}: the resource already exposes "${writeName}" —
|
|
32420
|
+
`${where}: the resource already exposes "${writeName}" — a write takes a name none of its members has: ${membersExposed.join(", ")}`,
|
|
32401
32421
|
);
|
|
32402
32422
|
}
|
|
32403
32423
|
if (!write || typeof write !== "object") {
|
|
@@ -77148,18 +77168,19 @@ const readRoom = () =>
|
|
|
77148
77168
|
visualViewportHeightSignal.peek() - getVirtualKeyboardOverlayHeight();
|
|
77149
77169
|
|
|
77150
77170
|
/**
|
|
77151
|
-
* Pushing a popup docked to an edge (a side panel, a bottom sheet
|
|
77152
|
-
* it came in closes it: it follows the finger while it travels,
|
|
77153
|
-
* either finishes the departure or brings it back to rest —
|
|
77154
|
-
* travel was already heading to.
|
|
77171
|
+
* Pushing a popup docked to an edge (a side panel, a bottom sheet, a toast)
|
|
77172
|
+
* back the way it came in closes it: it follows the finger while it travels,
|
|
77173
|
+
* and letting go either finishes the departure or brings it back to rest —
|
|
77174
|
+
* whichever the travel was already heading to. A popup may be given other ways
|
|
77175
|
+
* out than that one: a toast is also thrown sideways.
|
|
77155
77176
|
*
|
|
77156
77177
|
* Reading the pointer is not done here: when a press becomes a gesture, which
|
|
77157
77178
|
* axis it leans on, how fast it was going when it was let go, who else has a
|
|
77158
77179
|
* claim on it (a field, a scroller with room left, a box inside the panel that
|
|
77159
77180
|
* travels the same way) is one gesture system for the whole codebase
|
|
77160
77181
|
* (@jsenv/dom's startDragToTravel). A panel closing is a travel of exactly one
|
|
77161
|
-
* box,
|
|
77162
|
-
* file has to say.
|
|
77182
|
+
* box, out by one of the edges it may leave by and nowhere else — which is all
|
|
77183
|
+
* this file has to say.
|
|
77163
77184
|
*
|
|
77164
77185
|
* The release travel is driven here (Web Animations) rather than handed to the
|
|
77165
77186
|
* exit transition the `animation` prop may install: its pace is what is left to
|
|
@@ -77174,7 +77195,7 @@ const readRoom = () =>
|
|
|
77174
77195
|
// less, so the panel keeps the same speed whenever the finger let go.
|
|
77175
77196
|
const TRAVEL_DURATION = 220;
|
|
77176
77197
|
|
|
77177
|
-
// Which way a popup
|
|
77198
|
+
// Which way a popup leaving by each side travels — read by the panel itself to
|
|
77178
77199
|
// say so in the DOM, which is how a box travelling INSIDE it (a row of slides,
|
|
77179
77200
|
// a route travel) knows that axis is already walked.
|
|
77180
77201
|
const SWIPE_AXIS_BY_SIDE = {
|
|
@@ -77183,12 +77204,27 @@ const SWIPE_AXIS_BY_SIDE = {
|
|
|
77183
77204
|
top: "y",
|
|
77184
77205
|
bottom: "y",
|
|
77185
77206
|
};
|
|
77186
|
-
// Which way the finger pushes to send the panel
|
|
77187
|
-
//
|
|
77207
|
+
// Which way the finger pushes to send the panel out by each side, in screen
|
|
77208
|
+
// coordinates: positive is right/down, the same sign the gesture reports.
|
|
77188
77209
|
const CLOSE_DIRECTION_BY_SIDE = { left: -1, right: 1, top: -1, bottom: 1 };
|
|
77189
77210
|
|
|
77190
77211
|
/**
|
|
77191
|
-
*
|
|
77212
|
+
* The axes a popup leaving by `sides` travels on, as the gesture and the DOM
|
|
77213
|
+
* say them: "x", "y" or "xy".
|
|
77214
|
+
*/
|
|
77215
|
+
const swipeAxesOf = (sides) => {
|
|
77216
|
+
const hasX = sides.some((side) => SWIPE_AXIS_BY_SIDE[side] === "x");
|
|
77217
|
+
const hasY = sides.some((side) => SWIPE_AXIS_BY_SIDE[side] === "y");
|
|
77218
|
+
if (hasX && hasY) {
|
|
77219
|
+
return "xy";
|
|
77220
|
+
}
|
|
77221
|
+
return hasX ? "x" : "y";
|
|
77222
|
+
};
|
|
77223
|
+
|
|
77224
|
+
/**
|
|
77225
|
+
* Builds the `pointerdown` handler of a popup that leaves by any of `sides`:
|
|
77226
|
+
* usually the one edge it is docked to, the way it came in; a toast is also
|
|
77227
|
+
* thrown sideways, and leaves the way it was thrown.
|
|
77192
77228
|
*
|
|
77193
77229
|
* `grip` says where the gesture may start, as a selector the pressed element is
|
|
77194
77230
|
* matched against with `closest`. A popup that names one is held THERE and
|
|
@@ -77196,11 +77232,17 @@ const CLOSE_DIRECTION_BY_SIDE = { left: -1, right: 1, top: -1, bottom: 1 };
|
|
|
77196
77232
|
* and a press on it is that content's — including gestures of its own, which
|
|
77197
77233
|
* this file has no way of knowing about. A popup that names no grip is pushed
|
|
77198
77234
|
* from its whole surface, which only suits one made of nothing else (a side
|
|
77199
|
-
* panel showing a page).
|
|
77200
|
-
*/
|
|
77201
|
-
const createSwipeToClose = (
|
|
77202
|
-
const
|
|
77203
|
-
|
|
77235
|
+
* panel showing a page, a toast).
|
|
77236
|
+
*/
|
|
77237
|
+
const createSwipeToClose = (sides, { grip } = {}) => {
|
|
77238
|
+
const axes = swipeAxesOf(sides);
|
|
77239
|
+
// The side a push along `axis` towards `sign` leaves by, if it leaves at all.
|
|
77240
|
+
const sideTowards = (axis, sign) =>
|
|
77241
|
+
sides.find(
|
|
77242
|
+
(side) =>
|
|
77243
|
+
SWIPE_AXIS_BY_SIDE[side] === axis &&
|
|
77244
|
+
CLOSE_DIRECTION_BY_SIDE[side] === sign,
|
|
77245
|
+
);
|
|
77204
77246
|
|
|
77205
77247
|
return (pointerDownEvent) => {
|
|
77206
77248
|
const panelEl = pointerDownEvent.currentTarget;
|
|
@@ -77217,6 +77259,11 @@ const createSwipeToClose = (side, { grip } = {}) => {
|
|
|
77217
77259
|
}
|
|
77218
77260
|
}
|
|
77219
77261
|
|
|
77262
|
+
// Decided when the finger picks its axis (onStart), once for the gesture.
|
|
77263
|
+
let axis;
|
|
77264
|
+
// How far the panel travels to be out, for each way it can go on that axis.
|
|
77265
|
+
const distanceOutBySign = new Map();
|
|
77266
|
+
|
|
77220
77267
|
// How far the panel has been pulled, as the style showing it — written on
|
|
77221
77268
|
// the panel while the finger drives it, handed to the release travel as
|
|
77222
77269
|
// its two keyframes. The gesture reports a distance in screen coordinates,
|
|
@@ -77226,7 +77273,7 @@ const createSwipeToClose = (side, { grip } = {}) => {
|
|
|
77226
77273
|
// composes under the placement instead of replacing it.
|
|
77227
77274
|
const styleAt = (distance) => ({
|
|
77228
77275
|
transform: translateOf(axis, distance),
|
|
77229
|
-
clipPath: clipOf(
|
|
77276
|
+
clipPath: clipOf(sides, axis, distance),
|
|
77230
77277
|
});
|
|
77231
77278
|
const paint = (distance) => {
|
|
77232
77279
|
Object.assign(panelEl.style, styleAt(distance));
|
|
@@ -77238,21 +77285,22 @@ const createSwipeToClose = (side, { grip } = {}) => {
|
|
|
77238
77285
|
};
|
|
77239
77286
|
// The resting value is written first and the animation only covers the way
|
|
77240
77287
|
// to it: both end on the same number, so nothing is seen changing when the
|
|
77241
|
-
// animation hands the panel back to its own style.
|
|
77242
|
-
|
|
77288
|
+
// animation hands the panel back to its own style. `span` is the full travel
|
|
77289
|
+
// on that side, which the duration is a fraction of.
|
|
77290
|
+
const travelTo = (from, to, span, onArrival) => {
|
|
77243
77291
|
const covered = from > to ? from - to : to - from;
|
|
77244
77292
|
paint(to);
|
|
77245
77293
|
const animation = panelEl.animate([styleAt(from), styleAt(to)], {
|
|
77246
|
-
duration: (covered /
|
|
77294
|
+
duration: (covered / span) * TRAVEL_DURATION,
|
|
77247
77295
|
easing: "ease-out",
|
|
77248
77296
|
});
|
|
77249
77297
|
animation.finished.then(onArrival, () => {});
|
|
77250
77298
|
};
|
|
77251
|
-
const close = (event) => {
|
|
77299
|
+
const close = (event, out, span) => {
|
|
77252
77300
|
triggerNaviCommand(panelEl, "--navi-close", event);
|
|
77253
77301
|
if (panelEl.getAttribute("aria-expanded") === "true") {
|
|
77254
77302
|
// Refused: the panel is staying, so it goes back where it was.
|
|
77255
|
-
travelTo(
|
|
77303
|
+
travelTo(out, 0, span, restore);
|
|
77256
77304
|
return;
|
|
77257
77305
|
}
|
|
77258
77306
|
restore();
|
|
@@ -77260,79 +77308,94 @@ const createSwipeToClose = (side, { grip } = {}) => {
|
|
|
77260
77308
|
|
|
77261
77309
|
startDragToTravel(pointerDownEvent, {
|
|
77262
77310
|
element: panelEl,
|
|
77263
|
-
axes
|
|
77264
|
-
onStart: ({ sign, target }) => {
|
|
77265
|
-
|
|
77311
|
+
axes,
|
|
77312
|
+
onStart: ({ axis: gestureAxis, sign, target }) => {
|
|
77313
|
+
const side = sideTowards(gestureAxis, sign);
|
|
77314
|
+
if (!side) {
|
|
77266
77315
|
// Pushing the panel further in is not a travel: there is nowhere that
|
|
77267
77316
|
// way, and reading it as one would make the panel lean at every
|
|
77268
77317
|
// gesture that starts by going the wrong way.
|
|
77269
77318
|
return false;
|
|
77270
77319
|
}
|
|
77271
|
-
const size =
|
|
77320
|
+
const size = distanceOut(panelEl, side);
|
|
77272
77321
|
if (!size) {
|
|
77273
77322
|
return false;
|
|
77274
77323
|
}
|
|
77275
77324
|
// Something else with a better claim on the gesture: a scroller between
|
|
77276
77325
|
// the finger and the panel, with room left that way.
|
|
77277
|
-
if (scrollRoomTowards(target, panelEl,
|
|
77326
|
+
if (scrollRoomTowards(target, panelEl, gestureAxis, sign)) {
|
|
77278
77327
|
return false;
|
|
77279
77328
|
}
|
|
77329
|
+
axis = gestureAxis;
|
|
77330
|
+
distanceOutBySign.set(sign, size);
|
|
77331
|
+
const otherSide = sideTowards(gestureAxis, -sign);
|
|
77332
|
+
if (otherSide) {
|
|
77333
|
+
distanceOutBySign.set(-sign, distanceOut(panelEl, otherSide));
|
|
77334
|
+
}
|
|
77280
77335
|
panelEl.style.transitionProperty = "none";
|
|
77281
|
-
//
|
|
77282
|
-
// leans against a wall and comes back.
|
|
77336
|
+
// Only the ways out lead anywhere: pulling towards a side the panel
|
|
77337
|
+
// does not leave by leans against a wall and comes back.
|
|
77283
77338
|
return {
|
|
77284
77339
|
size,
|
|
77285
|
-
travelBack:
|
|
77286
|
-
travelOn:
|
|
77340
|
+
travelBack: Boolean(sideTowards(gestureAxis, 1)),
|
|
77341
|
+
travelOn: Boolean(sideTowards(gestureAxis, -1)),
|
|
77287
77342
|
};
|
|
77288
77343
|
},
|
|
77289
77344
|
onPull: ({ pulled }) => {
|
|
77290
77345
|
paint(pulled);
|
|
77291
77346
|
},
|
|
77292
|
-
onEnd: ({ pulled, size, travels, event }) => {
|
|
77347
|
+
onEnd: ({ pulled, size, sign, travels, event }) => {
|
|
77293
77348
|
if (travels) {
|
|
77294
|
-
|
|
77349
|
+
const span = distanceOutBySign.get(sign);
|
|
77350
|
+
travelTo(pulled, span * sign, span, () =>
|
|
77351
|
+
close(event, span * sign, span),
|
|
77352
|
+
);
|
|
77295
77353
|
return;
|
|
77296
77354
|
}
|
|
77297
|
-
travelTo(pulled, 0, restore);
|
|
77355
|
+
travelTo(pulled, 0, size, restore);
|
|
77298
77356
|
},
|
|
77299
77357
|
});
|
|
77300
77358
|
};
|
|
77301
77359
|
};
|
|
77302
77360
|
|
|
77303
|
-
|
|
77361
|
+
// What takes the popup out of sight: its own size, plus the gap a margin keeps
|
|
77362
|
+
// between it and the edge it is pushed through — the room applyNewPosition
|
|
77363
|
+
// publishes on that side. Travelling its size alone, a popup kept off its edge
|
|
77364
|
+
// (a toast 12px above a bar) would stop with that much of itself still showing.
|
|
77365
|
+
const distanceOut = (element, side) => {
|
|
77304
77366
|
const rect = element.getBoundingClientRect();
|
|
77305
|
-
|
|
77367
|
+
const size = SWIPE_AXIS_BY_SIDE[side] === "x" ? rect.width : rect.height;
|
|
77368
|
+
const room = parseFloat(
|
|
77369
|
+
element.style.getPropertyValue(`--container-position-room-${side}`),
|
|
77370
|
+
);
|
|
77371
|
+
return Number.isNaN(room) ? size : size + room;
|
|
77306
77372
|
};
|
|
77307
77373
|
const translateOf = (axis, distance) =>
|
|
77308
77374
|
axis === "x"
|
|
77309
77375
|
? `translate(${distance}px, 0px)`
|
|
77310
77376
|
: `translate(0px, ${distance}px)`;
|
|
77311
77377
|
// The same cut the entry/exit animation makes (popup_css.js), at a distance the
|
|
77312
|
-
// finger decides instead of a transition:
|
|
77313
|
-
//
|
|
77314
|
-
// edge where a band of glass lies past it, 100vmax out where the
|
|
77315
|
-
// edge does the cutting),
|
|
77316
|
-
//
|
|
77317
|
-
// the box — the pull never takes the popup
|
|
77318
|
-
// only shave what it legitimately paints
|
|
77319
|
-
// policy from popup_css.js rather than the
|
|
77320
|
-
// (a Web Animation on transform AND clip-path,
|
|
77321
|
-
// cut the entrance would not.
|
|
77378
|
+
// finger decides instead of a transition: each edge the popup may leave by on
|
|
77379
|
+
// the axis it travels, as popup_css.js placed it (--x-popup-cut-*-at-area — on
|
|
77380
|
+
// the area's edge where a band of glass lies past it, 100vmax out where the
|
|
77381
|
+
// window's own edge does the cutting), moved by what has been pulled towards
|
|
77382
|
+
// it, so the cut holds that line while the box travels under it, whichever way.
|
|
77383
|
+
// Every other side is left far outside the box — the pull never takes the popup
|
|
77384
|
+
// past them, and cutting there would only shave what it legitimately paints
|
|
77385
|
+
// outside its own box. Reading the policy from popup_css.js rather than the
|
|
77386
|
+
// rooms keeps the release travel (a Web Animation on transform AND clip-path,
|
|
77387
|
+
// two clocks) from animating a cut the entrance would not.
|
|
77322
77388
|
const UNCUT = "-100vmax";
|
|
77323
|
-
const
|
|
77324
|
-
|
|
77325
|
-
const
|
|
77326
|
-
|
|
77327
|
-
|
|
77328
|
-
|
|
77329
|
-
|
|
77330
|
-
return `
|
|
77331
|
-
}
|
|
77332
|
-
|
|
77333
|
-
return `inset(${UNCUT} ${UNCUT} ${cut} ${UNCUT})`;
|
|
77334
|
-
}
|
|
77335
|
-
return `inset(${UNCUT} ${UNCUT} ${UNCUT} ${cut})`;
|
|
77389
|
+
const INSET_ORDER = ["top", "right", "bottom", "left"];
|
|
77390
|
+
const clipOf = (sides, axis, distance) => {
|
|
77391
|
+
const insets = INSET_ORDER.map((side) => {
|
|
77392
|
+
if (!sides.includes(side) || SWIPE_AXIS_BY_SIDE[side] !== axis) {
|
|
77393
|
+
return UNCUT;
|
|
77394
|
+
}
|
|
77395
|
+
const towards = distance * CLOSE_DIRECTION_BY_SIDE[side];
|
|
77396
|
+
return `calc(var(--x-popup-cut-${side}-at-area, ${UNCUT}) + ${towards}px)`;
|
|
77397
|
+
});
|
|
77398
|
+
return `inset(${insets.join(" ")})`;
|
|
77336
77399
|
};
|
|
77337
77400
|
|
|
77338
77401
|
/**
|
|
@@ -79122,7 +79185,7 @@ const useDialogProps = props => {
|
|
|
79122
79185
|
// actually resting on an edge: anywhere else the gesture would send the
|
|
79123
79186
|
// dialog somewhere it never came from.
|
|
79124
79187
|
const swipeToCloseSide = isDocked && (parsedPositionArea.y === "top" || parsedPositionArea.y === "bottom") ? parsedPositionArea.y : null;
|
|
79125
|
-
const onSwipePointerDown = swipeToCloseSide ? createSwipeToClose(swipeToCloseSide, {
|
|
79188
|
+
const onSwipePointerDown = swipeToCloseSide ? createSwipeToClose([swipeToCloseSide], {
|
|
79126
79189
|
grip: DOCKED_SWIPE_GRIP
|
|
79127
79190
|
}) : null;
|
|
79128
79191
|
// A corner sitting exactly on the container's own corner must not be
|
|
@@ -80302,6 +80365,16 @@ const css$C = /* css */`
|
|
|
80302
80365
|
to filter). An entrance animation moves the popover through scale and
|
|
80303
80366
|
transform instead, which compose under it: see popup_css.js. */
|
|
80304
80367
|
|
|
80368
|
+
/* What a touch may do on a popover pushed sideways to close it (see
|
|
80369
|
+
swipe_to_close.js): the push takes the sideways axis, the page keeps the
|
|
80370
|
+
vertical scroll. A push up or down travels the way the page scrolls and
|
|
80371
|
+
cannot give that axis away: the browser keeps it, and the gesture is
|
|
80372
|
+
settled by keepTouchRefusable. */
|
|
80373
|
+
&[data-swipe-to-close~="left"],
|
|
80374
|
+
&[data-swipe-to-close~="right"] {
|
|
80375
|
+
touch-action: pan-y;
|
|
80376
|
+
}
|
|
80377
|
+
|
|
80305
80378
|
/* The clamped max, not --popover-maxmax-*: that one ignores what the
|
|
80306
80379
|
placement left available, which reaches here through
|
|
80307
80380
|
--container-position-remaining-* (see applyNewPosition) — the min()
|
|
@@ -80532,6 +80605,20 @@ const css$C = /* css */`
|
|
|
80532
80605
|
* overlap with the anchor; wrap it in `inset(...)` (e.g. `inset(top)`,
|
|
80533
80606
|
* `inset(top-left)`) to overlap the anchor instead (edges touching or
|
|
80534
80607
|
* inside it).
|
|
80608
|
+
* @param {boolean|Array<"top"|"right"|"bottom"|"left">} [props.closeByDrag=false] -
|
|
80609
|
+
* Pushing the popover out of its container closes it: it follows the pointer
|
|
80610
|
+
* and, when released, finishes leaving or comes back to rest. `true` pushes
|
|
80611
|
+
* it back through the edge it is docked to, the word `positionArea` leads
|
|
80612
|
+
* with (`"bottom"`, `"bottom-start"`: pushed down; `"left-end"`: pushed
|
|
80613
|
+
* left); a centred popover rests on no edge and is not pushed (warned in
|
|
80614
|
+
* dev). A list names the sides it may leave by, and it leaves the way it was
|
|
80615
|
+
* pushed: `["bottom", "left", "right"]` for a toast pushed back down behind
|
|
80616
|
+
* the bar or thrown sideways. A popover placed against an anchor at its
|
|
80617
|
+
* opening is not pushed either way (warned in dev). It is pushed from its
|
|
80618
|
+
* whole surface, so it suits a popover holding little else: a button inside
|
|
80619
|
+
* keeps its tap, and a press that turns into a push swallows the click it
|
|
80620
|
+
* would have left. The close keeps, as a cross's does. See docs/interactions.md
|
|
80621
|
+
* for a swipe on the popover's own element, which does not move it.
|
|
80535
80622
|
* @param {string} [props.positionAreaFixed] - Overrides `positionArea` once
|
|
80536
80623
|
* the popover has actually been positioned once, so a live reposition
|
|
80537
80624
|
* (e.g. anchor moved) doesn't jump to a different side.
|
|
@@ -80985,6 +81072,7 @@ const usePopoverProps = props => {
|
|
|
80985
81072
|
positionAreaWhenAnchorIsInvalid,
|
|
80986
81073
|
marginWithContainer = 0,
|
|
80987
81074
|
container = "app",
|
|
81075
|
+
closeByDrag = false,
|
|
80988
81076
|
// "close" → pointer press outside closes (a plain close = commit)
|
|
80989
81077
|
// "cancel" → pointer press outside closes AS A CANCEL (revert)
|
|
80990
81078
|
// "capture"→ absorb the press, stay open
|
|
@@ -81118,6 +81206,7 @@ const usePopoverProps = props => {
|
|
|
81118
81206
|
return popupGroup.register(openController);
|
|
81119
81207
|
}, [popupGroup, openController]);
|
|
81120
81208
|
const hasBackdrop = pressOutside !== "ignore" && backdrop && !popupGroup;
|
|
81209
|
+
const swipeToCloseSides = resolveSwipeToCloseSides(closeByDrag, positionArea);
|
|
81121
81210
|
if (!popupGroup && !backdrop && pressOutside === "capture") {
|
|
81122
81211
|
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".`);
|
|
81123
81212
|
}
|
|
@@ -81267,6 +81356,9 @@ const usePopoverProps = props => {
|
|
|
81267
81356
|
}
|
|
81268
81357
|
const hasAnchorElement = Boolean(anchorElement);
|
|
81269
81358
|
anchorElementRef.current = anchorElement || null;
|
|
81359
|
+
if (swipeToCloseSides && hasAnchorElement) {
|
|
81360
|
+
console.warn(`Popover: closeByDrag is ignored, this opening placed the popover against an anchor rather than in its container. A popover pushed away has no anchor: anchorCustomEventDetail="ignore" and no anchor prop.`, anchorElement);
|
|
81361
|
+
}
|
|
81270
81362
|
// getPositionedParent already resolves to document.documentElement for
|
|
81271
81363
|
// the via-attribute renderer on its own (popoverEl's own `popover`
|
|
81272
81364
|
// attribute, present from render regardless of whether it's actually
|
|
@@ -81961,6 +82053,7 @@ const usePopoverProps = props => {
|
|
|
81961
82053
|
}
|
|
81962
82054
|
}
|
|
81963
82055
|
});
|
|
82056
|
+
const swipeToClose = swipeToCloseSides ? createSwipeToClose(swipeToCloseSides) : null;
|
|
81964
82057
|
Object.assign(contentProps, {
|
|
81965
82058
|
id,
|
|
81966
82059
|
tabIndex,
|
|
@@ -82039,6 +82132,26 @@ const usePopoverProps = props => {
|
|
|
82039
82132
|
onKeyDown?.(e);
|
|
82040
82133
|
onKeyDownShortcuts(e);
|
|
82041
82134
|
},
|
|
82135
|
+
...(swipeToClose ? {
|
|
82136
|
+
"data-swipe-to-close": swipeToCloseSides.join(" "),
|
|
82137
|
+
// The axes it travels on when pushed out, said to the shared gesture
|
|
82138
|
+
// layer before any finger lands (see side_panel.jsx, which says the
|
|
82139
|
+
// same for the same reasons).
|
|
82140
|
+
"data-drag-travel": swipeAxesOf(swipeToCloseSides),
|
|
82141
|
+
"data-travel-by-drag": swipeAxesOf(swipeToCloseSides),
|
|
82142
|
+
"onTouchMove": e => {
|
|
82143
|
+
rest.onTouchMove?.(e);
|
|
82144
|
+
keepTouchRefusable(e);
|
|
82145
|
+
},
|
|
82146
|
+
"onPointerDown": e => {
|
|
82147
|
+
rest.onPointerDown?.(e);
|
|
82148
|
+
// Placed against an anchor at this opening: it is not in its
|
|
82149
|
+
// container to be pushed out of (warned at the opening).
|
|
82150
|
+
if (!anchorElementRef.current) {
|
|
82151
|
+
swipeToClose(e);
|
|
82152
|
+
}
|
|
82153
|
+
}
|
|
82154
|
+
} : null),
|
|
82042
82155
|
// A popup opened from inside this one is in front of it, not a peer of
|
|
82043
82156
|
// it: the group stops here.
|
|
82044
82157
|
"children": popupGroup ? jsx(PopupGroupContext.Provider, {
|
|
@@ -82112,6 +82225,37 @@ const resolvePositionAreaAndAnimationKind = ({
|
|
|
82112
82225
|
Popover.Close = PopupClose;
|
|
82113
82226
|
Popover.Title = PopupTitle;
|
|
82114
82227
|
|
|
82228
|
+
// The sides a popover pushed away leaves by, or null when it is not pushed.
|
|
82229
|
+
const resolveSwipeToCloseSides = (closeByDrag, positionArea) => {
|
|
82230
|
+
if (!closeByDrag) {
|
|
82231
|
+
return null;
|
|
82232
|
+
}
|
|
82233
|
+
if (Array.isArray(closeByDrag)) {
|
|
82234
|
+
const sides = closeByDrag.filter(side => side in SWIPE_AXIS_BY_SIDE);
|
|
82235
|
+
if (sides.length !== closeByDrag.length) {
|
|
82236
|
+
console.warn(`Popover: closeByDrag={${JSON.stringify(closeByDrag)}} names something that is not a side; the sides are "top", "right", "bottom" and "left".`);
|
|
82237
|
+
}
|
|
82238
|
+
return sides.length ? sides : null;
|
|
82239
|
+
}
|
|
82240
|
+
const side = dockedSideOf(positionArea);
|
|
82241
|
+
if (!side) {
|
|
82242
|
+
{
|
|
82243
|
+
console.warn(`Popover: closeByDrag with positionArea="${positionArea}" rests on no edge, so there is no way back to push it through. Dock it to one ("bottom", "top", "left", "right", or a token leading with one of them), or name the sides it leaves by: closeByDrag={["left", "right"]}.`);
|
|
82244
|
+
}
|
|
82245
|
+
return null;
|
|
82246
|
+
}
|
|
82247
|
+
return [side];
|
|
82248
|
+
};
|
|
82249
|
+
const INSET_TOKEN_RE = /^inset\(\s*([a-z-]+)\s*\)$/;
|
|
82250
|
+
// The edge a popover docked to its container rests on: the word its
|
|
82251
|
+
// positionArea leads with, "bottom-start" resting on the bottom and "left-end"
|
|
82252
|
+
// on the left. "center" rests on none.
|
|
82253
|
+
const dockedSideOf = positionArea => {
|
|
82254
|
+
const token = INSET_TOKEN_RE.exec(positionArea)?.[1] ?? positionArea;
|
|
82255
|
+
const side = token.split("-")[0];
|
|
82256
|
+
return side in SWIPE_AXIS_BY_SIDE ? side : null;
|
|
82257
|
+
};
|
|
82258
|
+
|
|
82115
82259
|
// The placement needs pixels: a value only CSS could evaluate (a percentage, a
|
|
82116
82260
|
// calc()…) would put the popover at NaN, which is the top of the page.
|
|
82117
82261
|
const resolveMarginInPixels = (margin, name) => {
|
|
@@ -92640,8 +92784,19 @@ const useItemStore = ({
|
|
|
92640
92784
|
// was asked for, and the window keeps growing meanwhile: dropping what the
|
|
92641
92785
|
// page did not cover empties items on screen.
|
|
92642
92786
|
const staleRanksRef = useRef(null);
|
|
92787
|
+
// A run standing for no item has no rank to mark: its count is what is from
|
|
92788
|
+
// before, and no window frames anything that would ask for it again.
|
|
92789
|
+
const countStaleRef = useRef(false);
|
|
92790
|
+
const markHeldStale = () => {
|
|
92791
|
+
staleRanksRef.current = new Set(pages.byIndex.keys());
|
|
92792
|
+
countStaleRef.current = pages.count === 0;
|
|
92793
|
+
};
|
|
92643
92794
|
if (staleRanksRef.current === null) {
|
|
92644
|
-
|
|
92795
|
+
if (restored) {
|
|
92796
|
+
markHeldStale();
|
|
92797
|
+
} else {
|
|
92798
|
+
staleRanksRef.current = new Set();
|
|
92799
|
+
}
|
|
92645
92800
|
}
|
|
92646
92801
|
const takeInPage = ({
|
|
92647
92802
|
pageItems,
|
|
@@ -92680,6 +92835,7 @@ const useItemStore = ({
|
|
|
92680
92835
|
i++;
|
|
92681
92836
|
}
|
|
92682
92837
|
pages.count = pageCount;
|
|
92838
|
+
countStaleRef.current = false;
|
|
92683
92839
|
// Which rank holds which id, kept by the source so a list drawing
|
|
92684
92840
|
// this collection again finds it drawn (see readComposition above).
|
|
92685
92841
|
if (itemsAction.writeComposition) {
|
|
@@ -92715,7 +92871,7 @@ const useItemStore = ({
|
|
|
92715
92871
|
const invalidationRef = useRef(invalidation);
|
|
92716
92872
|
if (invalidationRef.current !== invalidation) {
|
|
92717
92873
|
invalidationRef.current = invalidation;
|
|
92718
|
-
|
|
92874
|
+
markHeldStale();
|
|
92719
92875
|
}
|
|
92720
92876
|
// The one request in flight, with the means to call it off: a page asked for
|
|
92721
92877
|
// a window the list has left is work the server and the browser are doing for
|
|
@@ -92913,7 +93069,8 @@ const useItemStore = ({
|
|
|
92913
93069
|
rank++;
|
|
92914
93070
|
}
|
|
92915
93071
|
}
|
|
92916
|
-
const
|
|
93072
|
+
const countStale = countStaleRef.current;
|
|
93073
|
+
const revalidating = staleFrom !== -1 || countStale;
|
|
92917
93074
|
// The list is held on an item nothing on screen leads to: the items it holds
|
|
92918
93075
|
// do not contain it, so no window it could draw will ever bring it. Only
|
|
92919
93076
|
// asking for it by name does.
|
|
@@ -92929,6 +93086,10 @@ const useItemStore = ({
|
|
|
92929
93086
|
const from = typeof wanted.index === "number" ? rankOfIndex(wanted.index) - Math.floor(budget / 2) : 0;
|
|
92930
93087
|
start = from < 0 ? 0 : from;
|
|
92931
93088
|
end = start + budget - 1;
|
|
93089
|
+
} else if (countStale) {
|
|
93090
|
+
// Nothing drawn to anchor on: the first page, like the very first ask.
|
|
93091
|
+
start = 0;
|
|
93092
|
+
end = budget - 1;
|
|
92932
93093
|
} else if (revalidating) {
|
|
92933
93094
|
// The items from before that the window draws, and the missing ones
|
|
92934
93095
|
// with them: one ask.
|
|
@@ -93130,6 +93291,7 @@ const useItemStore = ({
|
|
|
93130
93291
|
staleRanksRef.current.delete(rank);
|
|
93131
93292
|
rank++;
|
|
93132
93293
|
}
|
|
93294
|
+
countStaleRef.current = false;
|
|
93133
93295
|
setRefreshing(false);
|
|
93134
93296
|
return;
|
|
93135
93297
|
}
|
|
@@ -110173,7 +110335,7 @@ const SidePanel = ({
|
|
|
110173
110335
|
}) => {
|
|
110174
110336
|
import.meta.css = [css, "@jsenv/navi/src/layout/side_panel.jsx"];
|
|
110175
110337
|
const rest = dropPickerPopupProps(restGiven, "SidePanel");
|
|
110176
|
-
const onSwipePointerDown = closeByDrag ? createSwipeToClose(side) : null;
|
|
110338
|
+
const onSwipePointerDown = closeByDrag ? createSwipeToClose([side]) : null;
|
|
110177
110339
|
return jsx(Popup, {
|
|
110178
110340
|
mode: mode
|
|
110179
110341
|
// Spread rather than written: the collision warning between `signal` and
|