@jsenv/navi 0.29.68 → 0.29.69

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.
@@ -6273,12 +6273,16 @@ const route = (pattern, { searchParams } = {}) => {
6273
6273
  const routeUrl = resolveRouteUrl(routeRelativeUrl);
6274
6274
  return routeUrl;
6275
6275
  };
6276
- route.navTo = (params) => {
6276
+ // Options travel as they do to navTo() itself — `routeTransition` is the
6277
+ // one that matters here: what this one navigation asks of a route
6278
+ // transition (see route_transition.jsx), which is the programmatic half of
6279
+ // what a <Link routeTransition> says.
6280
+ route.navTo = (params, options) => {
6277
6281
  if (!integration) {
6278
6282
  return Promise.resolve();
6279
6283
  }
6280
6284
  const routeUrl = route.buildUrl(params);
6281
- return integration.navTo(routeUrl);
6285
+ return integration.navTo(routeUrl, options);
6282
6286
  };
6283
6287
  route.redirectTo = (params, { callReason } = {}) => {
6284
6288
  if (!integration) {
@@ -9250,8 +9254,7 @@ const createCalloutManager = (
9250
9254
  callout.update(message, calloutOptions);
9251
9255
  return;
9252
9256
  }
9253
- const resolvedAnchorElement =
9254
- anchorElement || controller.ref.current;
9257
+ const resolvedAnchorElement = anchorElement || controller.ref.current;
9255
9258
  const removeCloseOnCleanup = addTeardown?.(() => {
9256
9259
  requestCloseCallout(new CustomEvent("cleanup"), "cleanup");
9257
9260
  });
@@ -14998,7 +15001,8 @@ const ONE_OF_CONSTRAINT = {
14998
15001
  );
14999
15002
  const isNoMatch = visibleOptions.length === 0;
15000
15003
  const message = field.controlHostProps["data-one-of-message"];
15001
- const noMatchMessage = field.controlHostProps["data-one-of-no-match-message"];
15004
+ const noMatchMessage =
15005
+ field.controlHostProps["data-one-of-no-match-message"];
15002
15006
  if (isNoMatch) {
15003
15007
  return noMatchMessage || naviI18n("constraint.one_of.no_match");
15004
15008
  }
@@ -15575,7 +15579,11 @@ const useDebugFocus = () => {
15575
15579
  const debug = useContext(DebugFocusContext);
15576
15580
  return debug || debugNoop;
15577
15581
  };
15578
- /** Logger for virtual scroll / wheel motion (drag, momentum, glide), or a no-op. */
15582
+ /**
15583
+ * Logger for virtual scroll / wheel motion (drag, momentum, glide) and for what
15584
+ * a virtualized list does about it — the render window moving, and the rows the
15585
+ * run asks for or decides not to ask for. Or a no-op.
15586
+ */
15579
15587
  const useDebugScroll = () => {
15580
15588
  const debug = useContext(DebugScrollContext);
15581
15589
  return debug || debugNoop;
@@ -15618,7 +15626,9 @@ const useDebugUIState = () => {
15618
15626
  * @param {boolean|Function} [props.debugCommand] - navi command dispatch (`--navi-*`).
15619
15627
  * @param {boolean|Function} [props.debugInteraction] - Gated interactions; also implies focus/scroll/popup.
15620
15628
  * @param {boolean|Function} [props.debugFocus] - Focus moves and focus-visible decisions.
15621
- * @param {boolean|Function} [props.debugScroll] - Virtual scroll / wheel motion.
15629
+ * @param {boolean|Function} [props.debugScroll] - Virtual scroll / wheel motion,
15630
+ * the render window of a virtualized list, and every pass of its run — what it
15631
+ * asked for, or why it asked for nothing (see docs/list_refresh.md).
15622
15632
  * @param {boolean|Function} [props.debugPopup] - Popover/dialog open/close/positioning.
15623
15633
  * @param {boolean|Function} [props.debugAction] - Action lifecycle.
15624
15634
  * @param {boolean|Function} [props.debugUIState] - UI-state transitions and validation.
@@ -22120,8 +22130,67 @@ const setupBrowserIntegrationViaHistory = ({
22120
22130
  visitedUrlsSignal.value++;
22121
22131
  };
22122
22132
 
22133
+ // The one thing the History API cannot say and the Navigation API can: what
22134
+ // stands NEXT to the current entry. A link to the page one just came from is
22135
+ // morally a back — pushed, it grows the stack (A, B, A, B…) and lands at the
22136
+ // top; traversed, the stack stays what the reader thinks it is and the page
22137
+ // comes back where they left it. So a push whose destination is the entry
22138
+ // right behind (or right ahead) is turned into a traversal, and the whole
22139
+ // traverse machinery (routing, scroll, movement) answers it as if the
22140
+ // browser's own button had been pressed.
22141
+ //
22142
+ // Only where the browser exposes the stack (window.navigation — everywhere
22143
+ // but Firefox today; without it a push stays a push, which is what this
22144
+ // whole file already does). And only towards entries of THIS document: a
22145
+ // traversal to another document is a full page load, which no press on a
22146
+ // link asked for — the entries that are ours are recorded as they are
22147
+ // created, starting with the one this document was loaded into.
22148
+ const sameDocumentEntryKeys = new Set();
22149
+ const rememberEntryIsOfThisDocument = () => {
22150
+ if (window.navigation) {
22151
+ sameDocumentEntryKeys.add(window.navigation.currentEntry.key);
22152
+ }
22153
+ };
22154
+ rememberEntryIsOfThisDocument();
22155
+ const adjacentEntryDelta = (url) => {
22156
+ const { navigation } = window;
22157
+ if (!navigation) {
22158
+ return 0;
22159
+ }
22160
+ const entries = navigation.entries();
22161
+ const index = navigation.currentEntry.index;
22162
+ // Behind first: when the same page stands on both sides (A, B, A and one
22163
+ // is on B), a link to it reads as going back.
22164
+ for (const delta of [-1, 1]) {
22165
+ const entry = entries[index + delta];
22166
+ if (entry && entry.url === url && sameDocumentEntryKeys.has(entry.key)) {
22167
+ return delta;
22168
+ }
22169
+ }
22170
+ return 0;
22171
+ };
22172
+
22123
22173
  let abortController = null;
22124
22174
  const handleRoutingTask = (url, options) => {
22175
+ // Decided before anything is announced: an elided push IS the traversal it
22176
+ // becomes, and the traversal will make its own announcements when the
22177
+ // browser answers — a before/after cycle here would be about a navigation
22178
+ // that never happens.
22179
+ if (
22180
+ options.navigationType === "push" &&
22181
+ options.state === undefined &&
22182
+ url !== window.location.href
22183
+ ) {
22184
+ const delta = adjacentEntryDelta(url);
22185
+ if (delta === -1) {
22186
+ window.history.back();
22187
+ return undefined;
22188
+ }
22189
+ if (delta === 1) {
22190
+ window.history.forward();
22191
+ return undefined;
22192
+ }
22193
+ }
22125
22194
  // Before anything is written: the visited set, the URL and every route are
22126
22195
  // about to change, and this is the last moment the page still stands as it
22127
22196
  // was. And after, whichever way the change went out — so that whoever took
@@ -22181,6 +22250,7 @@ const setupBrowserIntegrationViaHistory = ({
22181
22250
  } else {
22182
22251
  window.history.replaceState(effectiveState, null, url);
22183
22252
  }
22253
+ rememberEntryIsOfThisDocument();
22184
22254
  updateDocumentUrl(url);
22185
22255
  updateDocumentState(effectiveState);
22186
22256
  } else {
@@ -22285,6 +22355,12 @@ const setupBrowserIntegrationViaHistory = ({
22285
22355
  handleRoutingTask(href, {
22286
22356
  reason: `"click" on a[href="${href}"]`,
22287
22357
  navigationType: "push",
22358
+ // Who started it. Announced with the navigation because a press
22359
+ // carries things the url does not: what a link asks of a route
22360
+ // transition is the first of them (see route_transition.jsx). Read by
22361
+ // whoever knows what to do with it, and it is the anchor itself —
22362
+ // resolved here, where it already is.
22363
+ element: linkElement,
22288
22364
  });
22289
22365
  },
22290
22366
  { capture: true },
@@ -22322,11 +22398,15 @@ const setupBrowserIntegrationViaHistory = ({
22322
22398
  updateDocumentUrl(window.location.href);
22323
22399
  });
22324
22400
 
22325
- const navTo = async (url, { replace, state } = {}) => {
22401
+ const navTo = async (url, { replace, state, routeTransition } = {}) => {
22326
22402
  handleRoutingTask(url, {
22327
22403
  reason: `navTo called with "${url}"`,
22328
22404
  navigationType: replace ? "replace" : "push",
22329
22405
  state,
22406
+ // What this one navigation asks of a route transition, said by the call
22407
+ // that starts it rather than by an element — the programmatic half of
22408
+ // what a <Link routeTransition> says (see route_transition.jsx).
22409
+ routeTransition,
22330
22410
  });
22331
22411
  };
22332
22412
 
@@ -22478,7 +22558,9 @@ const applyRouting = (
22478
22558
  return { ...updateActionsResult, activeRouteSet };
22479
22559
  };
22480
22560
 
22481
- const browserIntegration = setupBrowserIntegrationViaHistory({
22561
+ const setupBrowserIntegration =
22562
+ setupBrowserIntegrationViaHistory;
22563
+ const browserIntegration = setupBrowserIntegration({
22482
22564
  applyActions,
22483
22565
  applyRouting,
22484
22566
  // Routes are declared by the consumer and registered through
@@ -38666,6 +38748,18 @@ installImportMetaCssBuild(import.meta);/**
38666
38748
  * own CSS (see the JSDoc below). Said without one, the relation plays the
38667
38749
  * browser's cross-fade.
38668
38750
  *
38751
+ * A relation holds for every way of reaching a page, and one navigation may
38752
+ * know better: the rare way round a pair — a badge that jumps back OUT to the
38753
+ * game it belongs to, a card that leads to the player it describes — is walked
38754
+ * against the map, and there is no telling it from the common way by the
38755
+ * routes alone. So the navigation itself may ask for something: a `<Link
38756
+ * routeTransition>`, or navTo(url, { routeTransition }). What it asks holds
38757
+ * for THAT
38758
+ * navigation and no other, and only for the fields it names — `{ direction:
38759
+ * "back" }` keeps the pair's movement and turns it round (see
38760
+ * readNavigationRequest). A pair no relation was ever written for animates the
38761
+ * same way, for the one press that asks.
38762
+ *
38669
38763
  * There is no box in the tree: by default what animates is the document itself
38670
38764
  * (its `root` view transition group), which is right for pages that ARE the
38671
38765
  * whole viewport. An application whose pages live between fixed bars marks the
@@ -38699,6 +38793,10 @@ const TRANSITION_DURATION_PROPERTY = "--navi-route-transition-duration";
38699
38793
  // right for a page that IS the whole viewport.
38700
38794
  const TRANSITION_AREA_ATTRIBUTE = "data-navi-route-transition-area";
38701
38795
  const TRANSITION_TARGET_ATTRIBUTE = "data-navi-route-transition-target";
38796
+ // What ONE navigation asks for, over whatever the relations say: worn by the
38797
+ // link being pressed (see <Link routeTransition>), or handed to navTo(). It answers
38798
+ // for that navigation and for no other — the next one is back to the relations.
38799
+ const TRANSITION_REQUEST_ATTRIBUTE = "data-navi-route-transition-request";
38702
38800
  const AREA_NAME = "navi-route-transition";
38703
38801
  // route_travel.jsx wears this on the root for the length of one of its
38704
38802
  // travels (its TRAVEL_ATTRIBUTE — a comment there mirrors this one). Read by
@@ -38844,10 +38942,13 @@ const css$T = /* css */`
38844
38942
  The movements. One of \`root\` and \`navi-route-transition\` exists at a
38845
38943
  time (see the opt-out above), so each is written for both.
38846
38944
  ------------------------------------------------------------------ */
38847
- &[data-navi-route-transition-type="slide-x"],
38848
- &[data-navi-route-transition-type="slide-y"],
38849
- &[data-navi-route-transition-type="cover-x"],
38850
- &[data-navi-route-transition-type="cover-y"] {
38945
+ /* What a NAMED movement is made of, whatever the movement is — the types
38946
+ navi ships and the ones an application writes alike. The attribute is
38947
+ present for a type and only for a type ("cross-fade" normalizes to no
38948
+ type at all, "none" starts nothing), so the browser's own cross-fade
38949
+ keeps every default below: scaling one picture into the other and seeing
38950
+ through both IS the movement there. */
38951
+ &[data-navi-route-transition-type] {
38851
38952
  &::view-transition-old(root),
38852
38953
  &::view-transition-new(root),
38853
38954
  &::view-transition-old(navi-route-transition),
@@ -38857,20 +38958,34 @@ const css$T = /* css */`
38857
38958
  the page it crosses. The picture is as wide as the box the browser
38858
38959
  gives it — the arriving one's — so a page leaving a narrower box (a
38859
38960
  scrollbar appeared, a side panel closed) would be seen zooming over
38860
- the length of the movement. Left to the untyped cross-fade, where
38861
- scaling one picture into the other is the whole idea. */
38961
+ the length of the movement, and one leaving a shorter box would be
38962
+ seen inflating. */
38862
38963
  height: auto;
38863
38964
  object-fit: none;
38864
38965
  object-position: top left;
38865
- /* The default cross-fade, dropped: two pages sliding past each other
38866
- are two solid things, and seeing through one to the other says they
38867
- are the same page changing its mind. */
38966
+ /* Two pages crossing are two solid things, and seeing through one to
38967
+ the other says they are the same page changing its mind. A movement
38968
+ that keeps the browser's fade on one of its two sides wants the
38969
+ opposite, and says so — see zoom below. */
38868
38970
  mix-blend-mode: normal;
38869
- animation-timing-function: ease;
38870
38971
  animation-fill-mode: both;
38871
38972
  }
38872
38973
  }
38873
38974
 
38975
+ /* Eased, which is a taste about THESE four: a custom type says its own
38976
+ curve. */
38977
+ &[data-navi-route-transition-type="slide-x"],
38978
+ &[data-navi-route-transition-type="slide-y"],
38979
+ &[data-navi-route-transition-type="cover-x"],
38980
+ &[data-navi-route-transition-type="cover-y"] {
38981
+ &::view-transition-old(root),
38982
+ &::view-transition-new(root),
38983
+ &::view-transition-old(navi-route-transition),
38984
+ &::view-transition-new(navi-route-transition) {
38985
+ animation-timing-function: ease;
38986
+ }
38987
+ }
38988
+
38874
38989
  &[data-navi-route-transition-type="slide-x"] {
38875
38990
  &[data-navi-route-transition="forward"] {
38876
38991
  &::view-transition-old(root),
@@ -38983,7 +39098,11 @@ const css$T = /* css */`
38983
39098
  &::view-transition-new(root),
38984
39099
  &::view-transition-old(navi-route-transition),
38985
39100
  &::view-transition-new(navi-route-transition) {
38986
- animation-fill-mode: both;
39101
+ /* One side of this one is the browser's fade, and a fade is two
39102
+ half-transparent pictures: they must ADD up rather than cover each
39103
+ other, or the page behind shows through the middle of the
39104
+ movement. */
39105
+ mix-blend-mode: plus-lighter;
38987
39106
  }
38988
39107
  &[data-navi-route-transition="forward"] {
38989
39108
  &::view-transition-new(root),
@@ -39136,6 +39255,12 @@ const RouteTransitionArea = ({
39136
39255
  * animation-name: my-spin-in;
39137
39256
  * }
39138
39257
  * }
39258
+ *
39259
+ * Whatever is written here is what EVERY crossing of the pair plays. One
39260
+ * crossing can ask for something else — `<Link routeTransition>`, or
39261
+ * navTo(url, { routeTransition }) — which overrides this field by field, for
39262
+ * that
39263
+ * navigation alone.
39139
39264
  * @returns {() => void} remove this relation.
39140
39265
  */
39141
39266
  const defineRouteTransition = (from, to, transition) => {
@@ -39152,13 +39277,11 @@ const defineRouteTransition = (from, to, transition) => {
39152
39277
  };
39153
39278
  relations.push(relation);
39154
39279
  rebuildWatcher();
39155
- updateRoutingObservers();
39156
39280
  return () => {
39157
39281
  const index = relations.indexOf(relation);
39158
39282
  if (index > -1) {
39159
39283
  relations.splice(index, 1);
39160
39284
  rebuildWatcher();
39161
- updateRoutingObservers();
39162
39285
  }
39163
39286
  };
39164
39287
  };
@@ -39180,11 +39303,9 @@ const defineRouteDefaultTransition = transition => {
39180
39303
  import.meta.css = [css$T, "@jsenv/navi/src/nav/route_transition.jsx"];
39181
39304
  const value = normalizeTransition(transition);
39182
39305
  defaultTransition = value;
39183
- updateRoutingObservers();
39184
39306
  return () => {
39185
39307
  if (defaultTransition === value) {
39186
39308
  defaultTransition = null;
39187
- updateRoutingObservers();
39188
39309
  }
39189
39310
  };
39190
39311
  };
@@ -39205,6 +39326,98 @@ const normalizeTransition = transition => {
39205
39326
  };
39206
39327
  };
39207
39328
 
39329
+ /**
39330
+ * What THIS navigation asked for, whatever the relations say.
39331
+ *
39332
+ * A relation is about the map of the app and holds for every way of reaching a
39333
+ * page; a request is about one crossing of it. The rare way round a pair — a
39334
+ * badge that jumps back out to the game it belongs to, a card that leads to
39335
+ * the player it describes — is a navigation that knows something the pair does
39336
+ * not, and this is where it says it.
39337
+ *
39338
+ * Two mouths, one meaning: the element being pressed wears it (a `<Link
39339
+ * routeTransition>`, or the attribute by hand on any anchor), or navTo() is
39340
+ * handed
39341
+ * it. Both arrive here through the announcement the navigation makes before it
39342
+ * writes anything (see before_routing.js).
39343
+ *
39344
+ * A request answers FIELD BY FIELD: what it does not say, the relation — or
39345
+ * the default — still answers for. So `{ direction: "back" }` keeps the pair's
39346
+ * movement and only turns it round, and `"none"` cuts where something would
39347
+ * have played.
39348
+ */
39349
+ const readNavigationRequest = ({
39350
+ routeTransition,
39351
+ element
39352
+ }) => {
39353
+ if (routeTransition !== undefined && routeTransition !== null) {
39354
+ return normalizeRequest(routeTransition);
39355
+ }
39356
+ if (element && element.getAttribute) {
39357
+ const asked = element.getAttribute(TRANSITION_REQUEST_ATTRIBUTE);
39358
+ if (asked === null) {
39359
+ return null;
39360
+ }
39361
+ const value = asked.trim();
39362
+ if (value === "") {
39363
+ return null;
39364
+ }
39365
+ // A type is a name, and a name is all most links have to say. Anything
39366
+ // more — a way round, a pace — is the same object the API takes
39367
+ // everywhere else, written as JSON so that it travels on an attribute
39368
+ // (and so that a plain <a> can say it too).
39369
+ if (value[0] === "{") {
39370
+ let parsed;
39371
+ try {
39372
+ parsed = JSON.parse(value);
39373
+ } catch {
39374
+ console.warn(`${TRANSITION_REQUEST_ATTRIBUTE} is neither a type name nor JSON: ${value}`);
39375
+ return null;
39376
+ }
39377
+ return normalizeRequest(parsed);
39378
+ }
39379
+ return normalizeRequest(value);
39380
+ }
39381
+ return null;
39382
+ };
39383
+ const normalizeRequest = transition => {
39384
+ const {
39385
+ type,
39386
+ duration,
39387
+ direction
39388
+ } = typeof transition === "string" ? {
39389
+ type: transition
39390
+ } : transition;
39391
+ return {
39392
+ type: type === "cross-fade" ? undefined : type,
39393
+ // Whether a type was SAID, which is not the same as having one: asking for
39394
+ // "cross-fade" is asking for the browser's own animation, and a request
39395
+ // that names no type at all keeps the relation's.
39396
+ typeSaid: type !== undefined,
39397
+ duration,
39398
+ direction
39399
+ };
39400
+ };
39401
+
39402
+ // The request first, field by field, then what was defined for this pair (or
39403
+ // for everything). Written as one function because both ends of the file
39404
+ // resolve the same way: the one that knows the pair, and the one that only
39405
+ // knows a navigation landed.
39406
+ const resolveTransition = (request, base) => {
39407
+ const baseType = base ? base.type : undefined;
39408
+ const baseDuration = base ? base.duration : undefined;
39409
+ if (!request) {
39410
+ return {
39411
+ type: baseType,
39412
+ duration: baseDuration
39413
+ };
39414
+ }
39415
+ return {
39416
+ type: request.typeSaid ? request.type : baseType,
39417
+ duration: request.duration === undefined ? baseDuration : request.duration
39418
+ };
39419
+ };
39420
+
39208
39421
  // Every relation defined, and the single watcher standing over all of them.
39209
39422
  const relations = [];
39210
39423
  let watcher = null;
@@ -39248,27 +39461,32 @@ const rebuildWatcher = () => {
39248
39461
  return;
39249
39462
  }
39250
39463
  const found = findRelation(pages[fromIndex], pages[index]);
39251
- if (!found) {
39252
- // No relation says anything about these two: they are side by side, and
39253
- // silence is the fact — not a missing case.
39464
+ if (!found && !navigationRequest) {
39465
+ // No relation says anything about these two and this navigation asked
39466
+ // for nothing: they are side by side, and silence is the fact — not a
39467
+ // missing case.
39254
39468
  return;
39255
39469
  }
39256
39470
  const {
39257
- direction,
39258
- relation
39259
- } = found;
39260
- if (relation.type === "none") {
39471
+ type,
39472
+ duration
39473
+ } = resolveTransition(navigationRequest, found ? found.relation : null);
39474
+ if (type === "none") {
39261
39475
  // Silence said out loud: this way of the pair was written to play
39262
- // nothing, where the reverse of the other wayor the default — would
39263
- // have played.
39476
+ // nothing or this one navigation asked for nothing where the reverse
39477
+ // of the other way, or the default, would have played.
39264
39478
  navigationAnimated = true;
39265
39479
  return;
39266
39480
  }
39267
39481
  beginTransition({
39268
39482
  page: pages[index],
39269
- direction,
39270
- type: relation.type,
39271
- duration: relation.duration
39483
+ // Which way it plays: what the navigation itself said first — the link
39484
+ // being pressed is where the way the app is being walked is known — then
39485
+ // the relation, and forward for a navigation that asked for a movement
39486
+ // between two pages no relation orders.
39487
+ direction: navigationRequest && navigationRequest.direction || found && found.direction || "forward",
39488
+ type,
39489
+ duration
39272
39490
  });
39273
39491
  };
39274
39492
  // `subscribe` rather than `effect`: it hands the value to a callback that is
@@ -39280,52 +39498,57 @@ const rebuildWatcher = () => {
39280
39498
  };
39281
39499
  };
39282
39500
 
39283
- // What plays when no relation matched (see defineRouteDefaultTransition), and
39284
- // whether the navigation now landing found an answer already a relation's
39285
- // transition, a "none", a RouteTravel travel. The flag is reset when a
39286
- // navigation begins, so it is always about the latest one.
39501
+ // What plays when no relation matched (see defineRouteDefaultTransition), what
39502
+ // the navigation now landing asked for on its own (see readNavigationRequest),
39503
+ // and whether it found an answer already a relation's transition, a "none",
39504
+ // a RouteTravel travel. The last two are read at the start of every
39505
+ // navigation, so they are always about the latest one.
39287
39506
  let defaultTransition = null;
39507
+ let navigationRequest = null;
39288
39508
  let navigationAnimated = false;
39289
39509
 
39290
- // The two ends of a navigation, watched while there is anyone to animate it.
39291
- // The picture of the page being left has to be honest, so rendering is held
39292
- // from before the navigation's first write (see rendering_hold.js) — and given
39293
- // back at the far end when the change turns out to be one nobody animates,
39294
- // which is also the one moment the DEFAULT can decide: every relation has had
39295
- // its say by then.
39296
- let stopRoutingObservers = null;
39297
- const updateRoutingObservers = () => {
39298
- const wanted = relations.length > 0 || defaultTransition !== null;
39299
- if (wanted && !stopRoutingObservers) {
39300
- const stopWatchingStart = observeBeforeRouting(() => {
39301
- navigationAnimated = false;
39302
- holdRenderingForRouting();
39303
- });
39304
- const stopWatchingEnd = observeAfterRouting(() => {
39305
- if (defaultTransition && defaultTransition.type !== "none" && !navigationAnimated) {
39306
- beginTransition({
39307
- page: null,
39308
- // A default has no direction: nothing says which of two arbitrary
39309
- // pages is before the other. The attribute is worn empty — present
39310
- // for whoever keys on "one of ours is playing", silent on the way.
39311
- direction: "",
39312
- type: defaultTransition.type,
39313
- duration: defaultTransition.duration
39314
- });
39315
- }
39316
- releaseRoutingRenderingHold();
39317
- });
39318
- stopRoutingObservers = () => {
39319
- stopWatchingStart();
39320
- stopWatchingEnd();
39321
- };
39510
+ // The two ends of every navigation, watched from here on. The picture of the
39511
+ // page being left has to be honest, so rendering is held from before the
39512
+ // navigation's first write (see rendering_hold.js) — but only when something
39513
+ // could be photographed: a document where nothing is defined and nothing is
39514
+ // asked for holds nothing. It is given back at the far end, which is also the
39515
+ // one moment the DEFAULT can decide: every relation has had its say by then.
39516
+ observeBeforeRouting(details => {
39517
+ navigationAnimated = false;
39518
+ navigationRequest = readNavigationRequest(details);
39519
+ if (relations.length === 0 && !defaultTransition && !navigationRequest) {
39322
39520
  return;
39323
39521
  }
39324
- if (!wanted && stopRoutingObservers) {
39325
- stopRoutingObservers();
39326
- stopRoutingObservers = null;
39522
+ holdRenderingForRouting();
39523
+ });
39524
+ observeAfterRouting(() => {
39525
+ const request = navigationRequest;
39526
+ // Read here and dropped here: a request answers for the navigation it was
39527
+ // made on, and the next one is back to the relations.
39528
+ navigationRequest = null;
39529
+ if (!navigationAnimated && (request || defaultTransition)) {
39530
+ const {
39531
+ type,
39532
+ duration
39533
+ } = resolveTransition(request, defaultTransition);
39534
+ if (type !== "none") {
39535
+ beginTransition({
39536
+ page: null,
39537
+ // A default has no direction: nothing says which of two arbitrary
39538
+ // pages is before the other, and the attribute is then worn empty —
39539
+ // present for whoever keys on "one of ours is playing", silent on the
39540
+ // way. A request is the other case: a navigation IS a way round, so a
39541
+ // press that names the movement means forward unless it says
39542
+ // otherwise — and a movement of navi's is written on the direction,
39543
+ // so left empty it would play nothing at all.
39544
+ direction: request && request.direction || (request && request.typeSaid ? "forward" : ""),
39545
+ type,
39546
+ duration
39547
+ });
39548
+ }
39327
39549
  }
39328
- };
39550
+ releaseRoutingRenderingHold();
39551
+ });
39329
39552
 
39330
39553
  // The exact way travelled first, over the whole registry, and only then the
39331
39554
  // reverses: a relation written B → A owns that way, and being the reverse of
@@ -43501,6 +43724,14 @@ Object.assign(PSEUDO_CLASSES, {
43501
43724
  * out of flow — the "#" anchor-on-hover pattern (e.g. inside a `Title`).
43502
43725
  * @param {boolean} [props.hrefFallback] - Use `href` as the visible text when
43503
43726
  * no children are given; defaults to `true` unless `anchor`.
43727
+ * @param {string|{type?: string, duration?: number|string, direction?: "forward"|"back"}} [props.routeTransition] -
43728
+ * What pressing THIS link asks of a route transition, for that one
43729
+ * navigation: a type name (`"slide-x"`, `"none"`, …), or an object to also
43730
+ * say the pace or which way it plays. It overrides field by field what
43731
+ * `defineRouteTransition` wrote for the pair — `{ direction: "back" }` keeps
43732
+ * the pair's movement and only turns it round, which is what the rare way
43733
+ * round a pair usually needs. Said nowhere else, the relations answer as
43734
+ * they always do.
43504
43735
  * @param {boolean} [props.preventDefault] - Call `event.preventDefault()` on
43505
43736
  * click (navigation suppressed; `onClick` still runs).
43506
43737
  * @param {(event: MouseEvent) => void} [props.onClick]
@@ -43564,6 +43795,7 @@ const LinkPlain = props => {
43564
43795
  endIcon,
43565
43796
  revealOnInteraction = false,
43566
43797
  hrefFallback = !anchor,
43798
+ routeTransition,
43567
43799
  children
43568
43800
  } = props;
43569
43801
  if (anchor && !props.id) {
@@ -43670,6 +43902,13 @@ const LinkPlain = props => {
43670
43902
  } else {
43671
43903
  innerEndIcon = endIcon;
43672
43904
  }
43905
+
43906
+ // What this link asks of a route transition, worn as an attribute so that
43907
+ // the navigation reads it off the element being pressed (see
43908
+ // route_transition.jsx, which owns the name and does the reading). A type is
43909
+ // a name; anything more travels as JSON, which is also how a plain <a>
43910
+ // writes it by hand.
43911
+ const routeTransitionRequest = routeTransition === undefined || routeTransition === null ? undefined : typeof routeTransition === "string" ? routeTransition : JSON.stringify(routeTransition);
43673
43912
  const innerChildren = children || (hrefFallback ? href : children);
43674
43913
  const startIconEl = startIcon;
43675
43914
  const endIconEl = innerEndIcon;
@@ -43720,6 +43959,8 @@ const LinkPlain = props => {
43720
43959
  startIcon: undefined,
43721
43960
  endIcon: undefined,
43722
43961
  hrefFallback: undefined,
43962
+ routeTransition: undefined,
43963
+ "data-navi-route-transition-request": routeTransitionRequest,
43723
43964
  onClick: e => {
43724
43965
  onClick?.(e);
43725
43966
  if (slide) {
@@ -60331,6 +60572,18 @@ const useItemStore = ({
60331
60572
  memoryBudget,
60332
60573
  onRequestStateChange
60333
60574
  }) => {
60575
+ // The run's asking, on the same channel as the window it asks for — they are
60576
+ // one subject: what the list is about to draw is what it goes to fetch (see
60577
+ // `useRequestMissing`, and `updateRenderWindow` which logs the other half).
60578
+ //
60579
+ // A run that decides NOT to ask is the case this exists for. It sends
60580
+ // nothing and changes no state, so nothing outside can see it: the network
60581
+ // is silent, and `onRequestStateChange` — which reports what a request is
60582
+ // doing — has no request to report. A run that declined and a run that was
60583
+ // never mounted look identical from the application's side, which makes
60584
+ // "this list stopped refreshing" a question with no observable answer.
60585
+ // Here it has one, and every pass says which.
60586
+ const debugScroll = useDebugScroll();
60334
60587
  // What the source kept of the collection when the screen it was on went away
60335
60588
  // (a range reader keeps the composition: see resource_range_reader.js). The
60336
60589
  // rows are drawn from it right away and the window is asked for again — the
@@ -60557,17 +60810,32 @@ const useItemStore = ({
60557
60810
  }
60558
60811
  }
60559
60812
  const ask = () => {
60813
+ // One line per pass, whatever the outcome — an absence in the trace
60814
+ // then means the run did not render, which is a different fact from
60815
+ // the run choosing not to ask. The state that decides is on the line
60816
+ // rather than left to be inferred: `revalidating` says the run knows
60817
+ // what it holds is from before, `holdPending` that the list is on its
60818
+ // way somewhere the window does not frame yet, `count` that it knows
60819
+ // how many rows it stands for.
60820
+ const debugAsk = outcome => {
60821
+ debugScroll(`ask ${start}-${end}: ${outcome}`, `(revalidating=${revalidating} holdPending=${virtual.holdPending} count=${pages.count})`);
60822
+ };
60560
60823
  if (start === -1) {
60824
+ // Nothing missing and nothing to revalidate: the run has what it
60825
+ // draws.
60826
+ debugAsk("nothing missing");
60561
60827
  return;
60562
60828
  }
60563
60829
  if (virtual.holdPending && pages.count !== undefined && !askingAroundWantedRow) {
60564
60830
  // The one ask a hold lets through: the row the list is held on is
60565
60831
  // what would lift the hold, and nothing else is going to bring it.
60832
+ debugAsk("held on a row not reached yet");
60566
60833
  return;
60567
60834
  }
60568
60835
  const request = requestRef.current;
60569
60836
  if (revalidating && request.busy) {
60570
60837
  if (request.revalidating) {
60838
+ debugAsk("already revalidating");
60571
60839
  return;
60572
60840
  }
60573
60841
  // A page for a window that is about to be replaced wholesale.
@@ -60585,6 +60853,7 @@ const useItemStore = ({
60585
60853
  // waiting for to exist at all.
60586
60854
  const stillWanted = pages.count === undefined || request.start <= windowTo && request.end >= windowFrom;
60587
60855
  if (stillWanted) {
60856
+ debugAsk("a request still covers this window");
60588
60857
  return;
60589
60858
  }
60590
60859
  request.controller?.abort();
@@ -60595,6 +60864,7 @@ const useItemStore = ({
60595
60864
  // nothing since, can only produce the same answer. A revalidation is
60596
60865
  // exactly the case where it produces another one.
60597
60866
  if (!revalidating && request.start === start && request.end === end && request.held === held) {
60867
+ debugAsk("this range was asked for already");
60598
60868
  return;
60599
60869
  }
60600
60870
  request.start = start;
@@ -60618,6 +60888,7 @@ const useItemStore = ({
60618
60888
  };
60619
60889
  request.busy = true;
60620
60890
  request.revalidating = revalidating;
60891
+ debugAsk("sent");
60621
60892
  if (revalidating) {
60622
60893
  setRefreshing(true);
60623
60894
  }