@jsenv/navi 0.29.365 → 0.29.367

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.
@@ -29721,6 +29721,9 @@ const TRANSITION_TARGET_ATTRIBUTE = "data-navi-route-transition-target";
29721
29721
  // link being pressed (see <Link routeTransition>), or handed to navTo(). It answers
29722
29722
  // for that navigation and for no other — the next one is back to the relations.
29723
29723
  const TRANSITION_REQUEST_ATTRIBUTE = "data-navi-route-transition-request";
29724
+ // What a history entry remembers of the crossing that created it, in the
29725
+ // entry's own state: `{ from, type, direction, duration }` (see recordCrossing).
29726
+ const CROSSING_STATE_KEY = "jsenv_route_transition";
29724
29727
  const AREA_NAME = "navi-route-transition";
29725
29728
  // The pictures carrying the movement, among everything else it takes along —
29726
29729
  // the pages', or the document's own when the pages ARE the document (see
@@ -30269,6 +30272,106 @@ const normalizeRequest = transition => {
30269
30272
  };
30270
30273
  };
30271
30274
 
30275
+ /**
30276
+ * What a TRAVERSAL asks for: the crossing it retraces.
30277
+ *
30278
+ * The entry a push creates remembers the crossing that created it (see
30279
+ * recordCrossing). A back onto the page that crossing came from undoes it —
30280
+ * the same movement, the other way — and a forward onto an entry whose
30281
+ * crossing came from the page being left plays it again as it was. Both
30282
+ * entries are read because each side is the only one that knows its case: the
30283
+ * entry being LEFT says how it was reached (a back), the entry being REACHED
30284
+ * says how it was reached (a forward).
30285
+ *
30286
+ * Both can be true at once — A, B, A again: leaving the second A for B is the
30287
+ * way back of A → B and the way in of B → A. The entries' depths in the stack
30288
+ * tell them apart; without a depth on both, a back is assumed, the traversal
30289
+ * by far the most often made.
30290
+ *
30291
+ * Answers in the shape of readNavigationRequest, with every field said, so
30292
+ * that the relations have nothing left to answer for — and marked as a replay,
30293
+ * because one relation still outranks it: the one written for the exact way
30294
+ * travelled (see the watcher's onMove).
30295
+ */
30296
+ const readTraversalReplay = ({
30297
+ url,
30298
+ state
30299
+ }, {
30300
+ fromUrl,
30301
+ fromState
30302
+ }) => {
30303
+ const to = absoluteUrl(url);
30304
+ const from = absoluteUrl(fromUrl);
30305
+ if (!to || !from) {
30306
+ return null;
30307
+ }
30308
+ const crossingIn = crossingRecordedOn(state);
30309
+ const crossingOut = crossingRecordedOn(fromState);
30310
+ const isForward = crossingIn !== null && crossingIn.from === from;
30311
+ const isBack = crossingOut !== null && crossingOut.from === to;
30312
+ if (isForward && isBack) {
30313
+ const depthIn = navDepthOf(state);
30314
+ const depthOut = navDepthOf(fromState);
30315
+ if (depthIn !== undefined && depthOut !== undefined && depthIn > depthOut) {
30316
+ return replayOf(crossingIn);
30317
+ }
30318
+ return replayOf(reverseCrossing(crossingOut));
30319
+ }
30320
+ if (isForward) {
30321
+ return replayOf(crossingIn);
30322
+ }
30323
+ if (isBack) {
30324
+ return replayOf(reverseCrossing(crossingOut));
30325
+ }
30326
+ return null;
30327
+ };
30328
+ const crossingRecordedOn = state => {
30329
+ if (!state) {
30330
+ return null;
30331
+ }
30332
+ const crossing = state[CROSSING_STATE_KEY];
30333
+ if (!crossing || typeof crossing.from !== "string") {
30334
+ return null;
30335
+ }
30336
+ return crossing;
30337
+ };
30338
+ const navDepthOf = state => {
30339
+ if (state && typeof state[NAV_DEPTH_STATE_KEY] === "number") {
30340
+ return state[NAV_DEPTH_STATE_KEY];
30341
+ }
30342
+ return undefined;
30343
+ };
30344
+ const reverseCrossing = crossing => {
30345
+ return {
30346
+ ...crossing,
30347
+ direction: reverseDirection(crossing.direction)
30348
+ };
30349
+ };
30350
+
30351
+ // A direction that is neither ("" — a default has none) stays what it is.
30352
+ const reverseDirection = direction => {
30353
+ if (direction === "forward") {
30354
+ return "back";
30355
+ }
30356
+ if (direction === "back") {
30357
+ return "forward";
30358
+ }
30359
+ return direction;
30360
+ };
30361
+ const replayOf = ({
30362
+ type,
30363
+ direction,
30364
+ duration
30365
+ }) => {
30366
+ return {
30367
+ type,
30368
+ typeSaid: true,
30369
+ duration,
30370
+ direction,
30371
+ replay: true
30372
+ };
30373
+ };
30374
+
30272
30375
  // The request first, field by field, then what was defined for this pair (or
30273
30376
  // for everything). Written as one function because both ends of the file
30274
30377
  // resolve the same way: the one that knows the pair, and the one that only
@@ -30344,7 +30447,14 @@ const rebuildWatcher = () => {
30344
30447
  const fromPage = fromIndex === -1 ? null : pages[fromIndex];
30345
30448
  const toPage = index === -1 ? null : pages[index];
30346
30449
  const found = findRelation(fromPage, toPage);
30347
- if (!found && !navigationRequest) {
30450
+ // A traversal retraces its crossing over everything deduced here — the
30451
+ // reverse of a pair, a page from anywhere — and not over a relation
30452
+ // written for this exact way: that line is the author's one tool for
30453
+ // breaking reciprocity, and the back button is the way back it has to
30454
+ // reach. A request made by a link or a navTo() is never dropped: it is
30455
+ // about this one crossing, and a written relation is about every one.
30456
+ const request = navigationRequest && navigationRequest.replay && found && found.written ? null : navigationRequest;
30457
+ if (!found && !request) {
30348
30458
  // No relation says anything about these two and this navigation asked
30349
30459
  // for nothing: they are side by side, and silence is the fact — not a
30350
30460
  // missing case.
@@ -30353,12 +30463,15 @@ const rebuildWatcher = () => {
30353
30463
  const {
30354
30464
  type,
30355
30465
  duration
30356
- } = resolveTransition(navigationRequest, found ? found.relation : null);
30466
+ } = resolveTransition(request, found ? found.relation : null);
30357
30467
  if (type === "none") {
30358
30468
  // Silence said out loud: this way of the pair was written to play
30359
30469
  // nothing — or this one navigation asked for nothing — where the reverse
30360
30470
  // of the other way, or the default, would have played.
30361
30471
  navigationAnimated = true;
30472
+ navigationDecision = {
30473
+ type: "none"
30474
+ };
30362
30475
  return;
30363
30476
  }
30364
30477
  beginTransition({
@@ -30366,10 +30479,11 @@ const rebuildWatcher = () => {
30366
30479
  url: navigationUrl,
30367
30480
  fromUrl: navigationFromUrl,
30368
30481
  // Which way it plays: what the navigation itself said first — the link
30369
- // being pressed is where the way the app is being walked is known — then
30370
- // the relation, and forward for a navigation that asked for a movement
30371
- // between two pages no relation orders.
30372
- direction: navigationRequest && navigationRequest.direction || found && found.direction || "forward",
30482
+ // being pressed is where the way the app is being walked is known, and
30483
+ // a traversal says the way it retraces then the relation, and forward
30484
+ // for a navigation that asked for a movement between two pages no
30485
+ // relation orders.
30486
+ direction: request && request.direction !== undefined ? request.direction : found && found.direction || "forward",
30373
30487
  type,
30374
30488
  duration
30375
30489
  });
@@ -30398,6 +30512,13 @@ let navigationUrl = null;
30398
30512
  // the address has already moved and location would answer with the destination.
30399
30513
  let navigationFromUrl = null;
30400
30514
  let navigationAnimated = false;
30515
+ // "push", "replace", "traverse", … — a push is the one navigation that creates
30516
+ // the entry a crossing is recorded on.
30517
+ let navigationType = null;
30518
+ // What was decided for the navigation now landing — a movement, or "none" —
30519
+ // which is what its entry remembers (see recordCrossing). Null while nothing
30520
+ // has been decided, and for a navigation nothing was said about.
30521
+ let navigationDecision = null;
30401
30522
 
30402
30523
  // The two ends of every navigation, watched from here on. The picture of the
30403
30524
  // page being left has to be honest, so rendering is held from before the
@@ -30407,9 +30528,18 @@ let navigationAnimated = false;
30407
30528
  // one moment the DEFAULT can decide: every relation has had its say by then.
30408
30529
  observeBeforeRouting(details => {
30409
30530
  navigationAnimated = false;
30410
- navigationRequest = readNavigationRequest(details);
30531
+ navigationDecision = null;
30532
+ navigationType = details.navigationType;
30411
30533
  navigationUrl = details.url;
30412
30534
  navigationFromUrl = documentUrlSignal.peek();
30535
+ // A traversal has no element and no call to ask anything: what it asks is
30536
+ // the crossing it retraces — reversed for a back, as it was for a forward.
30537
+ // Read before the document state moves, so the state peeked is the entry
30538
+ // being left.
30539
+ navigationRequest = navigationType === "traverse" ? readTraversalReplay(details, {
30540
+ fromUrl: navigationFromUrl,
30541
+ fromState: documentStateSignal.peek()
30542
+ }) : readNavigationRequest(details);
30413
30543
  if (relations.length === 0 && !defaultTransition && !navigationRequest) {
30414
30544
  return;
30415
30545
  }
@@ -30426,17 +30556,23 @@ observeAfterRouting(() => {
30426
30556
  const request = navigationRequest;
30427
30557
  const url = navigationUrl;
30428
30558
  const fromUrl = navigationFromUrl;
30559
+ const type = navigationType;
30429
30560
  // Read here and dropped here: a request answers for the navigation it was
30430
30561
  // made on, and the next one is back to the relations.
30431
30562
  navigationRequest = null;
30432
30563
  navigationUrl = null;
30433
30564
  navigationFromUrl = null;
30565
+ navigationType = null;
30434
30566
  if (!navigationAnimated && (request || defaultTransition)) {
30435
30567
  const {
30436
30568
  type,
30437
30569
  duration
30438
30570
  } = resolveTransition(request, defaultTransition);
30439
- if (type !== "none") {
30571
+ if (type === "none") {
30572
+ navigationDecision = {
30573
+ type: "none"
30574
+ };
30575
+ } else {
30440
30576
  beginTransition({
30441
30577
  page: null,
30442
30578
  url,
@@ -30448,15 +30584,70 @@ observeAfterRouting(() => {
30448
30584
  // press that names the movement means forward unless it says
30449
30585
  // otherwise — and a movement of navi's is written on the direction,
30450
30586
  // so left empty it would play nothing at all.
30451
- direction: request && request.direction || (request && request.typeSaid ? "forward" : ""),
30587
+ direction: request && request.direction !== undefined ? request.direction : request && request.typeSaid ? "forward" : "",
30452
30588
  type,
30453
30589
  duration
30454
30590
  });
30455
30591
  }
30456
30592
  }
30593
+ if (type === "push") {
30594
+ recordCrossing({
30595
+ url,
30596
+ fromUrl,
30597
+ decision: navigationDecision
30598
+ });
30599
+ }
30600
+ navigationDecision = null;
30457
30601
  releaseRoutingRenderingHold();
30458
30602
  });
30459
30603
 
30604
+ // The entry a push created remembers what was decided on the way in, so that
30605
+ // the traversals leaving it or landing on it retrace it (see
30606
+ // readTraversalReplay). Written once the navigation has landed — the decision
30607
+ // needs the pages to be current, which is after the entry was created — as a
30608
+ // state-only replace: it announces nothing and routes nothing. Nothing is
30609
+ // written when nothing was decided: the silence between two unrelated pages is
30610
+ // not a crossing to remember. A replace keeps its entry's state, so an entry
30611
+ // reached by one keeps the crossing that led to where it stands.
30612
+ const recordCrossing = ({
30613
+ url,
30614
+ fromUrl,
30615
+ decision
30616
+ }) => {
30617
+ if (!decision) {
30618
+ return;
30619
+ }
30620
+ const to = absoluteUrl(url);
30621
+ const from = absoluteUrl(fromUrl);
30622
+ if (!to || !from) {
30623
+ return;
30624
+ }
30625
+ if (documentUrlSignal.peek() !== to) {
30626
+ // Superseded before it landed: the entry now current is another one's.
30627
+ return;
30628
+ }
30629
+ const crossing = {
30630
+ from
30631
+ };
30632
+ if (decision.type !== undefined) {
30633
+ crossing.type = decision.type;
30634
+ }
30635
+ if (decision.direction !== undefined) {
30636
+ crossing.direction = decision.direction;
30637
+ }
30638
+ if (decision.duration !== undefined) {
30639
+ crossing.duration = decision.duration;
30640
+ }
30641
+ const state = documentStateSignal.peek();
30642
+ navTo(to, {
30643
+ replace: true,
30644
+ state: {
30645
+ ...(state || {}),
30646
+ [CROSSING_STATE_KEY]: crossing
30647
+ }
30648
+ });
30649
+ };
30650
+
30460
30651
  // The exact way travelled first, over the whole registry, then the reverses,
30461
30652
  // and last the pages written from anywhere.
30462
30653
  //
@@ -30469,6 +30660,11 @@ observeAfterRouting(() => {
30469
30660
  // same destination still owns its crossing — the map, where it was drawn, is
30470
30661
  // more precise than "from wherever". Arriving is read before leaving: between
30471
30662
  // two such pages, the one being opened says what plays.
30663
+ //
30664
+ // Only the first answer is `written`: a sentence the author wrote about this
30665
+ // exact way. Every other answer is deduced from a sentence about something
30666
+ // else, and a traversal retracing its own crossing knows better than a
30667
+ // deduction (see readTraversalReplay).
30472
30668
  const findRelation = (fromPage, toPage) => {
30473
30669
  for (const relation of relations) {
30474
30670
  if (!relation.from) {
@@ -30477,7 +30673,8 @@ const findRelation = (fromPage, toPage) => {
30477
30673
  if (samePage$1(relation.from, fromPage) && samePage$1(relation.to, toPage)) {
30478
30674
  return {
30479
30675
  direction: "forward",
30480
- relation
30676
+ relation,
30677
+ written: true
30481
30678
  };
30482
30679
  }
30483
30680
  }
@@ -30539,12 +30736,19 @@ const beginTransition = ({
30539
30736
  console.warn("A RouteTravel is animating this navigation; the route transition defined between these routes is skipped. Animate a pair of routes with RouteTravel or defineRouteTransition, not both.");
30540
30737
  return;
30541
30738
  }
30739
+ navigationDecision = {
30740
+ type,
30741
+ direction,
30742
+ duration
30743
+ };
30542
30744
  // The two ends of the crossing, kept for the length of the movement: they are
30543
30745
  // what lets the navigation after this one be recognised as its way back (see
30544
- // turnRunningTransitionRound).
30746
+ // turnRunningTransitionRound) — and what it decided, which that way back
30747
+ // then undoes.
30545
30748
  const transition = {
30546
30749
  fromUrl: absoluteUrl(fromUrl),
30547
30750
  url: absoluteUrl(url),
30751
+ decision: navigationDecision,
30548
30752
  walkHome: null,
30549
30753
  releaseReverting: null
30550
30754
  };
@@ -30720,6 +30924,7 @@ const turnRunningTransitionRound = (fromUrl, url) => {
30720
30924
  }
30721
30925
  const animations = viewTransitionAnimations();
30722
30926
  if (isWayInAgain) {
30927
+ navigationDecision = running.decision;
30723
30928
  // The token is dropped first: the walk it stands for is the one that must
30724
30929
  // not arrive anywhere anymore, and its promise is still pending.
30725
30930
  running.walkHome = null;
@@ -30737,6 +30942,9 @@ const turnRunningTransitionRound = (fromUrl, url) => {
30737
30942
  // played, and nothing was on screen to teleport.
30738
30943
  return false;
30739
30944
  }
30945
+ // What this navigation plays, for the entry it may create: the movement on
30946
+ // screen, the other way.
30947
+ navigationDecision = reverseCrossing(running.decision);
30740
30948
  // Which walk home this is, so the one that arrives is the one still wanted: a
30741
30949
  // walk turned round mid-way leaves a promise nobody cancelled, and it settles
30742
30950
  // when the pictures reach the far end.
@@ -30939,7 +31147,7 @@ const warnAboutBothWaysWritten = ({
30939
31147
  // wrote them in, and the order it will find them in to fix them.
30940
31148
  const written = `${describePage(reverse.from)} → ${describePage(reverse.to)}`;
30941
31149
  const added = `${describePage(from)} → ${describePage(to)}`;
30942
- warnOnce(`both-ways-written:${written}|${added}`, `${written} and ${added} are both written with the same movement, so BOTH crossings play forward and this pair can never say "back" — the back button included. A relation written for the exact way travelled wins over being the reverse of another (see findRelation), which is what makes reciprocity the default: write the way back only to give it a DIFFERENT movement, or "none" to silence it. A single crossing that walks the map backwards says so on itself instead: <Link routeTransition={{ direction: "forward" }}>, or navTo(url, { routeTransition: { direction: "forward" } }).`);
31150
+ warnOnce(`both-ways-written:${written}|${added}`, `${written} and ${added} are both written with the same movement, so BOTH crossings play forward and this pair can never say "back" — the back button included. A relation written for the exact way travelled wins over being the reverse of another, and over the crossing a history traversal retraces (see findRelation), which is what makes reciprocity the default: write the way back only to give it a DIFFERENT movement, or "none" to silence it. A single crossing that walks the map backwards says so on itself instead: <Link routeTransition={{ direction: "forward" }}>, or navTo(url, { routeTransition: { direction: "forward" } }).`);
30943
31151
  };
30944
31152
  const warnPagesBothCurrent = (pageKept, pageIgnored) => {
30945
31153
  const kept = describePage(pageKept);
@@ -64090,8 +64298,8 @@ const PickerCustom = props => {
64090
64298
  // before computing popupId below, so two Pickers without an explicit id never collide.
64091
64299
  // Captured before the fallback chain below overwrites props.id — needed to
64092
64300
  // know whether the id actually came from the caller (stable) or from
64093
- // useId()/ControlIdContext (not guaranteed stable across a reload), see
64094
- // pickerNavType below.
64301
+ // useId()/ControlIdContext (a generated id names one mount: a reload, or a
64302
+ // return to this page, generates another), see pickerNavType below.
64095
64303
  const hasExplicitId = Boolean(props.id);
64096
64304
  const idDefault = useId();
64097
64305
  const controlId = useContext(ControlIdContext);
@@ -64167,10 +64375,12 @@ const PickerCustom = props => {
64167
64375
  // pushes a history entry so the back button closes it. Every other case
64168
64376
  // (popover mode, or a dialog whose id was auto-generated via useId()/
64169
64377
  // ControlIdContext) replaces the current history state instead — a
64170
- // generated id isn't stable across a reload, so pushing it would either
64171
- // silently drop the entry or, worse, collide with a different
64172
- // component's own generated id (see useNavState's own fallback for the
64173
- // same concern, applied here proactively for the id we control).
64378
+ // generated id names one mount, so pushing it would either leave an entry
64379
+ // nothing reads or, worse, collide with a different component's own
64380
+ // generated id (see useNavState's own fallback for the same concern,
64381
+ // applied here proactively for the id we control). What a generated id
64382
+ // costs either way: the state is written, and the mount coming back to
64383
+ // the page (or a reload) finds it under a key it does not have.
64174
64384
  const pickerNavType = mode === "dialog" && hasExplicitId ? "push" : "replace";
64175
64385
  const [expanded, enterExpanded, leaveExpanded] = useNavState(popupId, {
64176
64386
  type: pickerNavType,