@jsenv/navi 0.29.363 → 0.29.365

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.
@@ -56555,6 +56555,10 @@ const SlideContainer = ({
56555
56555
  // one being travelled TO while the track moves. What the next travel departs
56556
56556
  // from, because it is what one is looking at.
56557
56557
  const drawnAreaRef = useRef(undefined);
56558
+ // The movement the track is playing, while it plays: the slide it is leaving
56559
+ // and the slide it is going to. A hand landing on the box takes it over from
56560
+ // its origin (see onStart in createTravelHandlers).
56561
+ const movingRef = useRef(null);
56558
56562
  // Which way the travel about to be drawn goes, when whatever asked for it
56559
56563
  // knows: a window stepping off its last slide comes back on its first, and
56560
56564
  // only the press says that is a step forward — the map, read between those
@@ -56823,6 +56827,7 @@ const SlideContainer = ({
56823
56827
  }
56824
56828
  stageRef.current = null;
56825
56829
  trackAnimationRef.current = null;
56830
+ movingRef.current = null;
56826
56831
  // At rest the picture IS the current slide, whatever the last gesture wrote
56827
56832
  // there: an indicator has nothing left to lean towards.
56828
56833
  paintTravelProgress(0);
@@ -57017,7 +57022,7 @@ const SlideContainer = ({
57017
57022
  // there, and when it is over this is what holds. Except under a hand-off
57018
57023
  // still waiting for its travel: the track stays where the finger left it,
57019
57024
  // until the render that moves it or the frame after which the gesture gives
57020
- // up on it (see returnToRest in onEnd).
57025
+ // up on it (see settleTowards in onEnd).
57021
57026
  if (!travelFromRef.current) {
57022
57027
  track.style.setProperty("--slide-container-offset", offset);
57023
57028
  }
@@ -57052,6 +57057,10 @@ const SlideContainer = ({
57052
57057
  duration: durationMs * travelRatio,
57053
57058
  easing
57054
57059
  });
57060
+ movingRef.current = {
57061
+ from: drawnArea,
57062
+ to: currentArea
57063
+ };
57055
57064
  // The trait travels with the slides: from where the gesture left it when
57056
57065
  // there was one, from a whole box away when the travel was asked for.
57057
57066
  const progressFrom = travelProgressFromRef.current ?? (travelStep ? travelStep.x || travelStep.y : 0);
@@ -57837,7 +57846,7 @@ const SlideContainer = ({
57837
57846
  // The two slides the gesture can bring in, placed one box either side of the
57838
57847
  // one being dragged — the same stage a travel builds, except that both ends
57839
57848
  // are set up at once because the finger has not said yet which way it goes.
57840
- const stageDrag = drag => {
57849
+ const stageDrag = (drag, currentArea) => {
57841
57850
  const {
57842
57851
  slideElements,
57843
57852
  placeOf
@@ -57865,7 +57874,7 @@ const SlideContainer = ({
57865
57874
  }
57866
57875
  stageRef.current = {
57867
57876
  placeByArea,
57868
- area: drag.area
57877
+ area: currentArea
57869
57878
  };
57870
57879
  for (const slideElement of slideElements) {
57871
57880
  const area = readArea(slideElement);
@@ -57881,33 +57890,63 @@ const SlideContainer = ({
57881
57890
  }
57882
57891
  };
57883
57892
 
57884
- // Let go of without enough of a gesture to travel: the slide comes back to
57885
- // where it was, over the distance it was pulled so a slide barely moved
57886
- // snaps back and one dragged most of the way there takes its time.
57887
- const returnToRest = drag => {
57893
+ // Let go of, and the slides go where the gesture leaves them: back on the
57894
+ // slide being dragged when nothing travels, on to the slide arriving when a
57895
+ // travel caught in flight is let carry on. Over the distance left to cover,
57896
+ // so a slide barely moved snaps back and one dragged most of the way there
57897
+ // takes its time.
57898
+ const settleTowards = (drag, area) => {
57888
57899
  const track = trackRef.current;
57889
57900
  if (!track) {
57890
57901
  return;
57891
57902
  }
57892
- const restOffset = `${drag.baseOffset.x}px ${drag.baseOffset.y}px`;
57903
+ const stage = stageRef.current;
57904
+ const {
57905
+ placeOf
57906
+ } = readMap();
57907
+ const place = stage?.placeByArea.get(area) || placeOf.get(area) || {
57908
+ x: 0,
57909
+ y: 0
57910
+ };
57911
+ const restPx = {
57912
+ x: -place.x * drag.box.width,
57913
+ y: -place.y * drag.box.height
57914
+ };
57915
+ const nowPx = {
57916
+ x: drag.baseOffset.x + drag.pull.x,
57917
+ y: drag.baseOffset.y + drag.pull.y
57918
+ };
57919
+ const restOffset = `${restPx.x}px ${restPx.y}px`;
57893
57920
  const durationMs = durationToMs(duration);
57894
- const pulled = Math.abs(drag.pull[drag.axis]);
57895
57921
  const size = drag.axis === "x" ? drag.box.width : drag.box.height;
57922
+ const left = Math.abs(restPx[drag.axis] - nowPx[drag.axis]);
57896
57923
  trackAnimationRef.current?.cancel();
57897
57924
  trackAnimationRef.current = null;
57898
57925
  track.style.setProperty("--slide-container-offset", restOffset);
57899
- if (!durationMs || !pulled) {
57926
+ if (!durationMs || !left) {
57900
57927
  paintTravelProgress(0);
57901
57928
  settleTravel();
57902
57929
  return;
57903
57930
  }
57904
- animateTravelProgress(drag.progress, durationMs * (pulled / size), "ease-out", drag.areaPulled);
57931
+ // Which other slide the picture leans on while it arrives: the one being
57932
+ // pulled in when it comes back on the dragged slide, the dragged slide
57933
+ // itself when it goes on to the one arriving — and the trait is then a
57934
+ // box short of that one, which is what it has to close.
57935
+ const arrivesOnBase = area === drag.area;
57936
+ const sign = drag.pull[drag.axis] > 0 ? 1 : -1;
57937
+ const progressFrom = arrivesOnBase ? drag.progress : drag.progress - sign;
57938
+ const otherArea = arrivesOnBase ? drag.areaPulled : drag.area;
57939
+ movingRef.current = {
57940
+ from: otherArea,
57941
+ to: area
57942
+ };
57943
+ animateTravelProgress(progressFrom, durationMs * (left / size), "ease-out", otherArea);
57905
57944
  const animation = track.animate([{
57906
- translate: drag.offset
57945
+ translate: `${nowPx.x}px ${nowPx.y}px`
57907
57946
  }, {
57908
57947
  translate: restOffset
57909
57948
  }], {
57910
- duration: durationMs * (pulled / size),
57949
+ duration: durationMs * (left / size),
57911
57950
  easing: "ease-out"
57912
57951
  });
57913
57952
  trackAnimationRef.current = animation;
@@ -57938,11 +57977,14 @@ const SlideContainer = ({
57938
57977
  // value underneath it, which is the far end of the travel — the very jump
57939
57978
  // this is about.
57940
57979
  trackElement.style.setProperty("--slide-container-offset", offsetOnScreen);
57980
+ const moving = movingRef.current || {};
57941
57981
  return {
57942
57982
  trackElement,
57943
57983
  onScreenPx,
57944
57984
  offsetOnScreen,
57945
- offsetTarget: offsetRef.current
57985
+ offsetTarget: offsetRef.current,
57986
+ from: moving.from,
57987
+ to: moving.to
57946
57988
  };
57947
57989
  };
57948
57990
  // Which way a caught travel was going: a hand reaching for something moving
@@ -57989,8 +58031,6 @@ const SlideContainer = ({
57989
58031
  sign,
57990
58032
  target
57991
58033
  }) => {
57992
- let areaBack = axis === "x" ? areaTowards(-1, 0) : areaTowards(0, -1);
57993
- let areaOn = axis === "x" ? areaTowards(1, 0) : areaTowards(0, 1);
57994
58034
  // Everything positional is read HERE rather than when the pointer
57995
58035
  // landed: the travel that was playing then may have arrived since, and
57996
58036
  // it is what the slides are doing at the moment the gesture takes them
@@ -58001,39 +58041,66 @@ const SlideContainer = ({
58001
58041
  placeOf
58002
58042
  } = readMap();
58003
58043
  const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current")) || slideElements[0];
58004
- // The hold goToArea reads at the release, read again HERE, off the same
58005
- // slide and the same attribute: a slide that will refuse the arrival
58006
- // must not offer the journey. A locked direction simply has nowhere to
58007
- // go for the length of this gesture — the one case the gesture already
58008
- // knows, being the last slide of a walk. The hand then gets the wall it
58009
- // can lean on and never walk through (see drag_to_travel), the slide
58010
- // behind it stays offstage instead of being read on the way, and the
58011
- // release has nothing left to refuse.
58012
- // Nothing here about `released` (--navi-done): that is one particular
58013
- // departure letting go, decided as it happens, and a gesture armed
58014
- // before it has no such thing to read — the attribute as rendered is
58015
- // what the hand is answered from.
58016
- if (currentElement?.hasAttribute("data-prevent-nav-previous")) {
58017
- areaBack = undefined;
58018
- }
58019
- if (currentElement?.hasAttribute("data-prevent-nav-next")) {
58020
- areaOn = undefined;
58021
- }
58022
58044
  const box = track.getBoundingClientRect();
58023
- if (!areaBack && !areaOn || !currentElement || !box.width || !box.height ||
58024
- // Something else with a better claim on the gesture: a scroller
58025
- // between the finger and the slide, with room left that way.
58026
- scrollRoomTowards(target, currentElement, axis, sign)) {
58045
+ if (!currentElement || !box.width || !box.height) {
58027
58046
  return false;
58028
58047
  }
58029
- const area = readArea(currentElement);
58030
- // Where the slide being dragged stands: where the stage put it while a
58031
- // travel is playing, its place on the map otherwise.
58048
+ const currentArea = readArea(currentElement);
58032
58049
  const stage = stageRef.current;
58033
- const basePlace = stage?.placeByArea.get(area) || placeOf.get(area) || {
58050
+ // Where a slide stands: where the stage put it while a travel is
58051
+ // playing, its place on the map otherwise.
58052
+ const placeOfArea = someArea => stage?.placeByArea.get(someArea) || placeOf.get(someArea) || {
58034
58053
  x: 0,
58035
58054
  y: 0
58036
58055
  };
58056
+ // A travel caught in flight is measured from the slide it SET OFF from,
58057
+ // with the slide it was going to as the one thing the hand can bring
58058
+ // in — the way drag_to_travel reads a `slack`: how far the box already
58059
+ // sits from its resting place, towards what is being pulled in. That is
58060
+ // what makes its answer "carry on" mean arriving. Based on the slide
58061
+ // arriving instead, the slack would point back at the origin, and a
58062
+ // hand that merely touched the box would send it all the way back.
58063
+ // A movement with no origin of its own (the track re-placed without a
58064
+ // travel) is dragged from the slide shown, like any slide at rest.
58065
+ const origin = caughtTravel && caughtTravel.from !== caughtTravel.to ? caughtTravel.from : null;
58066
+ let area;
58067
+ let areaBack;
58068
+ let areaOn;
58069
+ if (origin) {
58070
+ area = origin;
58071
+ const towardsEnd = axis === "x" ? placeOfArea(currentArea).x > placeOfArea(origin).x : placeOfArea(currentArea).y > placeOfArea(origin).y;
58072
+ areaBack = towardsEnd ? undefined : currentArea;
58073
+ areaOn = towardsEnd ? currentArea : undefined;
58074
+ } else {
58075
+ area = currentArea;
58076
+ areaBack = axis === "x" ? areaTowards(-1, 0) : areaTowards(0, -1);
58077
+ areaOn = axis === "x" ? areaTowards(1, 0) : areaTowards(0, 1);
58078
+ // The hold goToArea reads at the release, read again HERE, off the
58079
+ // same slide and the same attribute: a slide that will refuse the
58080
+ // arrival must not offer the journey. A locked direction simply has
58081
+ // nowhere to go for the length of this gesture — the one case the
58082
+ // gesture already knows, being the last slide of a walk. The hand
58083
+ // then gets the wall it can lean on and never walk through (see
58084
+ // drag_to_travel), the slide behind it stays offstage instead of
58085
+ // being read on the way, and the release has nothing left to refuse.
58086
+ // Nothing here about `released` (--navi-done): that is one particular
58087
+ // departure letting go, decided as it happens, and a gesture armed
58088
+ // before it has no such thing to read — the attribute as rendered is
58089
+ // what the hand is answered from.
58090
+ if (currentElement.hasAttribute("data-prevent-nav-previous")) {
58091
+ areaBack = undefined;
58092
+ }
58093
+ if (currentElement.hasAttribute("data-prevent-nav-next")) {
58094
+ areaOn = undefined;
58095
+ }
58096
+ if (!areaBack && !areaOn ||
58097
+ // Something else with a better claim on the gesture: a scroller
58098
+ // between the finger and the slide, with room left that way.
58099
+ scrollRoomTowards(target, currentElement, axis, sign)) {
58100
+ return false;
58101
+ }
58102
+ }
58103
+ const basePlace = placeOfArea(area);
58037
58104
  const baseOffset = {
58038
58105
  x: -basePlace.x * box.width,
58039
58106
  y: -basePlace.y * box.height
@@ -58064,7 +58131,8 @@ const SlideContainer = ({
58064
58131
  [axis]: slack
58065
58132
  };
58066
58133
  drag.progress = slack / size;
58067
- stageDrag(drag);
58134
+ drag.areaPulled = slack > 0 ? areaBack : slack < 0 ? areaOn : null;
58135
+ stageDrag(drag, currentArea);
58068
58136
  // From here the box is busy, whichever input asked: a wheel gesture and
58069
58137
  // a press must not both be moving the same track.
58070
58138
  dragRef.current = drag;
@@ -58095,24 +58163,50 @@ const SlideContainer = ({
58095
58163
  event
58096
58164
  }) => {
58097
58165
  dragRef.current = null;
58098
- if (!travels) {
58099
- returnToRest(drag);
58166
+ // Where the gesture leaves the slides: on the one it was pulling in
58167
+ // when it says so, back on the one it was dragging otherwise.
58168
+ const destination = travels ? sign > 0 ? drag.areaBack : drag.areaOn : drag.area;
58169
+ const {
58170
+ slideElements,
58171
+ placeOf
58172
+ } = readMap();
58173
+ const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current")) || slideElements[0];
58174
+ const currentArea = currentElement && readArea(currentElement);
58175
+ if (!destination || destination === currentArea) {
58176
+ // Nothing to ask of the state: the slide it names is already the one
58177
+ // shown — a drag let go of short of a travel, a travel caught in
58178
+ // flight and let carry on. The track goes there on its own.
58179
+ settleTowards(drag, destination || drag.area);
58100
58180
  return;
58101
58181
  }
58102
58182
  // Where the slide is being left, for the travel to depart from instead
58103
58183
  // of from the map.
58104
58184
  travelFromRef.current = drag.offset;
58105
58185
  // …and where the indicator is being left, said about the slide that is
58106
- // ARRIVING: the picture is `sign` of a box short of it, and that is
58107
- // what the travel about to be drawn has to close.
58108
- travelProgressFromRef.current = drag.progress - sign;
58109
- const moved = axis === "x" ? move(-sign, 0, event) : move(0, -sign, event);
58186
+ // ARRIVING: a box short of the slide being pulled in, right where the
58187
+ // finger left it when going back to the slide being dragged.
58188
+ travelProgressFromRef.current = travels ? drag.progress - sign : drag.progress;
58189
+ // Which way the travel goes, read off where the two slides stand
58190
+ // rather than off the pull: a travel caught in flight and put back
58191
+ // goes the way it came, whatever the finger did meanwhile.
58192
+ const stage = stageRef.current;
58193
+ const placeOfArea = someArea => stage?.placeByArea.get(someArea) || placeOf.get(someArea) || {
58194
+ x: 0,
58195
+ y: 0
58196
+ };
58197
+ const towards = Math.sign(placeOfArea(destination)[axis] - placeOfArea(currentArea)[axis]);
58198
+ const moved = goToArea(destination, {
58199
+ forward: towards > 0,
58200
+ event,
58201
+ dx: axis === "x" ? towards : 0,
58202
+ dy: axis === "y" ? towards : 0
58203
+ });
58110
58204
  if (!moved) {
58111
58205
  // Nowhere to go after all — a slide holding on to the user
58112
58206
  // (preventNav), or a caller that refused the change.
58113
58207
  travelFromRef.current = null;
58114
58208
  travelProgressFromRef.current = null;
58115
- returnToRest(drag);
58209
+ settleTowards(drag, currentArea);
58116
58210
  return;
58117
58211
  }
58118
58212
  // A container whose `current` is held outside and was not moved: no
@@ -58123,7 +58217,7 @@ const SlideContainer = ({
58123
58217
  if (travelFromRef.current) {
58124
58218
  travelFromRef.current = null;
58125
58219
  travelProgressFromRef.current = null;
58126
- returnToRest(drag);
58220
+ settleTowards(drag, currentArea);
58127
58221
  }
58128
58222
  });
58129
58223
  },
@@ -58138,9 +58232,15 @@ const SlideContainer = ({
58138
58232
  trackElement: trackCaught,
58139
58233
  offsetOnScreen,
58140
58234
  offsetTarget,
58141
- onScreenPx: caughtOnScreenPx
58235
+ onScreenPx: caughtOnScreenPx,
58236
+ from,
58237
+ to
58142
58238
  } = caughtTravel;
58143
58239
  caughtTravel = null;
58240
+ movingRef.current = {
58241
+ from,
58242
+ to
58243
+ };
58144
58244
  if (offsetOnScreen === offsetTarget) {
58145
58245
  settleTravel();
58146
58246
  return;
@@ -60467,6 +60567,13 @@ const clipOf = (side, distance) => {
60467
60567
  * (TARGET_WAIT_MS), on the half-strength frame where the anchor is still
60468
60568
  * readable, and lifts the moment it is there.
60469
60569
  *
60570
+ * A closing may land where no opening took off: a dialog opened the plain way
60571
+ * (`animation={{ open, close: "lifting" }}` in dialog.jsx), closing into a box the close
60572
+ * itself brings — the state `onClose` writes renders the place the lifted node
60573
+ * belongs to. So the box a closing comes back to is read once the change is
60574
+ * made, inside the transition, and when the caller named it (`liftAnchor`) it
60575
+ * is waited for (LANDING_WAIT_MS): the new picture is taken once it is there.
60576
+ *
60470
60577
  * One name serves the whole movement, because only one of the two boxes is on
60471
60578
  * screen at a time: it names the anchor while the popup is closed, and the
60472
60579
  * lifted node while it is open.
@@ -60509,6 +60616,12 @@ const ARRIVING_ATTRIBUTE = "data-navi-popup-lift-arriving";
60509
60616
  // stands, without a movement, so a target that never comes cannot keep it
60510
60617
  // unpainted.
60511
60618
  const TARGET_WAIT_MS = 1000;
60619
+ // How long a closing waits for the box it comes back to, when that box is
60620
+ // brought by the close (see this file's top comment). The page is frozen on
60621
+ // the picture of the open popup meanwhile, so the wait is short: what it
60622
+ // covers is a render, not a fetch. Past it the popup's picture plays out on
60623
+ // its own.
60624
+ const LANDING_WAIT_MS = 300;
60512
60625
  // The popup's own animation duration, published on the root because the
60513
60626
  // ::view-transition tree hangs off it and inherits from nowhere else.
60514
60627
  const DURATION_PROPERTY = "--navi-popup-lift-duration";
@@ -60549,14 +60662,16 @@ let releaseScrollHold = null;
60549
60662
  * a view transition morphing the anchor's box into the lifted node's, or back.
60550
60663
  *
60551
60664
  * `opened` says which way: the box being left is the anchor when the popup is
60552
- * opening and the lifted node when it is closing. `lift` is Dialog's own prop
60553
- * of that name.
60665
+ * opening and the lifted node when it is closing. `resolveAnchor` is read on
60666
+ * the spot for an opening, and once the change is made for a closing;
60667
+ * `waitForAnchor` has a closing wait for it when it is not there yet. `lift`
60668
+ * is Dialog's own prop of that name.
60554
60669
  */
60555
60670
  const liftPopupFromAnchor = (
60556
60671
  popupEl,
60557
- anchorElement,
60672
+ resolveAnchor,
60558
60673
  applyChange,
60559
- { opened, lift },
60674
+ { opened, lift, waitForAnchor },
60560
60675
  ) => {
60561
60676
  const startViewTransition = ensureDocumentStartViewTransition();
60562
60677
  // A movement still wearing the name would make the name two elements wide,
@@ -60564,7 +60679,7 @@ const liftPopupFromAnchor = (
60564
60679
  // document.
60565
60680
  releaseLiftInProgress?.();
60566
60681
 
60567
- const elementLeaving = opened ? anchorElement : resolveLiftTarget(popupEl);
60682
+ const elementLeaving = opened ? resolveAnchor() : resolveLiftTarget(popupEl);
60568
60683
  // Read before the first write: the read brings the style up to date, and a
60569
60684
  // write before it would make it bring it up to date once more.
60570
60685
  const duration = getComputedStyle(popupEl)
@@ -60588,12 +60703,14 @@ const liftPopupFromAnchor = (
60588
60703
 
60589
60704
  let giveBackNameArriving = null;
60590
60705
  let stopWaitingForTarget = null;
60706
+ let stopWaitingForAnchor = null;
60591
60707
  const release = () => {
60592
60708
  if (releaseLiftInProgress !== release) {
60593
60709
  return;
60594
60710
  }
60595
60711
  releaseLiftInProgress = null;
60596
60712
  stopWaitingForTarget?.();
60713
+ stopWaitingForAnchor?.();
60597
60714
  boxAnimationInProgress?.cancel();
60598
60715
  boxAnimationInProgress = null;
60599
60716
  releaseScrollHold?.();
@@ -60619,13 +60736,18 @@ const liftPopupFromAnchor = (
60619
60736
  const boxLeaving = room ? elementLeaving.getBoundingClientRect() : null;
60620
60737
  let boxArriving = null;
60621
60738
  let cornersArriving = null;
60622
- const viewTransition = startViewTransition(() => {
60739
+ const viewTransition = startViewTransition(async () => {
60623
60740
  // The name is the arriving box's from here on: worn by both, it is worn
60624
60741
  // by neither. Written rather than removed, so a name the element also
60625
60742
  // has from a stylesheet cannot resurface for the length of the movement.
60626
60743
  elementLeaving.style.setProperty(NAME_PROPERTY, "none");
60627
60744
  change();
60628
- const elementArriving = resolveElementArriving();
60745
+ const elementArriving = await resolveElementArriving();
60746
+ // Replaced while waiting for its landing: the movement replacing it
60747
+ // holds the name now.
60748
+ if (releaseLiftInProgress !== release) {
60749
+ return;
60750
+ }
60629
60751
  if (elementArriving) {
60630
60752
  giveBackNameArriving = wearLiftName(elementArriving);
60631
60753
  cornersArriving = readCorners(elementArriving);
@@ -60650,12 +60772,33 @@ const liftPopupFromAnchor = (
60650
60772
  };
60651
60773
 
60652
60774
  if (!opened) {
60653
- startMovement(applyChange, () =>
60775
+ startMovement(applyChange, () => {
60776
+ const anchorElement = resolveAnchor();
60777
+ if (anchorElement?.isConnected) {
60778
+ return anchorElement;
60779
+ }
60654
60780
  // Gone from the document while the popup was open (the row it stood in
60655
60781
  // was removed): nothing to arrive at, and the browser plays the popup's
60656
60782
  // picture out on its own.
60657
- anchorElement.isConnected ? anchorElement : null,
60658
- );
60783
+ if (!waitForAnchor) {
60784
+ return null;
60785
+ }
60786
+ return new Promise((resolve) => {
60787
+ stopWaitingForAnchor = whenAnchorAppears(resolveAnchor, (element) => {
60788
+ stopWaitingForAnchor = null;
60789
+ // Not when stopped by a movement replacing this one.
60790
+ if (
60791
+ !element &&
60792
+ releaseLiftInProgress === release
60793
+ ) {
60794
+ console.warn(
60795
+ `[navi] Dialog's "liftAnchor" named nothing on screen within ${LANDING_WAIT_MS}ms of the close, so the dialog closes without landing. The element it names is where the box comes back to: in the document at the close, or rendered by what the close changes (onClose).`,
60796
+ );
60797
+ }
60798
+ resolve(element);
60799
+ });
60800
+ });
60801
+ });
60659
60802
  return;
60660
60803
  }
60661
60804
 
@@ -60815,6 +60958,40 @@ const whenLiftTargetAppears = (popupEl, callback) => {
60815
60958
  return stop;
60816
60959
  };
60817
60960
 
60961
+ // Calls `callback` with the anchor once `resolveAnchor` finds it in the
60962
+ // document, or with null past LANDING_WAIT_MS — or when stopped, since the
60963
+ // transition's update is waiting on it. Returns how to stop.
60964
+ const whenAnchorAppears = (resolveAnchor, callback) => {
60965
+ const observer = new MutationObserver(() => {
60966
+ const anchorElement = resolveAnchor();
60967
+ if (anchorElement?.isConnected) {
60968
+ stop(anchorElement);
60969
+ }
60970
+ });
60971
+ observer.observe(document.documentElement, {
60972
+ childList: true,
60973
+ subtree: true,
60974
+ attributes: true,
60975
+ attributeFilter: ["id"],
60976
+ });
60977
+ const timeout = setTimeout(() => {
60978
+ stop(null);
60979
+ }, LANDING_WAIT_MS);
60980
+ let stopped = false;
60981
+ const stop = (anchorElement = null) => {
60982
+ if (stopped) {
60983
+ return;
60984
+ }
60985
+ stopped = true;
60986
+ observer.disconnect();
60987
+ clearTimeout(timeout);
60988
+ callback(anchorElement);
60989
+ };
60990
+ return () => {
60991
+ stop(null);
60992
+ };
60993
+ };
60994
+
60818
60995
  const ignore = () => {};
60819
60996
 
60820
60997
  // The room the fixed bars leave, in viewport coordinates; null without bars.
@@ -61480,6 +61657,18 @@ const css$E = /* css */`
61480
61657
  opacity: 0;
61481
61658
  }
61482
61659
 
61660
+ /* A closing lift is the dialog leaving as a picture: whatever exit its
61661
+ opening animation arms (animation={{ open, close: "lifting" }}, see
61662
+ popup_css.js) would
61663
+ keep it rendered into the picture of the state it closes into. */
61664
+ :root[data-navi-popup-lift="closing"] {
61665
+ .navi_dialog,
61666
+ .navi_dialog::backdrop,
61667
+ .navi_dialog_backdrop {
61668
+ transition: none;
61669
+ }
61670
+ }
61671
+
61483
61672
  /* While a dialog is lifting out of the element that opened it
61484
61673
  (popup_lift.js). The page around IS taken as a picture, the browser's own
61485
61674
  default, and on purpose: the wall and what the dialog holds around the
@@ -61722,7 +61911,7 @@ const css$E = /* css */`
61722
61911
  * scroll while open (its backdrop only covers the scrollport, so scrolling
61723
61912
  * there would reveal uncovered content); this prop extends the lock to the
61724
61913
  * whole page. Defaults to `true` for a dialog docked by `dockedOnSmallTouchScreen`.
61725
- * @param {boolean|"auto"|"fading"|"scaling"|"sliding"|"lifting"|`slide-from-${string}`} [props.animation]
61914
+ * @param {boolean|"auto"|"fading"|"scaling"|"sliding"|"lifting"|`slide-from-${string}`|{open: boolean|"auto"|"fading"|"scaling"|"sliding"|`slide-from-${string}`, close: "lifting"}} [props.animation]
61726
61915
  * - `true`/`"auto"` resolves to `"scaling"` for a centered `positionArea`,
61727
61916
  * or a concrete `"slide-from-*"` direction otherwise. Any other explicit
61728
61917
  * value is used as-is. `"lifting"` is the odd one out: every other kind
@@ -61741,6 +61930,15 @@ const css$E = /* css */`
61741
61930
  * being what the movement leaves rather than a context to keep readable;
61742
61931
  * `backdropVariant="discrete"` asks for the light wash back. See
61743
61932
  * `popup_lift.js`.
61933
+ * - `{ open, close: "lifting" }`: the close alone lifts. The dialog opens
61934
+ * with `open` (any value above but `"lifting"`), and its `data-lift` node
61935
+ * travels into `liftAnchor` on close — for a dialog that did not come out
61936
+ * of what it lands in (a banner opens a full-screen reveal, closing it puts
61937
+ * the crest in its place on the plate that replaces the banner). The box it
61938
+ * lands in may be rendered by the close itself: `liftAnchor` is read once
61939
+ * `onClose` has run, and waited for a moment when it is not there yet.
61940
+ * `"lifting"` is the only `close` that differs from the opening: every
61941
+ * other kind closes by playing its opening backwards.
61744
61942
  * @param {"box"|"scene"} [props.lift="box"] - Under `animation="lifting"`,
61745
61943
  * what the anchor and what it becomes are to each other, which decides
61746
61944
  * how their pictures sit in the box moving between them. `"box"`: one
@@ -61765,14 +61963,15 @@ const css$E = /* css */`
61765
61963
  * `document.getElementById` when the dialog opens — see popover.jsx's own
61766
61964
  * `anchor` doc for why (mainly `defaultOpen`).
61767
61965
  * @param {Element|{current: Element}|string} [props.liftAnchor] - Under
61768
- * `animation="lifting"`, where the closing brings the box back to, when that
61769
- * is no longer where it came from: a popup one walks through (a row of cards
61770
- * shown one at a time) has something else in front by the time it closes,
61771
- * and the box would otherwise fly back to the card the press opened on. Same
61772
- * grammar as `anchor` (element, ref or id), resolved at the close, so
61773
- * whatever names the card currently in frontan id built from the signal
61774
- * the walk is bound to, a ref moved with it is read then and not at the
61775
- * opening. Left out, the box comes back to the anchor it came out of.
61966
+ * `animation="lifting"` or `animation={{ open, close: "lifting" }}`, where the closing
61967
+ * brings the box back to, when that is not where it came from: a popup one
61968
+ * walks through (a row of cards shown one at a time) has something else in
61969
+ * front by the time it closes, and the box would otherwise fly back to the
61970
+ * card the press opened on. Same grammar as `anchor` (element, ref or id),
61971
+ * resolved once the close is made after `onClose` so whatever names the
61972
+ * card currently in front, or the box the close itself renders, is read
61973
+ * then and not at the opening. Left out, the box comes back to the anchor
61974
+ * it came out of.
61776
61975
  * @param {boolean} [props.sizeFromAnchor=false] - Whether the dialog takes the
61777
61976
  * anchor's width/height as a min-width/min-height floor
61778
61977
  * (`--anchor-width`/`--anchor-height`). Off by default: unlike a popover,
@@ -62260,7 +62459,18 @@ const useDialogProps = props => {
62260
62459
  flushEdges.left = expandX || x === "left" || x === "inset-left";
62261
62460
  flushEdges.right = expandX || x === "right" || x === "inset-right";
62262
62461
  }
62263
- const isAutoAnimation = animation === true || animation === "auto";
62462
+
62463
+ // `{ open, close }` says each way on its own; a single value says both.
62464
+ const {
62465
+ open: openAnimation,
62466
+ close: closeAnimation = openAnimation
62467
+ } = animation !== null && typeof animation === "object" ? animation : {
62468
+ open: animation
62469
+ };
62470
+ if (closeAnimation !== openAnimation && closeAnimation !== "lifting") {
62471
+ console.warn(`[navi] Dialog animation={{ close: ${JSON.stringify(closeAnimation)} }}: "lifting" is the only close that differs from the opening; every other kind closes by playing the opening backwards.`);
62472
+ }
62473
+ const isAutoAnimation = openAnimation === true || openAnimation === "auto";
62264
62474
  // The dialog and the anchor are one box, and what plays between them is the
62265
62475
  // browser's own morph (popup_lift.js) — nothing this dialog does to its own
62266
62476
  // box. So it arms no CSS transition of its own, which is not merely useless
@@ -62268,11 +62478,12 @@ const useDialogProps = props => {
62268
62478
  // with allow-discrete, and a dialog kept rendered for the length of its exit
62269
62479
  // is exactly what the picture taken of the state it closes into must not
62270
62480
  // show.
62271
- const lifting = animation === "lifting";
62481
+ const lifting = openAnimation === "lifting";
62482
+ const liftsOnClose = closeAnimation === "lifting";
62272
62483
  // Dialog never has a real anchor to POSITION against (see this file's top
62273
62484
  // comment), so this is always the "no anchor" path — the same one Popover's
62274
62485
  // own custom renderer falls into when it has no real anchor either.
62275
- const resolvedAnimationKind = isAutoAnimation ? resolveAutoAnimationKind(undefined, parsedPositionArea) : animation;
62486
+ const resolvedAnimationKind = isAutoAnimation ? resolveAutoAnimationKind(undefined, parsedPositionArea) : openAnimation;
62276
62487
  // Not gated on isAutoAnimation — an explicit animation="sliding" needs a
62277
62488
  // concrete direction just as much as an auto-resolved one does (same as
62278
62489
  // Popover's own "sliding"/"expanding" resolution step in openEffect).
@@ -62321,13 +62532,11 @@ const useDialogProps = props => {
62321
62532
  // is in front NOW, which only the caller knows. Resolved at the close for
62322
62533
  // that reason: the element it names changes while the popup is open, so
62323
62534
  // anything read at the opening would be the walk's starting point again.
62535
+ // Silent when it names nothing: the close may be what renders it, and
62536
+ // popup_lift.js asks again until it is there (and warns past that).
62324
62537
  const resolveLiftAnchorElement = () => {
62325
62538
  if (typeof liftAnchor === "string") {
62326
- const liftAnchorElementById = document.getElementById(liftAnchor);
62327
- if (!liftAnchorElementById) {
62328
- console.warn(`Dialog: liftAnchor="${liftAnchor}" did not match any element`);
62329
- }
62330
- return liftAnchorElementById;
62539
+ return document.getElementById(liftAnchor);
62331
62540
  }
62332
62541
  // A ref is unwrapped even when it holds nothing, the same way `anchor` is:
62333
62542
  // the ref object itself has no box to come back to.
@@ -62339,7 +62548,7 @@ const useDialogProps = props => {
62339
62548
  // provided the change happens between its two pictures, which is what
62340
62549
  // handing it to the controller buys (see popup_lift.js and
62341
62550
  // open_controller.js's own transitionChange).
62342
- openController.transitionChange = lifting ? (applyChange, {
62551
+ openController.transitionChange = liftsOnClose ? (applyChange, {
62343
62552
  opened,
62344
62553
  event
62345
62554
  }) => {
@@ -62347,29 +62556,29 @@ const useDialogProps = props => {
62347
62556
  // A mount-time opening was never seen closed (see openEffect's own
62348
62557
  // `silent`): there is no box it comes from, because nothing was shown
62349
62558
  // before it.
62350
- if (!dialogEl || opened && event.detail.silent) {
62559
+ if (!dialogEl || opened && (!lifting || event.detail.silent)) {
62351
62560
  applyChange();
62352
62561
  return;
62353
62562
  }
62354
- let anchorElement;
62355
- if (opened) {
62356
- anchorElement = resolveAnchorElement(event);
62357
- } else if (liftAnchor) {
62358
- anchorElement = resolveLiftAnchorElement();
62359
- } else {
62360
- anchorElement = anchorElementRef.current;
62563
+ if (!opened) {
62564
+ // Read once the close is made (popup_lift.js): the box it lands in
62565
+ // may be one the close itself renders.
62566
+ liftPopupFromAnchor(dialogEl, liftAnchor ? resolveLiftAnchorElement : () => anchorElementRef.current, applyChange, {
62567
+ opened,
62568
+ lift,
62569
+ waitForAnchor: Boolean(liftAnchor)
62570
+ });
62571
+ return;
62361
62572
  }
62573
+ const anchorElement = resolveAnchorElement(event);
62362
62574
  if (!anchorElement) {
62363
- if (opened) {
62575
+ {
62364
62576
  console.warn(`[navi] Dialog has animation="lifting" and no anchor to lift out of, so it simply appears. The anchor is whatever opened it — a <Button command="--navi-open">, the "source" given to triggerNaviCommand — or the "anchor" prop.`);
62365
62577
  }
62366
- if (!opened && liftAnchor) {
62367
- console.warn(`[navi] Dialog has animation="lifting" and a "liftAnchor" naming nothing on screen, so it simply closes. The element it names is where the box comes back to, and it has to be in the document at the close.`);
62368
- }
62369
62578
  applyChange();
62370
62579
  return;
62371
62580
  }
62372
- liftPopupFromAnchor(dialogEl, anchorElement, applyChange, {
62581
+ liftPopupFromAnchor(dialogEl, () => anchorElement, applyChange, {
62373
62582
  opened,
62374
62583
  lift
62375
62584
  });
@@ -65804,6 +66013,7 @@ const PickerContentInsidePopup = props => {
65804
66013
  popupWidthFitContent,
65805
66014
  animation,
65806
66015
  lift,
66016
+ liftAnchor,
65807
66017
  animationDuration,
65808
66018
  // mode="callout": what the callout says about what it holds, and paints
65809
66019
  // in its border and icon — "none" for a plain tooltip (see the callout
@@ -65900,6 +66110,7 @@ const PickerContentInsidePopup = props => {
65900
66110
  dockedOnSmallTouchScreen: isPopover ? undefined : dockedOnSmallTouchScreen,
65901
66111
  sizeFromAnchor: isPopover ? undefined : dialogSizeFromAnchor,
65902
66112
  lift: isPopover ? undefined : lift,
66113
+ liftAnchor: isPopover ? undefined : liftAnchor,
65903
66114
  children: jsx(PopupModeContext.Provider, {
65904
66115
  value: mode,
65905
66116
  children: children
@@ -75371,8 +75582,9 @@ const PickerFirstResolver = props => {
75371
75582
  * children?: import("ignore:preact").ComponentChildren,
75372
75583
  * mode?: "popover" | "dialog" | "callout",
75373
75584
  * openOn?: "press" | "longpress" | "contextmenu" | string | string[],
75374
- * animation?: boolean | "auto" | "fading" | "scaling" | "sliding" | "lifting" | `slide-from-${string}`,
75585
+ * animation?: boolean | "auto" | "fading" | "scaling" | "sliding" | "lifting" | `slide-from-${string}` | { open: boolean | "auto" | "fading" | "scaling" | "sliding" | `slide-from-${string}`, close: "lifting" },
75375
75586
  * lift?: "box" | "scene",
75587
+ * liftAnchor?: Element | { current: Element } | string,
75376
75588
  * animationDuration?: string,
75377
75589
  * calloutStatus?: "info" | "warning" | "error" | "success" | "none",
75378
75590
  * calloutIcon?: boolean,
@@ -75743,6 +75955,11 @@ const PickerFirstResolver = props => {
75743
75955
  * that opens precisely to get bigger. It brings an opaque, blurred backdrop
75744
75956
  * with it (`--navi-backdrop-lift-*`); `backdropVariant="discrete"` asks for
75745
75957
  * the light wash back.
75958
+ * @param {Element|{current: Element}|string} [liftAnchor] Dialog mode,
75959
+ * Dialog's own: where a lifting close lands when that is not the trigger
75960
+ * (`animation="lifting"`, or `animation={{ open, close: "lifting" }}` for a
75961
+ * popup that opens the plain way and only lands on close),
75962
+ * read once the close is made (after `onClose`).
75746
75963
  * @param {"box"|"scene"} [lift="box"] Dialog's own, under `animation="lifting"`:
75747
75964
  * `"box"` for a card that extends (its top stays, the box uncovers the rest),
75748
75965
  * `"scene"` for a thumbnail that is a band cut from the middle of the bigger