@jsenv/navi 0.30.35 → 0.30.36
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 +200 -60
- package/dist/dev/jsenv_navi.js.map +6 -6
- package/dist/jsenv_navi.js +175 -60
- package/dist/jsenv_navi.js.map +6 -6
- 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/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
|
{
|
|
@@ -77148,18 +77164,19 @@ const readRoom = () =>
|
|
|
77148
77164
|
visualViewportHeightSignal.peek() - getVirtualKeyboardOverlayHeight();
|
|
77149
77165
|
|
|
77150
77166
|
/**
|
|
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.
|
|
77167
|
+
* Pushing a popup docked to an edge (a side panel, a bottom sheet, a toast)
|
|
77168
|
+
* back the way it came in closes it: it follows the finger while it travels,
|
|
77169
|
+
* and letting go either finishes the departure or brings it back to rest —
|
|
77170
|
+
* whichever the travel was already heading to. A popup may be given other ways
|
|
77171
|
+
* out than that one: a toast is also thrown sideways.
|
|
77155
77172
|
*
|
|
77156
77173
|
* Reading the pointer is not done here: when a press becomes a gesture, which
|
|
77157
77174
|
* axis it leans on, how fast it was going when it was let go, who else has a
|
|
77158
77175
|
* claim on it (a field, a scroller with room left, a box inside the panel that
|
|
77159
77176
|
* travels the same way) is one gesture system for the whole codebase
|
|
77160
77177
|
* (@jsenv/dom's startDragToTravel). A panel closing is a travel of exactly one
|
|
77161
|
-
* box,
|
|
77162
|
-
* file has to say.
|
|
77178
|
+
* box, out by one of the edges it may leave by and nowhere else — which is all
|
|
77179
|
+
* this file has to say.
|
|
77163
77180
|
*
|
|
77164
77181
|
* The release travel is driven here (Web Animations) rather than handed to the
|
|
77165
77182
|
* exit transition the `animation` prop may install: its pace is what is left to
|
|
@@ -77174,7 +77191,7 @@ const readRoom = () =>
|
|
|
77174
77191
|
// less, so the panel keeps the same speed whenever the finger let go.
|
|
77175
77192
|
const TRAVEL_DURATION = 220;
|
|
77176
77193
|
|
|
77177
|
-
// Which way a popup
|
|
77194
|
+
// Which way a popup leaving by each side travels — read by the panel itself to
|
|
77178
77195
|
// say so in the DOM, which is how a box travelling INSIDE it (a row of slides,
|
|
77179
77196
|
// a route travel) knows that axis is already walked.
|
|
77180
77197
|
const SWIPE_AXIS_BY_SIDE = {
|
|
@@ -77183,12 +77200,27 @@ const SWIPE_AXIS_BY_SIDE = {
|
|
|
77183
77200
|
top: "y",
|
|
77184
77201
|
bottom: "y",
|
|
77185
77202
|
};
|
|
77186
|
-
// Which way the finger pushes to send the panel
|
|
77187
|
-
//
|
|
77203
|
+
// Which way the finger pushes to send the panel out by each side, in screen
|
|
77204
|
+
// coordinates: positive is right/down, the same sign the gesture reports.
|
|
77188
77205
|
const CLOSE_DIRECTION_BY_SIDE = { left: -1, right: 1, top: -1, bottom: 1 };
|
|
77189
77206
|
|
|
77190
77207
|
/**
|
|
77191
|
-
*
|
|
77208
|
+
* The axes a popup leaving by `sides` travels on, as the gesture and the DOM
|
|
77209
|
+
* say them: "x", "y" or "xy".
|
|
77210
|
+
*/
|
|
77211
|
+
const swipeAxesOf = (sides) => {
|
|
77212
|
+
const hasX = sides.some((side) => SWIPE_AXIS_BY_SIDE[side] === "x");
|
|
77213
|
+
const hasY = sides.some((side) => SWIPE_AXIS_BY_SIDE[side] === "y");
|
|
77214
|
+
if (hasX && hasY) {
|
|
77215
|
+
return "xy";
|
|
77216
|
+
}
|
|
77217
|
+
return hasX ? "x" : "y";
|
|
77218
|
+
};
|
|
77219
|
+
|
|
77220
|
+
/**
|
|
77221
|
+
* Builds the `pointerdown` handler of a popup that leaves by any of `sides`:
|
|
77222
|
+
* usually the one edge it is docked to, the way it came in; a toast is also
|
|
77223
|
+
* thrown sideways, and leaves the way it was thrown.
|
|
77192
77224
|
*
|
|
77193
77225
|
* `grip` says where the gesture may start, as a selector the pressed element is
|
|
77194
77226
|
* matched against with `closest`. A popup that names one is held THERE and
|
|
@@ -77196,11 +77228,17 @@ const CLOSE_DIRECTION_BY_SIDE = { left: -1, right: 1, top: -1, bottom: 1 };
|
|
|
77196
77228
|
* and a press on it is that content's — including gestures of its own, which
|
|
77197
77229
|
* this file has no way of knowing about. A popup that names no grip is pushed
|
|
77198
77230
|
* 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
|
-
|
|
77231
|
+
* panel showing a page, a toast).
|
|
77232
|
+
*/
|
|
77233
|
+
const createSwipeToClose = (sides, { grip } = {}) => {
|
|
77234
|
+
const axes = swipeAxesOf(sides);
|
|
77235
|
+
// The side a push along `axis` towards `sign` leaves by, if it leaves at all.
|
|
77236
|
+
const sideTowards = (axis, sign) =>
|
|
77237
|
+
sides.find(
|
|
77238
|
+
(side) =>
|
|
77239
|
+
SWIPE_AXIS_BY_SIDE[side] === axis &&
|
|
77240
|
+
CLOSE_DIRECTION_BY_SIDE[side] === sign,
|
|
77241
|
+
);
|
|
77204
77242
|
|
|
77205
77243
|
return (pointerDownEvent) => {
|
|
77206
77244
|
const panelEl = pointerDownEvent.currentTarget;
|
|
@@ -77217,6 +77255,11 @@ const createSwipeToClose = (side, { grip } = {}) => {
|
|
|
77217
77255
|
}
|
|
77218
77256
|
}
|
|
77219
77257
|
|
|
77258
|
+
// Decided when the finger picks its axis (onStart), once for the gesture.
|
|
77259
|
+
let axis;
|
|
77260
|
+
// How far the panel travels to be out, for each way it can go on that axis.
|
|
77261
|
+
const distanceOutBySign = new Map();
|
|
77262
|
+
|
|
77220
77263
|
// How far the panel has been pulled, as the style showing it — written on
|
|
77221
77264
|
// the panel while the finger drives it, handed to the release travel as
|
|
77222
77265
|
// its two keyframes. The gesture reports a distance in screen coordinates,
|
|
@@ -77226,7 +77269,7 @@ const createSwipeToClose = (side, { grip } = {}) => {
|
|
|
77226
77269
|
// composes under the placement instead of replacing it.
|
|
77227
77270
|
const styleAt = (distance) => ({
|
|
77228
77271
|
transform: translateOf(axis, distance),
|
|
77229
|
-
clipPath: clipOf(
|
|
77272
|
+
clipPath: clipOf(sides, axis, distance),
|
|
77230
77273
|
});
|
|
77231
77274
|
const paint = (distance) => {
|
|
77232
77275
|
Object.assign(panelEl.style, styleAt(distance));
|
|
@@ -77238,21 +77281,22 @@ const createSwipeToClose = (side, { grip } = {}) => {
|
|
|
77238
77281
|
};
|
|
77239
77282
|
// The resting value is written first and the animation only covers the way
|
|
77240
77283
|
// 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
|
-
|
|
77284
|
+
// animation hands the panel back to its own style. `span` is the full travel
|
|
77285
|
+
// on that side, which the duration is a fraction of.
|
|
77286
|
+
const travelTo = (from, to, span, onArrival) => {
|
|
77243
77287
|
const covered = from > to ? from - to : to - from;
|
|
77244
77288
|
paint(to);
|
|
77245
77289
|
const animation = panelEl.animate([styleAt(from), styleAt(to)], {
|
|
77246
|
-
duration: (covered /
|
|
77290
|
+
duration: (covered / span) * TRAVEL_DURATION,
|
|
77247
77291
|
easing: "ease-out",
|
|
77248
77292
|
});
|
|
77249
77293
|
animation.finished.then(onArrival, () => {});
|
|
77250
77294
|
};
|
|
77251
|
-
const close = (event) => {
|
|
77295
|
+
const close = (event, out, span) => {
|
|
77252
77296
|
triggerNaviCommand(panelEl, "--navi-close", event);
|
|
77253
77297
|
if (panelEl.getAttribute("aria-expanded") === "true") {
|
|
77254
77298
|
// Refused: the panel is staying, so it goes back where it was.
|
|
77255
|
-
travelTo(
|
|
77299
|
+
travelTo(out, 0, span, restore);
|
|
77256
77300
|
return;
|
|
77257
77301
|
}
|
|
77258
77302
|
restore();
|
|
@@ -77260,79 +77304,94 @@ const createSwipeToClose = (side, { grip } = {}) => {
|
|
|
77260
77304
|
|
|
77261
77305
|
startDragToTravel(pointerDownEvent, {
|
|
77262
77306
|
element: panelEl,
|
|
77263
|
-
axes
|
|
77264
|
-
onStart: ({ sign, target }) => {
|
|
77265
|
-
|
|
77307
|
+
axes,
|
|
77308
|
+
onStart: ({ axis: gestureAxis, sign, target }) => {
|
|
77309
|
+
const side = sideTowards(gestureAxis, sign);
|
|
77310
|
+
if (!side) {
|
|
77266
77311
|
// Pushing the panel further in is not a travel: there is nowhere that
|
|
77267
77312
|
// way, and reading it as one would make the panel lean at every
|
|
77268
77313
|
// gesture that starts by going the wrong way.
|
|
77269
77314
|
return false;
|
|
77270
77315
|
}
|
|
77271
|
-
const size =
|
|
77316
|
+
const size = distanceOut(panelEl, side);
|
|
77272
77317
|
if (!size) {
|
|
77273
77318
|
return false;
|
|
77274
77319
|
}
|
|
77275
77320
|
// Something else with a better claim on the gesture: a scroller between
|
|
77276
77321
|
// the finger and the panel, with room left that way.
|
|
77277
|
-
if (scrollRoomTowards(target, panelEl,
|
|
77322
|
+
if (scrollRoomTowards(target, panelEl, gestureAxis, sign)) {
|
|
77278
77323
|
return false;
|
|
77279
77324
|
}
|
|
77325
|
+
axis = gestureAxis;
|
|
77326
|
+
distanceOutBySign.set(sign, size);
|
|
77327
|
+
const otherSide = sideTowards(gestureAxis, -sign);
|
|
77328
|
+
if (otherSide) {
|
|
77329
|
+
distanceOutBySign.set(-sign, distanceOut(panelEl, otherSide));
|
|
77330
|
+
}
|
|
77280
77331
|
panelEl.style.transitionProperty = "none";
|
|
77281
|
-
//
|
|
77282
|
-
// leans against a wall and comes back.
|
|
77332
|
+
// Only the ways out lead anywhere: pulling towards a side the panel
|
|
77333
|
+
// does not leave by leans against a wall and comes back.
|
|
77283
77334
|
return {
|
|
77284
77335
|
size,
|
|
77285
|
-
travelBack:
|
|
77286
|
-
travelOn:
|
|
77336
|
+
travelBack: Boolean(sideTowards(gestureAxis, 1)),
|
|
77337
|
+
travelOn: Boolean(sideTowards(gestureAxis, -1)),
|
|
77287
77338
|
};
|
|
77288
77339
|
},
|
|
77289
77340
|
onPull: ({ pulled }) => {
|
|
77290
77341
|
paint(pulled);
|
|
77291
77342
|
},
|
|
77292
|
-
onEnd: ({ pulled, size, travels, event }) => {
|
|
77343
|
+
onEnd: ({ pulled, size, sign, travels, event }) => {
|
|
77293
77344
|
if (travels) {
|
|
77294
|
-
|
|
77345
|
+
const span = distanceOutBySign.get(sign);
|
|
77346
|
+
travelTo(pulled, span * sign, span, () =>
|
|
77347
|
+
close(event, span * sign, span),
|
|
77348
|
+
);
|
|
77295
77349
|
return;
|
|
77296
77350
|
}
|
|
77297
|
-
travelTo(pulled, 0, restore);
|
|
77351
|
+
travelTo(pulled, 0, size, restore);
|
|
77298
77352
|
},
|
|
77299
77353
|
});
|
|
77300
77354
|
};
|
|
77301
77355
|
};
|
|
77302
77356
|
|
|
77303
|
-
|
|
77357
|
+
// What takes the popup out of sight: its own size, plus the gap a margin keeps
|
|
77358
|
+
// between it and the edge it is pushed through — the room applyNewPosition
|
|
77359
|
+
// publishes on that side. Travelling its size alone, a popup kept off its edge
|
|
77360
|
+
// (a toast 12px above a bar) would stop with that much of itself still showing.
|
|
77361
|
+
const distanceOut = (element, side) => {
|
|
77304
77362
|
const rect = element.getBoundingClientRect();
|
|
77305
|
-
|
|
77363
|
+
const size = SWIPE_AXIS_BY_SIDE[side] === "x" ? rect.width : rect.height;
|
|
77364
|
+
const room = parseFloat(
|
|
77365
|
+
element.style.getPropertyValue(`--container-position-room-${side}`),
|
|
77366
|
+
);
|
|
77367
|
+
return Number.isNaN(room) ? size : size + room;
|
|
77306
77368
|
};
|
|
77307
77369
|
const translateOf = (axis, distance) =>
|
|
77308
77370
|
axis === "x"
|
|
77309
77371
|
? `translate(${distance}px, 0px)`
|
|
77310
77372
|
: `translate(0px, ${distance}px)`;
|
|
77311
77373
|
// 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.
|
|
77374
|
+
// finger decides instead of a transition: each edge the popup may leave by on
|
|
77375
|
+
// the axis it travels, as popup_css.js placed it (--x-popup-cut-*-at-area — on
|
|
77376
|
+
// the area's edge where a band of glass lies past it, 100vmax out where the
|
|
77377
|
+
// window's own edge does the cutting), moved by what has been pulled towards
|
|
77378
|
+
// it, so the cut holds that line while the box travels under it, whichever way.
|
|
77379
|
+
// Every other side is left far outside the box — the pull never takes the popup
|
|
77380
|
+
// past them, and cutting there would only shave what it legitimately paints
|
|
77381
|
+
// outside its own box. Reading the policy from popup_css.js rather than the
|
|
77382
|
+
// rooms keeps the release travel (a Web Animation on transform AND clip-path,
|
|
77383
|
+
// two clocks) from animating a cut the entrance would not.
|
|
77322
77384
|
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})`;
|
|
77385
|
+
const INSET_ORDER = ["top", "right", "bottom", "left"];
|
|
77386
|
+
const clipOf = (sides, axis, distance) => {
|
|
77387
|
+
const insets = INSET_ORDER.map((side) => {
|
|
77388
|
+
if (!sides.includes(side) || SWIPE_AXIS_BY_SIDE[side] !== axis) {
|
|
77389
|
+
return UNCUT;
|
|
77390
|
+
}
|
|
77391
|
+
const towards = distance * CLOSE_DIRECTION_BY_SIDE[side];
|
|
77392
|
+
return `calc(var(--x-popup-cut-${side}-at-area, ${UNCUT}) + ${towards}px)`;
|
|
77393
|
+
});
|
|
77394
|
+
return `inset(${insets.join(" ")})`;
|
|
77336
77395
|
};
|
|
77337
77396
|
|
|
77338
77397
|
/**
|
|
@@ -79122,7 +79181,7 @@ const useDialogProps = props => {
|
|
|
79122
79181
|
// actually resting on an edge: anywhere else the gesture would send the
|
|
79123
79182
|
// dialog somewhere it never came from.
|
|
79124
79183
|
const swipeToCloseSide = isDocked && (parsedPositionArea.y === "top" || parsedPositionArea.y === "bottom") ? parsedPositionArea.y : null;
|
|
79125
|
-
const onSwipePointerDown = swipeToCloseSide ? createSwipeToClose(swipeToCloseSide, {
|
|
79184
|
+
const onSwipePointerDown = swipeToCloseSide ? createSwipeToClose([swipeToCloseSide], {
|
|
79126
79185
|
grip: DOCKED_SWIPE_GRIP
|
|
79127
79186
|
}) : null;
|
|
79128
79187
|
// A corner sitting exactly on the container's own corner must not be
|
|
@@ -80302,6 +80361,16 @@ const css$C = /* css */`
|
|
|
80302
80361
|
to filter). An entrance animation moves the popover through scale and
|
|
80303
80362
|
transform instead, which compose under it: see popup_css.js. */
|
|
80304
80363
|
|
|
80364
|
+
/* What a touch may do on a popover pushed sideways to close it (see
|
|
80365
|
+
swipe_to_close.js): the push takes the sideways axis, the page keeps the
|
|
80366
|
+
vertical scroll. A push up or down travels the way the page scrolls and
|
|
80367
|
+
cannot give that axis away: the browser keeps it, and the gesture is
|
|
80368
|
+
settled by keepTouchRefusable. */
|
|
80369
|
+
&[data-swipe-to-close~="left"],
|
|
80370
|
+
&[data-swipe-to-close~="right"] {
|
|
80371
|
+
touch-action: pan-y;
|
|
80372
|
+
}
|
|
80373
|
+
|
|
80305
80374
|
/* The clamped max, not --popover-maxmax-*: that one ignores what the
|
|
80306
80375
|
placement left available, which reaches here through
|
|
80307
80376
|
--container-position-remaining-* (see applyNewPosition) — the min()
|
|
@@ -80532,6 +80601,20 @@ const css$C = /* css */`
|
|
|
80532
80601
|
* overlap with the anchor; wrap it in `inset(...)` (e.g. `inset(top)`,
|
|
80533
80602
|
* `inset(top-left)`) to overlap the anchor instead (edges touching or
|
|
80534
80603
|
* inside it).
|
|
80604
|
+
* @param {boolean|Array<"top"|"right"|"bottom"|"left">} [props.closeByDrag=false] -
|
|
80605
|
+
* Pushing the popover out of its container closes it: it follows the pointer
|
|
80606
|
+
* and, when released, finishes leaving or comes back to rest. `true` pushes
|
|
80607
|
+
* it back through the edge it is docked to, the word `positionArea` leads
|
|
80608
|
+
* with (`"bottom"`, `"bottom-start"`: pushed down; `"left-end"`: pushed
|
|
80609
|
+
* left); a centred popover rests on no edge and is not pushed (warned in
|
|
80610
|
+
* dev). A list names the sides it may leave by, and it leaves the way it was
|
|
80611
|
+
* pushed: `["bottom", "left", "right"]` for a toast pushed back down behind
|
|
80612
|
+
* the bar or thrown sideways. A popover placed against an anchor at its
|
|
80613
|
+
* opening is not pushed either way (warned in dev). It is pushed from its
|
|
80614
|
+
* whole surface, so it suits a popover holding little else: a button inside
|
|
80615
|
+
* keeps its tap, and a press that turns into a push swallows the click it
|
|
80616
|
+
* would have left. The close keeps, as a cross's does. See docs/interactions.md
|
|
80617
|
+
* for a swipe on the popover's own element, which does not move it.
|
|
80535
80618
|
* @param {string} [props.positionAreaFixed] - Overrides `positionArea` once
|
|
80536
80619
|
* the popover has actually been positioned once, so a live reposition
|
|
80537
80620
|
* (e.g. anchor moved) doesn't jump to a different side.
|
|
@@ -80985,6 +81068,7 @@ const usePopoverProps = props => {
|
|
|
80985
81068
|
positionAreaWhenAnchorIsInvalid,
|
|
80986
81069
|
marginWithContainer = 0,
|
|
80987
81070
|
container = "app",
|
|
81071
|
+
closeByDrag = false,
|
|
80988
81072
|
// "close" → pointer press outside closes (a plain close = commit)
|
|
80989
81073
|
// "cancel" → pointer press outside closes AS A CANCEL (revert)
|
|
80990
81074
|
// "capture"→ absorb the press, stay open
|
|
@@ -81118,6 +81202,7 @@ const usePopoverProps = props => {
|
|
|
81118
81202
|
return popupGroup.register(openController);
|
|
81119
81203
|
}, [popupGroup, openController]);
|
|
81120
81204
|
const hasBackdrop = pressOutside !== "ignore" && backdrop && !popupGroup;
|
|
81205
|
+
const swipeToCloseSides = resolveSwipeToCloseSides(closeByDrag, positionArea);
|
|
81121
81206
|
if (!popupGroup && !backdrop && pressOutside === "capture") {
|
|
81122
81207
|
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
81208
|
}
|
|
@@ -81267,6 +81352,9 @@ const usePopoverProps = props => {
|
|
|
81267
81352
|
}
|
|
81268
81353
|
const hasAnchorElement = Boolean(anchorElement);
|
|
81269
81354
|
anchorElementRef.current = anchorElement || null;
|
|
81355
|
+
if (swipeToCloseSides && hasAnchorElement) {
|
|
81356
|
+
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);
|
|
81357
|
+
}
|
|
81270
81358
|
// getPositionedParent already resolves to document.documentElement for
|
|
81271
81359
|
// the via-attribute renderer on its own (popoverEl's own `popover`
|
|
81272
81360
|
// attribute, present from render regardless of whether it's actually
|
|
@@ -81961,6 +82049,7 @@ const usePopoverProps = props => {
|
|
|
81961
82049
|
}
|
|
81962
82050
|
}
|
|
81963
82051
|
});
|
|
82052
|
+
const swipeToClose = swipeToCloseSides ? createSwipeToClose(swipeToCloseSides) : null;
|
|
81964
82053
|
Object.assign(contentProps, {
|
|
81965
82054
|
id,
|
|
81966
82055
|
tabIndex,
|
|
@@ -82039,6 +82128,26 @@ const usePopoverProps = props => {
|
|
|
82039
82128
|
onKeyDown?.(e);
|
|
82040
82129
|
onKeyDownShortcuts(e);
|
|
82041
82130
|
},
|
|
82131
|
+
...(swipeToClose ? {
|
|
82132
|
+
"data-swipe-to-close": swipeToCloseSides.join(" "),
|
|
82133
|
+
// The axes it travels on when pushed out, said to the shared gesture
|
|
82134
|
+
// layer before any finger lands (see side_panel.jsx, which says the
|
|
82135
|
+
// same for the same reasons).
|
|
82136
|
+
"data-drag-travel": swipeAxesOf(swipeToCloseSides),
|
|
82137
|
+
"data-travel-by-drag": swipeAxesOf(swipeToCloseSides),
|
|
82138
|
+
"onTouchMove": e => {
|
|
82139
|
+
rest.onTouchMove?.(e);
|
|
82140
|
+
keepTouchRefusable(e);
|
|
82141
|
+
},
|
|
82142
|
+
"onPointerDown": e => {
|
|
82143
|
+
rest.onPointerDown?.(e);
|
|
82144
|
+
// Placed against an anchor at this opening: it is not in its
|
|
82145
|
+
// container to be pushed out of (warned at the opening).
|
|
82146
|
+
if (!anchorElementRef.current) {
|
|
82147
|
+
swipeToClose(e);
|
|
82148
|
+
}
|
|
82149
|
+
}
|
|
82150
|
+
} : null),
|
|
82042
82151
|
// A popup opened from inside this one is in front of it, not a peer of
|
|
82043
82152
|
// it: the group stops here.
|
|
82044
82153
|
"children": popupGroup ? jsx(PopupGroupContext.Provider, {
|
|
@@ -82112,6 +82221,37 @@ const resolvePositionAreaAndAnimationKind = ({
|
|
|
82112
82221
|
Popover.Close = PopupClose;
|
|
82113
82222
|
Popover.Title = PopupTitle;
|
|
82114
82223
|
|
|
82224
|
+
// The sides a popover pushed away leaves by, or null when it is not pushed.
|
|
82225
|
+
const resolveSwipeToCloseSides = (closeByDrag, positionArea) => {
|
|
82226
|
+
if (!closeByDrag) {
|
|
82227
|
+
return null;
|
|
82228
|
+
}
|
|
82229
|
+
if (Array.isArray(closeByDrag)) {
|
|
82230
|
+
const sides = closeByDrag.filter(side => side in SWIPE_AXIS_BY_SIDE);
|
|
82231
|
+
if (sides.length !== closeByDrag.length) {
|
|
82232
|
+
console.warn(`Popover: closeByDrag={${JSON.stringify(closeByDrag)}} names something that is not a side; the sides are "top", "right", "bottom" and "left".`);
|
|
82233
|
+
}
|
|
82234
|
+
return sides.length ? sides : null;
|
|
82235
|
+
}
|
|
82236
|
+
const side = dockedSideOf(positionArea);
|
|
82237
|
+
if (!side) {
|
|
82238
|
+
{
|
|
82239
|
+
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"]}.`);
|
|
82240
|
+
}
|
|
82241
|
+
return null;
|
|
82242
|
+
}
|
|
82243
|
+
return [side];
|
|
82244
|
+
};
|
|
82245
|
+
const INSET_TOKEN_RE = /^inset\(\s*([a-z-]+)\s*\)$/;
|
|
82246
|
+
// The edge a popover docked to its container rests on: the word its
|
|
82247
|
+
// positionArea leads with, "bottom-start" resting on the bottom and "left-end"
|
|
82248
|
+
// on the left. "center" rests on none.
|
|
82249
|
+
const dockedSideOf = positionArea => {
|
|
82250
|
+
const token = INSET_TOKEN_RE.exec(positionArea)?.[1] ?? positionArea;
|
|
82251
|
+
const side = token.split("-")[0];
|
|
82252
|
+
return side in SWIPE_AXIS_BY_SIDE ? side : null;
|
|
82253
|
+
};
|
|
82254
|
+
|
|
82115
82255
|
// The placement needs pixels: a value only CSS could evaluate (a percentage, a
|
|
82116
82256
|
// calc()…) would put the popover at NaN, which is the top of the page.
|
|
82117
82257
|
const resolveMarginInPixels = (margin, name) => {
|
|
@@ -110173,7 +110313,7 @@ const SidePanel = ({
|
|
|
110173
110313
|
}) => {
|
|
110174
110314
|
import.meta.css = [css, "@jsenv/navi/src/layout/side_panel.jsx"];
|
|
110175
110315
|
const rest = dropPickerPopupProps(restGiven, "SidePanel");
|
|
110176
|
-
const onSwipePointerDown = closeByDrag ? createSwipeToClose(side) : null;
|
|
110316
|
+
const onSwipePointerDown = closeByDrag ? createSwipeToClose([side]) : null;
|
|
110177
110317
|
return jsx(Popup, {
|
|
110178
110318
|
mode: mode
|
|
110179
110319
|
// Spread rather than written: the collision warning between `signal` and
|