@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.
@@ -13920,7 +13920,9 @@ const createControlValidation = (
13920
13920
 
13921
13921
  let newConstraintValidityState = { valid: true };
13922
13922
  const constraintSet = getConstraintSet();
13923
- const elementSig = getElementSignature(controller.ref.current);
13923
+ // Only for the debug lines of a failure or a change: computed when one of
13924
+ // them is written, not at every check (every notch of a wheel).
13925
+ const elementSig = () => getElementSignature(controller.ref.current);
13924
13926
  // Not logged: every control checks its constraints on every interaction and
13925
13927
  // almost always passes, so this line alone was most of the debug output —
13926
13928
  // and with devtools open, formatting it costs a frame. What matters is a
@@ -13946,7 +13948,7 @@ const createControlValidation = (
13946
13948
  constraintValidityInfo.message,
13947
13949
  );
13948
13950
  debugUIState(
13949
- `${elementSig} constraint "${constraint.name}" failed -> ${constraintValidityInfo.message}`,
13951
+ `${elementSig()} constraint "${constraint.name}" failed -> ${constraintValidityInfo.message}`,
13950
13952
  );
13951
13953
  const thisConstraintFailureInfo = {
13952
13954
  name: constraint.name,
@@ -14018,7 +14020,7 @@ const createControlValidation = (
14018
14020
  if (element) {
14019
14021
  debugUIState(
14020
14022
  event,
14021
- `${elementSig} constraint validity changed -> dispatch ${NAVI_VALIDITY_CHANGE_CUSTOM_EVENT}`,
14023
+ `${elementSig()} constraint validity changed -> dispatch ${NAVI_VALIDITY_CHANGE_CUSTOM_EVENT}`,
14022
14024
  );
14023
14025
  dispatchPublicCustomEvent(element, NAVI_VALIDITY_CHANGE_CUSTOM_EVENT);
14024
14026
  }
@@ -14064,7 +14066,7 @@ const createControlValidation = (
14064
14066
  event,
14065
14067
  { report = false, fromRequestAction = false } = {},
14066
14068
  ) => {
14067
- const elementSig = getElementSignature(controller.ref.current);
14069
+ const elementSig = () => getElementSignature(controller.ref.current);
14068
14070
  const isValid = checkValidity({ event, fromRequestAction });
14069
14071
  if (failingManagedControlValidity) {
14070
14072
  // Group/form case: find the actual failing leaf and report on it.
@@ -14086,7 +14088,7 @@ const createControlValidation = (
14086
14088
  if (report) {
14087
14089
  debugUIState(
14088
14090
  event,
14089
- `syncValidity ${elementSig}: has failing constraint and report=true -> reportValidity`,
14091
+ `syncValidity ${elementSig()}: has failing constraint and report=true -> reportValidity`,
14090
14092
  );
14091
14093
  reportValidity({ event });
14092
14094
  } else if (failedConstraintInfo.status === "error") {
@@ -14096,12 +14098,12 @@ const createControlValidation = (
14096
14098
  // on the next action attempt via autoResetOnAction.
14097
14099
  debugUIState(
14098
14100
  event,
14099
- `syncValidity ${elementSig}: has error constraint and report=false -> keep callout open`,
14101
+ `syncValidity ${elementSig()}: has error constraint and report=false -> keep callout open`,
14100
14102
  );
14101
14103
  } else {
14102
14104
  debugUIState(
14103
14105
  event,
14104
- `syncValidity ${elementSig}: has failing constraint but report=false -> close callout if any`,
14106
+ `syncValidity ${elementSig()}: has failing constraint but report=false -> close callout if any`,
14105
14107
  );
14106
14108
  callout.removeOpenToken(VALIDATION_TOKEN, event);
14107
14109
  }
@@ -28088,6 +28090,7 @@ This prevents cross-test pollution and ensures clean state.`,
28088
28090
 
28089
28091
  // Store previous route states to detect changes
28090
28092
  const routePreviousStateMap = new WeakMap();
28093
+ let previousPath = null;
28091
28094
  const updateRoutes = (
28092
28095
  url,
28093
28096
  {
@@ -28096,6 +28099,14 @@ This prevents cross-test pollution and ensures clean state.`,
28096
28099
  } = {},
28097
28100
  ) => {
28098
28101
  currentUrl = url;
28102
+ // Whether a route matches is decided by the path alone: the search string
28103
+ // only fills the params (see matchUrl). An address that keeps its path — a
28104
+ // search param written, at every notch of a wheel bound to it — cannot make
28105
+ // a route start or stop matching, so only the routes already matching are
28106
+ // read again.
28107
+ const path = urlWithoutSearchAndHash(url);
28108
+ const pathKept = path === previousPath;
28109
+ previousPath = path;
28099
28110
  const returnValue = {};
28100
28111
  const routeMatchInfoSet = new Set();
28101
28112
  for (const route of routeSet) {
@@ -28111,7 +28122,8 @@ This prevents cross-test pollution and ensures clean state.`,
28111
28122
  const oldParams = previousState.params;
28112
28123
  const oldActionParams = previousState.actionParams;
28113
28124
 
28114
- let extractedParams = routePattern.applyOn(url);
28125
+ let extractedParams =
28126
+ pathKept && !oldMatching ? null : routePattern.applyOn(url);
28115
28127
  let newMatching = Boolean(extractedParams);
28116
28128
  let newParams;
28117
28129
  if (extractedParams) {
@@ -28180,6 +28192,25 @@ This prevents cross-test pollution and ensures clean state.`,
28180
28192
  }
28181
28193
  }
28182
28194
 
28195
+ // What the matching routes say, read once for every connection of every
28196
+ // route that does not match (below), rather than by walking all the
28197
+ // routes again for each of those connections.
28198
+ const matchingRawParamsArray = [];
28199
+ const paramNameDeclaredByMatchingRouteSet = new Set();
28200
+ const matchingFamilyRootSet = new Set();
28201
+ for (const matchingRoute of matchingRouteSet) {
28202
+ matchingRawParamsArray.push(matchingRoute.rawParamsSignal.value);
28203
+ const matchingPattern =
28204
+ getRoutePrivateProperties(matchingRoute).routePattern;
28205
+ for (const paramName of matchingPattern.queryConnectionMap.keys()) {
28206
+ paramNameDeclaredByMatchingRouteSet.add(paramName);
28207
+ }
28208
+ for (const paramName of matchingPattern.pathConnectionMap.keys()) {
28209
+ paramNameDeclaredByMatchingRouteSet.add(paramName);
28210
+ }
28211
+ matchingFamilyRootSet.add(matchingPattern.familyRoot);
28212
+ }
28213
+
28183
28214
  for (const {
28184
28215
  route,
28185
28216
  routePrivateProperties,
@@ -28195,40 +28226,21 @@ This prevents cross-test pollution and ensures clean state.`,
28195
28226
  const urlParamValue = rawParams[paramName];
28196
28227
 
28197
28228
  if (!newMatching) {
28198
- // Route doesn't match - check if any matching route extracts this parameter
28199
- let parameterExtractedByMatchingRoute = false;
28200
- let parameterDeclaredByMatchingRoute = false;
28201
- let matchingRouteInSameFamily = false;
28202
-
28203
- for (const otherRoute of routeSet) {
28204
- if (otherRoute === route || !otherRoute.matching) {
28205
- continue;
28206
- }
28207
- const otherRawParams = otherRoute.rawParamsSignal.value;
28208
-
28209
- // Check if this matching route extracts the parameter
28210
- if (paramName in otherRawParams) {
28211
- parameterExtractedByMatchingRoute = true;
28212
- }
28213
-
28214
- // Same param, on the page one is arriving at: what it is worth
28215
- // there is that page's business, url or no url — it may open on
28216
- // a default of its own (see searchParams' `{ signal, default }`).
28217
- const otherPattern =
28218
- getRoutePrivateProperties(otherRoute).routePattern;
28219
- if (
28220
- otherPattern.queryConnectionMap.has(paramName) ||
28221
- otherPattern.pathConnectionMap.has(paramName)
28222
- ) {
28223
- parameterDeclaredByMatchingRoute = true;
28224
- }
28225
-
28226
- // Same family = same topmost ancestor
28227
- // (familyRoot, computed in setupRoutePatterns)
28228
- if (otherPattern.familyRoot === routePattern.familyRoot) {
28229
- matchingRouteInSameFamily = true;
28230
- }
28231
- }
28229
+ // Whether a matching route extracts this parameter from the url
28230
+ const parameterExtractedByMatchingRoute =
28231
+ matchingRawParamsArray.some(
28232
+ (matchingRawParams) => paramName in matchingRawParams,
28233
+ );
28234
+ // Same param, on the page one is arriving at: what it is worth
28235
+ // there is that page's business, url or no url — it may open on
28236
+ // a default of its own (see searchParams' `{ signal, default }`).
28237
+ const parameterDeclaredByMatchingRoute =
28238
+ paramNameDeclaredByMatchingRouteSet.has(paramName);
28239
+ // Same family = same topmost ancestor
28240
+ // (familyRoot, computed in setupRoutePatterns)
28241
+ const matchingRouteInSameFamily = matchingFamilyRootSet.has(
28242
+ routePattern.familyRoot,
28243
+ );
28232
28244
 
28233
28245
  // A weak param qualifies one visit: leaving the route ends it,
28234
28246
  // whatever the family and whatever the default. Coming back by a
@@ -28480,6 +28492,11 @@ const getParamsCacheKey = (params) => {
28480
28492
  return key;
28481
28493
  };
28482
28494
 
28495
+ const urlWithoutSearchAndHash = (url) => {
28496
+ const urlObject = new URL(url);
28497
+ return `${urlObject.origin}${urlObject.pathname}`;
28498
+ };
28499
+
28483
28500
  /**
28484
28501
  * Is there an entry of THIS document behind the current one? And ahead of it?
28485
28502
  *
@@ -28923,7 +28940,13 @@ const forgetScrollerUnlessPageLeft = (name) => {
28923
28940
  }
28924
28941
  };
28925
28942
 
28926
- const getHrefTargetInfo = (href) => {
28943
+ // `currentUrlString`: the address the href is compared with — the document's by
28944
+ // default. A caller subscribed to documentUrlSignal passes the value it read,
28945
+ // which is ahead of window.location while a debounced write is pending.
28946
+ const getHrefTargetInfo = (
28947
+ href,
28948
+ currentUrlString = window.location.href,
28949
+ ) => {
28927
28950
  href = String(href);
28928
28951
 
28929
28952
  if (!href || href.trim() === "") {
@@ -28936,8 +28959,8 @@ const getHrefTargetInfo = (href) => {
28936
28959
  };
28937
28960
  }
28938
28961
 
28939
- const currentUrl = new URL(window.location.href);
28940
- const targetUrl = new URL(href, window.location.href);
28962
+ const currentUrl = new URL(currentUrlString);
28963
+ const targetUrl = new URL(href, currentUrlString);
28941
28964
 
28942
28965
  let isCurrent = false;
28943
28966
  {
@@ -28979,6 +29002,21 @@ const getHrefTargetInfo = (href) => {
28979
29002
  };
28980
29003
  };
28981
29004
 
29005
+ // A field of getHrefTargetInfo that depends on the page one is on (isCurrent,
29006
+ // isAnchor), read through a computed of that one boolean: the caller re-renders
29007
+ // when it flips, not at every address write — a search param bound to a wheel
29008
+ // writes the address at every notch.
29009
+ const useHrefTargetFlag = (href, flagName) => {
29010
+ const flagSignal = useMemo(
29011
+ () =>
29012
+ computed(
29013
+ () => getHrefTargetInfo(href, documentUrlSignal.value)[flagName],
29014
+ ),
29015
+ [href, flagName],
29016
+ );
29017
+ return flagSignal.value;
29018
+ };
29019
+
28982
29020
  /*
28983
29021
  * An app can ship more than one document — an admin panel, a status page, an
28984
29022
  * entry point of its own — and a link to one of them is not a route, it is a
@@ -47870,19 +47908,28 @@ const usePreloadOpeningOnPress = (ref, command, prefetch = true) => {
47870
47908
 
47871
47909
  /**
47872
47910
  * Hook that reactively checks if a URL is visited.
47873
- * Re-renders when the visited URL set changes.
47911
+ * Re-renders when that answer changes — not at every url added to the visited
47912
+ * set, which grows at every new address (a search param bound to a wheel
47913
+ * writes one at every notch).
47874
47914
  *
47875
47915
  * @param {string} url - The URL to check
47876
47916
  * @returns {boolean} Whether the URL has been visited
47877
47917
  */
47878
47918
  const useIsVisited = (url) => {
47879
- return useMemo(() => {
47880
- // Access the signal to create reactive dependency
47881
- // eslint-disable-next-line no-unused-expressions
47882
- visitedUrlsSignal.value;
47883
-
47884
- return isVisited(url);
47885
- }, [url, visitedUrlsSignal.value]);
47919
+ const visitedSignal = useMemo(
47920
+ () =>
47921
+ computed(() => {
47922
+ // Both are what the answer depends on: the set, and the address a
47923
+ // relative url is resolved against (see isVisited).
47924
+ // eslint-disable-next-line no-unused-expressions
47925
+ visitedUrlsSignal.value;
47926
+ // eslint-disable-next-line no-unused-expressions
47927
+ documentUrlSignal.value;
47928
+ return isVisited(url);
47929
+ }),
47930
+ [url],
47931
+ );
47932
+ return visitedSignal.value;
47886
47933
  };
47887
47934
 
47888
47935
  /**
@@ -48551,13 +48598,14 @@ const LinkPlain = props => {
48551
48598
  const loading = basePseudoState[":-navi-loading"];
48552
48599
  const shouldDimColor = readOnly || disabled;
48553
48600
  useDimColorWhen(props.ref, shouldDimColor);
48554
- // subscribe to document url to re-render and re-compute getHrefTargetInfo
48555
- useDocumentUrl();
48601
+ // Re-rendered when one of these flips, not at every address write (see
48602
+ // useHrefTargetFlag). Whether the href leaves the site does not depend on
48603
+ // the address.
48604
+ const isCurrent = useHrefTargetFlag(href, "isCurrent");
48605
+ const isAnchor = useHrefTargetFlag(href, "isAnchor");
48556
48606
  const {
48557
- isSameSite,
48558
- isAnchor,
48559
- isCurrent
48560
- } = getHrefTargetInfo(href);
48607
+ isSameSite
48608
+ } = getHrefTargetInfo(href, documentUrlSignal.peek());
48561
48609
  // A tab that is a SLIDE is current when the container is on it — which the
48562
48610
  // <Nav> around reads off that container, so nothing here has to be told.
48563
48611
  const innerCurrent = current || (slide ? nav?.currentSlideArea === slide : isCurrent);
@@ -53523,10 +53571,9 @@ const ButtonHrefResolver = props => {
53523
53571
  });
53524
53572
  };
53525
53573
 
53526
- // What the href means next to the page one is on depends on the document url,
53527
- // so a button with an href re-renders at every address write. One without
53528
- // never reaches here: it has nothing to re-compute, and every button on screen
53529
- // would otherwise pay for each write.
53574
+ // Whether the href is the page one is on depends on the address, and re-renders
53575
+ // the button only when it flips (see useHrefTargetFlag). Whether it leaves the
53576
+ // site does not depend on the address.
53530
53577
  const ButtonWithHref = props => {
53531
53578
  const Next = useNextResolver();
53532
53579
  const {
@@ -53535,12 +53582,10 @@ const ButtonWithHref = props => {
53535
53582
  rel,
53536
53583
  pseudoState
53537
53584
  } = props;
53538
- // Read for the subscription: getHrefTargetInfo reads window.location.
53539
- useDocumentUrl();
53585
+ const isCurrent = useHrefTargetFlag(href, "isCurrent");
53540
53586
  const {
53541
- isSameSite,
53542
- isCurrent
53543
- } = getHrefTargetInfo(href);
53587
+ isSameSite
53588
+ } = getHrefTargetInfo(href, documentUrlSignal.peek());
53544
53589
  return jsx(Next, {
53545
53590
  ...props,
53546
53591
  target: target === undefined ? isSameSite ? undefined : "_blank" : target,
@@ -71143,6 +71188,11 @@ const css$x = /* css */`@layer navi {
71143
71188
  display: flex;
71144
71189
  overflow: hidden;
71145
71190
 
71191
+ &[data-borderless] {
71192
+ --x-list-border-radius: 0px;
71193
+ overflow: visible;
71194
+ }
71195
+
71146
71196
  &[data-overflow-visible="both"] {
71147
71197
  overflow: visible;
71148
71198
  }
@@ -71827,6 +71877,7 @@ const ListUI = props => {
71827
71877
  "data-item-transition": itemTransition ? "" : undefined,
71828
71878
  popover: popover,
71829
71879
  "data-horizontal": horizontal ? "" : undefined,
71880
+ "data-borderless": isBorderless(rest) ? "" : undefined,
71830
71881
  "data-scroller": getScrollerAttribute(scroller),
71831
71882
  "data-overflow-visible": getOverflowVisibleAttribute(overflow, overflowX, overflowY),
71832
71883
  "navi-hover-while-scrolling": hoverWhileScrolling ? "" : undefined,
@@ -73296,6 +73347,25 @@ const getScrollerAttribute = scroller => {
73296
73347
  return "parent";
73297
73348
  };
73298
73349
 
73350
+ // A radius asked for explicitly is kept: a borderless list painting a
73351
+ // background of its own may want that surface rounded, and clipped to it.
73352
+ const isBorderless = ({
73353
+ border,
73354
+ borderWidth,
73355
+ borderRadius
73356
+ }) => {
73357
+ if (borderRadius !== undefined) {
73358
+ return false;
73359
+ }
73360
+ if (borderWidth !== undefined) {
73361
+ return parseFloat(borderWidth) === 0;
73362
+ }
73363
+ if (border === undefined) {
73364
+ return false;
73365
+ }
73366
+ return border === "none" || parseFloat(border) === 0;
73367
+ };
73368
+
73299
73369
  // overflow lands as an inline style on the inner scroll element, which is
73300
73370
  // enough for every value but "visible": that one only means anything once the
73301
73371
  // frame around it stops clipping too. Reading the per-axis props over the