@jsenv/navi 0.30.34 → 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 +235 -93
- package/dist/dev/jsenv_navi.js.map +7 -7
- package/dist/jsenv_navi.js +210 -93
- package/dist/jsenv_navi.js.map +7 -7
- 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
|
{
|
|
@@ -42836,9 +42852,23 @@ const RouteTravel = ({
|
|
|
42836
42852
|
params
|
|
42837
42853
|
}) => new URL(route.buildUrl(params), window.location.href).pathname;
|
|
42838
42854
|
|
|
42855
|
+
// The pane laid out as the page on screen for the length of a movement, let go of
|
|
42856
|
+
// when the row rests. Not what readers are told the page held is
|
|
42857
|
+
// (route_travel_hold.js): that follows the hand, and goes at its lift.
|
|
42858
|
+
const heldPaneRef = useRef(null);
|
|
42859
|
+
const releasePaneAtRest = () => {
|
|
42860
|
+
const heldPane = heldPaneRef.current;
|
|
42861
|
+
if (heldPane) {
|
|
42862
|
+
heldPaneRef.current = null;
|
|
42863
|
+
heldPane.removeAttribute("data-route-row-held");
|
|
42864
|
+
}
|
|
42865
|
+
};
|
|
42866
|
+
|
|
42839
42867
|
// One movement, whoever set it off, from the first thing that moves to the row
|
|
42840
|
-
// at rest. Null when nothing moves.
|
|
42841
|
-
|
|
42868
|
+
// at rest. Null when nothing moves. `paneAtRest` is the pane the row rests on as it
|
|
42869
|
+
// begins: laid out as the page on screen until the row rests again, wherever the
|
|
42870
|
+
// address goes meanwhile (see the CSS).
|
|
42871
|
+
const beginMovement = (fields, paneAtRest) => {
|
|
42842
42872
|
const row = rowRef.current;
|
|
42843
42873
|
if (movementRef.current) {
|
|
42844
42874
|
Object.assign(movementRef.current, fields);
|
|
@@ -42853,6 +42883,11 @@ const RouteTravel = ({
|
|
|
42853
42883
|
...fields
|
|
42854
42884
|
};
|
|
42855
42885
|
movementRef.current = movement;
|
|
42886
|
+
// In a window the page is a picture, contained with what it holds.
|
|
42887
|
+
if (!windowed && paneAtRest) {
|
|
42888
|
+
heldPaneRef.current = paneAtRest;
|
|
42889
|
+
paneAtRest.setAttribute("data-route-row-held", "");
|
|
42890
|
+
}
|
|
42856
42891
|
if (movement.scrollsDocument) {
|
|
42857
42892
|
// Whatever the browser does with the offset meanwhile (clamping it to a page
|
|
42858
42893
|
// shorter than the trip) is not the reader scrolling, and the address it
|
|
@@ -43090,24 +43125,6 @@ const RouteTravel = ({
|
|
|
43090
43125
|
rowRef.current.removeAttribute("data-route-row-travelling");
|
|
43091
43126
|
};
|
|
43092
43127
|
|
|
43093
|
-
// The address runs ahead of a hand to build the page it pulls towards (onScroll):
|
|
43094
|
-
// the pane under the hand stays laid out as the page on screen until the hand lets
|
|
43095
|
-
// go, whatever `inert` says (see the CSS). What reads the page held is told apart
|
|
43096
|
-
// (route_travel_hold.js), and both are let go of together.
|
|
43097
|
-
const heldPaneRef = useRef(null);
|
|
43098
|
-
const holdPaneOnScreen = pane => {
|
|
43099
|
-
heldPaneRef.current = pane;
|
|
43100
|
-
pane.setAttribute("data-route-row-held", "");
|
|
43101
|
-
};
|
|
43102
|
-
const releaseHold = () => {
|
|
43103
|
-
releaseHeldPage();
|
|
43104
|
-
const heldPane = heldPaneRef.current;
|
|
43105
|
-
if (heldPane) {
|
|
43106
|
-
heldPaneRef.current = null;
|
|
43107
|
-
heldPane.removeAttribute("data-route-row-held");
|
|
43108
|
-
}
|
|
43109
|
-
};
|
|
43110
|
-
|
|
43111
43128
|
// Every column back where the map says, and the row on the page of the address, in
|
|
43112
43129
|
// one go: that page stands at the same place on screen either way.
|
|
43113
43130
|
const putRowBack = movement => {
|
|
@@ -43136,7 +43153,7 @@ const RouteTravel = ({
|
|
|
43136
43153
|
const position = row.scrollLeft;
|
|
43137
43154
|
movement.rowTaken = true;
|
|
43138
43155
|
// A travel no hand set off holds nothing: it goes where it goes.
|
|
43139
|
-
|
|
43156
|
+
releaseHeldPage();
|
|
43140
43157
|
row.style.setProperty("--navi-route-row-stand", `${-position}px`);
|
|
43141
43158
|
row.setAttribute("data-route-row-taken", "");
|
|
43142
43159
|
afterFrames(2, () => {
|
|
@@ -43168,7 +43185,8 @@ const RouteTravel = ({
|
|
|
43168
43185
|
putRowBack(movement);
|
|
43169
43186
|
row.removeAttribute("data-route-row-taken");
|
|
43170
43187
|
publishRouteRowMove(null);
|
|
43171
|
-
|
|
43188
|
+
releaseHeldPage();
|
|
43189
|
+
releasePaneAtRest();
|
|
43172
43190
|
if (liveRef.current) {
|
|
43173
43191
|
liveRef.current.style.top = "";
|
|
43174
43192
|
}
|
|
@@ -43281,9 +43299,11 @@ const RouteTravel = ({
|
|
|
43281
43299
|
return;
|
|
43282
43300
|
}
|
|
43283
43301
|
const movementBefore = movementRef.current;
|
|
43302
|
+
// The row still rests on the page answered before. Held in the commit that made
|
|
43303
|
+
// its pane inert, so no frame paints it contained.
|
|
43284
43304
|
const movement = beginMovement({
|
|
43285
43305
|
travelling: true
|
|
43286
|
-
});
|
|
43306
|
+
}, panesOf(row)[pages.findIndex(each => samePage(each, answered))]);
|
|
43287
43307
|
if (movementBefore?.byHand && !movementBefore.rowTaken) {
|
|
43288
43308
|
sayRowMoves();
|
|
43289
43309
|
takeRowFromHand(movement);
|
|
@@ -43351,7 +43371,7 @@ const RouteTravel = ({
|
|
|
43351
43371
|
}
|
|
43352
43372
|
const movement = moving || beginMovement({
|
|
43353
43373
|
byHand: true
|
|
43354
|
-
});
|
|
43374
|
+
}, pane);
|
|
43355
43375
|
sayRowMoves();
|
|
43356
43376
|
if (movement.travelling || !travelByDrag || !pane) {
|
|
43357
43377
|
return;
|
|
@@ -43388,12 +43408,8 @@ const RouteTravel = ({
|
|
|
43388
43408
|
return;
|
|
43389
43409
|
}
|
|
43390
43410
|
leavePictureOfLive(movement);
|
|
43391
|
-
} else {
|
|
43392
|
-
|
|
43393
|
-
return;
|
|
43394
|
-
}
|
|
43395
|
-
// Before the address moves: the render that makes it inert must find it held.
|
|
43396
|
-
holdPaneOnScreen(pane);
|
|
43411
|
+
} else if (paneTowards.hasAttribute("data-built")) {
|
|
43412
|
+
return;
|
|
43397
43413
|
}
|
|
43398
43414
|
holdPage(pages[currentIndex], page);
|
|
43399
43415
|
askFor(page);
|
|
@@ -43424,8 +43440,9 @@ const RouteTravel = ({
|
|
|
43424
43440
|
askForPageOfRow(index, movement);
|
|
43425
43441
|
}
|
|
43426
43442
|
// After the address: a pull coming back to its page must not light the tab the
|
|
43427
|
-
// address ran ahead to, even for a render.
|
|
43428
|
-
|
|
43443
|
+
// address ran ahead to, even for a render. Only what readers are told: the
|
|
43444
|
+
// panes keep their layout until the row rests.
|
|
43445
|
+
releaseHeldPage();
|
|
43429
43446
|
};
|
|
43430
43447
|
const stopWatching = watchRowScroll(row, {
|
|
43431
43448
|
onScroll,
|
|
@@ -43481,7 +43498,8 @@ const RouteTravel = ({
|
|
|
43481
43498
|
row.removeAttribute("data-route-row-travelling");
|
|
43482
43499
|
movementRef.current = null;
|
|
43483
43500
|
publishRouteRowMove(null);
|
|
43484
|
-
|
|
43501
|
+
releaseHeldPage();
|
|
43502
|
+
releasePaneAtRest();
|
|
43485
43503
|
};
|
|
43486
43504
|
}, [windowed]);
|
|
43487
43505
|
|
|
@@ -77146,18 +77164,19 @@ const readRoom = () =>
|
|
|
77146
77164
|
visualViewportHeightSignal.peek() - getVirtualKeyboardOverlayHeight();
|
|
77147
77165
|
|
|
77148
77166
|
/**
|
|
77149
|
-
* Pushing a popup docked to an edge (a side panel, a bottom sheet
|
|
77150
|
-
* it came in closes it: it follows the finger while it travels,
|
|
77151
|
-
* either finishes the departure or brings it back to rest —
|
|
77152
|
-
* 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.
|
|
77153
77172
|
*
|
|
77154
77173
|
* Reading the pointer is not done here: when a press becomes a gesture, which
|
|
77155
77174
|
* axis it leans on, how fast it was going when it was let go, who else has a
|
|
77156
77175
|
* claim on it (a field, a scroller with room left, a box inside the panel that
|
|
77157
77176
|
* travels the same way) is one gesture system for the whole codebase
|
|
77158
77177
|
* (@jsenv/dom's startDragToTravel). A panel closing is a travel of exactly one
|
|
77159
|
-
* box,
|
|
77160
|
-
* 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.
|
|
77161
77180
|
*
|
|
77162
77181
|
* The release travel is driven here (Web Animations) rather than handed to the
|
|
77163
77182
|
* exit transition the `animation` prop may install: its pace is what is left to
|
|
@@ -77172,7 +77191,7 @@ const readRoom = () =>
|
|
|
77172
77191
|
// less, so the panel keeps the same speed whenever the finger let go.
|
|
77173
77192
|
const TRAVEL_DURATION = 220;
|
|
77174
77193
|
|
|
77175
|
-
// Which way a popup
|
|
77194
|
+
// Which way a popup leaving by each side travels — read by the panel itself to
|
|
77176
77195
|
// say so in the DOM, which is how a box travelling INSIDE it (a row of slides,
|
|
77177
77196
|
// a route travel) knows that axis is already walked.
|
|
77178
77197
|
const SWIPE_AXIS_BY_SIDE = {
|
|
@@ -77181,12 +77200,27 @@ const SWIPE_AXIS_BY_SIDE = {
|
|
|
77181
77200
|
top: "y",
|
|
77182
77201
|
bottom: "y",
|
|
77183
77202
|
};
|
|
77184
|
-
// Which way the finger pushes to send the panel
|
|
77185
|
-
//
|
|
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.
|
|
77186
77205
|
const CLOSE_DIRECTION_BY_SIDE = { left: -1, right: 1, top: -1, bottom: 1 };
|
|
77187
77206
|
|
|
77188
77207
|
/**
|
|
77189
|
-
*
|
|
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.
|
|
77190
77224
|
*
|
|
77191
77225
|
* `grip` says where the gesture may start, as a selector the pressed element is
|
|
77192
77226
|
* matched against with `closest`. A popup that names one is held THERE and
|
|
@@ -77194,11 +77228,17 @@ const CLOSE_DIRECTION_BY_SIDE = { left: -1, right: 1, top: -1, bottom: 1 };
|
|
|
77194
77228
|
* and a press on it is that content's — including gestures of its own, which
|
|
77195
77229
|
* this file has no way of knowing about. A popup that names no grip is pushed
|
|
77196
77230
|
* from its whole surface, which only suits one made of nothing else (a side
|
|
77197
|
-
* panel showing a page).
|
|
77198
|
-
*/
|
|
77199
|
-
const createSwipeToClose = (
|
|
77200
|
-
const
|
|
77201
|
-
|
|
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
|
+
);
|
|
77202
77242
|
|
|
77203
77243
|
return (pointerDownEvent) => {
|
|
77204
77244
|
const panelEl = pointerDownEvent.currentTarget;
|
|
@@ -77215,6 +77255,11 @@ const createSwipeToClose = (side, { grip } = {}) => {
|
|
|
77215
77255
|
}
|
|
77216
77256
|
}
|
|
77217
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
|
+
|
|
77218
77263
|
// How far the panel has been pulled, as the style showing it — written on
|
|
77219
77264
|
// the panel while the finger drives it, handed to the release travel as
|
|
77220
77265
|
// its two keyframes. The gesture reports a distance in screen coordinates,
|
|
@@ -77224,7 +77269,7 @@ const createSwipeToClose = (side, { grip } = {}) => {
|
|
|
77224
77269
|
// composes under the placement instead of replacing it.
|
|
77225
77270
|
const styleAt = (distance) => ({
|
|
77226
77271
|
transform: translateOf(axis, distance),
|
|
77227
|
-
clipPath: clipOf(
|
|
77272
|
+
clipPath: clipOf(sides, axis, distance),
|
|
77228
77273
|
});
|
|
77229
77274
|
const paint = (distance) => {
|
|
77230
77275
|
Object.assign(panelEl.style, styleAt(distance));
|
|
@@ -77236,21 +77281,22 @@ const createSwipeToClose = (side, { grip } = {}) => {
|
|
|
77236
77281
|
};
|
|
77237
77282
|
// The resting value is written first and the animation only covers the way
|
|
77238
77283
|
// to it: both end on the same number, so nothing is seen changing when the
|
|
77239
|
-
// animation hands the panel back to its own style.
|
|
77240
|
-
|
|
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) => {
|
|
77241
77287
|
const covered = from > to ? from - to : to - from;
|
|
77242
77288
|
paint(to);
|
|
77243
77289
|
const animation = panelEl.animate([styleAt(from), styleAt(to)], {
|
|
77244
|
-
duration: (covered /
|
|
77290
|
+
duration: (covered / span) * TRAVEL_DURATION,
|
|
77245
77291
|
easing: "ease-out",
|
|
77246
77292
|
});
|
|
77247
77293
|
animation.finished.then(onArrival, () => {});
|
|
77248
77294
|
};
|
|
77249
|
-
const close = (event) => {
|
|
77295
|
+
const close = (event, out, span) => {
|
|
77250
77296
|
triggerNaviCommand(panelEl, "--navi-close", event);
|
|
77251
77297
|
if (panelEl.getAttribute("aria-expanded") === "true") {
|
|
77252
77298
|
// Refused: the panel is staying, so it goes back where it was.
|
|
77253
|
-
travelTo(
|
|
77299
|
+
travelTo(out, 0, span, restore);
|
|
77254
77300
|
return;
|
|
77255
77301
|
}
|
|
77256
77302
|
restore();
|
|
@@ -77258,79 +77304,94 @@ const createSwipeToClose = (side, { grip } = {}) => {
|
|
|
77258
77304
|
|
|
77259
77305
|
startDragToTravel(pointerDownEvent, {
|
|
77260
77306
|
element: panelEl,
|
|
77261
|
-
axes
|
|
77262
|
-
onStart: ({ sign, target }) => {
|
|
77263
|
-
|
|
77307
|
+
axes,
|
|
77308
|
+
onStart: ({ axis: gestureAxis, sign, target }) => {
|
|
77309
|
+
const side = sideTowards(gestureAxis, sign);
|
|
77310
|
+
if (!side) {
|
|
77264
77311
|
// Pushing the panel further in is not a travel: there is nowhere that
|
|
77265
77312
|
// way, and reading it as one would make the panel lean at every
|
|
77266
77313
|
// gesture that starts by going the wrong way.
|
|
77267
77314
|
return false;
|
|
77268
77315
|
}
|
|
77269
|
-
const size =
|
|
77316
|
+
const size = distanceOut(panelEl, side);
|
|
77270
77317
|
if (!size) {
|
|
77271
77318
|
return false;
|
|
77272
77319
|
}
|
|
77273
77320
|
// Something else with a better claim on the gesture: a scroller between
|
|
77274
77321
|
// the finger and the panel, with room left that way.
|
|
77275
|
-
if (scrollRoomTowards(target, panelEl,
|
|
77322
|
+
if (scrollRoomTowards(target, panelEl, gestureAxis, sign)) {
|
|
77276
77323
|
return false;
|
|
77277
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
|
+
}
|
|
77278
77331
|
panelEl.style.transitionProperty = "none";
|
|
77279
|
-
//
|
|
77280
|
-
// 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.
|
|
77281
77334
|
return {
|
|
77282
77335
|
size,
|
|
77283
|
-
travelBack:
|
|
77284
|
-
travelOn:
|
|
77336
|
+
travelBack: Boolean(sideTowards(gestureAxis, 1)),
|
|
77337
|
+
travelOn: Boolean(sideTowards(gestureAxis, -1)),
|
|
77285
77338
|
};
|
|
77286
77339
|
},
|
|
77287
77340
|
onPull: ({ pulled }) => {
|
|
77288
77341
|
paint(pulled);
|
|
77289
77342
|
},
|
|
77290
|
-
onEnd: ({ pulled, size, travels, event }) => {
|
|
77343
|
+
onEnd: ({ pulled, size, sign, travels, event }) => {
|
|
77291
77344
|
if (travels) {
|
|
77292
|
-
|
|
77345
|
+
const span = distanceOutBySign.get(sign);
|
|
77346
|
+
travelTo(pulled, span * sign, span, () =>
|
|
77347
|
+
close(event, span * sign, span),
|
|
77348
|
+
);
|
|
77293
77349
|
return;
|
|
77294
77350
|
}
|
|
77295
|
-
travelTo(pulled, 0, restore);
|
|
77351
|
+
travelTo(pulled, 0, size, restore);
|
|
77296
77352
|
},
|
|
77297
77353
|
});
|
|
77298
77354
|
};
|
|
77299
77355
|
};
|
|
77300
77356
|
|
|
77301
|
-
|
|
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) => {
|
|
77302
77362
|
const rect = element.getBoundingClientRect();
|
|
77303
|
-
|
|
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;
|
|
77304
77368
|
};
|
|
77305
77369
|
const translateOf = (axis, distance) =>
|
|
77306
77370
|
axis === "x"
|
|
77307
77371
|
? `translate(${distance}px, 0px)`
|
|
77308
77372
|
: `translate(0px, ${distance}px)`;
|
|
77309
77373
|
// The same cut the entry/exit animation makes (popup_css.js), at a distance the
|
|
77310
|
-
// finger decides instead of a transition:
|
|
77311
|
-
//
|
|
77312
|
-
// edge where a band of glass lies past it, 100vmax out where the
|
|
77313
|
-
// edge does the cutting),
|
|
77314
|
-
//
|
|
77315
|
-
// the box — the pull never takes the popup
|
|
77316
|
-
// only shave what it legitimately paints
|
|
77317
|
-
// policy from popup_css.js rather than the
|
|
77318
|
-
// (a Web Animation on transform AND clip-path,
|
|
77319
|
-
// 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.
|
|
77320
77384
|
const UNCUT = "-100vmax";
|
|
77321
|
-
const
|
|
77322
|
-
|
|
77323
|
-
const
|
|
77324
|
-
|
|
77325
|
-
|
|
77326
|
-
|
|
77327
|
-
|
|
77328
|
-
return `
|
|
77329
|
-
}
|
|
77330
|
-
|
|
77331
|
-
return `inset(${UNCUT} ${UNCUT} ${cut} ${UNCUT})`;
|
|
77332
|
-
}
|
|
77333
|
-
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(" ")})`;
|
|
77334
77395
|
};
|
|
77335
77396
|
|
|
77336
77397
|
/**
|
|
@@ -79120,7 +79181,7 @@ const useDialogProps = props => {
|
|
|
79120
79181
|
// actually resting on an edge: anywhere else the gesture would send the
|
|
79121
79182
|
// dialog somewhere it never came from.
|
|
79122
79183
|
const swipeToCloseSide = isDocked && (parsedPositionArea.y === "top" || parsedPositionArea.y === "bottom") ? parsedPositionArea.y : null;
|
|
79123
|
-
const onSwipePointerDown = swipeToCloseSide ? createSwipeToClose(swipeToCloseSide, {
|
|
79184
|
+
const onSwipePointerDown = swipeToCloseSide ? createSwipeToClose([swipeToCloseSide], {
|
|
79124
79185
|
grip: DOCKED_SWIPE_GRIP
|
|
79125
79186
|
}) : null;
|
|
79126
79187
|
// A corner sitting exactly on the container's own corner must not be
|
|
@@ -80300,6 +80361,16 @@ const css$C = /* css */`
|
|
|
80300
80361
|
to filter). An entrance animation moves the popover through scale and
|
|
80301
80362
|
transform instead, which compose under it: see popup_css.js. */
|
|
80302
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
|
+
|
|
80303
80374
|
/* The clamped max, not --popover-maxmax-*: that one ignores what the
|
|
80304
80375
|
placement left available, which reaches here through
|
|
80305
80376
|
--container-position-remaining-* (see applyNewPosition) — the min()
|
|
@@ -80530,6 +80601,20 @@ const css$C = /* css */`
|
|
|
80530
80601
|
* overlap with the anchor; wrap it in `inset(...)` (e.g. `inset(top)`,
|
|
80531
80602
|
* `inset(top-left)`) to overlap the anchor instead (edges touching or
|
|
80532
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.
|
|
80533
80618
|
* @param {string} [props.positionAreaFixed] - Overrides `positionArea` once
|
|
80534
80619
|
* the popover has actually been positioned once, so a live reposition
|
|
80535
80620
|
* (e.g. anchor moved) doesn't jump to a different side.
|
|
@@ -80983,6 +81068,7 @@ const usePopoverProps = props => {
|
|
|
80983
81068
|
positionAreaWhenAnchorIsInvalid,
|
|
80984
81069
|
marginWithContainer = 0,
|
|
80985
81070
|
container = "app",
|
|
81071
|
+
closeByDrag = false,
|
|
80986
81072
|
// "close" → pointer press outside closes (a plain close = commit)
|
|
80987
81073
|
// "cancel" → pointer press outside closes AS A CANCEL (revert)
|
|
80988
81074
|
// "capture"→ absorb the press, stay open
|
|
@@ -81116,6 +81202,7 @@ const usePopoverProps = props => {
|
|
|
81116
81202
|
return popupGroup.register(openController);
|
|
81117
81203
|
}, [popupGroup, openController]);
|
|
81118
81204
|
const hasBackdrop = pressOutside !== "ignore" && backdrop && !popupGroup;
|
|
81205
|
+
const swipeToCloseSides = resolveSwipeToCloseSides(closeByDrag, positionArea);
|
|
81119
81206
|
if (!popupGroup && !backdrop && pressOutside === "capture") {
|
|
81120
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".`);
|
|
81121
81208
|
}
|
|
@@ -81265,6 +81352,9 @@ const usePopoverProps = props => {
|
|
|
81265
81352
|
}
|
|
81266
81353
|
const hasAnchorElement = Boolean(anchorElement);
|
|
81267
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
|
+
}
|
|
81268
81358
|
// getPositionedParent already resolves to document.documentElement for
|
|
81269
81359
|
// the via-attribute renderer on its own (popoverEl's own `popover`
|
|
81270
81360
|
// attribute, present from render regardless of whether it's actually
|
|
@@ -81959,6 +82049,7 @@ const usePopoverProps = props => {
|
|
|
81959
82049
|
}
|
|
81960
82050
|
}
|
|
81961
82051
|
});
|
|
82052
|
+
const swipeToClose = swipeToCloseSides ? createSwipeToClose(swipeToCloseSides) : null;
|
|
81962
82053
|
Object.assign(contentProps, {
|
|
81963
82054
|
id,
|
|
81964
82055
|
tabIndex,
|
|
@@ -82037,6 +82128,26 @@ const usePopoverProps = props => {
|
|
|
82037
82128
|
onKeyDown?.(e);
|
|
82038
82129
|
onKeyDownShortcuts(e);
|
|
82039
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),
|
|
82040
82151
|
// A popup opened from inside this one is in front of it, not a peer of
|
|
82041
82152
|
// it: the group stops here.
|
|
82042
82153
|
"children": popupGroup ? jsx(PopupGroupContext.Provider, {
|
|
@@ -82110,6 +82221,37 @@ const resolvePositionAreaAndAnimationKind = ({
|
|
|
82110
82221
|
Popover.Close = PopupClose;
|
|
82111
82222
|
Popover.Title = PopupTitle;
|
|
82112
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
|
+
|
|
82113
82255
|
// The placement needs pixels: a value only CSS could evaluate (a percentage, a
|
|
82114
82256
|
// calc()…) would put the popover at NaN, which is the top of the page.
|
|
82115
82257
|
const resolveMarginInPixels = (margin, name) => {
|
|
@@ -110171,7 +110313,7 @@ const SidePanel = ({
|
|
|
110171
110313
|
}) => {
|
|
110172
110314
|
import.meta.css = [css, "@jsenv/navi/src/layout/side_panel.jsx"];
|
|
110173
110315
|
const rest = dropPickerPopupProps(restGiven, "SidePanel");
|
|
110174
|
-
const onSwipePointerDown = closeByDrag ? createSwipeToClose(side) : null;
|
|
110316
|
+
const onSwipePointerDown = closeByDrag ? createSwipeToClose([side]) : null;
|
|
110175
110317
|
return jsx(Popup, {
|
|
110176
110318
|
mode: mode
|
|
110177
110319
|
// Spread rather than written: the collision warning between `signal` and
|