@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.
@@ -41536,9 +41536,23 @@ const RouteTravel = ({
41536
41536
  params
41537
41537
  }) => new URL(route.buildUrl(params), window.location.href).pathname;
41538
41538
 
41539
+ // The pane laid out as the page on screen for the length of a movement, let go of
41540
+ // when the row rests. Not what readers are told the page held is
41541
+ // (route_travel_hold.js): that follows the hand, and goes at its lift.
41542
+ const heldPaneRef = useRef(null);
41543
+ const releasePaneAtRest = () => {
41544
+ const heldPane = heldPaneRef.current;
41545
+ if (heldPane) {
41546
+ heldPaneRef.current = null;
41547
+ heldPane.removeAttribute("data-route-row-held");
41548
+ }
41549
+ };
41550
+
41539
41551
  // One movement, whoever set it off, from the first thing that moves to the row
41540
- // at rest. Null when nothing moves.
41541
- const beginMovement = fields => {
41552
+ // at rest. Null when nothing moves. `paneAtRest` is the pane the row rests on as it
41553
+ // begins: laid out as the page on screen until the row rests again, wherever the
41554
+ // address goes meanwhile (see the CSS).
41555
+ const beginMovement = (fields, paneAtRest) => {
41542
41556
  const row = rowRef.current;
41543
41557
  if (movementRef.current) {
41544
41558
  Object.assign(movementRef.current, fields);
@@ -41553,6 +41567,11 @@ const RouteTravel = ({
41553
41567
  ...fields
41554
41568
  };
41555
41569
  movementRef.current = movement;
41570
+ // In a window the page is a picture, contained with what it holds.
41571
+ if (!windowed && paneAtRest) {
41572
+ heldPaneRef.current = paneAtRest;
41573
+ paneAtRest.setAttribute("data-route-row-held", "");
41574
+ }
41556
41575
  if (movement.scrollsDocument) {
41557
41576
  // Whatever the browser does with the offset meanwhile (clamping it to a page
41558
41577
  // shorter than the trip) is not the reader scrolling, and the address it
@@ -41790,24 +41809,6 @@ const RouteTravel = ({
41790
41809
  rowRef.current.removeAttribute("data-route-row-travelling");
41791
41810
  };
41792
41811
 
41793
- // The address runs ahead of a hand to build the page it pulls towards (onScroll):
41794
- // the pane under the hand stays laid out as the page on screen until the hand lets
41795
- // go, whatever `inert` says (see the CSS). What reads the page held is told apart
41796
- // (route_travel_hold.js), and both are let go of together.
41797
- const heldPaneRef = useRef(null);
41798
- const holdPaneOnScreen = pane => {
41799
- heldPaneRef.current = pane;
41800
- pane.setAttribute("data-route-row-held", "");
41801
- };
41802
- const releaseHold = () => {
41803
- releaseHeldPage();
41804
- const heldPane = heldPaneRef.current;
41805
- if (heldPane) {
41806
- heldPaneRef.current = null;
41807
- heldPane.removeAttribute("data-route-row-held");
41808
- }
41809
- };
41810
-
41811
41812
  // Every column back where the map says, and the row on the page of the address, in
41812
41813
  // one go: that page stands at the same place on screen either way.
41813
41814
  const putRowBack = movement => {
@@ -41836,7 +41837,7 @@ const RouteTravel = ({
41836
41837
  const position = row.scrollLeft;
41837
41838
  movement.rowTaken = true;
41838
41839
  // A travel no hand set off holds nothing: it goes where it goes.
41839
- releaseHold();
41840
+ releaseHeldPage();
41840
41841
  row.style.setProperty("--navi-route-row-stand", `${-position}px`);
41841
41842
  row.setAttribute("data-route-row-taken", "");
41842
41843
  afterFrames(2, () => {
@@ -41868,7 +41869,8 @@ const RouteTravel = ({
41868
41869
  putRowBack(movement);
41869
41870
  row.removeAttribute("data-route-row-taken");
41870
41871
  publishRouteRowMove(null);
41871
- releaseHold();
41872
+ releaseHeldPage();
41873
+ releasePaneAtRest();
41872
41874
  if (liveRef.current) {
41873
41875
  liveRef.current.style.top = "";
41874
41876
  }
@@ -41981,9 +41983,11 @@ const RouteTravel = ({
41981
41983
  return;
41982
41984
  }
41983
41985
  const movementBefore = movementRef.current;
41986
+ // The row still rests on the page answered before. Held in the commit that made
41987
+ // its pane inert, so no frame paints it contained.
41984
41988
  const movement = beginMovement({
41985
41989
  travelling: true
41986
- });
41990
+ }, panesOf(row)[pages.findIndex(each => samePage(each, answered))]);
41987
41991
  if (movementBefore?.byHand && !movementBefore.rowTaken) {
41988
41992
  sayRowMoves();
41989
41993
  takeRowFromHand(movement);
@@ -42051,7 +42055,7 @@ const RouteTravel = ({
42051
42055
  }
42052
42056
  const movement = moving || beginMovement({
42053
42057
  byHand: true
42054
- });
42058
+ }, pane);
42055
42059
  sayRowMoves();
42056
42060
  if (movement.travelling || !travelByDrag || !pane) {
42057
42061
  return;
@@ -42088,12 +42092,8 @@ const RouteTravel = ({
42088
42092
  return;
42089
42093
  }
42090
42094
  leavePictureOfLive(movement);
42091
- } else {
42092
- if (paneTowards.hasAttribute("data-built")) {
42093
- return;
42094
- }
42095
- // Before the address moves: the render that makes it inert must find it held.
42096
- holdPaneOnScreen(pane);
42095
+ } else if (paneTowards.hasAttribute("data-built")) {
42096
+ return;
42097
42097
  }
42098
42098
  holdPage(pages[currentIndex], page);
42099
42099
  askFor(page);
@@ -42124,8 +42124,9 @@ const RouteTravel = ({
42124
42124
  askForPageOfRow(index, movement);
42125
42125
  }
42126
42126
  // After the address: a pull coming back to its page must not light the tab the
42127
- // address ran ahead to, even for a render.
42128
- releaseHold();
42127
+ // address ran ahead to, even for a render. Only what readers are told: the
42128
+ // panes keep their layout until the row rests.
42129
+ releaseHeldPage();
42129
42130
  };
42130
42131
  const stopWatching = watchRowScroll(row, {
42131
42132
  onScroll,
@@ -42181,7 +42182,8 @@ const RouteTravel = ({
42181
42182
  row.removeAttribute("data-route-row-travelling");
42182
42183
  movementRef.current = null;
42183
42184
  publishRouteRowMove(null);
42184
- releaseHold();
42185
+ releaseHeldPage();
42186
+ releasePaneAtRest();
42185
42187
  };
42186
42188
  }, [windowed]);
42187
42189
 
@@ -75470,18 +75472,19 @@ const readRoom = () =>
75470
75472
  visualViewportHeightSignal.peek() - getVirtualKeyboardOverlayHeight();
75471
75473
 
75472
75474
  /**
75473
- * Pushing a popup docked to an edge (a side panel, a bottom sheet) back the way
75474
- * it came in closes it: it follows the finger while it travels, and letting go
75475
- * either finishes the departure or brings it back to rest — whichever the
75476
- * travel was already heading to.
75475
+ * Pushing a popup docked to an edge (a side panel, a bottom sheet, a toast)
75476
+ * back the way it came in closes it: it follows the finger while it travels,
75477
+ * and letting go either finishes the departure or brings it back to rest —
75478
+ * whichever the travel was already heading to. A popup may be given other ways
75479
+ * out than that one: a toast is also thrown sideways.
75477
75480
  *
75478
75481
  * Reading the pointer is not done here: when a press becomes a gesture, which
75479
75482
  * axis it leans on, how fast it was going when it was let go, who else has a
75480
75483
  * claim on it (a field, a scroller with room left, a box inside the panel that
75481
75484
  * travels the same way) is one gesture system for the whole codebase
75482
75485
  * (@jsenv/dom's startDragToTravel). A panel closing is a travel of exactly one
75483
- * box, towards the edge it is docked to and nowhere else — which is all this
75484
- * file has to say.
75486
+ * box, out by one of the edges it may leave by and nowhere else — which is all
75487
+ * this file has to say.
75485
75488
  *
75486
75489
  * The release travel is driven here (Web Animations) rather than handed to the
75487
75490
  * exit transition the `animation` prop may install: its pace is what is left to
@@ -75496,7 +75499,7 @@ const readRoom = () =>
75496
75499
  // less, so the panel keeps the same speed whenever the finger let go.
75497
75500
  const TRAVEL_DURATION = 220;
75498
75501
 
75499
- // Which way a popup docked to each side travels — read by the panel itself to
75502
+ // Which way a popup leaving by each side travels — read by the panel itself to
75500
75503
  // say so in the DOM, which is how a box travelling INSIDE it (a row of slides,
75501
75504
  // a route travel) knows that axis is already walked.
75502
75505
  const SWIPE_AXIS_BY_SIDE = {
@@ -75505,12 +75508,27 @@ const SWIPE_AXIS_BY_SIDE = {
75505
75508
  top: "y",
75506
75509
  bottom: "y",
75507
75510
  };
75508
- // Which way the finger pushes to send the panel back where it came from, in
75509
- // screen coordinates: positive is right/down, the same sign the gesture reports.
75511
+ // Which way the finger pushes to send the panel out by each side, in screen
75512
+ // coordinates: positive is right/down, the same sign the gesture reports.
75510
75513
  const CLOSE_DIRECTION_BY_SIDE = { left: -1, right: 1, top: -1, bottom: 1 };
75511
75514
 
75512
75515
  /**
75513
- * Builds the `pointerdown` handler a popup docked to `side` answers with.
75516
+ * The axes a popup leaving by `sides` travels on, as the gesture and the DOM
75517
+ * say them: "x", "y" or "xy".
75518
+ */
75519
+ const swipeAxesOf = (sides) => {
75520
+ const hasX = sides.some((side) => SWIPE_AXIS_BY_SIDE[side] === "x");
75521
+ const hasY = sides.some((side) => SWIPE_AXIS_BY_SIDE[side] === "y");
75522
+ if (hasX && hasY) {
75523
+ return "xy";
75524
+ }
75525
+ return hasX ? "x" : "y";
75526
+ };
75527
+
75528
+ /**
75529
+ * Builds the `pointerdown` handler of a popup that leaves by any of `sides`:
75530
+ * usually the one edge it is docked to, the way it came in; a toast is also
75531
+ * thrown sideways, and leaves the way it was thrown.
75514
75532
  *
75515
75533
  * `grip` says where the gesture may start, as a selector the pressed element is
75516
75534
  * matched against with `closest`. A popup that names one is held THERE and
@@ -75518,11 +75536,17 @@ const CLOSE_DIRECTION_BY_SIDE = { left: -1, right: 1, top: -1, bottom: 1 };
75518
75536
  * and a press on it is that content's — including gestures of its own, which
75519
75537
  * this file has no way of knowing about. A popup that names no grip is pushed
75520
75538
  * from its whole surface, which only suits one made of nothing else (a side
75521
- * panel showing a page).
75522
- */
75523
- const createSwipeToClose = (side, { grip } = {}) => {
75524
- const axis = SWIPE_AXIS_BY_SIDE[side];
75525
- const closeDirection = CLOSE_DIRECTION_BY_SIDE[side];
75539
+ * panel showing a page, a toast).
75540
+ */
75541
+ const createSwipeToClose = (sides, { grip } = {}) => {
75542
+ const axes = swipeAxesOf(sides);
75543
+ // The side a push along `axis` towards `sign` leaves by, if it leaves at all.
75544
+ const sideTowards = (axis, sign) =>
75545
+ sides.find(
75546
+ (side) =>
75547
+ SWIPE_AXIS_BY_SIDE[side] === axis &&
75548
+ CLOSE_DIRECTION_BY_SIDE[side] === sign,
75549
+ );
75526
75550
 
75527
75551
  return (pointerDownEvent) => {
75528
75552
  const panelEl = pointerDownEvent.currentTarget;
@@ -75539,6 +75563,11 @@ const createSwipeToClose = (side, { grip } = {}) => {
75539
75563
  }
75540
75564
  }
75541
75565
 
75566
+ // Decided when the finger picks its axis (onStart), once for the gesture.
75567
+ let axis;
75568
+ // How far the panel travels to be out, for each way it can go on that axis.
75569
+ const distanceOutBySign = new Map();
75570
+
75542
75571
  // How far the panel has been pulled, as the style showing it — written on
75543
75572
  // the panel while the finger drives it, handed to the release travel as
75544
75573
  // its two keyframes. The gesture reports a distance in screen coordinates,
@@ -75548,7 +75577,7 @@ const createSwipeToClose = (side, { grip } = {}) => {
75548
75577
  // composes under the placement instead of replacing it.
75549
75578
  const styleAt = (distance) => ({
75550
75579
  transform: translateOf(axis, distance),
75551
- clipPath: clipOf(side, distance),
75580
+ clipPath: clipOf(sides, axis, distance),
75552
75581
  });
75553
75582
  const paint = (distance) => {
75554
75583
  Object.assign(panelEl.style, styleAt(distance));
@@ -75560,21 +75589,22 @@ const createSwipeToClose = (side, { grip } = {}) => {
75560
75589
  };
75561
75590
  // The resting value is written first and the animation only covers the way
75562
75591
  // to it: both end on the same number, so nothing is seen changing when the
75563
- // animation hands the panel back to its own style.
75564
- const travelTo = (from, to, onArrival) => {
75592
+ // animation hands the panel back to its own style. `span` is the full travel
75593
+ // on that side, which the duration is a fraction of.
75594
+ const travelTo = (from, to, span, onArrival) => {
75565
75595
  const covered = from > to ? from - to : to - from;
75566
75596
  paint(to);
75567
75597
  const animation = panelEl.animate([styleAt(from), styleAt(to)], {
75568
- duration: (covered / sizeOf(panelEl, axis)) * TRAVEL_DURATION,
75598
+ duration: (covered / span) * TRAVEL_DURATION,
75569
75599
  easing: "ease-out",
75570
75600
  });
75571
75601
  animation.finished.then(onArrival, () => {});
75572
75602
  };
75573
- const close = (event) => {
75603
+ const close = (event, out, span) => {
75574
75604
  triggerNaviCommand(panelEl, "--navi-close", event);
75575
75605
  if (panelEl.getAttribute("aria-expanded") === "true") {
75576
75606
  // Refused: the panel is staying, so it goes back where it was.
75577
- travelTo(sizeOf(panelEl, axis) * closeDirection, 0, restore);
75607
+ travelTo(out, 0, span, restore);
75578
75608
  return;
75579
75609
  }
75580
75610
  restore();
@@ -75582,79 +75612,94 @@ const createSwipeToClose = (side, { grip } = {}) => {
75582
75612
 
75583
75613
  startDragToTravel(pointerDownEvent, {
75584
75614
  element: panelEl,
75585
- axes: axis,
75586
- onStart: ({ sign, target }) => {
75587
- if (sign !== closeDirection) {
75615
+ axes,
75616
+ onStart: ({ axis: gestureAxis, sign, target }) => {
75617
+ const side = sideTowards(gestureAxis, sign);
75618
+ if (!side) {
75588
75619
  // Pushing the panel further in is not a travel: there is nowhere that
75589
75620
  // way, and reading it as one would make the panel lean at every
75590
75621
  // gesture that starts by going the wrong way.
75591
75622
  return false;
75592
75623
  }
75593
- const size = sizeOf(panelEl, axis);
75624
+ const size = distanceOut(panelEl, side);
75594
75625
  if (!size) {
75595
75626
  return false;
75596
75627
  }
75597
75628
  // Something else with a better claim on the gesture: a scroller between
75598
75629
  // the finger and the panel, with room left that way.
75599
- if (scrollRoomTowards(target, panelEl, axis, sign)) {
75630
+ if (scrollRoomTowards(target, panelEl, gestureAxis, sign)) {
75600
75631
  return false;
75601
75632
  }
75633
+ axis = gestureAxis;
75634
+ distanceOutBySign.set(sign, size);
75635
+ const otherSide = sideTowards(gestureAxis, -sign);
75636
+ if (otherSide) {
75637
+ distanceOutBySign.set(-sign, distanceOut(panelEl, otherSide));
75638
+ }
75602
75639
  panelEl.style.transitionProperty = "none";
75603
- // The way out is the only way there is anything: pulling the other way
75604
- // leans against a wall and comes back.
75640
+ // Only the ways out lead anywhere: pulling towards a side the panel
75641
+ // does not leave by leans against a wall and comes back.
75605
75642
  return {
75606
75643
  size,
75607
- travelBack: closeDirection > 0,
75608
- travelOn: closeDirection < 0,
75644
+ travelBack: Boolean(sideTowards(gestureAxis, 1)),
75645
+ travelOn: Boolean(sideTowards(gestureAxis, -1)),
75609
75646
  };
75610
75647
  },
75611
75648
  onPull: ({ pulled }) => {
75612
75649
  paint(pulled);
75613
75650
  },
75614
- onEnd: ({ pulled, size, travels, event }) => {
75651
+ onEnd: ({ pulled, size, sign, travels, event }) => {
75615
75652
  if (travels) {
75616
- travelTo(pulled, size * closeDirection, () => close(event));
75653
+ const span = distanceOutBySign.get(sign);
75654
+ travelTo(pulled, span * sign, span, () =>
75655
+ close(event, span * sign, span),
75656
+ );
75617
75657
  return;
75618
75658
  }
75619
- travelTo(pulled, 0, restore);
75659
+ travelTo(pulled, 0, size, restore);
75620
75660
  },
75621
75661
  });
75622
75662
  };
75623
75663
  };
75624
75664
 
75625
- const sizeOf = (element, axis) => {
75665
+ // What takes the popup out of sight: its own size, plus the gap a margin keeps
75666
+ // between it and the edge it is pushed through — the room applyNewPosition
75667
+ // publishes on that side. Travelling its size alone, a popup kept off its edge
75668
+ // (a toast 12px above a bar) would stop with that much of itself still showing.
75669
+ const distanceOut = (element, side) => {
75626
75670
  const rect = element.getBoundingClientRect();
75627
- return axis === "x" ? rect.width : rect.height;
75671
+ const size = SWIPE_AXIS_BY_SIDE[side] === "x" ? rect.width : rect.height;
75672
+ const room = parseFloat(
75673
+ element.style.getPropertyValue(`--container-position-room-${side}`),
75674
+ );
75675
+ return Number.isNaN(room) ? size : size + room;
75628
75676
  };
75629
75677
  const translateOf = (axis, distance) =>
75630
75678
  axis === "x"
75631
75679
  ? `translate(${distance}px, 0px)`
75632
75680
  : `translate(0px, ${distance}px)`;
75633
75681
  // The same cut the entry/exit animation makes (popup_css.js), at a distance the
75634
- // finger decides instead of a transition: the edge the popup is pushed back
75635
- // through, as popup_css.js placed it (--x-popup-cut-*-at-area — on the area's
75636
- // edge where a band of glass lies past it, 100vmax out where the window's own
75637
- // edge does the cutting), plus what has been pulled, so the cut holds that line
75638
- // while the box travels under it. The three other sides are left far outside
75639
- // the box — the pull never takes the popup past them, and cutting there would
75640
- // only shave what it legitimately paints outside its own box. Reading the
75641
- // policy from popup_css.js rather than the rooms keeps the release travel
75642
- // (a Web Animation on transform AND clip-path, two clocks) from animating a
75643
- // cut the entrance would not.
75682
+ // finger decides instead of a transition: each edge the popup may leave by on
75683
+ // the axis it travels, as popup_css.js placed it (--x-popup-cut-*-at-area — on
75684
+ // the area's edge where a band of glass lies past it, 100vmax out where the
75685
+ // window's own edge does the cutting), moved by what has been pulled towards
75686
+ // it, so the cut holds that line while the box travels under it, whichever way.
75687
+ // Every other side is left far outside the box — the pull never takes the popup
75688
+ // past them, and cutting there would only shave what it legitimately paints
75689
+ // outside its own box. Reading the policy from popup_css.js rather than the
75690
+ // rooms keeps the release travel (a Web Animation on transform AND clip-path,
75691
+ // two clocks) from animating a cut the entrance would not.
75644
75692
  const UNCUT = "-100vmax";
75645
- const clipOf = (side, distance) => {
75646
- const pulled = distance < 0 ? -distance : distance;
75647
- const cut = `calc(var(--x-popup-cut-${side}-at-area, ${UNCUT}) + ${pulled}px)`;
75648
- if (side === "top") {
75649
- return `inset(${cut} ${UNCUT} ${UNCUT} ${UNCUT})`;
75650
- }
75651
- if (side === "right") {
75652
- return `inset(${UNCUT} ${cut} ${UNCUT} ${UNCUT})`;
75653
- }
75654
- if (side === "bottom") {
75655
- return `inset(${UNCUT} ${UNCUT} ${cut} ${UNCUT})`;
75656
- }
75657
- return `inset(${UNCUT} ${UNCUT} ${UNCUT} ${cut})`;
75693
+ const INSET_ORDER = ["top", "right", "bottom", "left"];
75694
+ const clipOf = (sides, axis, distance) => {
75695
+ const insets = INSET_ORDER.map((side) => {
75696
+ if (!sides.includes(side) || SWIPE_AXIS_BY_SIDE[side] !== axis) {
75697
+ return UNCUT;
75698
+ }
75699
+ const towards = distance * CLOSE_DIRECTION_BY_SIDE[side];
75700
+ return `calc(var(--x-popup-cut-${side}-at-area, ${UNCUT}) + ${towards}px)`;
75701
+ });
75702
+ return `inset(${insets.join(" ")})`;
75658
75703
  };
75659
75704
 
75660
75705
  /**
@@ -77436,7 +77481,7 @@ const useDialogProps = props => {
77436
77481
  // actually resting on an edge: anywhere else the gesture would send the
77437
77482
  // dialog somewhere it never came from.
77438
77483
  const swipeToCloseSide = isDocked && (parsedPositionArea.y === "top" || parsedPositionArea.y === "bottom") ? parsedPositionArea.y : null;
77439
- const onSwipePointerDown = swipeToCloseSide ? createSwipeToClose(swipeToCloseSide, {
77484
+ const onSwipePointerDown = swipeToCloseSide ? createSwipeToClose([swipeToCloseSide], {
77440
77485
  grip: DOCKED_SWIPE_GRIP
77441
77486
  }) : null;
77442
77487
  // A corner sitting exactly on the container's own corner must not be
@@ -78529,6 +78574,16 @@ const css$C = /* css */`
78529
78574
  to filter). An entrance animation moves the popover through scale and
78530
78575
  transform instead, which compose under it: see popup_css.js. */
78531
78576
 
78577
+ /* What a touch may do on a popover pushed sideways to close it (see
78578
+ swipe_to_close.js): the push takes the sideways axis, the page keeps the
78579
+ vertical scroll. A push up or down travels the way the page scrolls and
78580
+ cannot give that axis away: the browser keeps it, and the gesture is
78581
+ settled by keepTouchRefusable. */
78582
+ &[data-swipe-to-close~="left"],
78583
+ &[data-swipe-to-close~="right"] {
78584
+ touch-action: pan-y;
78585
+ }
78586
+
78532
78587
  /* The clamped max, not --popover-maxmax-*: that one ignores what the
78533
78588
  placement left available, which reaches here through
78534
78589
  --container-position-remaining-* (see applyNewPosition) — the min()
@@ -78759,6 +78814,20 @@ const css$C = /* css */`
78759
78814
  * overlap with the anchor; wrap it in `inset(...)` (e.g. `inset(top)`,
78760
78815
  * `inset(top-left)`) to overlap the anchor instead (edges touching or
78761
78816
  * inside it).
78817
+ * @param {boolean|Array<"top"|"right"|"bottom"|"left">} [props.closeByDrag=false] -
78818
+ * Pushing the popover out of its container closes it: it follows the pointer
78819
+ * and, when released, finishes leaving or comes back to rest. `true` pushes
78820
+ * it back through the edge it is docked to, the word `positionArea` leads
78821
+ * with (`"bottom"`, `"bottom-start"`: pushed down; `"left-end"`: pushed
78822
+ * left); a centred popover rests on no edge and is not pushed (warned in
78823
+ * dev). A list names the sides it may leave by, and it leaves the way it was
78824
+ * pushed: `["bottom", "left", "right"]` for a toast pushed back down behind
78825
+ * the bar or thrown sideways. A popover placed against an anchor at its
78826
+ * opening is not pushed either way (warned in dev). It is pushed from its
78827
+ * whole surface, so it suits a popover holding little else: a button inside
78828
+ * keeps its tap, and a press that turns into a push swallows the click it
78829
+ * would have left. The close keeps, as a cross's does. See docs/interactions.md
78830
+ * for a swipe on the popover's own element, which does not move it.
78762
78831
  * @param {string} [props.positionAreaFixed] - Overrides `positionArea` once
78763
78832
  * the popover has actually been positioned once, so a live reposition
78764
78833
  * (e.g. anchor moved) doesn't jump to a different side.
@@ -79210,6 +79279,7 @@ const usePopoverProps = props => {
79210
79279
  positionAreaWhenAnchorIsInvalid,
79211
79280
  marginWithContainer = 0,
79212
79281
  container = "app",
79282
+ closeByDrag = false,
79213
79283
  // "close" → pointer press outside closes (a plain close = commit)
79214
79284
  // "cancel" → pointer press outside closes AS A CANCEL (revert)
79215
79285
  // "capture"→ absorb the press, stay open
@@ -79337,6 +79407,7 @@ const usePopoverProps = props => {
79337
79407
  return popupGroup.register(openController);
79338
79408
  }, [popupGroup, openController]);
79339
79409
  const hasBackdrop = pressOutside !== "ignore" && backdrop && !popupGroup;
79410
+ const swipeToCloseSides = resolveSwipeToCloseSides(closeByDrag, positionArea);
79340
79411
  if (!popupGroup && !backdrop && pressOutside === "capture") {
79341
79412
  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".`);
79342
79413
  }
@@ -80170,6 +80241,7 @@ const usePopoverProps = props => {
80170
80241
  }
80171
80242
  }
80172
80243
  });
80244
+ const swipeToClose = swipeToCloseSides ? createSwipeToClose(swipeToCloseSides) : null;
80173
80245
  Object.assign(contentProps, {
80174
80246
  id,
80175
80247
  tabIndex,
@@ -80248,6 +80320,26 @@ const usePopoverProps = props => {
80248
80320
  onKeyDown?.(e);
80249
80321
  onKeyDownShortcuts(e);
80250
80322
  },
80323
+ ...(swipeToClose ? {
80324
+ "data-swipe-to-close": swipeToCloseSides.join(" "),
80325
+ // The axes it travels on when pushed out, said to the shared gesture
80326
+ // layer before any finger lands (see side_panel.jsx, which says the
80327
+ // same for the same reasons).
80328
+ "data-drag-travel": swipeAxesOf(swipeToCloseSides),
80329
+ "data-travel-by-drag": swipeAxesOf(swipeToCloseSides),
80330
+ "onTouchMove": e => {
80331
+ rest.onTouchMove?.(e);
80332
+ keepTouchRefusable(e);
80333
+ },
80334
+ "onPointerDown": e => {
80335
+ rest.onPointerDown?.(e);
80336
+ // Placed against an anchor at this opening: it is not in its
80337
+ // container to be pushed out of (warned at the opening).
80338
+ if (!anchorElementRef.current) {
80339
+ swipeToClose(e);
80340
+ }
80341
+ }
80342
+ } : null),
80251
80343
  // A popup opened from inside this one is in front of it, not a peer of
80252
80344
  // it: the group stops here.
80253
80345
  "children": popupGroup ? jsx(PopupGroupContext.Provider, {
@@ -80321,6 +80413,31 @@ const resolvePositionAreaAndAnimationKind = ({
80321
80413
  Popover.Close = PopupClose;
80322
80414
  Popover.Title = PopupTitle;
80323
80415
 
80416
+ // The sides a popover pushed away leaves by, or null when it is not pushed.
80417
+ const resolveSwipeToCloseSides = (closeByDrag, positionArea) => {
80418
+ if (!closeByDrag) {
80419
+ return null;
80420
+ }
80421
+ if (Array.isArray(closeByDrag)) {
80422
+ const sides = closeByDrag.filter(side => side in SWIPE_AXIS_BY_SIDE);
80423
+ return sides.length ? sides : null;
80424
+ }
80425
+ const side = dockedSideOf(positionArea);
80426
+ if (!side) {
80427
+ return null;
80428
+ }
80429
+ return [side];
80430
+ };
80431
+ const INSET_TOKEN_RE = /^inset\(\s*([a-z-]+)\s*\)$/;
80432
+ // The edge a popover docked to its container rests on: the word its
80433
+ // positionArea leads with, "bottom-start" resting on the bottom and "left-end"
80434
+ // on the left. "center" rests on none.
80435
+ const dockedSideOf = positionArea => {
80436
+ const token = INSET_TOKEN_RE.exec(positionArea)?.[1] ?? positionArea;
80437
+ const side = token.split("-")[0];
80438
+ return side in SWIPE_AXIS_BY_SIDE ? side : null;
80439
+ };
80440
+
80324
80441
  // The placement needs pixels: a value only CSS could evaluate (a percentage, a
80325
80442
  // calc()…) would put the popover at NaN, which is the top of the page.
80326
80443
  const resolveMarginInPixels = (margin, name) => {
@@ -108028,7 +108145,7 @@ const SidePanel = ({
108028
108145
  }) => {
108029
108146
  import.meta.css = [css, "@jsenv/navi/src/layout/side_panel.jsx"];
108030
108147
  const rest = dropPickerPopupProps(restGiven);
108031
- const onSwipePointerDown = closeByDrag ? createSwipeToClose(side) : null;
108148
+ const onSwipePointerDown = closeByDrag ? createSwipeToClose([side]) : null;
108032
108149
  return jsx(Popup, {
108033
108150
  mode: mode
108034
108151
  // Spread rather than written: the collision warning between `signal` and