@jsenv/navi 0.29.232 → 0.29.234

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.
@@ -1952,6 +1952,8 @@ const stringifyForDisplay = (
1952
1952
  return String(value);
1953
1953
  };
1954
1954
 
1955
+ const SYMBOL_OBJECT_SIGNAL = Symbol.for("navi_object_signal");
1956
+
1955
1957
  /*
1956
1958
  * Deep structural equality for arbitrary JS values — what `===` can't do but this
1957
1959
  * codebase constantly needs: memoization cache keys ({ id: 1 } equal to { id: 1 }),
@@ -2060,6 +2062,21 @@ const compareTwoJsValues = (
2060
2062
  return result;
2061
2063
  };
2062
2064
  const compareComposite = (a, b) => {
2065
+ // An object carrying its signal (a resource relation read through
2066
+ // `.one()` or `.many()`) is that signal in the costume of its current
2067
+ // value: it says "follow this", not "this". Two of them are the same
2068
+ // thing when they follow the same signal, whatever they show right now —
2069
+ // and neither is ever a plain value that happens to look alike. Without
2070
+ // this a params object bound to the relation and one holding the row as
2071
+ // data would pass for equal, and the binding would be handed back for the
2072
+ // row (see bindParams' child cache in actions.js). Decided ahead of
2073
+ // SYMBOL_IDENTITY: the costume is a copy of the row and carries its
2074
+ // identity, which is exactly the sameness that must not count here.
2075
+ const aObjectSignal = a[SYMBOL_OBJECT_SIGNAL];
2076
+ const bObjectSignal = b[SYMBOL_OBJECT_SIGNAL];
2077
+ if (aObjectSignal || bObjectSignal) {
2078
+ return aObjectSignal === bObjectSignal;
2079
+ }
2063
2080
  const aIsArray = Array.isArray(a);
2064
2081
  const bIsArray = Array.isArray(b);
2065
2082
  if (aIsArray !== bIsArray) {
@@ -2540,8 +2557,6 @@ const setActionPrivateProperties = (action, properties) => {
2540
2557
  actionPrivatePropertiesWeakMap.set(action, properties);
2541
2558
  };
2542
2559
 
2543
- const SYMBOL_OBJECT_SIGNAL = Symbol.for("navi_object_signal");
2544
-
2545
2560
  /*
2546
2561
  * Actions: async callbacks wrapped in reactive state.
2547
2562
  *
@@ -4182,22 +4197,16 @@ const createActionProxyFromSignal = (
4182
4197
  dataSignal: proxySignal("dataSignal", "data"),
4183
4198
  });
4184
4199
  Object.preventExtensions(actionProxy);
4185
- // Watch for changes in the original paramsSignal and update ours
4186
- // (original signal wins over any replaceParams calls)
4187
- weakEffect(
4188
- [paramsSignal, proxyParamsSignal],
4189
- (paramsSignalRef, proxyParamsSignalRef) => {
4190
- const newParams = paramsSignalRef.value;
4191
- proxyParamsSignalRef.value = newParams;
4192
- },
4193
- );
4194
- weakEffect([action], () => {
4195
- // eslint-disable-next-line no-unused-expressions
4196
- proxyParamsSignal.value;
4197
- _updateTarget({
4198
- changeCause: "params_signal_change",
4199
- });
4200
- });
4200
+
4201
+ // What the proxy shows of the action it stands for is wired BEFORE the
4202
+ // effects below, because those resolve the first target as they are created:
4203
+ // a subscriber registered after them misses that first change and reads the
4204
+ // placeholders assigned above until a SECOND one happens. A proxy answering
4205
+ // `params: undefined` is then taken for a proxy holding no params, and
4206
+ // whoever asks it to run on truthy params does nothing (actionRunEffect's
4207
+ // onChange). The caller's own callbacks (onChange, runOnce, rerunOnChange)
4208
+ // stay below on purpose: they are told about changes, not about the proxy
4209
+ // coming into existence.
4201
4210
  onActionTargetChange((actionTarget) => {
4202
4211
  const currentAction = actionTarget || action;
4203
4212
  nameSignal.value = `[Proxy] ${currentAction.name}`;
@@ -4232,7 +4241,22 @@ const createActionProxyFromSignal = (
4232
4241
  });
4233
4242
  setActionPrivateProperties(actionProxy, proxyPrivateProperties);
4234
4243
  }
4235
-
4244
+ // Watch for changes in the original paramsSignal and update ours
4245
+ // (original signal wins over any replaceParams calls)
4246
+ weakEffect(
4247
+ [paramsSignal, proxyParamsSignal],
4248
+ (paramsSignalRef, proxyParamsSignalRef) => {
4249
+ const newParams = paramsSignalRef.value;
4250
+ proxyParamsSignalRef.value = newParams;
4251
+ },
4252
+ );
4253
+ weakEffect([action], () => {
4254
+ // eslint-disable-next-line no-unused-expressions
4255
+ proxyParamsSignal.value;
4256
+ _updateTarget({
4257
+ changeCause: "params_signal_change",
4258
+ });
4259
+ });
4236
4260
  actionProxy.replaceParams = (newParams) => {
4237
4261
  if (currentAction === action) {
4238
4262
  const currentParams = proxyParamsSignal.value;
@@ -22806,10 +22830,11 @@ let baseUrl;
22806
22830
  * "/places/:placeId". The url "/admin/places/42" matches, and `buildUrl` puts
22807
22831
  * the prefix back.
22808
22832
  *
22809
- * Call it in the routes module, above the `route()` calls it governs — they
22810
- * read the base url as they are created, so an entry point calling it after
22811
- * importing them is too late. The server must serve the document for every
22812
- * address below it (see docs/navigation.md).
22833
+ * Call it in the routes module, above `setupRoutes()`: the base url is read
22834
+ * whenever an address is matched or built, and the first match happens as the
22835
+ * routes are set up — an entry point calling it after importing them is too
22836
+ * late. The server must serve the document for every address below it (see
22837
+ * docs/navigation.md).
22813
22838
  *
22814
22839
  * @param {string} value - the document's own url, absolute or relative.
22815
22840
  */
@@ -48380,6 +48405,16 @@ const ButtonConfirm = ({
48380
48405
  restoreFocusRef.current = restoreFocus;
48381
48406
  setAsking(false);
48382
48407
  };
48408
+ const {
48409
+ action,
48410
+ command,
48411
+ onActionEnd,
48412
+ onClick
48413
+ } = props;
48414
+ // A submit button does not run anything itself: the form around it does,
48415
+ // with the button as requester, and the outcome never comes back through the
48416
+ // button's own action events. The question then waits on the form.
48417
+ const waitsForForm = !action && command === "--navi-send";
48383
48418
  useLayoutEffect(() => {
48384
48419
  if (asking) {
48385
48420
  // The press was aimed at this button, and the button that answers now is
@@ -48392,7 +48427,39 @@ const ButtonConfirm = ({
48392
48427
  if (found) {
48393
48428
  moveFocusTo(found.target);
48394
48429
  }
48395
- return;
48430
+ if (!waitsForForm) {
48431
+ return undefined;
48432
+ }
48433
+ const buttonEl = props.ref.current;
48434
+ const formEl = buttonEl.form || buttonEl.closest("form");
48435
+ if (!formEl) {
48436
+ return undefined;
48437
+ }
48438
+ // Same reading as watchActionCompletion (control_action.js): a send that
48439
+ // failed leaves its message on the button that asked, so the question
48440
+ // stays up with it; one that went through is answered.
48441
+ const onFormActionStart = actionStartEvent => {
48442
+ const {
48443
+ requester,
48444
+ addSideEffect
48445
+ } = actionStartEvent.detail;
48446
+ if (requester && requester !== buttonEl) {
48447
+ return;
48448
+ }
48449
+ addSideEffect(({
48450
+ error,
48451
+ aborted
48452
+ }) => {
48453
+ if (error || aborted) {
48454
+ return;
48455
+ }
48456
+ cancel(true);
48457
+ });
48458
+ };
48459
+ formEl.addEventListener("navi_action_start", onFormActionStart);
48460
+ return () => {
48461
+ formEl.removeEventListener("navi_action_start", onFormActionStart);
48462
+ };
48396
48463
  }
48397
48464
  if (restoreFocusRef.current) {
48398
48465
  restoreFocusRef.current = false;
@@ -48404,28 +48471,22 @@ const ButtonConfirm = ({
48404
48471
  moveFocusTo(buttonEl);
48405
48472
  }
48406
48473
  }
48474
+ return undefined;
48407
48475
  }, [asking]);
48408
48476
  if (!asking) {
48409
48477
  return jsx(Next, {
48410
48478
  ...props,
48411
- // The first press is the question, not the act — so nothing that acts
48412
- // is handed to the resolvers below. `type` with them: a submit button
48413
- // would be given `--navi-send` back.
48414
- type: "button",
48479
+ // The first press is the question, not the act: nothing that acts
48480
+ // reaches the ui. What was decided about the button itself (readOnly,
48481
+ // cta, the label) stays.
48415
48482
  action: undefined,
48416
48483
  command: undefined,
48417
48484
  href: undefined,
48418
- route: undefined,
48419
48485
  onClick: () => {
48420
48486
  setAsking(true);
48421
48487
  }
48422
48488
  });
48423
48489
  }
48424
- const {
48425
- action,
48426
- onActionEnd,
48427
- onClick
48428
- } = props;
48429
48490
  return jsxs("span", {
48430
48491
  className: "navi_button_confirm",
48431
48492
  ref: askingRef,
@@ -48451,7 +48512,7 @@ const ButtonConfirm = ({
48451
48512
  "data-testid": confirmTestId || props["data-testid"],
48452
48513
  onClick: e => {
48453
48514
  onClick?.(e);
48454
- if (!action) {
48515
+ if (!action && !waitsForForm) {
48455
48516
  // A command runs on the press itself: there is nothing to wait for,
48456
48517
  // the question is answered as soon as it is pressed.
48457
48518
  cancel(true);
@@ -48686,7 +48747,10 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
48686
48747
  * button with this question and a "Confirmer"/"Annuler" pair, the second one
48687
48748
  * does what the button was for. `true` asks navi's default question. The
48688
48749
  * press stays under the finger and there is nothing to dismiss — "Annuler",
48689
- * Escape, or the focus leaving puts the button back. Use `<Picker
48750
+ * Escape, or the focus leaving puts the button back. So does the act itself,
48751
+ * once it went through: an `action` that succeeded, a submit whose form has
48752
+ * sent, a command on the press that runs it — while a failure keeps the
48753
+ * question up, its message on the button that asked. Use `<Picker
48690
48754
  * type="confirm">` instead when the question is long enough to want a popup,
48691
48755
  * or when the row has no space for it.
48692
48756
  * @param {import("ignore:preact").ComponentChildren} [confirmLabel] Label of the
@@ -48709,7 +48773,7 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
48709
48773
  * (it wears it itself) and loses the shrink under the finger, which has
48710
48774
  * nothing left to scale but the interactive area itself.
48711
48775
  */
48712
- const Button = createComponentResolver([ButtonFirstResolver, ButtonConfirmResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonUI]);
48776
+ const Button = createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonConfirmResolver, ButtonUI]);
48713
48777
 
48714
48778
  installImportMetaCssBuild(import.meta);
48715
48779
  const css$O = /* css */`.navi_control_swap {