@jsenv/navi 0.29.507 → 0.29.509

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.
@@ -8918,7 +8918,7 @@ const route = (pattern, options = {}) => {
8918
8918
  };
8919
8919
  route.redirectTo = (
8920
8920
  params,
8921
- { callReason, history = "replace", debounceUrl } = {},
8921
+ { callReason, history = "replace", debounceUrl, isSignalChange } = {},
8922
8922
  ) => {
8923
8923
  if (!integration) {
8924
8924
  {
@@ -8933,6 +8933,7 @@ const route = (pattern, options = {}) => {
8933
8933
  replace: history !== "push",
8934
8934
  debounce: debounceUrl,
8935
8935
  callReason,
8936
+ fromSignal: isSignalChange,
8936
8937
  });
8937
8938
  };
8938
8939
  route.replaceParams = (
@@ -8947,52 +8948,18 @@ const route = (pattern, options = {}) => {
8947
8948
  return null;
8948
8949
  }
8949
8950
 
8950
- // Find all matching routes and update their actions, then delegate to most specific (deeper = more specific)
8951
- let mostSpecificRoute = route;
8952
- const routePrivateProperties = getRoutePrivateProperties(route);
8953
- let maxDepth = routePrivateProperties.routePattern.depth;
8954
- for (const routeCandidate of routeSet) {
8955
- if (routeCandidate === route) {
8956
- continue;
8957
- }
8958
- if (!routeCandidate.matching) {
8959
- continue;
8960
- }
8961
- const matchingRoute = routeCandidate;
8962
- const matchingRoutePrivateProperties =
8963
- getRoutePrivateProperties(matchingRoute);
8964
- const depth = matchingRoutePrivateProperties.routePattern.depth;
8965
- if (depth > maxDepth) {
8966
- maxDepth = depth;
8967
- mostSpecificRoute = matchingRoute;
8968
- }
8969
- }
8951
+ const mostSpecificRoute = findMostSpecificMatchingRoute(route, routeSet);
8970
8952
  const isMostSpecificRoute = mostSpecificRoute === route;
8971
8953
 
8972
- // If we found a more specific route, delegate to it; otherwise handle it ourselves
8954
+ // If we found a more specific route, delegate to it; otherwise handle it ourselves.
8955
+ // A signal change reaches this only when the more specific route does not
8956
+ // carry the param (see writeValueInUrl): left to it, the change would be dropped.
8973
8957
  if (!isMostSpecificRoute) {
8974
- // For signal-originated calls, only skip delegation if the more specific route
8975
- // has its own signal connection for the same params — meaning its own effect will
8976
- // fire and handle the redirect. If it doesn't have the connection, the signal
8977
- // change would be silently dropped, so we must delegate anyway.
8978
- if (isSignalChange) {
8979
- const mostSpecificRoutePrivateProperties =
8980
- getRoutePrivateProperties(mostSpecificRoute);
8981
- const { pathConnectionMap, queryConnectionMap } =
8982
- mostSpecificRoutePrivateProperties.routePattern;
8983
- const willHandleItself = Object.keys(newParams).every(
8984
- (paramName) =>
8985
- pathConnectionMap.has(paramName) ||
8986
- queryConnectionMap.has(paramName),
8987
- );
8988
- if (willHandleItself) {
8989
- return null;
8990
- }
8991
- }
8992
8958
  return mostSpecificRoute.redirectTo(newParams, {
8993
8959
  callReason: `replaceParams delegation from ${route} to ${mostSpecificRoute} (original reason: ${callReason})`,
8994
8960
  history,
8995
8961
  debounceUrl,
8962
+ isSignalChange,
8996
8963
  });
8997
8964
  }
8998
8965
 
@@ -9016,6 +8983,7 @@ const route = (pattern, options = {}) => {
9016
8983
  replace: history !== "push",
9017
8984
  debounce: debounceUrl,
9018
8985
  callReason,
8986
+ fromSignal: isSignalChange,
9019
8987
  });
9020
8988
  };
9021
8989
  route.matchesParams = (providedParams) => {
@@ -9078,7 +9046,7 @@ const route = (pattern, options = {}) => {
9078
9046
  });
9079
9047
  // Signal -> URL sync: When signal changes, update URL to reflect meaningful state
9080
9048
  // Only sync non-default values to keep URLs clean (static fallbacks stay invisible)
9081
- registerSetup(() => {
9049
+ registerSetup(({ routeSet }) => {
9082
9050
  const cleanupSignalUrlEffectSet = new Set();
9083
9051
  // important: read connections at setup time so it includes query connections
9084
9052
  // inherited from ancestor patterns
@@ -9117,6 +9085,24 @@ const route = (pattern, options = {}) => {
9117
9085
  // Route not matching, no URL sync needed
9118
9086
  return;
9119
9087
  }
9088
+ // A param declared on an ancestor is carried by every route below it,
9089
+ // each with its own connection: all of them hear the write, the most
9090
+ // specific one makes it. The others step aside before the history is
9091
+ // read — a stateSignalGroup's push goes to the first write reading it.
9092
+ const mostSpecificRoute = findMostSpecificMatchingRoute(
9093
+ route,
9094
+ routeSet,
9095
+ );
9096
+ if (mostSpecificRoute !== route) {
9097
+ const { pathConnectionMap, queryConnectionMap } =
9098
+ getRoutePrivateProperties(mostSpecificRoute).routePattern;
9099
+ if (
9100
+ pathConnectionMap.has(paramName) ||
9101
+ queryConnectionMap.has(paramName)
9102
+ ) {
9103
+ return;
9104
+ }
9105
+ }
9120
9106
  if (urlParamValue === undefined) {
9121
9107
  // No URL parameter exists - check if signal has meaningful value to add
9122
9108
  if (connection.isDefaultValue(value)) {
@@ -9909,6 +9895,27 @@ const urlWithoutSearchAndHash = (url) => {
9909
9895
  return `${urlObject.origin}${urlObject.pathname}`;
9910
9896
  };
9911
9897
 
9898
+ // Deeper is more specific: among the routes matching the address, the one a
9899
+ // params write lands on.
9900
+ const findMostSpecificMatchingRoute = (route, routeSet) => {
9901
+ let mostSpecificRoute = route;
9902
+ let maxDepth = getRoutePrivateProperties(route).routePattern.depth;
9903
+ for (const routeCandidate of routeSet) {
9904
+ if (routeCandidate === route) {
9905
+ continue;
9906
+ }
9907
+ if (!routeCandidate.matching) {
9908
+ continue;
9909
+ }
9910
+ const depth = getRoutePrivateProperties(routeCandidate).routePattern.depth;
9911
+ if (depth > maxDepth) {
9912
+ maxDepth = depth;
9913
+ mostSpecificRoute = routeCandidate;
9914
+ }
9915
+ }
9916
+ return mostSpecificRoute;
9917
+ };
9918
+
9912
9919
  // Every route action, with whether it asks something of the address at the
9913
9920
  // moment: its route matches and its params getter returns something.
9914
9921
  const routeActionEntriesSignal = signal([]);
@@ -14496,11 +14503,15 @@ const setupBrowserIntegrationViaHistory = ({
14496
14503
  // browser answers — a before/after cycle here would be about a navigation
14497
14504
  // that never happens. A push with something of its own for its entry — a
14498
14505
  // state, what the press handed over (see carryByLinkNavigation) — is a
14499
- // new entry, never one standing there already.
14506
+ // new entry, never one standing there already. So is a push a signal write
14507
+ // makes: the writes after it in the same move (a layer, then what is open
14508
+ // inside it) build on its address at once, and a traversal would only land
14509
+ // after they went to the entry being left.
14500
14510
  if (
14501
14511
  options.navigationType === "push" &&
14502
14512
  options.state === undefined &&
14503
14513
  !options.pressWrites &&
14514
+ !options.fromSignal &&
14504
14515
  url !== readAddress()
14505
14516
  ) {
14506
14517
  const delta = adjacentEntryDelta(url);
@@ -14923,12 +14934,23 @@ const setupBrowserIntegrationViaHistory = ({
14923
14934
 
14924
14935
  const navTo = async (
14925
14936
  url,
14926
- { replace, state, routeTransition, debounce, asReturn, open } = {},
14937
+ {
14938
+ replace,
14939
+ state,
14940
+ routeTransition,
14941
+ debounce,
14942
+ asReturn,
14943
+ open,
14944
+ fromSignal,
14945
+ } = {},
14927
14946
  ) => {
14928
14947
  const routingOptions = {
14929
14948
  reason: `navTo called with "${url}"`,
14930
14949
  navigationType: replace ? "replace" : "push",
14931
14950
  state,
14951
+ // Made by a signal written into the url (see route.js writeValueInUrl):
14952
+ // such a push is never turned into a traversal (see handleRoutingTask).
14953
+ fromSignal,
14932
14954
  // A push that lands where its address was last left (see
14933
14955
  // link_return.js).
14934
14956
  asReturn,
@@ -57481,6 +57503,10 @@ const css$$ = /* css */`@layer navi {
57481
57503
  --link-color: unset;
57482
57504
  --link-text-decoration: none;
57483
57505
  }
57506
+
57507
+ .navi_link[data-color-inherit] {
57508
+ --link-color-visited: var(--navi-link-color-visited, var(--link-color));
57509
+ }
57484
57510
  }
57485
57511
 
57486
57512
  .navi_link {
@@ -58313,6 +58339,7 @@ const LinkPlain = props => {
58313
58339
  // Visual
58314
58340
  ,
58315
58341
  "data-variant": variant,
58342
+ "data-color-inherit": props.color === "inherit" ? "" : undefined,
58316
58343
  "data-current-effect-bold": currentEffectBoldTechnique,
58317
58344
  "data-current-effect-shadow": currentEffectShadow ? "" : undefined,
58318
58345
  "data-current-indicator-position": currentIndicatorPosition,
@@ -70445,7 +70472,7 @@ const SlideContainer = ({
70445
70472
  // A transfer waiting for a travel that never happened — a controlled
70446
70473
  // `current` the caller chose not to move: dropped, the focus has no
70447
70474
  // business going anywhere. The one for a travel that DID happen stays,
70448
- // and lands when the slide it is for says it is settled (see settleFocus).
70475
+ // and lands once the slide it is for is final (see landFocusHandOver).
70449
70476
  if (focusHandOverRef.current && focusHandOverRef.current.area !== readArea(currentElement)) {
70450
70477
  focusHandOverRef.current = null;
70451
70478
  }
@@ -70599,6 +70626,11 @@ const SlideContainer = ({
70599
70626
  requestAnimationFrame(rollBack);
70600
70627
  }
70601
70628
  }
70629
+ // The slide a transfer waits for is current (any other one was dropped
70630
+ // above): it lands once the renders this commit sets off are over.
70631
+ if (focusHandOverRef.current) {
70632
+ landFocusHandOver();
70633
+ }
70602
70634
  // The keyboard is on a slide about to go out of reach and nobody has moved
70603
70635
  // it: inert would drop it on the floor (document.body), so it is handed to
70604
70636
  // the slide on screen instead. No event to read — this travel was asked for
@@ -70616,9 +70648,10 @@ const SlideContainer = ({
70616
70648
  // Out of reach LAST, once the keyboard has already moved on: a browser
70617
70649
  // takes the focus off an element the moment it becomes inert, and on its
70618
70650
  // own schedule — doing this first would let that undo the landing above and
70619
- // leave the focus on nothing.
70620
- for (const slideElement of slideElements) {
70621
- slideElement.toggleAttribute("inert", slideElement !== currentElement);
70651
+ // leave the focus on nothing. A transfer still on its way does it itself,
70652
+ // once landed.
70653
+ if (!focusHandOverRef.current) {
70654
+ markOutOfReach(currentElement);
70622
70655
  }
70623
70656
  // What is on screen now, for the travel after this one to depart from.
70624
70657
  // While a travel is playing the stage is the one that knows (it holds the
@@ -70969,14 +71002,7 @@ const SlideContainer = ({
70969
71002
  // dialog is OPEN, not when it was asked to open). `prepareFocusTransfer` has
70970
71003
  // to read the interaction — which element a mousedown landed on, whether the
70971
71004
  // modality is the keyboard — while the event is still being dispatched. The
70972
- // landing waits for the slide to say its DOM is settled (settleFocus below),
70973
- // which is later than one would think: a travel hands the slide a value
70974
- // (useSlideValue) and that value reaches it through context, so Preact
70975
- // re-renders the slide in a flush of its own — the container's own layout
70976
- // effect runs BEFORE its children have seen the value. A screen keyed by it,
70977
- // a form whose uncontrolled fields must start again from a new prefill,
70978
- // replaces its whole subtree there. Landing before that took the focus to a
70979
- // node about to be thrown away, and the focus went to document.body with it.
71005
+ // landing waits for the slide's DOM to be final (see landFocusHandOver).
70980
71006
  const handOverFocus = (slideElement, focusTransfer, event) => {
70981
71007
  focusTransfer.transferFocus(event, slideElement);
70982
71008
  // Asked of the SLIDE, not of this box: the box contains itself, so asking
@@ -71001,21 +71027,46 @@ const SlideContainer = ({
71001
71027
  }
71002
71028
  };
71003
71029
 
71004
- // A slide reporting that its DOM is settled (its own layout effect, see
71005
- // Slide) — the moment the transfer prepared in goToArea can land. Every
71006
- // slide reports on every commit; only the one a transfer is waiting for,
71007
- // and only while it is the current one, takes it.
71008
- const settleFocus = slideArea => {
71030
+ // The transfer prepared in goToArea lands once the slide it is for is the
71031
+ // current one AND everything the travel set off has rendered — which is
71032
+ // later than this box's own commit. A travel hands the slide a value
71033
+ // (useSlideValue), and what reads it below a component that skipped its own
71034
+ // render is re-rendered by Preact in a commit of its own, after this one. A
71035
+ // screen keyed by it, a form whose uncontrolled fields must start again from
71036
+ // a new prefill, replaces its whole subtree there: a focus landed before it
71037
+ // goes to document.body with the node. Nor can the slide say when it is done
71038
+ // from its own layout effect: rendered in the same pass as this box, its
71039
+ // effects run before this box has made it current.
71040
+ // Preact drains its render queue in one go, the renders it cascades into
71041
+ // included, so a microtask queued from this commit runs after all of them.
71042
+ // Until then the slide being left stays within reach (see markOutOfReach):
71043
+ // inert would take the keyboard off it first, and drop it on document.body.
71044
+ const landFocusHandOver = () => {
71009
71045
  const focusHandOver = focusHandOverRef.current;
71010
- if (!focusHandOver || focusHandOver.area !== slideArea) {
71011
- return;
71012
- }
71013
- const slideElement = readMap().slideElements.find(slideElement => readArea(slideElement) === slideArea);
71014
- if (!slideElement || !slideElement.hasAttribute("data-current")) {
71015
- return;
71046
+ queueMicrotask(() => {
71047
+ // Already landed by an earlier commit of this cascade, given up on, or
71048
+ // replaced by the transfer of a newer travel.
71049
+ if (focusHandOverRef.current !== focusHandOver || !trackRef.current) {
71050
+ return;
71051
+ }
71052
+ focusHandOverRef.current = null;
71053
+ const currentElement = readMap().slideElements.find(slideElement => slideElement.hasAttribute("data-current"));
71054
+ if (!currentElement) {
71055
+ return;
71056
+ }
71057
+ if (readArea(currentElement) === focusHandOver.area) {
71058
+ handOverFocus(currentElement, focusHandOver.focusTransfer, focusHandOver.event);
71059
+ }
71060
+ markOutOfReach(currentElement);
71061
+ });
71062
+ };
71063
+
71064
+ // Every slide but the one on screen out of reach — of the keyboard and of
71065
+ // assistive technology alike.
71066
+ const markOutOfReach = currentElement => {
71067
+ for (const slideElement of readMap().slideElements) {
71068
+ slideElement.toggleAttribute("inert", slideElement !== currentElement);
71016
71069
  }
71017
- focusHandOverRef.current = null;
71018
- handOverFocus(slideElement, focusHandOver.focusTransfer, focusHandOver.event);
71019
71070
  };
71020
71071
 
71021
71072
  /**
@@ -72203,7 +72254,6 @@ const SlideContainer = ({
72203
72254
  answeredAreas,
72204
72255
  done,
72205
72256
  valueByArea,
72206
- settleFocus,
72207
72257
  // What a slide reads to know whether its content is rendered yet
72208
72258
  // (see Slide's `mount`).
72209
72259
  mount,
@@ -72307,15 +72357,6 @@ const Slide = ({
72307
72357
  }), [nextIsLocked, preventNavPrevious]);
72308
72358
  // What the travel into this slide carried, if anything (see useSlideValue).
72309
72359
  const slideValue = container?.valueByArea?.[slideArea];
72310
- // This slide's DOM is settled — said after EVERY commit, no dependency
72311
- // array, because it is a fact about the render that just happened, not a
72312
- // reaction to a particular prop. The container is the one holding a focus
72313
- // transfer waiting for it (see settleFocus): a travel's value reaches this
72314
- // slide through context one flush after the container moved, and what it
72315
- // shows (a form keyed by a prefill) is only final here.
72316
- useLayoutEffect(() => {
72317
- container?.settleFocus?.(slideArea);
72318
- });
72319
72360
  return jsx(SlideContext.Provider, {
72320
72361
  value: locks,
72321
72362
  children: jsx(SlideValueContext.Provider, {