@jsenv/navi 0.29.172 → 0.29.173

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.
@@ -1489,6 +1489,11 @@ naviI18n.addAll({
1489
1489
  fr: "Cette action attend une modification.",
1490
1490
  en: "This action is waiting for a change.",
1491
1491
  },
1492
+ // parallelGuard: the surface already has as many runs in flight as it allows
1493
+ "constraint.readonly.parallel_guard": {
1494
+ fr: "[max] action[s] déjà en cours, attendez qu'une se termine.",
1495
+ en: "[max] action[s] already in progress, wait for one to finish.",
1496
+ },
1492
1497
  "constraint.readonly.network_policy": {
1493
1498
  fr: "Hors ligne : ça ne peut pas partir.",
1494
1499
  en: "Offline: this cannot be sent.",
@@ -8365,7 +8370,16 @@ const createCalloutManager = (
8365
8370
  // Resolved as the token is added, and kept with it: a token shown later
8366
8371
  // (when the one covering it goes) must be drawn where it was pointing at
8367
8372
  // the time, and the control itself is where a token pointing nowhere goes.
8368
- anchorElement: anchorElement || controller.ref.current,
8373
+ //
8374
+ // Unless the control has somewhere better to send it, which is why it is
8375
+ // given the event: a control drawn by something else — a list row whose
8376
+ // selection is a visually hidden checkbox — answers for a whole box, and
8377
+ // the sentence belongs where the press landed inside it rather than in
8378
+ // its middle (see getCalloutAnchorElement in list_selectable.jsx).
8379
+ anchorElement:
8380
+ anchorElement ||
8381
+ controller.getCalloutAnchorElement?.(event) ||
8382
+ controller.ref.current,
8369
8383
  };
8370
8384
  tokens.set(token, tokenData);
8371
8385
  showToken(tokenData, event);
@@ -10227,6 +10241,15 @@ const readOnlyMessage = (field) => {
10227
10241
  if (reason === "network-policy") {
10228
10242
  return getNetworkPolicyReadOnlyMessage();
10229
10243
  }
10244
+ // As many runs as the surface allows are already out (see parallel_guard.js):
10245
+ // what stops the press is the queue, and it says how long it is.
10246
+ if (reason === "parallel-guard") {
10247
+ const { max } = field.parallelGuard;
10248
+ return naviI18n("constraint.readonly.parallel_guard", {
10249
+ max,
10250
+ s: max > 1 ? "s" : "",
10251
+ });
10252
+ }
10230
10253
  // A send button held back by the form above it, which holds nothing new (see
10231
10254
  // Button's own `readOnlyWhileFormUnchanged`) — what stops the press is not the
10232
10255
  // button, it is the form still waiting for a change, so that is what it says.
@@ -33522,6 +33545,70 @@ document.body.addEventListener(
33522
33545
  { capture: true },
33523
33546
  );
33524
33547
 
33548
+ /**
33549
+ * How many runs a surface lets happen at once.
33550
+ *
33551
+ * A list whose rows each carry their own action is a place to act, row by row,
33552
+ * and nothing about it stops someone from starting a run on every row before
33553
+ * any of them comes back: a dozen requests in flight, a dozen rows waiting, and
33554
+ * a server asked to do a dozen things at once because a list happened to be
33555
+ * long. The guard is what makes the surface say "not yet" instead — the rows
33556
+ * that are not running go read-only and say what they are waiting for, and the
33557
+ * next press is possible again the moment one of the runs comes back.
33558
+ *
33559
+ * It counts runs, not values — which is what separates it from
33560
+ * `maxLengthGuard`, the one that says how many things a selection may HOLD.
33561
+ * Both refuse a gesture; they refuse it for unrelated reasons and a surface can
33562
+ * carry either, or both.
33563
+ */
33564
+ const ParallelGuardContext = createContext(null);
33565
+
33566
+ /**
33567
+ * @param {number} max - how many runs may be in flight at once. `Infinity`
33568
+ * lifts the guard without taking it out of the tree.
33569
+ */
33570
+ const useParallelGuard = (max) => {
33571
+ const guardRef = useRef(null);
33572
+ if (!guardRef.current) {
33573
+ // A Set rather than a counter: a control asks about ITSELF (its own run is
33574
+ // never what blocks it), and a start that arrives twice for one run must
33575
+ // not be counted twice.
33576
+ const runningSet = new Set();
33577
+ const runningCountSignal = signal(0);
33578
+ const guard = {
33579
+ max,
33580
+ runningCountSignal,
33581
+ claim: (controller) => {
33582
+ if (runningSet.has(controller)) {
33583
+ return;
33584
+ }
33585
+ runningSet.add(controller);
33586
+ runningCountSignal.value = runningSet.size;
33587
+ },
33588
+ release: (controller) => {
33589
+ if (!runningSet.delete(controller)) {
33590
+ return;
33591
+ }
33592
+ runningCountSignal.value = runningSet.size;
33593
+ },
33594
+ blocks: (controller) => {
33595
+ if (guard.max === Infinity) {
33596
+ return false;
33597
+ }
33598
+ if (runningSet.has(controller)) {
33599
+ return false;
33600
+ }
33601
+ // Read through the signal so a control held back re-renders — and
33602
+ // becomes pressable again — the moment one of the runs comes back.
33603
+ return runningCountSignal.value >= guard.max;
33604
+ },
33605
+ };
33606
+ guardRef.current = guard;
33607
+ }
33608
+ guardRef.current.max = max;
33609
+ return guardRef.current;
33610
+ };
33611
+
33525
33612
  /**
33526
33613
  * How a control tells the labels pointing at it what it is (disabled, readOnly,
33527
33614
  * required) and when it goes away.
@@ -34437,6 +34524,9 @@ const useUIStateController = (
34437
34524
  resetUIState: (e) => {
34438
34525
  controller.setUIState(controller.state, e);
34439
34526
  },
34527
+ // Read by the callout manager when it has nowhere else to point.
34528
+ getCalloutAnchorElement: (event) =>
34529
+ resolveCalloutAnchorElement(controller.ref.current, event),
34440
34530
  // What the control shows becomes what is acknowledged: the outside said
34441
34531
  // yes to it, so it is where a rollback goes back to. Left alone for a
34442
34532
  // control whose value is GIVEN — the prop is already its truth — and
@@ -34660,6 +34750,44 @@ const firstDefinedChildUIState = (children) => {
34660
34750
  return undefined;
34661
34751
  };
34662
34752
 
34753
+ // Where a control that cannot point at itself puts what it has to say.
34754
+ //
34755
+ // A control drawn by something ELSE answers for a box bigger than itself: a
34756
+ // list row's selection is a visually hidden checkbox inside a whole pressable
34757
+ // row, so a callout anchored on the control lands in the middle of that row,
34758
+ // pointing at nothing. What the press landed on is what the sentence is about
34759
+ // — another control in the same box takes it (the button that invites, the
34760
+ // picker that opens), and the box itself takes what was aimed at the box.
34761
+ //
34762
+ // Answering null leaves the callout where it would have gone, which is also
34763
+ // what a box asks for once and for all with `data-callout-anchor="item"`.
34764
+ const resolveCalloutAnchorElement = (element, event) => {
34765
+ if (!element || !element.closest("[navi-visually-hidden]")) {
34766
+ return null;
34767
+ }
34768
+ const target = event?.target;
34769
+ if (!target || !target.closest) {
34770
+ return null;
34771
+ }
34772
+ const controlRoot = findControlRoot(element);
34773
+ if (
34774
+ !controlRoot ||
34775
+ controlRoot.getAttribute("data-callout-anchor") === "item"
34776
+ ) {
34777
+ return null;
34778
+ }
34779
+ const controlHost = target.closest("[navi-control-host]");
34780
+ if (
34781
+ !controlHost ||
34782
+ controlHost === element ||
34783
+ !controlRoot.contains(controlHost) ||
34784
+ controlHost.closest("[navi-visually-hidden]")
34785
+ ) {
34786
+ return null;
34787
+ }
34788
+ return controlHost;
34789
+ };
34790
+
34663
34791
  // A run that came back with a yes settles what the control is worth — but only
34664
34792
  // for the control the run BELONGS to. Anything with a `command` still fires its
34665
34793
  // own navi_action_* events (it is given a placeholder action so they exist),
@@ -37495,6 +37623,7 @@ const useInteractiveProps = (props, {
37495
37623
  const controlRequired = useContext(RequiredContext);
37496
37624
  const controlLoadingFromAbove = useContext(LoadingContext$1);
37497
37625
  const parentActionRequester = useContext(ActionRequesterContext);
37626
+ const parallelGuard = useContext(ParallelGuardContext);
37498
37627
  const parentAction = useContext(ActionContext);
37499
37628
  const actionStatus = useActionStatus(boundAction);
37500
37629
  const networkPolicyReason = useNetworkPolicyReason();
@@ -37532,7 +37661,12 @@ const useInteractiveProps = (props, {
37532
37661
  // why (see network_policy.js): a press that looks accepted and fails after
37533
37662
  // is what the policy exists to avoid.
37534
37663
  const heldByNetworkPolicy = networkPolicyReason !== null && (isWriteAction(boundAction) || Boolean(zoneStateApplies && parentAction && isWriteAction(parentAction)));
37535
- const readOnlyBase = readOnly || controlReadOnly || loadingBase || readOnlyFromParentMaxLengthGuard || controlInfo.readOnlyUncontrolled || heldByNetworkPolicy;
37664
+ // Held back because the surface above already has as many runs in flight as
37665
+ // it allows (see parallel_guard.js). Only a control that would START one:
37666
+ // anything without an action of its own runs nothing and has nothing to
37667
+ // wait its turn for.
37668
+ const heldByParallelGuard = Boolean(zoneStateApplies && parallelGuard && props.action && parallelGuard.blocks(uiStateController));
37669
+ const readOnlyBase = readOnly || controlReadOnly || loadingBase || readOnlyFromParentMaxLengthGuard || heldByParallelGuard || controlInfo.readOnlyUncontrolled || heldByNetworkPolicy;
37536
37670
  // An optimistic control trusts its action to succeed: the state the user
37537
37671
  // just set stays visible and interactive while the action runs — no
37538
37672
  // loading, no readonly. On failure resetOnError rolls the state back and
@@ -37577,6 +37711,13 @@ const useInteractiveProps = (props, {
37577
37711
  } else {
37578
37712
  controlHostProps["aria-readonly"] = readOnlyResolved ? "true" : "false";
37579
37713
  }
37714
+ // Read by READONLY_CONSTRAINT to say how many runs are already out, and by
37715
+ // the action handlers below, which are where a run joins and leaves the
37716
+ // count (a labelled block away, hence through the controller).
37717
+ uiStateController.parallelGuard = parallelGuard;
37718
+ if (heldByParallelGuard) {
37719
+ controlHostProps["data-readonly-reason"] = "parallel-guard";
37720
+ }
37580
37721
  if (heldByNetworkPolicy) {
37581
37722
  // Read by READONLY_CONSTRAINT; wins over any other reason the control
37582
37723
  // carries — nothing leaves, whatever else was holding it.
@@ -37820,6 +37961,7 @@ const useInteractiveProps = (props, {
37820
37961
  // carried by every navi_action_* event of that run).
37821
37962
  uiStateController.pendingActionEvent = e.detail.event;
37822
37963
  uiStateController.actionInFlight = true;
37964
+ uiStateController.parallelGuard?.claim(uiStateController);
37823
37965
  // The very instance this run uses, resolved now — while the UI state
37824
37966
  // still holds the value the run was made for. detail.action may be a
37825
37967
  // proxy following that state, and by the time anyone wants to abort
@@ -37834,6 +37976,7 @@ const useInteractiveProps = (props, {
37834
37976
  e.detail.addSideEffect(outcome => {
37835
37977
  uiStateController.actionInFlight = false;
37836
37978
  uiStateController.runningAction = null;
37979
+ uiStateController.parallelGuard?.release(uiStateController);
37837
37980
  const queuedEvent = uiStateController.queuedActionAllowedEvent;
37838
37981
  if (!queuedEvent) {
37839
37982
  return;
@@ -57108,6 +57251,7 @@ const translateOf = (axis, distance) =>
57108
57251
 
57109
57252
  const PopupClose = ({
57110
57253
  label,
57254
+ iconSize,
57111
57255
  ...rest
57112
57256
  }) => {
57113
57257
  return jsx(Button, {
@@ -57131,6 +57275,7 @@ const PopupClose = ({
57131
57275
  "aria-label": label === undefined ? naviI18n("button.close") : label,
57132
57276
  ...rest,
57133
57277
  children: jsx(Icon, {
57278
+ size: iconSize,
57134
57279
  children: jsx(CloseSvg, {})
57135
57280
  })
57136
57281
  });
@@ -64007,6 +64152,11 @@ const ListUI = props => {
64007
64152
  })
64008
64153
  });
64009
64154
  };
64155
+ // How many of a list's own rows may be acting at once, when the rows are what
64156
+ // carry the action. Four rather than none: a list is a place where the same
64157
+ // gesture is available many times over, and nothing else stops a long one from
64158
+ // putting a request out for every row it draws.
64159
+ const PARALLEL_GUARD_DEFAULT = 4;
64010
64160
  const ListFirstResolver = props => {
64011
64161
  const Next = useNextResolver();
64012
64162
  const refDefault = useRef(null);
@@ -64018,8 +64168,13 @@ const ListFirstResolver = props => {
64018
64168
  virtualRef.current = createListVirtual();
64019
64169
  }
64020
64170
  props.virtual = virtualRef.current;
64021
- return jsx(Next, {
64022
- ...props
64171
+ const parallelGuard = useParallelGuard(props.parallelGuard ?? PARALLEL_GUARD_DEFAULT);
64172
+ return jsx(ParallelGuardContext.Provider, {
64173
+ value: parallelGuard,
64174
+ children: jsx(Next, {
64175
+ ...props,
64176
+ parallelGuard: undefined
64177
+ })
64023
64178
  });
64024
64179
  };
64025
64180
 
@@ -64033,6 +64188,7 @@ const ListFirstResolver = props => {
64033
64188
  * deselectable?: boolean,
64034
64189
  * maxLength?: number,
64035
64190
  * maxLengthGuard?: number,
64191
+ * parallelGuard?: number,
64036
64192
  * standalone?: boolean,
64037
64193
  * action?: (value: any) => void,
64038
64194
  * uiAction?: (value: any) => void,
@@ -64209,6 +64365,13 @@ const ListFirstResolver = props => {
64209
64365
  * taking — and `uiAction` is not called. The selected ones stay takeable
64210
64366
  * back, so a selection that arrived too long can always be brought back
64211
64367
  * under the limit. Implies `maxLength` for validity.
64368
+ * @param {number} [props.parallelGuard=4]
64369
+ * How many runs the rows may have in flight at once, for a list whose rows
64370
+ * carry their own `action` (a button per row). While that many are out, every
64371
+ * row that is not running goes read-only and says how many it is waiting on;
64372
+ * the next press is possible again as soon as one comes back. `Infinity`
64373
+ * lifts it. Counts runs, not values — `maxLengthGuard` above is the one that
64374
+ * says how many things the selection may hold.
64212
64375
  * @param {boolean} [props.standalone]
64213
64376
  * This list answers for itself: it does not register with the control group
64214
64377
  * or picker around it, so its selection stays out of that value and nothing