@jsenv/navi 0.29.409 → 0.29.411

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.
@@ -13437,7 +13437,9 @@ const createControlValidation = (
13437
13437
 
13438
13438
  let newConstraintValidityState = { valid: true };
13439
13439
  const constraintSet = getConstraintSet();
13440
- const elementSig = getElementSignature(controller.ref.current);
13440
+ // Only for the debug lines of a failure or a change: computed when one of
13441
+ // them is written, not at every check (every notch of a wheel).
13442
+ const elementSig = () => getElementSignature(controller.ref.current);
13441
13443
  // Not logged: every control checks its constraints on every interaction and
13442
13444
  // almost always passes, so this line alone was most of the debug output —
13443
13445
  // and with devtools open, formatting it costs a frame. What matters is a
@@ -13463,7 +13465,7 @@ const createControlValidation = (
13463
13465
  constraintValidityInfo.message,
13464
13466
  );
13465
13467
  debugUIState(
13466
- `${elementSig} constraint "${constraint.name}" failed -> ${constraintValidityInfo.message}`,
13468
+ `${elementSig()} constraint "${constraint.name}" failed -> ${constraintValidityInfo.message}`,
13467
13469
  );
13468
13470
  const thisConstraintFailureInfo = {
13469
13471
  name: constraint.name,
@@ -13535,7 +13537,7 @@ const createControlValidation = (
13535
13537
  if (element) {
13536
13538
  debugUIState(
13537
13539
  event,
13538
- `${elementSig} constraint validity changed -> dispatch ${NAVI_VALIDITY_CHANGE_CUSTOM_EVENT}`,
13540
+ `${elementSig()} constraint validity changed -> dispatch ${NAVI_VALIDITY_CHANGE_CUSTOM_EVENT}`,
13539
13541
  );
13540
13542
  dispatchPublicCustomEvent(element, NAVI_VALIDITY_CHANGE_CUSTOM_EVENT);
13541
13543
  }
@@ -13581,7 +13583,7 @@ const createControlValidation = (
13581
13583
  event,
13582
13584
  { report = false, fromRequestAction = false } = {},
13583
13585
  ) => {
13584
- const elementSig = getElementSignature(controller.ref.current);
13586
+ const elementSig = () => getElementSignature(controller.ref.current);
13585
13587
  const isValid = checkValidity({ event, fromRequestAction });
13586
13588
  if (failingManagedControlValidity) {
13587
13589
  // Group/form case: find the actual failing leaf and report on it.
@@ -13603,7 +13605,7 @@ const createControlValidation = (
13603
13605
  if (report) {
13604
13606
  debugUIState(
13605
13607
  event,
13606
- `syncValidity ${elementSig}: has failing constraint and report=true -> reportValidity`,
13608
+ `syncValidity ${elementSig()}: has failing constraint and report=true -> reportValidity`,
13607
13609
  );
13608
13610
  reportValidity({ event });
13609
13611
  } else if (failedConstraintInfo.status === "error") {
@@ -13613,12 +13615,12 @@ const createControlValidation = (
13613
13615
  // on the next action attempt via autoResetOnAction.
13614
13616
  debugUIState(
13615
13617
  event,
13616
- `syncValidity ${elementSig}: has error constraint and report=false -> keep callout open`,
13618
+ `syncValidity ${elementSig()}: has error constraint and report=false -> keep callout open`,
13617
13619
  );
13618
13620
  } else {
13619
13621
  debugUIState(
13620
13622
  event,
13621
- `syncValidity ${elementSig}: has failing constraint but report=false -> close callout if any`,
13623
+ `syncValidity ${elementSig()}: has failing constraint but report=false -> close callout if any`,
13622
13624
  );
13623
13625
  callout.removeOpenToken(VALIDATION_TOKEN, event);
13624
13626
  }
@@ -27352,6 +27354,7 @@ This prevents cross-test pollution and ensures clean state.`,
27352
27354
 
27353
27355
  // Store previous route states to detect changes
27354
27356
  const routePreviousStateMap = new WeakMap();
27357
+ let previousPath = null;
27355
27358
  const updateRoutes = (
27356
27359
  url,
27357
27360
  {
@@ -27360,6 +27363,14 @@ This prevents cross-test pollution and ensures clean state.`,
27360
27363
  } = {},
27361
27364
  ) => {
27362
27365
  currentUrl = url;
27366
+ // Whether a route matches is decided by the path alone: the search string
27367
+ // only fills the params (see matchUrl). An address that keeps its path — a
27368
+ // search param written, at every notch of a wheel bound to it — cannot make
27369
+ // a route start or stop matching, so only the routes already matching are
27370
+ // read again.
27371
+ const path = urlWithoutSearchAndHash(url);
27372
+ const pathKept = path === previousPath;
27373
+ previousPath = path;
27363
27374
  const returnValue = {};
27364
27375
  const routeMatchInfoSet = new Set();
27365
27376
  for (const route of routeSet) {
@@ -27375,7 +27386,8 @@ This prevents cross-test pollution and ensures clean state.`,
27375
27386
  const oldParams = previousState.params;
27376
27387
  const oldActionParams = previousState.actionParams;
27377
27388
 
27378
- let extractedParams = routePattern.applyOn(url);
27389
+ let extractedParams =
27390
+ pathKept && !oldMatching ? null : routePattern.applyOn(url);
27379
27391
  let newMatching = Boolean(extractedParams);
27380
27392
  let newParams;
27381
27393
  if (extractedParams) {
@@ -27444,6 +27456,25 @@ This prevents cross-test pollution and ensures clean state.`,
27444
27456
  }
27445
27457
  }
27446
27458
 
27459
+ // What the matching routes say, read once for every connection of every
27460
+ // route that does not match (below), rather than by walking all the
27461
+ // routes again for each of those connections.
27462
+ const matchingRawParamsArray = [];
27463
+ const paramNameDeclaredByMatchingRouteSet = new Set();
27464
+ const matchingFamilyRootSet = new Set();
27465
+ for (const matchingRoute of matchingRouteSet) {
27466
+ matchingRawParamsArray.push(matchingRoute.rawParamsSignal.value);
27467
+ const matchingPattern =
27468
+ getRoutePrivateProperties(matchingRoute).routePattern;
27469
+ for (const paramName of matchingPattern.queryConnectionMap.keys()) {
27470
+ paramNameDeclaredByMatchingRouteSet.add(paramName);
27471
+ }
27472
+ for (const paramName of matchingPattern.pathConnectionMap.keys()) {
27473
+ paramNameDeclaredByMatchingRouteSet.add(paramName);
27474
+ }
27475
+ matchingFamilyRootSet.add(matchingPattern.familyRoot);
27476
+ }
27477
+
27447
27478
  for (const {
27448
27479
  route,
27449
27480
  routePrivateProperties,
@@ -27459,40 +27490,21 @@ This prevents cross-test pollution and ensures clean state.`,
27459
27490
  const urlParamValue = rawParams[paramName];
27460
27491
 
27461
27492
  if (!newMatching) {
27462
- // Route doesn't match - check if any matching route extracts this parameter
27463
- let parameterExtractedByMatchingRoute = false;
27464
- let parameterDeclaredByMatchingRoute = false;
27465
- let matchingRouteInSameFamily = false;
27466
-
27467
- for (const otherRoute of routeSet) {
27468
- if (otherRoute === route || !otherRoute.matching) {
27469
- continue;
27470
- }
27471
- const otherRawParams = otherRoute.rawParamsSignal.value;
27472
-
27473
- // Check if this matching route extracts the parameter
27474
- if (paramName in otherRawParams) {
27475
- parameterExtractedByMatchingRoute = true;
27476
- }
27477
-
27478
- // Same param, on the page one is arriving at: what it is worth
27479
- // there is that page's business, url or no url — it may open on
27480
- // a default of its own (see searchParams' `{ signal, default }`).
27481
- const otherPattern =
27482
- getRoutePrivateProperties(otherRoute).routePattern;
27483
- if (
27484
- otherPattern.queryConnectionMap.has(paramName) ||
27485
- otherPattern.pathConnectionMap.has(paramName)
27486
- ) {
27487
- parameterDeclaredByMatchingRoute = true;
27488
- }
27489
-
27490
- // Same family = same topmost ancestor
27491
- // (familyRoot, computed in setupRoutePatterns)
27492
- if (otherPattern.familyRoot === routePattern.familyRoot) {
27493
- matchingRouteInSameFamily = true;
27494
- }
27495
- }
27493
+ // Whether a matching route extracts this parameter from the url
27494
+ const parameterExtractedByMatchingRoute =
27495
+ matchingRawParamsArray.some(
27496
+ (matchingRawParams) => paramName in matchingRawParams,
27497
+ );
27498
+ // Same param, on the page one is arriving at: what it is worth
27499
+ // there is that page's business, url or no url — it may open on
27500
+ // a default of its own (see searchParams' `{ signal, default }`).
27501
+ const parameterDeclaredByMatchingRoute =
27502
+ paramNameDeclaredByMatchingRouteSet.has(paramName);
27503
+ // Same family = same topmost ancestor
27504
+ // (familyRoot, computed in setupRoutePatterns)
27505
+ const matchingRouteInSameFamily = matchingFamilyRootSet.has(
27506
+ routePattern.familyRoot,
27507
+ );
27496
27508
 
27497
27509
  // A weak param qualifies one visit: leaving the route ends it,
27498
27510
  // whatever the family and whatever the default. Coming back by a
@@ -27725,6 +27737,11 @@ const getParamsCacheKey = (params) => {
27725
27737
  return key;
27726
27738
  };
27727
27739
 
27740
+ const urlWithoutSearchAndHash = (url) => {
27741
+ const urlObject = new URL(url);
27742
+ return `${urlObject.origin}${urlObject.pathname}`;
27743
+ };
27744
+
27728
27745
  /**
27729
27746
  * Is there an entry of THIS document behind the current one? And ahead of it?
27730
27747
  *
@@ -28168,7 +28185,13 @@ const forgetScrollerUnlessPageLeft = (name) => {
28168
28185
  }
28169
28186
  };
28170
28187
 
28171
- const getHrefTargetInfo = (href) => {
28188
+ // `currentUrlString`: the address the href is compared with — the document's by
28189
+ // default. A caller subscribed to documentUrlSignal passes the value it read,
28190
+ // which is ahead of window.location while a debounced write is pending.
28191
+ const getHrefTargetInfo = (
28192
+ href,
28193
+ currentUrlString = window.location.href,
28194
+ ) => {
28172
28195
  href = String(href);
28173
28196
 
28174
28197
  if (!href || href.trim() === "") {
@@ -28181,8 +28204,8 @@ const getHrefTargetInfo = (href) => {
28181
28204
  };
28182
28205
  }
28183
28206
 
28184
- const currentUrl = new URL(window.location.href);
28185
- const targetUrl = new URL(href, window.location.href);
28207
+ const currentUrl = new URL(currentUrlString);
28208
+ const targetUrl = new URL(href, currentUrlString);
28186
28209
 
28187
28210
  let isCurrent = false;
28188
28211
  {
@@ -28224,6 +28247,21 @@ const getHrefTargetInfo = (href) => {
28224
28247
  };
28225
28248
  };
28226
28249
 
28250
+ // A field of getHrefTargetInfo that depends on the page one is on (isCurrent,
28251
+ // isAnchor), read through a computed of that one boolean: the caller re-renders
28252
+ // when it flips, not at every address write — a search param bound to a wheel
28253
+ // writes the address at every notch.
28254
+ const useHrefTargetFlag = (href, flagName) => {
28255
+ const flagSignal = useMemo(
28256
+ () =>
28257
+ computed(
28258
+ () => getHrefTargetInfo(href, documentUrlSignal.value)[flagName],
28259
+ ),
28260
+ [href, flagName],
28261
+ );
28262
+ return flagSignal.value;
28263
+ };
28264
+
28227
28265
  /*
28228
28266
  * An app can ship more than one document — an admin panel, a status page, an
28229
28267
  * entry point of its own — and a link to one of them is not a route, it is a
@@ -46729,19 +46767,28 @@ const usePreloadOpeningOnPress = (ref, command, prefetch = true) => {
46729
46767
 
46730
46768
  /**
46731
46769
  * Hook that reactively checks if a URL is visited.
46732
- * Re-renders when the visited URL set changes.
46770
+ * Re-renders when that answer changes — not at every url added to the visited
46771
+ * set, which grows at every new address (a search param bound to a wheel
46772
+ * writes one at every notch).
46733
46773
  *
46734
46774
  * @param {string} url - The URL to check
46735
46775
  * @returns {boolean} Whether the URL has been visited
46736
46776
  */
46737
46777
  const useIsVisited = (url) => {
46738
- return useMemo(() => {
46739
- // Access the signal to create reactive dependency
46740
- // eslint-disable-next-line no-unused-expressions
46741
- visitedUrlsSignal.value;
46742
-
46743
- return isVisited(url);
46744
- }, [url, visitedUrlsSignal.value]);
46778
+ const visitedSignal = useMemo(
46779
+ () =>
46780
+ computed(() => {
46781
+ // Both are what the answer depends on: the set, and the address a
46782
+ // relative url is resolved against (see isVisited).
46783
+ // eslint-disable-next-line no-unused-expressions
46784
+ visitedUrlsSignal.value;
46785
+ // eslint-disable-next-line no-unused-expressions
46786
+ documentUrlSignal.value;
46787
+ return isVisited(url);
46788
+ }),
46789
+ [url],
46790
+ );
46791
+ return visitedSignal.value;
46745
46792
  };
46746
46793
 
46747
46794
  /**
@@ -47401,13 +47448,14 @@ const LinkPlain = props => {
47401
47448
  const loading = basePseudoState[":-navi-loading"];
47402
47449
  const shouldDimColor = readOnly || disabled;
47403
47450
  useDimColorWhen(props.ref, shouldDimColor);
47404
- // subscribe to document url to re-render and re-compute getHrefTargetInfo
47405
- useDocumentUrl();
47451
+ // Re-rendered when one of these flips, not at every address write (see
47452
+ // useHrefTargetFlag). Whether the href leaves the site does not depend on
47453
+ // the address.
47454
+ const isCurrent = useHrefTargetFlag(href, "isCurrent");
47455
+ const isAnchor = useHrefTargetFlag(href, "isAnchor");
47406
47456
  const {
47407
- isSameSite,
47408
- isAnchor,
47409
- isCurrent
47410
- } = getHrefTargetInfo(href);
47457
+ isSameSite
47458
+ } = getHrefTargetInfo(href, documentUrlSignal.peek());
47411
47459
  // A tab that is a SLIDE is current when the container is on it — which the
47412
47460
  // <Nav> around reads off that container, so nothing here has to be told.
47413
47461
  const innerCurrent = current || (slide ? nav?.currentSlideArea === slide : isCurrent);
@@ -52373,10 +52421,9 @@ const ButtonHrefResolver = props => {
52373
52421
  });
52374
52422
  };
52375
52423
 
52376
- // What the href means next to the page one is on depends on the document url,
52377
- // so a button with an href re-renders at every address write. One without
52378
- // never reaches here: it has nothing to re-compute, and every button on screen
52379
- // would otherwise pay for each write.
52424
+ // Whether the href is the page one is on depends on the address, and re-renders
52425
+ // the button only when it flips (see useHrefTargetFlag). Whether it leaves the
52426
+ // site does not depend on the address.
52380
52427
  const ButtonWithHref = props => {
52381
52428
  const Next = useNextResolver();
52382
52429
  const {
@@ -52385,12 +52432,10 @@ const ButtonWithHref = props => {
52385
52432
  rel,
52386
52433
  pseudoState
52387
52434
  } = props;
52388
- // Read for the subscription: getHrefTargetInfo reads window.location.
52389
- useDocumentUrl();
52435
+ const isCurrent = useHrefTargetFlag(href, "isCurrent");
52390
52436
  const {
52391
- isSameSite,
52392
- isCurrent
52393
- } = getHrefTargetInfo(href);
52437
+ isSameSite
52438
+ } = getHrefTargetInfo(href, documentUrlSignal.peek());
52394
52439
  return jsx(Next, {
52395
52440
  ...props,
52396
52441
  target: target === undefined ? isSameSite ? undefined : "_blank" : target,
@@ -69770,6 +69815,11 @@ const css$x = /* css */`@layer navi {
69770
69815
  display: flex;
69771
69816
  overflow: hidden;
69772
69817
 
69818
+ &[data-borderless] {
69819
+ --x-list-border-radius: 0px;
69820
+ overflow: visible;
69821
+ }
69822
+
69773
69823
  &[data-overflow-visible="both"] {
69774
69824
  overflow: visible;
69775
69825
  }
@@ -70454,6 +70504,7 @@ const ListUI = props => {
70454
70504
  "data-item-transition": itemTransition ? "" : undefined,
70455
70505
  popover: popover,
70456
70506
  "data-horizontal": horizontal ? "" : undefined,
70507
+ "data-borderless": isBorderless(rest) ? "" : undefined,
70457
70508
  "data-scroller": getScrollerAttribute(scroller),
70458
70509
  "data-overflow-visible": getOverflowVisibleAttribute(overflow, overflowX, overflowY),
70459
70510
  "navi-hover-while-scrolling": hoverWhileScrolling ? "" : undefined,
@@ -71824,6 +71875,25 @@ const getScrollerAttribute = scroller => {
71824
71875
  return "parent";
71825
71876
  };
71826
71877
 
71878
+ // A radius asked for explicitly is kept: a borderless list painting a
71879
+ // background of its own may want that surface rounded, and clipped to it.
71880
+ const isBorderless = ({
71881
+ border,
71882
+ borderWidth,
71883
+ borderRadius
71884
+ }) => {
71885
+ if (borderRadius !== undefined) {
71886
+ return false;
71887
+ }
71888
+ if (borderWidth !== undefined) {
71889
+ return parseFloat(borderWidth) === 0;
71890
+ }
71891
+ if (border === undefined) {
71892
+ return false;
71893
+ }
71894
+ return border === "none" || parseFloat(border) === 0;
71895
+ };
71896
+
71827
71897
  // overflow lands as an inline style on the inner scroll element, which is
71828
71898
  // enough for every value but "visible": that one only means anything once the
71829
71899
  // frame around it stops clipping too. Reading the per-axis props over the