@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.
- package/dist/jsenv_navi.js +167 -4
- package/dist/jsenv_navi.js.map +10 -8
- package/docs/AI_INSTRUCTIONS.md +3 -1
- package/docs/list_action.md +44 -0
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64022
|
-
|
|
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
|