@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.
@@ -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) back the way
77152
- * it came in closes it: it follows the finger while it travels, and letting go
77153
- * either finishes the departure or brings it back to rest — whichever the
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, towards the edge it is docked to and nowhere else — which is all this
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 docked to each side travels — read by the panel itself to
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 back where it came from, in
77187
- // screen coordinates: positive is right/down, the same sign the gesture reports.
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
- * Builds the `pointerdown` handler a popup docked to `side` answers with.
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 = (side, { grip } = {}) => {
77202
- const axis = SWIPE_AXIS_BY_SIDE[side];
77203
- const closeDirection = CLOSE_DIRECTION_BY_SIDE[side];
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(side, distance),
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
- const travelTo = (from, to, onArrival) => {
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 / sizeOf(panelEl, axis)) * TRAVEL_DURATION,
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(sizeOf(panelEl, axis) * closeDirection, 0, restore);
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: axis,
77264
- onStart: ({ sign, target }) => {
77265
- if (sign !== closeDirection) {
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 = sizeOf(panelEl, axis);
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, axis, sign)) {
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
- // The way out is the only way there is anything: pulling the other way
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: closeDirection > 0,
77286
- travelOn: closeDirection < 0,
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
- travelTo(pulled, size * closeDirection, () => close(event));
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
- const sizeOf = (element, axis) => {
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
- return axis === "x" ? rect.width : rect.height;
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: the edge the popup is pushed back
77313
- // through, as popup_css.js placed it (--x-popup-cut-*-at-area — on the area's
77314
- // edge where a band of glass lies past it, 100vmax out where the window's own
77315
- // edge does the cutting), plus what has been pulled, so the cut holds that line
77316
- // while the box travels under it. The three other sides are left far outside
77317
- // the box — the pull never takes the popup past them, and cutting there would
77318
- // only shave what it legitimately paints outside its own box. Reading the
77319
- // policy from popup_css.js rather than the rooms keeps the release travel
77320
- // (a Web Animation on transform AND clip-path, two clocks) from animating a
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 clipOf = (side, distance) => {
77324
- const pulled = distance < 0 ? -distance : distance;
77325
- const cut = `calc(var(--x-popup-cut-${side}-at-area, ${UNCUT}) + ${pulled}px)`;
77326
- if (side === "top") {
77327
- return `inset(${cut} ${UNCUT} ${UNCUT} ${UNCUT})`;
77328
- }
77329
- if (side === "right") {
77330
- return `inset(${UNCUT} ${cut} ${UNCUT} ${UNCUT})`;
77331
- }
77332
- if (side === "bottom") {
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