@jsenv/navi 0.29.508 → 0.29.510

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.
@@ -8553,7 +8553,7 @@ const route = (pattern, options = {}) => {
8553
8553
  };
8554
8554
  route.redirectTo = (
8555
8555
  params,
8556
- { callReason, history = "replace", debounceUrl } = {},
8556
+ { callReason, history = "replace", debounceUrl, isSignalChange } = {},
8557
8557
  ) => {
8558
8558
  if (!integration) {
8559
8559
  return Promise.resolve();
@@ -8563,6 +8563,7 @@ const route = (pattern, options = {}) => {
8563
8563
  replace: history !== "push",
8564
8564
  debounce: debounceUrl,
8565
8565
  callReason,
8566
+ fromSignal: isSignalChange,
8566
8567
  });
8567
8568
  };
8568
8569
  route.replaceParams = (
@@ -8577,52 +8578,18 @@ const route = (pattern, options = {}) => {
8577
8578
  return null;
8578
8579
  }
8579
8580
 
8580
- // Find all matching routes and update their actions, then delegate to most specific (deeper = more specific)
8581
- let mostSpecificRoute = route;
8582
- const routePrivateProperties = getRoutePrivateProperties(route);
8583
- let maxDepth = routePrivateProperties.routePattern.depth;
8584
- for (const routeCandidate of routeSet) {
8585
- if (routeCandidate === route) {
8586
- continue;
8587
- }
8588
- if (!routeCandidate.matching) {
8589
- continue;
8590
- }
8591
- const matchingRoute = routeCandidate;
8592
- const matchingRoutePrivateProperties =
8593
- getRoutePrivateProperties(matchingRoute);
8594
- const depth = matchingRoutePrivateProperties.routePattern.depth;
8595
- if (depth > maxDepth) {
8596
- maxDepth = depth;
8597
- mostSpecificRoute = matchingRoute;
8598
- }
8599
- }
8581
+ const mostSpecificRoute = findMostSpecificMatchingRoute(route, routeSet);
8600
8582
  const isMostSpecificRoute = mostSpecificRoute === route;
8601
8583
 
8602
- // If we found a more specific route, delegate to it; otherwise handle it ourselves
8584
+ // If we found a more specific route, delegate to it; otherwise handle it ourselves.
8585
+ // A signal change reaches this only when the more specific route does not
8586
+ // carry the param (see writeValueInUrl): left to it, the change would be dropped.
8603
8587
  if (!isMostSpecificRoute) {
8604
- // For signal-originated calls, only skip delegation if the more specific route
8605
- // has its own signal connection for the same params — meaning its own effect will
8606
- // fire and handle the redirect. If it doesn't have the connection, the signal
8607
- // change would be silently dropped, so we must delegate anyway.
8608
- if (isSignalChange) {
8609
- const mostSpecificRoutePrivateProperties =
8610
- getRoutePrivateProperties(mostSpecificRoute);
8611
- const { pathConnectionMap, queryConnectionMap } =
8612
- mostSpecificRoutePrivateProperties.routePattern;
8613
- const willHandleItself = Object.keys(newParams).every(
8614
- (paramName) =>
8615
- pathConnectionMap.has(paramName) ||
8616
- queryConnectionMap.has(paramName),
8617
- );
8618
- if (willHandleItself) {
8619
- return null;
8620
- }
8621
- }
8622
8588
  return mostSpecificRoute.redirectTo(newParams, {
8623
8589
  callReason: `replaceParams delegation from ${route} to ${mostSpecificRoute} (original reason: ${callReason})`,
8624
8590
  history,
8625
8591
  debounceUrl,
8592
+ isSignalChange,
8626
8593
  });
8627
8594
  }
8628
8595
 
@@ -8641,6 +8608,7 @@ const route = (pattern, options = {}) => {
8641
8608
  replace: history !== "push",
8642
8609
  debounce: debounceUrl,
8643
8610
  callReason,
8611
+ fromSignal: isSignalChange,
8644
8612
  });
8645
8613
  };
8646
8614
  route.matchesParams = (providedParams) => {
@@ -8703,7 +8671,7 @@ const route = (pattern, options = {}) => {
8703
8671
  });
8704
8672
  // Signal -> URL sync: When signal changes, update URL to reflect meaningful state
8705
8673
  // Only sync non-default values to keep URLs clean (static fallbacks stay invisible)
8706
- registerSetup(() => {
8674
+ registerSetup(({ routeSet }) => {
8707
8675
  const cleanupSignalUrlEffectSet = new Set();
8708
8676
  // important: read connections at setup time so it includes query connections
8709
8677
  // inherited from ancestor patterns
@@ -8742,6 +8710,24 @@ const route = (pattern, options = {}) => {
8742
8710
  // Route not matching, no URL sync needed
8743
8711
  return;
8744
8712
  }
8713
+ // A param declared on an ancestor is carried by every route below it,
8714
+ // each with its own connection: all of them hear the write, the most
8715
+ // specific one makes it. The others step aside before the history is
8716
+ // read — a stateSignalGroup's push goes to the first write reading it.
8717
+ const mostSpecificRoute = findMostSpecificMatchingRoute(
8718
+ route,
8719
+ routeSet,
8720
+ );
8721
+ if (mostSpecificRoute !== route) {
8722
+ const { pathConnectionMap, queryConnectionMap } =
8723
+ getRoutePrivateProperties(mostSpecificRoute).routePattern;
8724
+ if (
8725
+ pathConnectionMap.has(paramName) ||
8726
+ queryConnectionMap.has(paramName)
8727
+ ) {
8728
+ return;
8729
+ }
8730
+ }
8745
8731
  if (urlParamValue === undefined) {
8746
8732
  // No URL parameter exists - check if signal has meaningful value to add
8747
8733
  if (connection.isDefaultValue(value)) {
@@ -9515,6 +9501,27 @@ const urlWithoutSearchAndHash = (url) => {
9515
9501
  return `${urlObject.origin}${urlObject.pathname}`;
9516
9502
  };
9517
9503
 
9504
+ // Deeper is more specific: among the routes matching the address, the one a
9505
+ // params write lands on.
9506
+ const findMostSpecificMatchingRoute = (route, routeSet) => {
9507
+ let mostSpecificRoute = route;
9508
+ let maxDepth = getRoutePrivateProperties(route).routePattern.depth;
9509
+ for (const routeCandidate of routeSet) {
9510
+ if (routeCandidate === route) {
9511
+ continue;
9512
+ }
9513
+ if (!routeCandidate.matching) {
9514
+ continue;
9515
+ }
9516
+ const depth = getRoutePrivateProperties(routeCandidate).routePattern.depth;
9517
+ if (depth > maxDepth) {
9518
+ maxDepth = depth;
9519
+ mostSpecificRoute = routeCandidate;
9520
+ }
9521
+ }
9522
+ return mostSpecificRoute;
9523
+ };
9524
+
9518
9525
  // Every route action, with whether it asks something of the address at the
9519
9526
  // moment: its route matches and its params getter returns something.
9520
9527
  const routeActionEntriesSignal = signal([]);
@@ -13890,11 +13897,15 @@ const setupBrowserIntegrationViaHistory = ({
13890
13897
  // browser answers — a before/after cycle here would be about a navigation
13891
13898
  // that never happens. A push with something of its own for its entry — a
13892
13899
  // state, what the press handed over (see carryByLinkNavigation) — is a
13893
- // new entry, never one standing there already.
13900
+ // new entry, never one standing there already. So is a push a signal write
13901
+ // makes: the writes after it in the same move (a layer, then what is open
13902
+ // inside it) build on its address at once, and a traversal would only land
13903
+ // after they went to the entry being left.
13894
13904
  if (
13895
13905
  options.navigationType === "push" &&
13896
13906
  options.state === undefined &&
13897
13907
  !options.pressWrites &&
13908
+ !options.fromSignal &&
13898
13909
  url !== readAddress()
13899
13910
  ) {
13900
13911
  const delta = adjacentEntryDelta(url);
@@ -14317,12 +14328,23 @@ const setupBrowserIntegrationViaHistory = ({
14317
14328
 
14318
14329
  const navTo = async (
14319
14330
  url,
14320
- { replace, state, routeTransition, debounce, asReturn, open } = {},
14331
+ {
14332
+ replace,
14333
+ state,
14334
+ routeTransition,
14335
+ debounce,
14336
+ asReturn,
14337
+ open,
14338
+ fromSignal,
14339
+ } = {},
14321
14340
  ) => {
14322
14341
  const routingOptions = {
14323
14342
  reason: `navTo called with "${url}"`,
14324
14343
  navigationType: replace ? "replace" : "push",
14325
14344
  state,
14345
+ // Made by a signal written into the url (see route.js writeValueInUrl):
14346
+ // such a push is never turned into a traversal (see handleRoutingTask).
14347
+ fromSignal,
14326
14348
  // A push that lands where its address was last left (see
14327
14349
  // link_return.js).
14328
14350
  asReturn,
@@ -47077,7 +47099,7 @@ const useRenderScope = (init, update) => {
47077
47099
  * interface UIStateController {
47078
47100
  * id: string;
47079
47101
  * controlType: string; // Used for debug logging
47080
- * uiStateSignal: Signal; // Accessed by button children to inherit parent value
47102
+ * uiStateSignal: Signal | null; // What a button child with no value of its own inherits (null: nothing)
47081
47103
  * registerChild(child): void; // Called on child mount
47082
47104
  * onChildUIAction(child, e, { stateChanged: boolean }): void; // Called when a child fires a UI action
47083
47105
  * unregisterChild(child): void; // Called on child unmount
@@ -49447,7 +49469,12 @@ const useUIFacadeStateController = (props, realUIStateController) => {
49447
49469
  props,
49448
49470
  pushStateDownToChild,
49449
49471
  ref: realUIStateController.ref,
49450
- uiStateSignal: realUIStateController.uiStateSignal,
49472
+ // Nothing for a button in the popup to inherit. The picker's value is
49473
+ // what its one child holds, and a button is refused as that child
49474
+ // (canRegisterAsFacadeChild): it answers for itself. Handed the value,
49475
+ // its action would run with the picker's selection over the params it
49476
+ // was bound with — a "refresh" in a sort picker sending the sort order.
49477
+ uiStateSignal: null,
49451
49478
  controlHostProps: realUIStateController.controlHostProps,
49452
49479
  readOnlyWithoutRun: realUIStateController.readOnlyWithoutRun,
49453
49480
  registerChild: (child) => {
@@ -49577,7 +49604,6 @@ const useUIFacadeStateController = (props, realUIStateController) => {
49577
49604
  (s) => {
49578
49605
  s.controller.props = props;
49579
49606
  s.controller.ref = realUIStateController.ref;
49580
- s.controller.uiStateSignal = realUIStateController.uiStateSignal;
49581
49607
  s.controller.controlHostProps = realUIStateController.controlHostProps;
49582
49608
  s.controller.readOnlyWithoutRun =
49583
49609
  realUIStateController.readOnlyWithoutRun;
@@ -55229,18 +55255,69 @@ const TextShrinkWrap = props => {
55229
55255
  shrinkWrap: undefined
55230
55256
  });
55231
55257
  };
55232
- // Whether the text is cut is read on the axis navi cuts it on: one line ends
55233
- // in an ellipsis on the inline axis, n lines are clamped on the block one. The
55234
- // 1px is the rounding of a box that exactly fits.
55258
+ // Whether the text is cut is read from its line boxes against the content box,
55259
+ // on the axis navi cuts it on: one line ends in an ellipsis on the inline axis,
55260
+ // n lines are clamped on the block one. Not from scrollWidth/scrollHeight: the
55261
+ // scroll extent holds the padding, so a text cut inside its end padding would
55262
+ // read as fitting.
55235
55263
  const isTextTruncated = (element, maxLines) => {
55236
55264
  if (maxLines === 1 || maxLines === "1") {
55237
- return element.scrollWidth - element.clientWidth > 1;
55265
+ const content = getContentEdges(element);
55266
+ // The ellipsis is painted over a line laid out at its full width. The
55267
+ // 0.5px absorbs the rounding of a box that exactly fits.
55268
+ return getLineRects(element).some(rect => rect.right - content.right > 0.5 || content.left - rect.left > 0.5);
55238
55269
  }
55239
55270
  if (maxLines > 1) {
55240
- return element.scrollHeight - element.clientHeight > 1;
55271
+ const content = getContentEdges(element);
55272
+ // A clamped line is still laid out, under the content box. Its middle is
55273
+ // read, not its top: under a line-height of 1 or less the glyphs of the
55274
+ // next line start above the bottom of the one before.
55275
+ return getLineRects(element).some(rect => rect.top + rect.height / 2 > content.bottom);
55241
55276
  }
55242
55277
  return false;
55243
55278
  };
55279
+ // Read from the in-flow children: an overlay positioned over the box (a loading
55280
+ // skeleton) fills the padding and is not part of a line.
55281
+ const getLineRects = element => {
55282
+ const range = document.createRange();
55283
+ const rects = [];
55284
+ for (const child of element.childNodes) {
55285
+ if (child.nodeType === Node.ELEMENT_NODE) {
55286
+ const {
55287
+ position
55288
+ } = window.getComputedStyle(child);
55289
+ if (position === "absolute" || position === "fixed") {
55290
+ continue;
55291
+ }
55292
+ }
55293
+ range.selectNode(child);
55294
+ rects.push(...range.getClientRects());
55295
+ }
55296
+ return rects;
55297
+ };
55298
+ // In the coordinates of the line rects: a transformed ancestor (a pressed
55299
+ // button's scale) shrinks the rects and the box, not the computed paddings.
55300
+ const getContentEdges = element => {
55301
+ const style = window.getComputedStyle(element);
55302
+ const box = element.getBoundingClientRect();
55303
+ const scaleX = scaleOf(box.width, element.offsetWidth);
55304
+ const scaleY = scaleOf(box.height, element.offsetHeight);
55305
+ const startX = parseFloat(style.borderLeftWidth) + parseFloat(style.paddingLeft);
55306
+ const endX = parseFloat(style.borderRightWidth) + parseFloat(style.paddingRight);
55307
+ const endY = parseFloat(style.borderBottomWidth) + parseFloat(style.paddingBottom);
55308
+ return {
55309
+ left: box.left + startX * scaleX,
55310
+ right: box.right - endX * scaleX,
55311
+ bottom: box.bottom - endY * scaleY
55312
+ };
55313
+ };
55314
+ const scaleOf = (screenSize, layoutSize) => {
55315
+ // A box with no size has no padding or border to scale either.
55316
+ if (layoutSize === 0) {
55317
+ return 1;
55318
+ }
55319
+ return screenSize / layoutSize;
55320
+ };
55244
55321
  const TextReportingTruncation = ({
55245
55322
  onTruncatedChange,
55246
55323
  ...props