@jsenv/navi 0.29.170 → 0.29.172
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 +617 -156
- package/dist/jsenv_navi.js.map +21 -19
- package/docs/AI_INSTRUCTIONS.md +25 -2
- package/docs/actions.md +34 -0
- package/docs/interactions.md +6 -0
- package/docs/list_action.md +121 -0
- package/docs/popup_open.md +70 -6
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -1501,17 +1501,36 @@ naviI18n.addAll({
|
|
|
1501
1501
|
fr: "Cette action est en cours...",
|
|
1502
1502
|
en: "This action is in progress...",
|
|
1503
1503
|
},
|
|
1504
|
+
// What a ROW is waiting on: the row as a thing the list holds, joining it,
|
|
1505
|
+
// leaving it or being saved. Said "à la liste" / "to the list" on purpose —
|
|
1506
|
+
// in a selectable list, "en cours d'ajout" alone reads as "being added to
|
|
1507
|
+
// the selection", which is the sentence below and a different event.
|
|
1504
1508
|
"constraint.busy.item": {
|
|
1505
1509
|
fr: "Cet élément est en cours de synchronisation.",
|
|
1506
1510
|
en: "This item is being synchronized.",
|
|
1507
1511
|
},
|
|
1508
1512
|
"constraint.busy.item.adding": {
|
|
1509
|
-
fr: "Cet élément est en cours d'ajout.",
|
|
1510
|
-
en: "This item is being added.",
|
|
1513
|
+
fr: "Cet élément est en cours d'ajout à la liste.",
|
|
1514
|
+
en: "This item is being added to the list.",
|
|
1511
1515
|
},
|
|
1512
1516
|
"constraint.busy.item.removing": {
|
|
1513
|
-
fr: "Cet élément est en cours de
|
|
1514
|
-
en: "This item is being removed.",
|
|
1517
|
+
fr: "Cet élément est en cours de retrait de la liste.",
|
|
1518
|
+
en: "This item is being removed from the list.",
|
|
1519
|
+
},
|
|
1520
|
+
"constraint.busy.item.updating": {
|
|
1521
|
+
fr: "Cet élément est en cours de mise à jour.",
|
|
1522
|
+
en: "This item is being updated.",
|
|
1523
|
+
},
|
|
1524
|
+
// What the LIST is waiting on: the choice just made, on its way. The subject
|
|
1525
|
+
// is the selection, never a row — the row this is shown on is the one the
|
|
1526
|
+
// user just pressed, which is not the one being committed.
|
|
1527
|
+
"constraint.busy.selection": {
|
|
1528
|
+
fr: "La sélection est en cours d'enregistrement...",
|
|
1529
|
+
en: "This selection is being saved...",
|
|
1530
|
+
},
|
|
1531
|
+
"constraint.busy.choice": {
|
|
1532
|
+
fr: "Le choix est en cours d'enregistrement...",
|
|
1533
|
+
en: "This choice is being saved...",
|
|
1515
1534
|
},
|
|
1516
1535
|
"constraint.busy.default": {
|
|
1517
1536
|
fr: "Cet élément est occupé.",
|
|
@@ -2945,6 +2964,13 @@ const debounceSignal = (
|
|
|
2945
2964
|
? compareTwoJsValues(value, debouncedValue)
|
|
2946
2965
|
: value === debouncedValue
|
|
2947
2966
|
) {
|
|
2967
|
+
// The signal came back to where the debounced one already is: whatever it
|
|
2968
|
+
// passed through on the way is not on its way any more. Leaving the timer
|
|
2969
|
+
// armed would publish that in-between value once the delay elapses, and
|
|
2970
|
+
// settlingSignal would then say a newer value is coming, forever.
|
|
2971
|
+
clearTimeout(timeoutId);
|
|
2972
|
+
timeoutId = undefined;
|
|
2973
|
+
latestValue = debouncedValue;
|
|
2948
2974
|
return;
|
|
2949
2975
|
}
|
|
2950
2976
|
clearTimeout(timeoutId);
|
|
@@ -3275,15 +3301,23 @@ const SYMBOL_OBJECT_SIGNAL = Symbol.for("navi_object_signal");
|
|
|
3275
3301
|
* resolves priorities between the four operation sets (reset > rerun > run >
|
|
3276
3302
|
* prerun) and performs them.
|
|
3277
3303
|
*
|
|
3278
|
-
* Memory design (the surprising part): nothing here keeps actions
|
|
3279
|
-
* Child actions are held through ephemerons (createJsValueWeakMap) so a
|
|
3280
|
-
* and its params are garbage-collected together, running actions live in
|
|
3281
|
-
* iterable *weak* sets, and property/signal mirroring uses weakEffect.
|
|
3304
|
+
* Memory design (the surprising part): by default nothing here keeps actions
|
|
3305
|
+
* alive. Child actions are held through ephemerons (createJsValueWeakMap) so a
|
|
3306
|
+
* child and its params are garbage-collected together, running actions live in
|
|
3307
|
+
* iterable *weak* sets, and property/signal mirroring uses weakEffect. Three
|
|
3282
3308
|
* consequences to be aware of:
|
|
3283
3309
|
* - an action can exist in several places only if everyone shares the same
|
|
3284
3310
|
* instance (the caches above are what makes lookups return it);
|
|
3285
3311
|
* - prerun actions may have no other reference yet, so
|
|
3286
|
-
* prerunProtectionRegistry pins them
|
|
3312
|
+
* prerunProtectionRegistry pins them until they answer;
|
|
3313
|
+
* - an answer outlives the screen that asked for it only where the question
|
|
3314
|
+
* declared `keep` (createKeptAnswerRegistry); everything else is asked again.
|
|
3315
|
+
*
|
|
3316
|
+
* What makes that design fragile: reading an action's signals from inside an
|
|
3317
|
+
* effect subscribes that effect to them, and the root action never dies — so
|
|
3318
|
+
* the effect, and everything its closure holds, never dies either. Stringifying
|
|
3319
|
+
* an action for a debug message is the easy way to do it by accident, which is
|
|
3320
|
+
* why toString peeks.
|
|
3287
3321
|
*
|
|
3288
3322
|
* A failing run rejects, and writes its error into errorSignal as well. The
|
|
3289
3323
|
* callers that cannot take a rejection — a run started from a signal effect, a
|
|
@@ -3398,6 +3432,58 @@ const prerunProtectionRegistry = (() => {
|
|
|
3398
3432
|
};
|
|
3399
3433
|
})();
|
|
3400
3434
|
|
|
3435
|
+
/**
|
|
3436
|
+
* The answers a root action keeps once the screen that asked for them is gone.
|
|
3437
|
+
*
|
|
3438
|
+
* An action holds its answer for exactly as long as something references it, so
|
|
3439
|
+
* without this the lifetime of an answer is whatever the garbage collector
|
|
3440
|
+
* decides: coming back to a screen re-asks the same question, or does not,
|
|
3441
|
+
* depending on when a collection happened to run.
|
|
3442
|
+
*
|
|
3443
|
+
* Age is read when someone looks an answer up, never on a timer. A screen
|
|
3444
|
+
* nobody is on must not talk to the network, and an answer expiring under a
|
|
3445
|
+
* screen someone IS on would empty it mid-read.
|
|
3446
|
+
*/
|
|
3447
|
+
const createKeptAnswerRegistry = ({ ms, max }) => {
|
|
3448
|
+
// insertion order is recency order: looking an answer up re-inserts it, so
|
|
3449
|
+
// the first key is always the least recently used one
|
|
3450
|
+
const answeredAtMap = new Map();
|
|
3451
|
+
|
|
3452
|
+
return {
|
|
3453
|
+
keep: (action) => {
|
|
3454
|
+
answeredAtMap.delete(action);
|
|
3455
|
+
answeredAtMap.set(action, Date.now());
|
|
3456
|
+
if (max === undefined) {
|
|
3457
|
+
return;
|
|
3458
|
+
}
|
|
3459
|
+
while (answeredAtMap.size > max) {
|
|
3460
|
+
const [leastRecentlyUsed] = answeredAtMap.keys();
|
|
3461
|
+
answeredAtMap.delete(leastRecentlyUsed);
|
|
3462
|
+
}
|
|
3463
|
+
},
|
|
3464
|
+
|
|
3465
|
+
drop: (action) => {
|
|
3466
|
+
answeredAtMap.delete(action);
|
|
3467
|
+
},
|
|
3468
|
+
|
|
3469
|
+
touch: (action) => {
|
|
3470
|
+
const answeredAt = answeredAtMap.get(action);
|
|
3471
|
+
if (answeredAt === undefined) {
|
|
3472
|
+
return;
|
|
3473
|
+
}
|
|
3474
|
+
answeredAtMap.delete(action);
|
|
3475
|
+
if (ms !== undefined && Date.now() - answeredAt > ms) {
|
|
3476
|
+
// reset rather than drop the instance: equal params must keep giving
|
|
3477
|
+
// the same action, otherwise whoever still holds this one would be
|
|
3478
|
+
// reading a different instance than whoever asks for it next.
|
|
3479
|
+
action.reset({ reason: "kept answer expired" });
|
|
3480
|
+
return;
|
|
3481
|
+
}
|
|
3482
|
+
answeredAtMap.set(action, answeredAt);
|
|
3483
|
+
},
|
|
3484
|
+
};
|
|
3485
|
+
};
|
|
3486
|
+
|
|
3401
3487
|
const formatActionSet = (actionSet, prefix = "") => {
|
|
3402
3488
|
let message = prefix;
|
|
3403
3489
|
for (const action of actionSet) {
|
|
@@ -3797,12 +3883,38 @@ const mergeActionParams = (currentParams, newParams) => {
|
|
|
3797
3883
|
};
|
|
3798
3884
|
|
|
3799
3885
|
const actionWeakMap = new WeakMap();
|
|
3886
|
+
/**
|
|
3887
|
+
* Wraps an async callback into an action: a question, its answer, and the
|
|
3888
|
+
* signals saying where that answer is.
|
|
3889
|
+
*
|
|
3890
|
+
* @param {Function} callback
|
|
3891
|
+
* @param {object} [rootOptions]
|
|
3892
|
+
* @param {{ ms?: number, max?: number }} [rootOptions.keep] - how long an
|
|
3893
|
+
* answer stays good once it has landed. Without it an answer lives exactly as
|
|
3894
|
+
* long as something references it: the screen that asked goes away, and
|
|
3895
|
+
* coming back asks the same question again at a moment nothing in the code
|
|
3896
|
+
* decides.
|
|
3897
|
+
*
|
|
3898
|
+
* `ms` counts from the moment the answer landed; `max` caps how many answers
|
|
3899
|
+
* are held at once, the least recently looked-at going first. Age is read
|
|
3900
|
+
* when someone looks, never on a timer — a screen nobody is on must not talk
|
|
3901
|
+
* to the network — and an expired answer is `reset()`, so the question goes
|
|
3902
|
+
* back out through whoever asks for it next.
|
|
3903
|
+
*
|
|
3904
|
+
* It is declared here rather than on each binding because the answer to "how
|
|
3905
|
+
* long is this still true?" belongs to the question: a court list is good for
|
|
3906
|
+
* the session, a count of people watching grows on its own and is worth a few
|
|
3907
|
+
* minutes, a payment status is worth nothing at all.
|
|
3908
|
+
*/
|
|
3800
3909
|
const createAction = (callback, rootOptions = {}) => {
|
|
3801
3910
|
const existing = actionWeakMap.get(callback);
|
|
3802
3911
|
if (existing) {
|
|
3803
3912
|
return existing;
|
|
3804
3913
|
}
|
|
3805
3914
|
|
|
3915
|
+
const keptAnswerRegistry = rootOptions.keep
|
|
3916
|
+
? createKeptAnswerRegistry(rootOptions.keep)
|
|
3917
|
+
: null;
|
|
3806
3918
|
let rootAction;
|
|
3807
3919
|
|
|
3808
3920
|
const createActionCore = (options, { parentAction } = {}) => {
|
|
@@ -4005,6 +4117,9 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4005
4117
|
* During the delay the instance is still the previous one, holding the
|
|
4006
4118
|
* previous answer — `paramsSettlingSignal` is what says a newer one is
|
|
4007
4119
|
* coming, and `useAsyncData(action, { loading: true })` reads it.
|
|
4120
|
+
* The instance follows where the signal settles, never a value it only
|
|
4121
|
+
* passed through: a signal back where it started before the delay elapses
|
|
4122
|
+
* asks nothing.
|
|
4008
4123
|
*/
|
|
4009
4124
|
const bindParams = (newParamsOrSignal, options = {}) => {
|
|
4010
4125
|
const { debounce, ...optionsWithoutDebounce } = options;
|
|
@@ -4031,6 +4146,9 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4031
4146
|
}
|
|
4032
4147
|
const existingChildAction = childActionWeakMap.get(newParamsOrSignal);
|
|
4033
4148
|
if (existingChildAction) {
|
|
4149
|
+
if (keptAnswerRegistry) {
|
|
4150
|
+
keptAnswerRegistry.touch(existingChildAction);
|
|
4151
|
+
}
|
|
4034
4152
|
return existingChildAction;
|
|
4035
4153
|
}
|
|
4036
4154
|
const childAction = _bindParams(
|
|
@@ -4169,7 +4287,10 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4169
4287
|
paramsSignal.value = nextParams;
|
|
4170
4288
|
return true;
|
|
4171
4289
|
},
|
|
4172
|
-
|
|
4290
|
+
// peek: an action gets stringified into debug messages built inside
|
|
4291
|
+
// effects, and subscribing there would keep those effects — and everything
|
|
4292
|
+
// their closures hold — alive for as long as the action itself.
|
|
4293
|
+
toString: () => actionCallSourceSignal.peek(),
|
|
4173
4294
|
meta,
|
|
4174
4295
|
debug: (...args) => {
|
|
4175
4296
|
if (!meta.debug && !DEBUG$2) {
|
|
@@ -4333,6 +4454,9 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4333
4454
|
onComplete?.(data, action);
|
|
4334
4455
|
completeSideEffectCleanup = completeSideEffect?.(action);
|
|
4335
4456
|
});
|
|
4457
|
+
if (keptAnswerRegistry) {
|
|
4458
|
+
keptAnswerRegistry.keep(action);
|
|
4459
|
+
}
|
|
4336
4460
|
if (DEBUG$2) {
|
|
4337
4461
|
console.log(`"${action}": completed`);
|
|
4338
4462
|
}
|
|
@@ -4448,6 +4572,9 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4448
4572
|
}
|
|
4449
4573
|
|
|
4450
4574
|
prerunProtectionRegistry.unprotect(action);
|
|
4575
|
+
if (keptAnswerRegistry) {
|
|
4576
|
+
keptAnswerRegistry.drop(action);
|
|
4577
|
+
}
|
|
4451
4578
|
|
|
4452
4579
|
if (sideEffectCleanup) {
|
|
4453
4580
|
sideEffectCleanup(reason);
|
|
@@ -4697,7 +4824,7 @@ const createActionProxyFromSignal = (
|
|
|
4697
4824
|
);
|
|
4698
4825
|
},
|
|
4699
4826
|
replaceParams: null, // Will be set below
|
|
4700
|
-
toString: () =>
|
|
4827
|
+
toString: () => callSourceSignal.peek(), // peek: see the action's toString
|
|
4701
4828
|
meta: action.meta,
|
|
4702
4829
|
|
|
4703
4830
|
paramsSignal: proxyParamsSignal,
|
|
@@ -4900,7 +5027,9 @@ const runUnwatched = (startRun) => {
|
|
|
4900
5027
|
* keystroke: set `debounce: 500` and the request is sent only after the user stops interacting
|
|
4901
5028
|
* with the filters for 500 ms. During that delay the action still holds the previous answer;
|
|
4902
5029
|
* `action.paramsSettlingSignal` is what says a newer one is coming, and
|
|
4903
|
-
* `useAsyncData(action, { loading: true })` reads it.
|
|
5030
|
+
* `useAsyncData(action, { loading: true })` reads it. The run follows where
|
|
5031
|
+
* the params settle, never a value they only passed through: params back
|
|
5032
|
+
* where they started before the delay elapses run nothing.
|
|
4904
5033
|
*
|
|
4905
5034
|
* Example — auto-refresh a result list while the user tweaks filters:
|
|
4906
5035
|
* ```js
|
|
@@ -5594,6 +5723,7 @@ const CONSTRAINT_NAME_TO_PROP = {
|
|
|
5594
5723
|
min_special_char: "minSpecialCharMessage",
|
|
5595
5724
|
one_of: "oneOfMessage",
|
|
5596
5725
|
readonly: "readOnlyMessage",
|
|
5726
|
+
busy: "busyMessage",
|
|
5597
5727
|
available: "availableMessage",
|
|
5598
5728
|
};
|
|
5599
5729
|
|
|
@@ -6209,18 +6339,32 @@ const findControlProxies = (el) => {
|
|
|
6209
6339
|
};
|
|
6210
6340
|
|
|
6211
6341
|
/**
|
|
6212
|
-
* Given a real control element, returns the proxy
|
|
6342
|
+
* Given a real control element, returns the proxy a gesture on the control
|
|
6343
|
+
* lands on — the one thing said about the control is drawn on.
|
|
6213
6344
|
*
|
|
6214
6345
|
* Use when you need a single visible stand-in for the real control — anchoring
|
|
6215
6346
|
* a callout, for instance. Anything notifying proxies of a state change wants
|
|
6216
6347
|
* `findControlProxies` instead, so a control represented by several of them
|
|
6217
6348
|
* updates all of them.
|
|
6218
6349
|
*
|
|
6219
|
-
*
|
|
6350
|
+
* A proxy the pointer cannot reach is drawn for the control without standing
|
|
6351
|
+
* for it: the surface around it takes the presses for everything it holds, so
|
|
6352
|
+
* that surface is where a refusal about the control belongs — not a box inside
|
|
6353
|
+
* it the finger never has to find. A selectable list row whose whole area is
|
|
6354
|
+
* the click target is exactly that arrangement (see `selectableArea`), and it
|
|
6355
|
+
* is read from the arrangement itself rather than from a flag repeating it:
|
|
6356
|
+
* where a gesture can land IS pointer-events.
|
|
6357
|
+
*
|
|
6358
|
+
* Returns `null` when no proxy stands for `el`.
|
|
6220
6359
|
*/
|
|
6221
6360
|
const findControlProxy = (el) => {
|
|
6222
|
-
const
|
|
6223
|
-
|
|
6361
|
+
for (const proxyElement of findControlProxies(el)) {
|
|
6362
|
+
if (getComputedStyle(proxyElement).pointerEvents === "none") {
|
|
6363
|
+
continue;
|
|
6364
|
+
}
|
|
6365
|
+
return proxyElement;
|
|
6366
|
+
}
|
|
6367
|
+
return null;
|
|
6224
6368
|
};
|
|
6225
6369
|
|
|
6226
6370
|
/**
|
|
@@ -7222,6 +7366,33 @@ const openCallout = (message, {
|
|
|
7222
7366
|
preventScroll: true
|
|
7223
7367
|
});
|
|
7224
7368
|
}
|
|
7369
|
+
close_on_escape_from_anywhere: {
|
|
7370
|
+
// Escape closes a callout by reaching its anchor (above), which works while
|
|
7371
|
+
// the anchor holds the keyboard. Some do not: a callout opened with
|
|
7372
|
+
// `skipFocus` leaves the focus where it was, and a press on a blocked list
|
|
7373
|
+
// row leaves it nowhere at all — the row refuses the press, focus included.
|
|
7374
|
+
// The callout is then unreachable by keyboard, and "click somewhere else to
|
|
7375
|
+
// make it go" is not an answer for someone not using a pointer.
|
|
7376
|
+
//
|
|
7377
|
+
// Only for the ones that cannot be heard otherwise, and only while they are
|
|
7378
|
+
// open: a callout the anchor can hear about is left to the anchor, so
|
|
7379
|
+
// Escape goes on meaning what it means where the user is.
|
|
7380
|
+
if (anchorElement && anchorElement.contains(document.activeElement)) {
|
|
7381
|
+
break close_on_escape_from_anywhere;
|
|
7382
|
+
}
|
|
7383
|
+
const onDocumentKeydown = e => {
|
|
7384
|
+
if (e.key !== "Escape") {
|
|
7385
|
+
return;
|
|
7386
|
+
}
|
|
7387
|
+
requestClose(e, "escape_key");
|
|
7388
|
+
e.stopPropagation();
|
|
7389
|
+
e.preventDefault();
|
|
7390
|
+
};
|
|
7391
|
+
document.addEventListener("keydown", onDocumentKeydown, true);
|
|
7392
|
+
addTeardown(() => {
|
|
7393
|
+
document.removeEventListener("keydown", onDocumentKeydown, true);
|
|
7394
|
+
});
|
|
7395
|
+
}
|
|
7225
7396
|
{
|
|
7226
7397
|
const handleCustomCloseEvent = e => {
|
|
7227
7398
|
onRequestClose(e);
|
|
@@ -8008,6 +8179,11 @@ const generateSvgWithoutArrow = (width, height) => {
|
|
|
8008
8179
|
*/
|
|
8009
8180
|
|
|
8010
8181
|
|
|
8182
|
+
// The close reason the manager gives itself when the callout has to be drawn on
|
|
8183
|
+
// another element: it tears the callout down and opens it again, and this is
|
|
8184
|
+
// what tells that apart from the callout being dismissed.
|
|
8185
|
+
const MOVING_TO_ANOTHER_ANCHOR = "moving_to_another_anchor";
|
|
8186
|
+
|
|
8011
8187
|
/**
|
|
8012
8188
|
* Creates an opaque token used as a key for callout open reasons.
|
|
8013
8189
|
* Each caller (validation, interaction, …) owns one token.
|
|
@@ -8031,10 +8207,106 @@ const createCalloutManager = (
|
|
|
8031
8207
|
const [notifyCalloutOpen, onCalloutOpen] = createPubSub();
|
|
8032
8208
|
|
|
8033
8209
|
let callout = null;
|
|
8210
|
+
// Where the open callout is drawn. What a token points at is compared against
|
|
8211
|
+
// it to know whether showing that token is a new message on the same element
|
|
8212
|
+
// or the same conversation moving to another one.
|
|
8213
|
+
let calloutAnchorElement = null;
|
|
8034
8214
|
// Tracks open tokens → their constraint info.
|
|
8035
8215
|
// The callout closes automatically when the last token is removed.
|
|
8036
8216
|
const tokens = new Map();
|
|
8037
8217
|
|
|
8218
|
+
const openCalloutForToken = (tokenData, event) => {
|
|
8219
|
+
const { anchorElement } = tokenData;
|
|
8220
|
+
calloutAnchorElement = anchorElement;
|
|
8221
|
+
const removeCloseOnCleanup = addTeardown?.(() => {
|
|
8222
|
+
requestCloseCallout(new CustomEvent("cleanup"), "cleanup");
|
|
8223
|
+
});
|
|
8224
|
+
// `openResults` is referenced in onClose which runs later — forward ref is intentional.
|
|
8225
|
+
let openResults = [];
|
|
8226
|
+
callout = openCallout(tokenData.message, {
|
|
8227
|
+
status: tokenData.status,
|
|
8228
|
+
testId: tokenData.testId,
|
|
8229
|
+
icon: tokenData.icon,
|
|
8230
|
+
closeButton: tokenData.closeButton,
|
|
8231
|
+
closeOnClickOutside: tokenData.status !== "error",
|
|
8232
|
+
anchorElement,
|
|
8233
|
+
openingEvent: event,
|
|
8234
|
+
skipFocus: tokenData.skipFocus,
|
|
8235
|
+
debug: debugPopup,
|
|
8236
|
+
onClose: ({
|
|
8237
|
+
event: closeEvent,
|
|
8238
|
+
reason,
|
|
8239
|
+
shouldTransferFocusFromCallout,
|
|
8240
|
+
}) => {
|
|
8241
|
+
removeCloseOnCleanup?.();
|
|
8242
|
+
for (const result of openResults) {
|
|
8243
|
+
if (typeof result === "function") {
|
|
8244
|
+
result();
|
|
8245
|
+
}
|
|
8246
|
+
}
|
|
8247
|
+
callout = null;
|
|
8248
|
+
calloutAnchorElement = null;
|
|
8249
|
+
if (reason === MOVING_TO_ANOTHER_ANCHOR) {
|
|
8250
|
+
// The control still has the same things to say, on another element:
|
|
8251
|
+
// the tokens stand and nothing was dismissed, so neither the token
|
|
8252
|
+
// callbacks nor the focus hand-back below apply.
|
|
8253
|
+
return;
|
|
8254
|
+
}
|
|
8255
|
+
// User dismissed the callout — notify all active tokens then clear.
|
|
8256
|
+
// Told what closed it: a token whose content is a popup of its own (a
|
|
8257
|
+
// picker in callout mode) closes that popup on the same event.
|
|
8258
|
+
for (const [, otherTokenData] of tokens) {
|
|
8259
|
+
otherTokenData.onClose?.({ event: closeEvent, reason });
|
|
8260
|
+
}
|
|
8261
|
+
tokens.clear();
|
|
8262
|
+
const element = controller.ref.current;
|
|
8263
|
+
if (
|
|
8264
|
+
shouldTransferFocusFromCallout &&
|
|
8265
|
+
element &&
|
|
8266
|
+
!element.closest('[aria-hidden="true"]')
|
|
8267
|
+
) {
|
|
8268
|
+
const focusTarget =
|
|
8269
|
+
findFocusDelegateTarget(anchorElement) || anchorElement;
|
|
8270
|
+
debugFocus(
|
|
8271
|
+
closeEvent,
|
|
8272
|
+
`callout is closing with focus, give focus back to the control ${getElementSignature(focusTarget)}.focus()`,
|
|
8273
|
+
);
|
|
8274
|
+
focusTarget.focus();
|
|
8275
|
+
}
|
|
8276
|
+
},
|
|
8277
|
+
});
|
|
8278
|
+
openResults = notifyCalloutOpen(event);
|
|
8279
|
+
};
|
|
8280
|
+
|
|
8281
|
+
// What a token has to say, drawn where that token points. A control has one
|
|
8282
|
+
// callout, so a token takes it over from whoever held it — and a token about
|
|
8283
|
+
// another element (an action refused about the row that was pressed, on a
|
|
8284
|
+
// list that owns the action) moves it there, because a sentence about a row
|
|
8285
|
+
// drawn on the list is a sentence pointing at the wrong thing.
|
|
8286
|
+
const showToken = (tokenData, event) => {
|
|
8287
|
+
if (tokenData.anchorElement && !tokenData.anchorElement.isConnected) {
|
|
8288
|
+
// What the token pointed at has left the page — a row removed while its
|
|
8289
|
+
// refusal was still waiting behind another one. There is nothing left to
|
|
8290
|
+
// draw on there, and the control holding the token is still here, so it
|
|
8291
|
+
// takes it (same rule as an action error whose requester is gone, see
|
|
8292
|
+
// use_execute_action.js).
|
|
8293
|
+
tokenData.anchorElement = controller.ref.current;
|
|
8294
|
+
}
|
|
8295
|
+
if (callout) {
|
|
8296
|
+
if (tokenData.anchorElement === calloutAnchorElement) {
|
|
8297
|
+
callout.update(tokenData.message, {
|
|
8298
|
+
status: tokenData.status,
|
|
8299
|
+
testId: tokenData.testId,
|
|
8300
|
+
icon: tokenData.icon,
|
|
8301
|
+
closeButton: tokenData.closeButton,
|
|
8302
|
+
});
|
|
8303
|
+
return;
|
|
8304
|
+
}
|
|
8305
|
+
callout.requestClose(event, MOVING_TO_ANOTHER_ANCHOR);
|
|
8306
|
+
}
|
|
8307
|
+
openCalloutForToken(tokenData, event);
|
|
8308
|
+
};
|
|
8309
|
+
|
|
8038
8310
|
// Remove a token. Closes the callout only when no tokens remain.
|
|
8039
8311
|
// If other tokens are still active, updates the callout to show the first remaining one.
|
|
8040
8312
|
const removeOpenToken = (token, event) => {
|
|
@@ -8045,12 +8317,7 @@ const createCalloutManager = (
|
|
|
8045
8317
|
if (tokens.size > 0) {
|
|
8046
8318
|
if (callout) {
|
|
8047
8319
|
const [, remainingTokenData] = tokens.entries().next().value;
|
|
8048
|
-
|
|
8049
|
-
status: remainingTokenData.status,
|
|
8050
|
-
testId: remainingTokenData.testId,
|
|
8051
|
-
icon: remainingTokenData.icon,
|
|
8052
|
-
closeButton: remainingTokenData.closeButton,
|
|
8053
|
-
});
|
|
8320
|
+
showToken(remainingTokenData, event);
|
|
8054
8321
|
}
|
|
8055
8322
|
return false;
|
|
8056
8323
|
}
|
|
@@ -8087,70 +8354,21 @@ const createCalloutManager = (
|
|
|
8087
8354
|
removeOpenToken(token, event);
|
|
8088
8355
|
return;
|
|
8089
8356
|
}
|
|
8090
|
-
const
|
|
8357
|
+
const tokenData = {
|
|
8358
|
+
message,
|
|
8091
8359
|
status,
|
|
8092
8360
|
testId,
|
|
8093
8361
|
icon,
|
|
8094
8362
|
closeButton,
|
|
8095
|
-
closeOnClickOutside: status !== "error",
|
|
8096
|
-
};
|
|
8097
|
-
|
|
8098
|
-
tokens.set(token, { message, status, testId, icon, closeButton, onClose });
|
|
8099
|
-
if (callout) {
|
|
8100
|
-
callout.update(message, calloutOptions);
|
|
8101
|
-
return;
|
|
8102
|
-
}
|
|
8103
|
-
const resolvedAnchorElement = anchorElement || controller.ref.current;
|
|
8104
|
-
const removeCloseOnCleanup = addTeardown?.(() => {
|
|
8105
|
-
requestCloseCallout(new CustomEvent("cleanup"), "cleanup");
|
|
8106
|
-
});
|
|
8107
|
-
// `openResults` is referenced in onClose which runs later — forward ref is intentional.
|
|
8108
|
-
let openResults = [];
|
|
8109
|
-
callout = openCallout(message, {
|
|
8110
|
-
...calloutOptions,
|
|
8111
|
-
anchorElement: resolvedAnchorElement,
|
|
8112
|
-
openingEvent: event,
|
|
8113
8363
|
skipFocus,
|
|
8114
|
-
|
|
8115
|
-
|
|
8116
|
-
|
|
8117
|
-
|
|
8118
|
-
|
|
8119
|
-
|
|
8120
|
-
|
|
8121
|
-
|
|
8122
|
-
if (typeof result === "function") {
|
|
8123
|
-
result();
|
|
8124
|
-
}
|
|
8125
|
-
}
|
|
8126
|
-
callout = null;
|
|
8127
|
-
// User dismissed the callout — notify all active tokens then clear.
|
|
8128
|
-
// Told what closed it: a token whose content is a popup of its own (a
|
|
8129
|
-
// picker in callout mode) closes that popup on the same event.
|
|
8130
|
-
for (const [, tokenData] of tokens) {
|
|
8131
|
-
tokenData.onClose?.({ event: closeEvent, reason });
|
|
8132
|
-
}
|
|
8133
|
-
tokens.clear();
|
|
8134
|
-
const element = controller.ref.current;
|
|
8135
|
-
if (
|
|
8136
|
-
shouldTransferFocusFromCallout &&
|
|
8137
|
-
element &&
|
|
8138
|
-
!element.closest('[aria-hidden="true"]')
|
|
8139
|
-
) {
|
|
8140
|
-
const focusTarget =
|
|
8141
|
-
findFocusDelegateTarget(resolvedAnchorElement) ||
|
|
8142
|
-
resolvedAnchorElement;
|
|
8143
|
-
debugFocus(
|
|
8144
|
-
closeEvent,
|
|
8145
|
-
`callout is closing with focus, give focus back to the control ${getElementSignature(focusTarget)}.focus()`,
|
|
8146
|
-
);
|
|
8147
|
-
focusTarget.focus();
|
|
8148
|
-
}
|
|
8149
|
-
},
|
|
8150
|
-
});
|
|
8151
|
-
// `onOpen` can be a createPubSub publisher — its return value is an array of cleanup fns.
|
|
8152
|
-
// Or just a plain callback — wrap the single return value in an array.
|
|
8153
|
-
openResults = notifyCalloutOpen(event);
|
|
8364
|
+
onClose,
|
|
8365
|
+
// Resolved as the token is added, and kept with it: a token shown later
|
|
8366
|
+
// (when the one covering it goes) must be drawn where it was pointing at
|
|
8367
|
+
// the time, and the control itself is where a token pointing nowhere goes.
|
|
8368
|
+
anchorElement: anchorElement || controller.ref.current,
|
|
8369
|
+
};
|
|
8370
|
+
tokens.set(token, tokenData);
|
|
8371
|
+
showToken(tokenData, event);
|
|
8154
8372
|
};
|
|
8155
8373
|
|
|
8156
8374
|
const calloutManager = {
|
|
@@ -8176,12 +8394,21 @@ const BUSY_CONSTRAINT = {
|
|
|
8176
8394
|
transient: true,
|
|
8177
8395
|
// Unlike readonly/disabled, a busy element DOES block its parent from
|
|
8178
8396
|
// submitting — the element is mid-operation and cannot safely participate.
|
|
8179
|
-
check: (field) => {
|
|
8397
|
+
check: (field, { intent } = {}) => {
|
|
8180
8398
|
const isBusy = isControlBusy(field);
|
|
8181
8399
|
if (!isBusy) {
|
|
8182
8400
|
return null;
|
|
8183
8401
|
}
|
|
8184
8402
|
|
|
8403
|
+
// Busy, and what it opens still opens: a picker's answer lives in a shape
|
|
8404
|
+
// only its popup draws, and refusing to open leaves it unreadable for as
|
|
8405
|
+
// long as the wait lasts. Opening reads and nothing more — the same
|
|
8406
|
+
// exemption read-only makes, on the same controls (see
|
|
8407
|
+
// READONLY_CONSTRAINT), so that "read" means one thing to both.
|
|
8408
|
+
if (intent === "read" && field.readOnlyOpens) {
|
|
8409
|
+
return null;
|
|
8410
|
+
}
|
|
8411
|
+
|
|
8185
8412
|
const isButton = field.controlType === "button";
|
|
8186
8413
|
const message = isButton
|
|
8187
8414
|
? naviI18n("constraint.busy.button")
|
|
@@ -8199,9 +8426,9 @@ CONSTRAINT_ATTRIBUTE_SET.add("data-busy");
|
|
|
8199
8426
|
// whether it may finally close, a slide whether it may move on).
|
|
8200
8427
|
//
|
|
8201
8428
|
// The action's running state is a signal, so it is already right there. A
|
|
8202
|
-
// control busy only because the group above is
|
|
8203
|
-
//
|
|
8204
|
-
//
|
|
8429
|
+
// control busy only because the group above is waiting has no state of its own
|
|
8430
|
+
// to read — the group's answer IS its answer, so it asks upward and inherits
|
|
8431
|
+
// the same live reading.
|
|
8205
8432
|
const isControlBusy = (field) => {
|
|
8206
8433
|
if (field.loadingFromOwnProp) {
|
|
8207
8434
|
return true;
|
|
@@ -8225,7 +8452,7 @@ const isControlBusy = (field) => {
|
|
|
8225
8452
|
return true;
|
|
8226
8453
|
}
|
|
8227
8454
|
}
|
|
8228
|
-
if (field.
|
|
8455
|
+
if (field.loadingFromAbove) {
|
|
8229
8456
|
const parent = field.parentUIStateController;
|
|
8230
8457
|
return parent ? isControlBusy(parent) : false;
|
|
8231
8458
|
}
|
|
@@ -10063,6 +10290,9 @@ const createControlInteraction = (
|
|
|
10063
10290
|
let failingManagedInteraction = null;
|
|
10064
10291
|
// The title this rule put on the element, if any (see checkInteractivity).
|
|
10065
10292
|
let titleWritten = null;
|
|
10293
|
+
// The constraint this control last refused a press with, while that refusal
|
|
10294
|
+
// is on screen — what refreshReport has to take back once it stops holding.
|
|
10295
|
+
let reportedConstraint = null;
|
|
10066
10296
|
|
|
10067
10297
|
const checkInteractivity = ({ event, intent = "write" } = {}) => {
|
|
10068
10298
|
interactionFailedConstraintInfo = null;
|
|
@@ -10153,20 +10383,50 @@ const createControlInteraction = (
|
|
|
10153
10383
|
|
|
10154
10384
|
const canInteract =
|
|
10155
10385
|
!interactionFailedConstraintInfo && !failingManagedInteraction;
|
|
10156
|
-
// When the control
|
|
10386
|
+
// When the control can be interacted with, remove the interaction token
|
|
10157
10387
|
// so the callout closes if no other tokens (e.g. validation) are active.
|
|
10158
10388
|
if (canInteract) {
|
|
10159
10389
|
callout.removeOpenToken(INTERACTION_TOKEN, event);
|
|
10390
|
+
reportedConstraint = null;
|
|
10160
10391
|
}
|
|
10161
10392
|
return canInteract;
|
|
10162
10393
|
};
|
|
10163
10394
|
|
|
10395
|
+
// What this rule says out loud is a LIVE reading, and nothing else comes back
|
|
10396
|
+
// to correct it: busy stops being true on its own (see BUSY_CONSTRAINT's
|
|
10397
|
+
// `transient`), and read-only inherited from a group goes with the run that
|
|
10398
|
+
// set it. Read again whenever the control's interactivity moves, so a refusal
|
|
10399
|
+
// is taken back once what it says stops being true — a callout left open over
|
|
10400
|
+
// a control it no longer describes also sits on top of it and swallows the
|
|
10401
|
+
// next press.
|
|
10402
|
+
//
|
|
10403
|
+
// Only when something WAS said: reading a control that has refused nothing
|
|
10404
|
+
// would start writing titles nobody asked for.
|
|
10405
|
+
const refreshReport = (event) => {
|
|
10406
|
+
const constraintReported = reportedConstraint;
|
|
10407
|
+
if (!constraintReported && titleWritten === null) {
|
|
10408
|
+
return;
|
|
10409
|
+
}
|
|
10410
|
+
checkInteractivity({ event });
|
|
10411
|
+
if (!reportedConstraint) {
|
|
10412
|
+
return;
|
|
10413
|
+
}
|
|
10414
|
+
if (interactionFailedConstraintInfo?.constraint === constraintReported) {
|
|
10415
|
+
return;
|
|
10416
|
+
}
|
|
10417
|
+
// Held still, but by something else — and the press that was answered is
|
|
10418
|
+
// long over, so it is taken back rather than answered a second time.
|
|
10419
|
+
callout.removeOpenToken(INTERACTION_TOKEN, event);
|
|
10420
|
+
reportedConstraint = null;
|
|
10421
|
+
};
|
|
10422
|
+
|
|
10164
10423
|
const reportInteractivity = ({ event } = {}) => {
|
|
10165
10424
|
if (failingManagedInteraction) {
|
|
10166
10425
|
// Report on the specific child that is blocking, not the parent.
|
|
10167
10426
|
failingManagedInteraction.reportInteractivity({ event });
|
|
10168
10427
|
return;
|
|
10169
10428
|
}
|
|
10429
|
+
reportedConstraint = interactionFailedConstraintInfo.constraint;
|
|
10170
10430
|
debugInteraction(
|
|
10171
10431
|
event,
|
|
10172
10432
|
`reportInteractivity (${interactionFailedConstraintInfo.name})`,
|
|
@@ -10189,6 +10449,7 @@ const createControlInteraction = (
|
|
|
10189
10449
|
const controlInteraction = {
|
|
10190
10450
|
checkInteractivity,
|
|
10191
10451
|
reportInteractivity,
|
|
10452
|
+
refreshReport,
|
|
10192
10453
|
};
|
|
10193
10454
|
Object.defineProperty(controlInteraction, "interactionFailedConstraintInfo", {
|
|
10194
10455
|
get: () => interactionFailedConstraintInfo,
|
|
@@ -10237,6 +10498,10 @@ const onRequestInteraction = (
|
|
|
10237
10498
|
// through (see READONLY_CONSTRAINT).
|
|
10238
10499
|
intent = "write",
|
|
10239
10500
|
bypassInteractivity = false,
|
|
10501
|
+
// navi continuing a gesture on its own rather than a user asking for
|
|
10502
|
+
// something: the decision still applies, the explanation does not (see
|
|
10503
|
+
// below).
|
|
10504
|
+
automatic = false,
|
|
10240
10505
|
// Who asked, when the control is not answering the gesture on its own —
|
|
10241
10506
|
// the source of a command (see the self-interactions step-back below).
|
|
10242
10507
|
requester,
|
|
@@ -10297,7 +10562,16 @@ const onRequestInteraction = (
|
|
|
10297
10562
|
const reason = failedInfo
|
|
10298
10563
|
? `failing interaction constraint "${failedInfo.name}"`
|
|
10299
10564
|
: "not interactable";
|
|
10300
|
-
|
|
10565
|
+
// A refusal is explained to whoever asked, and an automatic follow-up
|
|
10566
|
+
// has nobody waiting for one: a group whose action a part just asked
|
|
10567
|
+
// for is busy running exactly that (see auto_group_action in
|
|
10568
|
+
// control_hooks.jsx), so the follow-up is turned down — which is what
|
|
10569
|
+
// keeps the action from running twice — while the press that started
|
|
10570
|
+
// it went through. Saying "this element is busy" there answers an
|
|
10571
|
+
// accepted press with a refusal.
|
|
10572
|
+
if (!automatic) {
|
|
10573
|
+
ci.reportInteractivity({ event });
|
|
10574
|
+
}
|
|
10301
10575
|
onPrevented(reason);
|
|
10302
10576
|
return false;
|
|
10303
10577
|
}
|
|
@@ -10351,6 +10625,11 @@ const dispatchRequestAction = (
|
|
|
10351
10625
|
{
|
|
10352
10626
|
event,
|
|
10353
10627
|
name = "dispatchRequestAction",
|
|
10628
|
+
// The gate's own options, named here so they reach it: everything left in
|
|
10629
|
+
// actionOptions goes to the action alone, so a gate option arriving under
|
|
10630
|
+
// any other name is dropped without a word.
|
|
10631
|
+
automatic,
|
|
10632
|
+
bypassInteractivity,
|
|
10354
10633
|
prevented,
|
|
10355
10634
|
allowed,
|
|
10356
10635
|
always,
|
|
@@ -10360,6 +10639,8 @@ const dispatchRequestAction = (
|
|
|
10360
10639
|
return dispatchRequestInteraction(element, {
|
|
10361
10640
|
event,
|
|
10362
10641
|
name,
|
|
10642
|
+
automatic,
|
|
10643
|
+
bypassInteractivity,
|
|
10363
10644
|
// The gate needs it as much as the action does: the requester may be an
|
|
10364
10645
|
// affordance that claimed the press to ask for this very action, and the
|
|
10365
10646
|
// control must not read that claim as "this press was not for me" — see
|
|
@@ -12965,9 +13246,10 @@ const useExecuteAction = (
|
|
|
12965
13246
|
}, [error]);
|
|
12966
13247
|
|
|
12967
13248
|
const addErrorMessage = (error, { requester } = {}) => {
|
|
12968
|
-
// The error is stored on the element that owns the action (the form/element
|
|
12969
|
-
// The requester
|
|
12970
|
-
//
|
|
13249
|
+
// The error is stored on the element that owns the action (the form/element
|
|
13250
|
+
// itself). The requester — the submit button that sent, the row of a list
|
|
13251
|
+
// whose command sent — is stored as the callout display target, so the
|
|
13252
|
+
// message appears on whoever asked rather than on the whole control.
|
|
12971
13253
|
const element = elementRef.current;
|
|
12972
13254
|
let target = requester;
|
|
12973
13255
|
// A requester that is no longer on the page is not a place to show
|
|
@@ -31300,6 +31582,12 @@ const anyMatchingRouteSignal = (routes) => {
|
|
|
31300
31582
|
* @param {Element} [options.anchor] Where a popup this opens should be placed,
|
|
31301
31583
|
* when that is not the element asking: a menu opened by a press belongs at the
|
|
31302
31584
|
* point the press happened, and the row that was pressed is not that point.
|
|
31585
|
+
* @param {Element} [options.requester] Who asked, when that is not the source.
|
|
31586
|
+
* A group triggers its own command from itself — what the command aims at is
|
|
31587
|
+
* read around the group, not around the child — while the gesture was on one
|
|
31588
|
+
* of its children, and what a refusal is about is that child. Same role as
|
|
31589
|
+
* the submit button a form's send names: the action belongs to the control,
|
|
31590
|
+
* the answer is drawn on whoever asked for it.
|
|
31303
31591
|
*/
|
|
31304
31592
|
const triggerNaviCommand = (element, command, event, options) => {
|
|
31305
31593
|
const run = resolveNaviCommand(element, command, event, options);
|
|
@@ -31330,7 +31618,7 @@ const resolveNaviCommand = (
|
|
|
31330
31618
|
element,
|
|
31331
31619
|
command,
|
|
31332
31620
|
event,
|
|
31333
|
-
{ optional, value, anchor } = {},
|
|
31621
|
+
{ optional, value, anchor, requester } = {},
|
|
31334
31622
|
) => {
|
|
31335
31623
|
if (!event) {
|
|
31336
31624
|
throw new Error(
|
|
@@ -31359,6 +31647,7 @@ const resolveNaviCommand = (
|
|
|
31359
31647
|
// source: the attribute form has a `value` to read (`<Button value={id}>`),
|
|
31360
31648
|
// a JS decision has none, and both must be able to say the same thing.
|
|
31361
31649
|
value,
|
|
31650
|
+
requester,
|
|
31362
31651
|
});
|
|
31363
31652
|
if (!execute) {
|
|
31364
31653
|
if (optional) {
|
|
@@ -31542,7 +31831,8 @@ const onNaviCommand = (e, { debugCommand = () => {} } = {}) => {
|
|
|
31542
31831
|
};
|
|
31543
31832
|
|
|
31544
31833
|
const NAVI_COMMANDS = {};
|
|
31545
|
-
// commandHandler(source, event
|
|
31834
|
+
// commandHandler(source, event, { argument, anchor, value, requester })
|
|
31835
|
+
// → { target, implementation } | undefined
|
|
31546
31836
|
// - Each handler calls resolveExplicitTarget(source) first, then falls back to
|
|
31547
31837
|
// its own DOM resolution logic (closest expandable, parent control, etc.).
|
|
31548
31838
|
// - Returns undefined when no target can be found — this is a normal outcome for
|
|
@@ -31780,7 +32070,7 @@ const resolveAfterSend = (target, requester) => {
|
|
|
31780
32070
|
return undefined;
|
|
31781
32071
|
};
|
|
31782
32072
|
|
|
31783
|
-
registerNaviCommand("--navi-send", (source, event) => {
|
|
32073
|
+
registerNaviCommand("--navi-send", (source, event, { requester }) => {
|
|
31784
32074
|
const expandable = resolveExpandableAround(source);
|
|
31785
32075
|
const target =
|
|
31786
32076
|
resolveExplicitTarget(source) ||
|
|
@@ -31823,14 +32113,21 @@ registerNaviCommand("--navi-send", (source, event) => {
|
|
|
31823
32113
|
return {
|
|
31824
32114
|
target,
|
|
31825
32115
|
implementation: () => {
|
|
31826
|
-
|
|
31827
|
-
|
|
31828
|
-
|
|
31829
|
-
|
|
31830
|
-
|
|
31831
|
-
|
|
31832
|
-
|
|
31833
|
-
|
|
32116
|
+
// Who asked, taken from whoever triggered the command when it said so —
|
|
32117
|
+
// a group sends from itself and names the child the gesture was on (see
|
|
32118
|
+
// triggerNaviCommand's `requester`). Named means named: the stand-in
|
|
32119
|
+
// below is for a source nobody spoke for.
|
|
32120
|
+
let sendRequester = requester;
|
|
32121
|
+
if (!sendRequester) {
|
|
32122
|
+
sendRequester = source;
|
|
32123
|
+
if (!source.matches(submitSelector)) {
|
|
32124
|
+
// When present, use the first submit button as the requester, not the input.
|
|
32125
|
+
// This aligns with browser behavior where Enter in a text input triggers
|
|
32126
|
+
// the first submit button of the form, not the input itself.
|
|
32127
|
+
const firstButtonSubmitting = target.querySelector(submitSelector);
|
|
32128
|
+
if (firstButtonSubmitting) {
|
|
32129
|
+
sendRequester = firstButtonSubmitting;
|
|
32130
|
+
}
|
|
31834
32131
|
}
|
|
31835
32132
|
}
|
|
31836
32133
|
// Nothing is committed when a constraint fails, so nothing is decided
|
|
@@ -31844,7 +32141,7 @@ registerNaviCommand("--navi-send", (source, event) => {
|
|
|
31844
32141
|
// from the response writes it on the form while it runs
|
|
31845
32142
|
// (data-after-send), and this is what picks it up.
|
|
31846
32143
|
const runAfterSend = () => {
|
|
31847
|
-
const afterSend = resolveAfterSend(target,
|
|
32144
|
+
const afterSend = resolveAfterSend(target, sendRequester);
|
|
31848
32145
|
if (!afterSend) {
|
|
31849
32146
|
return;
|
|
31850
32147
|
}
|
|
@@ -31879,7 +32176,7 @@ registerNaviCommand("--navi-send", (source, event) => {
|
|
|
31879
32176
|
initiator.preventDefault();
|
|
31880
32177
|
}
|
|
31881
32178
|
},
|
|
31882
|
-
requester,
|
|
32179
|
+
requester: sendRequester,
|
|
31883
32180
|
}),
|
|
31884
32181
|
);
|
|
31885
32182
|
if (sent === false || invalid) {
|
|
@@ -34140,8 +34437,20 @@ const useUIStateController = (
|
|
|
34140
34437
|
resetUIState: (e) => {
|
|
34141
34438
|
controller.setUIState(controller.state, e);
|
|
34142
34439
|
},
|
|
34440
|
+
// What the control shows becomes what is acknowledged: the outside said
|
|
34441
|
+
// yes to it, so it is where a rollback goes back to. Left alone for a
|
|
34442
|
+
// control whose value is GIVEN — the prop is already its truth — and
|
|
34443
|
+
// while a newer request waits behind this one, whose own answer is what
|
|
34444
|
+
// will settle what is accepted.
|
|
34445
|
+
acknowledgeUIState: () => {
|
|
34446
|
+
if (controller.hasStateProp || controller.queuedActionAllowedEvent) {
|
|
34447
|
+
return;
|
|
34448
|
+
}
|
|
34449
|
+
controller.state = controller.uiState;
|
|
34450
|
+
},
|
|
34143
34451
|
onActionEnd: (e) => {
|
|
34144
34452
|
debugUIState(`"${controlType}" actionEnd called`);
|
|
34453
|
+
acknowledgeOwnAction(controller);
|
|
34145
34454
|
controller.rules.validation.syncValidity(e);
|
|
34146
34455
|
},
|
|
34147
34456
|
onActionError: (e) => {
|
|
@@ -34351,6 +34660,18 @@ const firstDefinedChildUIState = (children) => {
|
|
|
34351
34660
|
return undefined;
|
|
34352
34661
|
};
|
|
34353
34662
|
|
|
34663
|
+
// A run that came back with a yes settles what the control is worth — but only
|
|
34664
|
+
// for the control the run BELONGS to. Anything with a `command` still fires its
|
|
34665
|
+
// own navi_action_* events (it is given a placeholder action so they exist),
|
|
34666
|
+
// and taking those for an acknowledgement is how a row would remember its own
|
|
34667
|
+
// selection as accepted and hand it back on the group's rollback.
|
|
34668
|
+
const acknowledgeOwnAction = (controller) => {
|
|
34669
|
+
if (!controller.props.action) {
|
|
34670
|
+
return;
|
|
34671
|
+
}
|
|
34672
|
+
controller.acknowledgeUIState();
|
|
34673
|
+
};
|
|
34674
|
+
|
|
34354
34675
|
// Default aggregate/distribute implementations keyed by controlType or stateType.
|
|
34355
34676
|
// Looked up in useUIGroupStateController to fill in omitted aggregateChildStates /
|
|
34356
34677
|
// distributeChildUIState. If neither a default nor an explicit impl is found for a
|
|
@@ -34818,7 +35139,7 @@ const useUIGroupStateController = (
|
|
|
34818
35139
|
{ except: actingChild },
|
|
34819
35140
|
);
|
|
34820
35141
|
}
|
|
34821
|
-
applyState(groupUIState, e);
|
|
35142
|
+
applyState(groupUIState, e, { actingChild });
|
|
34822
35143
|
} else if (notifyExternal === "silent") {
|
|
34823
35144
|
controller.syncInternalState(groupUIState);
|
|
34824
35145
|
s.parentUIStateController?.onChildUIAction(controller, e, {
|
|
@@ -34872,7 +35193,11 @@ const useUIGroupStateController = (
|
|
|
34872
35193
|
childUIStateController.setUIState(childNewState, e);
|
|
34873
35194
|
};
|
|
34874
35195
|
|
|
34875
|
-
const applyState = (
|
|
35196
|
+
const applyState = (
|
|
35197
|
+
newUIState,
|
|
35198
|
+
e,
|
|
35199
|
+
{ internalBehavior = false, actingChild } = {},
|
|
35200
|
+
) => {
|
|
34876
35201
|
const { controller } = s;
|
|
34877
35202
|
const currentUIState = controller.uiState;
|
|
34878
35203
|
controller.uiState = newUIState;
|
|
@@ -34893,7 +35218,10 @@ const useUIGroupStateController = (
|
|
|
34893
35218
|
s.parentUIStateController?.onChildUIAction(controller, e, {
|
|
34894
35219
|
stateChanged: true,
|
|
34895
35220
|
});
|
|
34896
|
-
controller.onUIAction(e, {
|
|
35221
|
+
controller.onUIAction(e, {
|
|
35222
|
+
skipCommand: internalBehavior,
|
|
35223
|
+
actingChild,
|
|
35224
|
+
});
|
|
34897
35225
|
const el = controller.ref.current;
|
|
34898
35226
|
if (el) {
|
|
34899
35227
|
dispatchInternalCustomEvent(el, "navi_ui_state_change", {
|
|
@@ -35032,7 +35360,7 @@ const useUIGroupStateController = (
|
|
|
35032
35360
|
uiStateSignal.value = newUIState;
|
|
35033
35361
|
publishUIState(newUIState);
|
|
35034
35362
|
},
|
|
35035
|
-
onUIAction: (e, { skipCommand } = {}) => {
|
|
35363
|
+
onUIAction: (e, { skipCommand, actingChild } = {}) => {
|
|
35036
35364
|
const currentUIState = controller.uiState;
|
|
35037
35365
|
// The same write applyState/onChange make (see writeBoundSignal), for
|
|
35038
35366
|
// the case where the state did not move but the user acted all the
|
|
@@ -35046,7 +35374,14 @@ const useUIGroupStateController = (
|
|
|
35046
35374
|
if (!skipCommand && controller.props.command) {
|
|
35047
35375
|
const el = controller.ref.current;
|
|
35048
35376
|
if (el) {
|
|
35049
|
-
|
|
35377
|
+
// The command is the GROUP's — what it aims at is read around the
|
|
35378
|
+
// group, not around the child — but the child is who asked, and a
|
|
35379
|
+
// refusal is drawn on whoever asked (see triggerNaviCommand's
|
|
35380
|
+
// `requester`). Without it a list refusing a name says so at its
|
|
35381
|
+
// own foot, rows away from the name that was touched.
|
|
35382
|
+
triggerNaviCommand(el, controller.props.command, e, {
|
|
35383
|
+
requester: actingChild?.ref.current,
|
|
35384
|
+
});
|
|
35050
35385
|
}
|
|
35051
35386
|
}
|
|
35052
35387
|
},
|
|
@@ -35141,7 +35476,7 @@ const useUIGroupStateController = (
|
|
|
35141
35476
|
actingChild: childUIStateController,
|
|
35142
35477
|
});
|
|
35143
35478
|
} else {
|
|
35144
|
-
controller.onUIAction(e);
|
|
35479
|
+
controller.onUIAction(e, { actingChild: childUIStateController });
|
|
35145
35480
|
}
|
|
35146
35481
|
},
|
|
35147
35482
|
unregisterChild: (childUIStateController) => {
|
|
@@ -35203,7 +35538,19 @@ const useUIGroupStateController = (
|
|
|
35203
35538
|
}
|
|
35204
35539
|
onChange(e, { notifyExternal: true });
|
|
35205
35540
|
},
|
|
35541
|
+
// The group was told yes, so what each of its parts shows is accepted
|
|
35542
|
+
// too: a group holds no value of its own — its rollback puts the
|
|
35543
|
+
// children back where they were (see resetUIState), and they can only
|
|
35544
|
+
// go back to what was acknowledged if they were told.
|
|
35545
|
+
acknowledgeUIState: () => {
|
|
35546
|
+
for (const c of childUIStateControllerArray) {
|
|
35547
|
+
if (shouldPropagateStateToChild(c)) {
|
|
35548
|
+
c.acknowledgeUIState();
|
|
35549
|
+
}
|
|
35550
|
+
}
|
|
35551
|
+
},
|
|
35206
35552
|
onActionEnd: (e) => {
|
|
35553
|
+
acknowledgeOwnAction(controller);
|
|
35207
35554
|
controller.rules.validation.syncValidity(e);
|
|
35208
35555
|
},
|
|
35209
35556
|
onActionError: (e) => {
|
|
@@ -35291,7 +35638,21 @@ const useUIGroupStateController = (
|
|
|
35291
35638
|
// down last time. A popup reopened on another subject hands down the very
|
|
35292
35639
|
// same empty value it did before, and the selection left inside it from
|
|
35293
35640
|
// the previous opening is what has to go.
|
|
35294
|
-
|
|
35641
|
+
//
|
|
35642
|
+
// Except while the group is asking about that very difference: the run in
|
|
35643
|
+
// flight carries what the user just chose, and the value is what the
|
|
35644
|
+
// owner holds until it answers. Putting the children back on it here
|
|
35645
|
+
// would take the choice off the screen on the first re-render — which the
|
|
35646
|
+
// run itself causes, by making the group busy — so the press would appear
|
|
35647
|
+
// to do nothing until the answer arrived, and the rollback on a refusal
|
|
35648
|
+
// would have nothing left to undo.
|
|
35649
|
+
const askingAboutIt =
|
|
35650
|
+
controller.actionInFlight || controller.queuedActionAllowedEvent;
|
|
35651
|
+
if (
|
|
35652
|
+
hasValueProp &&
|
|
35653
|
+
!askingAboutIt &&
|
|
35654
|
+
!compareTwoJsValues(value, controller.uiState)
|
|
35655
|
+
) {
|
|
35295
35656
|
placeChildrenFrom(value);
|
|
35296
35657
|
}
|
|
35297
35658
|
if (
|
|
@@ -37188,10 +37549,19 @@ const useInteractiveProps = (props, {
|
|
|
37188
37549
|
// live source rather than from the rendered aria-busy, which conflates all
|
|
37189
37550
|
// three and is a frame behind. See its own comment.
|
|
37190
37551
|
uiStateController.loadingFromOwnProp = Boolean(loading);
|
|
37191
|
-
|
|
37552
|
+
// The group above waiting on something, which reaches every control in it:
|
|
37553
|
+
// each one is held for as long as that lasts, and "wait" is what they have
|
|
37554
|
+
// to say about it — not the read-only they wear to express it, which is a
|
|
37555
|
+
// settled word (see BUSY_CONSTRAINT and READONLY_CONSTRAINT).
|
|
37556
|
+
uiStateController.loadingFromAbove = Boolean(controlLoading);
|
|
37192
37557
|
// Read by BUSY_CONSTRAINT: an optimistic control stays interactive while
|
|
37193
37558
|
// its bound action runs (a new toggle replaces the run instead of waiting).
|
|
37194
37559
|
uiStateController.optimistic = Boolean(optimistic);
|
|
37560
|
+
// What the interaction rule last refused is only true while the control is
|
|
37561
|
+
// held; the state it was read from moves here (see refreshReport).
|
|
37562
|
+
useLayoutEffect(() => {
|
|
37563
|
+
uiStateController.rules.interaction.refreshReport(new CustomEvent("interactivity_change"));
|
|
37564
|
+
}, [disabledResolved, readOnlyResolved, loadingResolved, controlLoading, uiStateController]);
|
|
37195
37565
|
Object.assign(controlHostProps, {
|
|
37196
37566
|
"required": requiredResolved,
|
|
37197
37567
|
"aria-busy": loadingResolved ? "true" : "false",
|
|
@@ -37538,16 +37908,13 @@ const useInteractiveProps = (props, {
|
|
|
37538
37908
|
event: e.detail.eventChain[0],
|
|
37539
37909
|
name: "auto_group_action",
|
|
37540
37910
|
requester: e.detail.requester,
|
|
37541
|
-
//
|
|
37542
|
-
//
|
|
37543
|
-
//
|
|
37544
|
-
//
|
|
37545
|
-
//
|
|
37546
|
-
//
|
|
37547
|
-
|
|
37548
|
-
// busy constraint would refuse the group for an action that
|
|
37549
|
-
// is already over. The validity gate still applies.
|
|
37550
|
-
bypassInteractivity: true
|
|
37911
|
+
// Nobody asked for this one: it is the group's own continuation
|
|
37912
|
+
// of a press the child's gate already accepted. The gate still
|
|
37913
|
+
// decides — a group told to send by its own `command` is
|
|
37914
|
+
// already running that very action, and letting this through
|
|
37915
|
+
// would run it a second time — but a refusal here is not
|
|
37916
|
+
// reported, since the press it would answer went through.
|
|
37917
|
+
automatic: true
|
|
37551
37918
|
});
|
|
37552
37919
|
}
|
|
37553
37920
|
}
|
|
@@ -56747,6 +57114,15 @@ const PopupClose = ({
|
|
|
56747
57114
|
command: "--navi-close",
|
|
56748
57115
|
icon: true,
|
|
56749
57116
|
variant: "discrete"
|
|
57117
|
+
// Dismissing writes nothing to whatever control the popup belongs to, so
|
|
57118
|
+
// that control's read-only and disabled are not about this cross: a
|
|
57119
|
+
// read-only picker still opens to be read (see readonly_constraint.js),
|
|
57120
|
+
// and what is opened has to be closable. Whether closing is allowed at
|
|
57121
|
+
// this instant is the popup's own question, answered by the popup — a
|
|
57122
|
+
// dialog holds the close while an action inside it runs (see
|
|
57123
|
+
// findBusyElementInside in dialog.jsx).
|
|
57124
|
+
,
|
|
57125
|
+
whenSelfInteractionsBlocked: "ignore"
|
|
56750
57126
|
// The cross is drawn at the control size, which is a few millimetres
|
|
56751
57127
|
// wide; the padding is what makes it a target a thumb can hit.
|
|
56752
57128
|
,
|
|
@@ -62011,6 +62387,42 @@ const ListItemFooter = props => {
|
|
|
62011
62387
|
});
|
|
62012
62388
|
};
|
|
62013
62389
|
|
|
62390
|
+
// What a row may say it is waiting on, all of them about the row as a thing
|
|
62391
|
+
// the LIST holds: joining it, leaving it, or being saved where it is. A
|
|
62392
|
+
// selectable row waiting on the list's own send is a different event with a
|
|
62393
|
+
// different subject — the selection, not the row (see list_selectable.jsx).
|
|
62394
|
+
const LOADING_REASON_SET = new Set(["adding", "removing", "updating"]);
|
|
62395
|
+
|
|
62396
|
+
/**
|
|
62397
|
+
* What a row waiting on something has to say when it is pressed. `loading` may
|
|
62398
|
+
* say WHAT it is waiting for: a row being created is not simply "busy", and
|
|
62399
|
+
* saying which one it is tells the user what to expect.
|
|
62400
|
+
*
|
|
62401
|
+
* Shared with the selectable row, which carries the same `loading` on the
|
|
62402
|
+
* hidden input holding its selection — the row and that control must answer a
|
|
62403
|
+
* press with one sentence, whichever of them catches it.
|
|
62404
|
+
*/
|
|
62405
|
+
const listItemBusyMessage = (loading, props) => {
|
|
62406
|
+
if (LOADING_REASON_SET.has(loading)) {
|
|
62407
|
+
return naviI18n(`constraint.busy.item.${loading}`, props);
|
|
62408
|
+
}
|
|
62409
|
+
return naviI18n("constraint.busy.item", props);
|
|
62410
|
+
};
|
|
62411
|
+
|
|
62412
|
+
/**
|
|
62413
|
+
* Why the row cannot be acted on, in the row's own terms — or in the caller's,
|
|
62414
|
+
* where it gave them. The same two props every navi control answers a refusal
|
|
62415
|
+
* with (`readOnlyMessage`, `busyMessage`), so a row and the control it carries
|
|
62416
|
+
* cannot end up saying two different things about one press: whichever of them
|
|
62417
|
+
* catches it, the sentence is the same.
|
|
62418
|
+
*/
|
|
62419
|
+
const listItemBlockedMessage = (loading, props) => {
|
|
62420
|
+
if (loading) {
|
|
62421
|
+
return props.busyMessage ?? listItemBusyMessage(loading, props);
|
|
62422
|
+
}
|
|
62423
|
+
return props.readOnlyMessage ?? naviI18n("constraint.readonly.item", props);
|
|
62424
|
+
};
|
|
62425
|
+
|
|
62014
62426
|
installImportMetaCssBuild(import.meta);const css$y = /* css */`@layer navi {
|
|
62015
62427
|
.navi_list_container[navi-selectable] {
|
|
62016
62428
|
--list-item-outline-width: var(--navi-focus-outline-width);
|
|
@@ -62226,7 +62638,17 @@ const ListSelectable = props => {
|
|
|
62226
62638
|
}
|
|
62227
62639
|
return kept;
|
|
62228
62640
|
};
|
|
62229
|
-
const [listControlRootProps, listControlProps, childrenWrapperProps] = useControlgroupProps(
|
|
62641
|
+
const [listControlRootProps, listControlProps, childrenWrapperProps] = useControlgroupProps({
|
|
62642
|
+
// The defaults of the group this IS (see CheckboxGroup / RadioGroup):
|
|
62643
|
+
// what a selectable list shows is a claim about what was accepted, so
|
|
62644
|
+
// a run that ends without an answer puts the rows back where they were
|
|
62645
|
+
// — a row left ticked over a refusal says the opposite of what
|
|
62646
|
+
// happened, and the error callout beside it does not untick it.
|
|
62647
|
+
resetOnCancel: true,
|
|
62648
|
+
resetOnAbort: true,
|
|
62649
|
+
resetOnError: true,
|
|
62650
|
+
...props
|
|
62651
|
+
}, {
|
|
62230
62652
|
stateType: multiple ? "array" : "",
|
|
62231
62653
|
controlType: multiple ? "checkbox_group" : "radio_group",
|
|
62232
62654
|
aggregateChildStates
|
|
@@ -62513,6 +62935,14 @@ const ListItemSelectable = props => {
|
|
|
62513
62935
|
selected,
|
|
62514
62936
|
pointed,
|
|
62515
62937
|
selectableArea = "all",
|
|
62938
|
+
// True of the ROW as much as of the control it carries, so both are told.
|
|
62939
|
+
// Left in `rest` they would reach the hidden input alone — the control
|
|
62940
|
+
// system claims them (they are control props) and the row would never see
|
|
62941
|
+
// them, which for a row means no outline, no dimming, no press swallowed
|
|
62942
|
+
// and none of the buttons it contains held back (see ListItemReal). Any
|
|
62943
|
+
// prop a row and a control both understand belongs here.
|
|
62944
|
+
loading,
|
|
62945
|
+
readOnly,
|
|
62516
62946
|
// The row's own click handler, kept out of the control props below: those
|
|
62517
62947
|
// describe the hidden input that carries the selection, and a caller
|
|
62518
62948
|
// writing onClick on a <List.Item> is talking about the row they see.
|
|
@@ -62523,23 +62953,33 @@ const ListItemSelectable = props => {
|
|
|
62523
62953
|
// A checkbox toggles on its own; only a radio has to be told it may let go.
|
|
62524
62954
|
const deselectable = useContext(SelectableListDeselectableContext) && !multiple;
|
|
62525
62955
|
// Whose reason it is that this row cannot be taken. Read-only reaching it
|
|
62526
|
-
// from above is the LIST's, and
|
|
62527
|
-
//
|
|
62528
|
-
//
|
|
62529
|
-
//
|
|
62530
|
-
//
|
|
62531
|
-
//
|
|
62532
|
-
//
|
|
62956
|
+
// from above is the LIST's, and that is the whole answer — said as the
|
|
62957
|
+
// selection where several things are taken, as the choice where one thing
|
|
62958
|
+
// is. Said of each row in turn, "this option is not available" describes
|
|
62959
|
+
// something else entirely: a list where each row happens to be unavailable
|
|
62960
|
+
// for its own reasons, which is what a row held on its own says.
|
|
62961
|
+
//
|
|
62962
|
+
// A list waiting on something is not unavailable, it is not answering yet —
|
|
62963
|
+
// busy rather than read-only (see BUSY_CONSTRAINT, which is asked first),
|
|
62964
|
+
// and the wait is about the same thing the read-only would be about.
|
|
62533
62965
|
const readOnlyFromAbove = useContext(ReadOnlyContext);
|
|
62534
|
-
const
|
|
62535
|
-
|
|
62536
|
-
|
|
62966
|
+
const readOnlyMessageKey = readOnlyFromAbove ? multiple ? `constraint.readonly.selection` : `constraint.readonly.choice` : `constraint.readonly.option`;
|
|
62967
|
+
// Handed to the row as well as to the control: whichever of the two catches
|
|
62968
|
+
// the press answers it, and they must not answer it differently.
|
|
62969
|
+
const readOnlyMessageResolved = props.readOnlyMessage ?? naviI18n(readOnlyMessageKey, props);
|
|
62970
|
+
const busyMessageResolved = props.busyMessage ?? (loading ? listItemBusyMessage(loading, props) : naviI18n(multiple ? `constraint.busy.selection` : `constraint.busy.choice`, props));
|
|
62537
62971
|
const inputRef = useRef();
|
|
62538
62972
|
const inputType = multiple ? "checkbox" : "radio";
|
|
62539
62973
|
const inputId = `${id}_input`;
|
|
62540
62974
|
inputRef.nullCanHappen = true; // virtualization
|
|
62541
62975
|
const [checkableRootProps, checkableProps, controlChildrenWrapperProps] = useCheckableProps({
|
|
62542
|
-
readOnlyMessage:
|
|
62976
|
+
readOnlyMessage: readOnlyMessageResolved,
|
|
62977
|
+
// What the row itself is waiting for when it says so, the wait of the
|
|
62978
|
+
// list around it otherwise — said as the selection where several things
|
|
62979
|
+
// are taken, as the choice where one thing is.
|
|
62980
|
+
busyMessage: busyMessageResolved,
|
|
62981
|
+
loading,
|
|
62982
|
+
readOnly,
|
|
62543
62983
|
...rest,
|
|
62544
62984
|
ref: inputRef,
|
|
62545
62985
|
id: inputId,
|
|
@@ -62556,17 +62996,19 @@ const ListItemSelectable = props => {
|
|
|
62556
62996
|
basePseudoState,
|
|
62557
62997
|
children
|
|
62558
62998
|
} = checkableProps;
|
|
62559
|
-
|
|
62999
|
+
// Everything holding the control back, not only the row's own `readOnly`
|
|
63000
|
+
// above: the list above it, a maxLengthGuard with no room left.
|
|
63001
|
+
const readOnlyResolved = basePseudoState[":read-only"];
|
|
62560
63002
|
const realInputContextValue = useMemo(() => {
|
|
62561
63003
|
return {
|
|
62562
63004
|
id: inputId,
|
|
62563
63005
|
type: inputType,
|
|
62564
63006
|
checked,
|
|
62565
|
-
readOnly,
|
|
63007
|
+
readOnly: readOnlyResolved,
|
|
62566
63008
|
value,
|
|
62567
63009
|
deselectable
|
|
62568
63010
|
};
|
|
62569
|
-
}, [inputId, inputType, checked,
|
|
63011
|
+
}, [inputId, inputType, checked, readOnlyResolved, value, deselectable]);
|
|
62570
63012
|
return jsxs(Next, {
|
|
62571
63013
|
id: id,
|
|
62572
63014
|
index: index,
|
|
@@ -62588,6 +63030,10 @@ const ListItemSelectable = props => {
|
|
|
62588
63030
|
},
|
|
62589
63031
|
ref: props.ref,
|
|
62590
63032
|
onClick: onClick,
|
|
63033
|
+
loading: loading,
|
|
63034
|
+
readOnly: readOnly,
|
|
63035
|
+
readOnlyMessage: readOnlyMessageResolved,
|
|
63036
|
+
busyMessage: busyMessageResolved,
|
|
62591
63037
|
selectable: undefined,
|
|
62592
63038
|
"navi-selectable-area-all": selectableArea === "all" ? "" : undefined,
|
|
62593
63039
|
children: [jsx(SelectableRealInput, {
|
|
@@ -65755,13 +66201,30 @@ const ListItemReal = props => {
|
|
|
65755
66201
|
if (event.button !== 0 || isOverlaidOnRow(event)) {
|
|
65756
66202
|
return;
|
|
65757
66203
|
}
|
|
65758
|
-
blockInteraction(event);
|
|
65759
66204
|
pressStartedHereRef.current = true;
|
|
66205
|
+
// A row that IS a control has one already able to answer this, in the same
|
|
66206
|
+
// words it uses for the keyboard, and it is the one the callout would be
|
|
66207
|
+
// anchored on either way — two answers would be two callouts on one anchor,
|
|
66208
|
+
// toggling each other off (see `reopen` in openCallout).
|
|
66209
|
+
//
|
|
66210
|
+
// Asked before the press is blocked, not after: a gate handed an event
|
|
66211
|
+
// already prevented reads it as "someone else took this one" and steps
|
|
66212
|
+
// back without a word (see onRequestInteraction).
|
|
66213
|
+
const controlHost = isControlRoot(event.currentTarget) ? findControlHost(event.currentTarget) : null;
|
|
66214
|
+
if (controlHost) {
|
|
66215
|
+
dispatchRequestInteraction(controlHost, {
|
|
66216
|
+
event,
|
|
66217
|
+
name: "press on a blocked row"
|
|
66218
|
+
});
|
|
66219
|
+
blockInteraction(event);
|
|
66220
|
+
return;
|
|
66221
|
+
}
|
|
66222
|
+
blockInteraction(event);
|
|
65760
66223
|
// One at a time, and not the one that just dismissed it (see the refs).
|
|
65761
66224
|
if (calloutRef.current && calloutRef.current.opened) {
|
|
65762
66225
|
return;
|
|
65763
66226
|
}
|
|
65764
|
-
calloutRef.current = openCallout(
|
|
66227
|
+
calloutRef.current = openCallout(listItemBlockedMessage(loading, props), {
|
|
65765
66228
|
anchorElement: event.currentTarget,
|
|
65766
66229
|
status: "info",
|
|
65767
66230
|
openingEvent: event
|
|
@@ -65788,6 +66251,11 @@ const ListItemReal = props => {
|
|
|
65788
66251
|
...itemColumnsOverrideProps,
|
|
65789
66252
|
index: undefined,
|
|
65790
66253
|
selected: undefined
|
|
66254
|
+
// Read by listItemBlockedMessage, not by the element.
|
|
66255
|
+
,
|
|
66256
|
+
|
|
66257
|
+
readOnlyMessage: undefined,
|
|
66258
|
+
busyMessage: undefined
|
|
65791
66259
|
// We use aria-hidden and not hidden because hidden would be forced to
|
|
65792
66260
|
// display: none while here we want to keep it in the DOM to avoid layout shift
|
|
65793
66261
|
// but visually hidden
|
|
@@ -65838,18 +66306,6 @@ const ListItemReal = props => {
|
|
|
65838
66306
|
})]
|
|
65839
66307
|
});
|
|
65840
66308
|
};
|
|
65841
|
-
// Why the row cannot be acted on, in the row's own terms. `loading` may say
|
|
65842
|
-
// what it is waiting for ("adding", "removing"): a row being created is not
|
|
65843
|
-
// simply "busy", and saying which one it is tells the user what to expect.
|
|
65844
|
-
const blockedMessage = (loading, readOnly, props) => {
|
|
65845
|
-
if (!loading) {
|
|
65846
|
-
return naviI18n("constraint.readonly.item", props);
|
|
65847
|
-
}
|
|
65848
|
-
if (loading === "adding" || loading === "removing") {
|
|
65849
|
-
return naviI18n(`constraint.busy.item.${loading}`, props);
|
|
65850
|
-
}
|
|
65851
|
-
return naviI18n("constraint.busy.item", props);
|
|
65852
|
-
};
|
|
65853
66309
|
const LIST_ITEM_STYLE_CSS_VARS = {
|
|
65854
66310
|
"borderRadius": "--list-item-border-radius",
|
|
65855
66311
|
"borderWidth": "--list-item-border-width",
|
|
@@ -65904,7 +66360,7 @@ const LIST_ITEM_STYLE_CSS_VARS = {
|
|
|
65904
66360
|
* skeleton?: boolean,
|
|
65905
66361
|
* error?: boolean | import("ignore:preact").ComponentChildren,
|
|
65906
66362
|
* onErrorDismiss?: (event: Event) => void,
|
|
65907
|
-
* loading?: boolean | "adding" | "removing",
|
|
66363
|
+
* loading?: boolean | "adding" | "removing" | "updating",
|
|
65908
66364
|
* readOnly?: boolean,
|
|
65909
66365
|
* filtered?: boolean,
|
|
65910
66366
|
* hidden?: boolean,
|
|
@@ -65945,12 +66401,17 @@ const LIST_ITEM_STYLE_CSS_VARS = {
|
|
|
65945
66401
|
* like the list's own error. `true` shows a generic sentence. When
|
|
65946
66402
|
* `onErrorDismiss` is given, a dismiss button is drawn next to the message
|
|
65947
66403
|
* and calls it.
|
|
65948
|
-
* @param {boolean|"adding"|"removing"} [props.loading]
|
|
66404
|
+
* @param {boolean|"adding"|"removing"|"updating"} [props.loading]
|
|
65949
66405
|
* The row is waiting on something: it draws a loading outline and, like
|
|
65950
66406
|
* readOnly, stops taking clicks. Works on any item, not only a selectable
|
|
65951
|
-
* one — a list is edited row by row.
|
|
65952
|
-
*
|
|
65953
|
-
*
|
|
66407
|
+
* one — a list is edited row by row. Rather than true, say WHAT it is
|
|
66408
|
+
* waiting for, which is what a press on it then answers: "adding" (joining
|
|
66409
|
+
* the list), "removing" (leaving it), "updating" (being saved where it is).
|
|
66410
|
+
*
|
|
66411
|
+
* All three are about the row as a thing the LIST holds, never about the
|
|
66412
|
+
* selection: a selectable row taken while its list sends says so on its own
|
|
66413
|
+
* ("la sélection est en cours d'enregistrement"), and needs no `loading` for
|
|
66414
|
+
* that.
|
|
65954
66415
|
* @param {boolean} [props.readOnly]
|
|
65955
66416
|
* The row cannot be acted on: dimmed and click-through-proof, buttons inside
|
|
65956
66417
|
* it included.
|