@jsenv/navi 0.29.354 → 0.29.355

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.
@@ -13486,10 +13486,12 @@ const createControlValidation = (
13486
13486
  }
13487
13487
  }
13488
13488
 
13489
- if (fromRequestAction) {
13490
- // The value is about to be read and sent: whoever may correct it gets
13491
- // the last word before the constraints judge it, so a field is never
13492
- // refused for something navi knows how to put right.
13489
+ // The value is about to be read and sent: whoever may correct it gets the
13490
+ // last word before the constraints judge it, so a field is never refused
13491
+ // for something navi knows how to put right. Unless the request comes from
13492
+ // typing an action run as you type (a search, debounced or not) is not
13493
+ // a commit, and correcting there would eat the space before the next word.
13494
+ if (fromRequestAction && !findEvent(event, "input")) {
13493
13495
  applyAutoFix(event);
13494
13496
  }
13495
13497
 
@@ -26382,10 +26384,27 @@ let isUpdatingRoutesFromUrl = false;
26382
26384
  * route("/legacy/:id", { redirectRoute: GAME_PAGE, redirectRouteParams: ({ id }) => ({ gameId: id }) });
26383
26385
  * route("/:gameId/invite", { redirectRoute: HOME_PAGE, redirectRouteParams: null });
26384
26386
  * ```
26387
+ * @param {string[]} [options.dropSearchParams]
26388
+ * Search params this route's address never keeps: read by whoever fetched
26389
+ * the address (a link preview crawler, a cache), never by the app. When this
26390
+ * route's own address arrives carrying one, the navigation goes to the same
26391
+ * address without it, at the door, like a redirection: no history entry, no
26392
+ * route action, no signal written. Every other search param stays as written.
26393
+ *
26394
+ * ```js
26395
+ * // ?v= makes WhatsApp fetch a new preview, the address bar never shows it
26396
+ * route(`/games/:gameId=${gamePageIdSignal}`, { dropSearchParams: ["v"] });
26397
+ * ```
26385
26398
  */
26386
26399
  const route = (
26387
26400
  pattern,
26388
- { searchParams, params, redirectRoute, redirectRouteParams } = {},
26401
+ {
26402
+ searchParams,
26403
+ params,
26404
+ redirectRoute,
26405
+ redirectRouteParams,
26406
+ dropSearchParams,
26407
+ } = {},
26389
26408
  ) => {
26390
26409
  const routePattern = createRoutePattern(pattern, { searchParams, params });
26391
26410
  const { cleanPattern } = routePattern;
@@ -26445,6 +26464,7 @@ const route = (
26445
26464
  routePattern,
26446
26465
  redirectRoute,
26447
26466
  redirectRouteParams,
26467
+ dropSearchParams,
26448
26468
  setup: null,
26449
26469
  updateStatus: null,
26450
26470
  cleanup: null,
@@ -26817,6 +26837,7 @@ const route = (
26817
26837
  const [publishRouteMutations, observeRouteMutations] = createPubSub();
26818
26838
 
26819
26839
  let redirectingRouteSet = null;
26840
+ let droppingRouteSet = null;
26820
26841
  /**
26821
26842
  * Where does this url really lead?
26822
26843
  *
@@ -26833,14 +26854,21 @@ let redirectingRouteSet = null;
26833
26854
  * @returns {string|null} The url to go to instead, or null.
26834
26855
  */
26835
26856
  const resolveRouteRedirection = (url) => {
26836
- if (!redirectingRouteSet || redirectingRouteSet.size === 0) {
26857
+ if (
26858
+ !redirectingRouteSet ||
26859
+ (redirectingRouteSet.size === 0 && droppingRouteSet.size === 0)
26860
+ ) {
26837
26861
  return null;
26838
26862
  }
26839
26863
  let urlToResolve = url;
26840
26864
  let redirectionUrl = null;
26841
26865
  const urlChain = [url];
26842
26866
  while (true) {
26843
- const nextUrl = resolveRedirectionOnce(urlToResolve);
26867
+ // A redirection first: the address it lands on may carry a param its own
26868
+ // route drops, and the next turn removes it there.
26869
+ const nextUrl =
26870
+ resolveRedirectionOnce(urlToResolve) ||
26871
+ dropSearchParamsOnce(urlToResolve);
26844
26872
  if (!nextUrl || nextUrl === urlToResolve) {
26845
26873
  break;
26846
26874
  }
@@ -26881,6 +26909,35 @@ const resolveRedirectionOnce = (url) => {
26881
26909
  }
26882
26910
  return buildRedirectionUrl(redirectingRoute, redirectingRouteParams);
26883
26911
  };
26912
+ // The query is edited as written rather than rebuilt through URLSearchParams,
26913
+ // which would rewrite every param kept ("?weather" into "?weather=", commas
26914
+ // encoded — see extractSearchParams).
26915
+ const dropSearchParamsOnce = (url) => {
26916
+ const urlObject = new URL(url);
26917
+ if (!urlObject.search) {
26918
+ return null;
26919
+ }
26920
+ for (const route of droppingRouteSet) {
26921
+ const { routePattern, dropSearchParams } = getRoutePrivateProperties(route);
26922
+ // exact, for the same reason as a redirection: the addresses below a
26923
+ // trailing slash belong to the routes declared for them
26924
+ if (!routePattern.applyOn(url, { exact: true })) {
26925
+ continue;
26926
+ }
26927
+ const pairs = urlObject.search.slice(1).split("&");
26928
+ const pairsKept = pairs.filter((pair) => {
26929
+ const eqIndex = pair.indexOf("=");
26930
+ const key = eqIndex > -1 ? pair.slice(0, eqIndex) : pair;
26931
+ return !dropSearchParams.includes(decodeURIComponent(key));
26932
+ });
26933
+ if (pairsKept.length === pairs.length) {
26934
+ continue;
26935
+ }
26936
+ urlObject.search = pairsKept.length ? `?${pairsKept.join("&")}` : "";
26937
+ return urlObject.href;
26938
+ }
26939
+ return null;
26940
+ };
26884
26941
  const buildRedirectionUrl = (route, urlParams) => {
26885
26942
  const { redirectRoute, redirectRouteParams } =
26886
26943
  getRoutePrivateProperties(route);
@@ -26984,8 +27041,13 @@ This prevents cross-test pollution and ensures clean state.`,
26984
27041
  // Checked here rather than at declaration: a route may redirect to one
26985
27042
  // declared after it, and reading the target then would forbid that order.
26986
27043
  redirectingRouteSet = new Set();
27044
+ droppingRouteSet = new Set();
26987
27045
  for (const route of routeSet) {
26988
- const { redirectRoute } = getRoutePrivateProperties(route);
27046
+ const { redirectRoute, dropSearchParams } =
27047
+ getRoutePrivateProperties(route);
27048
+ if (dropSearchParams && dropSearchParams.length) {
27049
+ droppingRouteSet.add(route);
27050
+ }
26989
27051
  if (!redirectRoute) {
26990
27052
  continue;
26991
27053
  }
@@ -27271,6 +27333,7 @@ This prevents cross-test pollution and ensures clean state.`,
27271
27333
  }
27272
27334
  routeSet.clear();
27273
27335
  redirectingRouteSet = null;
27336
+ droppingRouteSet = null;
27274
27337
  setupRoutesCalled = false;
27275
27338
  activeRouteSet = null;
27276
27339
  };
@@ -40809,7 +40872,9 @@ const useControlProps = (props, {
40809
40872
  requester: control
40810
40873
  });
40811
40874
  if (dispatched) {
40812
- lastActionValueRef.current = currentValue;
40875
+ // Read again: the gate may have corrected the value (see applyAutoFix),
40876
+ // and what the next request is compared with is what went out.
40877
+ lastActionValueRef.current = readControlValue(control);
40813
40878
  }
40814
40879
  return dispatched;
40815
40880
  };