@jsenv/navi 0.29.169 → 0.29.171
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 +474 -157
- package/dist/jsenv_navi.js.map +20 -18
- package/docs/AI_INSTRUCTIONS.md +7 -0
- package/docs/actions.md +34 -0
- 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);
|
|
@@ -4005,6 +4031,9 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4005
4031
|
* During the delay the instance is still the previous one, holding the
|
|
4006
4032
|
* previous answer — `paramsSettlingSignal` is what says a newer one is
|
|
4007
4033
|
* coming, and `useAsyncData(action, { loading: true })` reads it.
|
|
4034
|
+
* The instance follows where the signal settles, never a value it only
|
|
4035
|
+
* passed through: a signal back where it started before the delay elapses
|
|
4036
|
+
* asks nothing.
|
|
4008
4037
|
*/
|
|
4009
4038
|
const bindParams = (newParamsOrSignal, options = {}) => {
|
|
4010
4039
|
const { debounce, ...optionsWithoutDebounce } = options;
|
|
@@ -4900,7 +4929,9 @@ const runUnwatched = (startRun) => {
|
|
|
4900
4929
|
* keystroke: set `debounce: 500` and the request is sent only after the user stops interacting
|
|
4901
4930
|
* with the filters for 500 ms. During that delay the action still holds the previous answer;
|
|
4902
4931
|
* `action.paramsSettlingSignal` is what says a newer one is coming, and
|
|
4903
|
-
* `useAsyncData(action, { loading: true })` reads it.
|
|
4932
|
+
* `useAsyncData(action, { loading: true })` reads it. The run follows where
|
|
4933
|
+
* the params settle, never a value they only passed through: params back
|
|
4934
|
+
* where they started before the delay elapses run nothing.
|
|
4904
4935
|
*
|
|
4905
4936
|
* Example — auto-refresh a result list while the user tweaks filters:
|
|
4906
4937
|
* ```js
|
|
@@ -5594,6 +5625,7 @@ const CONSTRAINT_NAME_TO_PROP = {
|
|
|
5594
5625
|
min_special_char: "minSpecialCharMessage",
|
|
5595
5626
|
one_of: "oneOfMessage",
|
|
5596
5627
|
readonly: "readOnlyMessage",
|
|
5628
|
+
busy: "busyMessage",
|
|
5597
5629
|
available: "availableMessage",
|
|
5598
5630
|
};
|
|
5599
5631
|
|
|
@@ -6209,18 +6241,32 @@ const findControlProxies = (el) => {
|
|
|
6209
6241
|
};
|
|
6210
6242
|
|
|
6211
6243
|
/**
|
|
6212
|
-
* Given a real control element, returns the proxy
|
|
6244
|
+
* Given a real control element, returns the proxy a gesture on the control
|
|
6245
|
+
* lands on — the one thing said about the control is drawn on.
|
|
6213
6246
|
*
|
|
6214
6247
|
* Use when you need a single visible stand-in for the real control — anchoring
|
|
6215
6248
|
* a callout, for instance. Anything notifying proxies of a state change wants
|
|
6216
6249
|
* `findControlProxies` instead, so a control represented by several of them
|
|
6217
6250
|
* updates all of them.
|
|
6218
6251
|
*
|
|
6219
|
-
*
|
|
6252
|
+
* A proxy the pointer cannot reach is drawn for the control without standing
|
|
6253
|
+
* for it: the surface around it takes the presses for everything it holds, so
|
|
6254
|
+
* that surface is where a refusal about the control belongs — not a box inside
|
|
6255
|
+
* it the finger never has to find. A selectable list row whose whole area is
|
|
6256
|
+
* the click target is exactly that arrangement (see `selectableArea`), and it
|
|
6257
|
+
* is read from the arrangement itself rather than from a flag repeating it:
|
|
6258
|
+
* where a gesture can land IS pointer-events.
|
|
6259
|
+
*
|
|
6260
|
+
* Returns `null` when no proxy stands for `el`.
|
|
6220
6261
|
*/
|
|
6221
6262
|
const findControlProxy = (el) => {
|
|
6222
|
-
const
|
|
6223
|
-
|
|
6263
|
+
for (const proxyElement of findControlProxies(el)) {
|
|
6264
|
+
if (getComputedStyle(proxyElement).pointerEvents === "none") {
|
|
6265
|
+
continue;
|
|
6266
|
+
}
|
|
6267
|
+
return proxyElement;
|
|
6268
|
+
}
|
|
6269
|
+
return null;
|
|
6224
6270
|
};
|
|
6225
6271
|
|
|
6226
6272
|
/**
|
|
@@ -8008,6 +8054,11 @@ const generateSvgWithoutArrow = (width, height) => {
|
|
|
8008
8054
|
*/
|
|
8009
8055
|
|
|
8010
8056
|
|
|
8057
|
+
// The close reason the manager gives itself when the callout has to be drawn on
|
|
8058
|
+
// another element: it tears the callout down and opens it again, and this is
|
|
8059
|
+
// what tells that apart from the callout being dismissed.
|
|
8060
|
+
const MOVING_TO_ANOTHER_ANCHOR = "moving_to_another_anchor";
|
|
8061
|
+
|
|
8011
8062
|
/**
|
|
8012
8063
|
* Creates an opaque token used as a key for callout open reasons.
|
|
8013
8064
|
* Each caller (validation, interaction, …) owns one token.
|
|
@@ -8031,10 +8082,106 @@ const createCalloutManager = (
|
|
|
8031
8082
|
const [notifyCalloutOpen, onCalloutOpen] = createPubSub();
|
|
8032
8083
|
|
|
8033
8084
|
let callout = null;
|
|
8085
|
+
// Where the open callout is drawn. What a token points at is compared against
|
|
8086
|
+
// it to know whether showing that token is a new message on the same element
|
|
8087
|
+
// or the same conversation moving to another one.
|
|
8088
|
+
let calloutAnchorElement = null;
|
|
8034
8089
|
// Tracks open tokens → their constraint info.
|
|
8035
8090
|
// The callout closes automatically when the last token is removed.
|
|
8036
8091
|
const tokens = new Map();
|
|
8037
8092
|
|
|
8093
|
+
const openCalloutForToken = (tokenData, event) => {
|
|
8094
|
+
const { anchorElement } = tokenData;
|
|
8095
|
+
calloutAnchorElement = anchorElement;
|
|
8096
|
+
const removeCloseOnCleanup = addTeardown?.(() => {
|
|
8097
|
+
requestCloseCallout(new CustomEvent("cleanup"), "cleanup");
|
|
8098
|
+
});
|
|
8099
|
+
// `openResults` is referenced in onClose which runs later — forward ref is intentional.
|
|
8100
|
+
let openResults = [];
|
|
8101
|
+
callout = openCallout(tokenData.message, {
|
|
8102
|
+
status: tokenData.status,
|
|
8103
|
+
testId: tokenData.testId,
|
|
8104
|
+
icon: tokenData.icon,
|
|
8105
|
+
closeButton: tokenData.closeButton,
|
|
8106
|
+
closeOnClickOutside: tokenData.status !== "error",
|
|
8107
|
+
anchorElement,
|
|
8108
|
+
openingEvent: event,
|
|
8109
|
+
skipFocus: tokenData.skipFocus,
|
|
8110
|
+
debug: debugPopup,
|
|
8111
|
+
onClose: ({
|
|
8112
|
+
event: closeEvent,
|
|
8113
|
+
reason,
|
|
8114
|
+
shouldTransferFocusFromCallout,
|
|
8115
|
+
}) => {
|
|
8116
|
+
removeCloseOnCleanup?.();
|
|
8117
|
+
for (const result of openResults) {
|
|
8118
|
+
if (typeof result === "function") {
|
|
8119
|
+
result();
|
|
8120
|
+
}
|
|
8121
|
+
}
|
|
8122
|
+
callout = null;
|
|
8123
|
+
calloutAnchorElement = null;
|
|
8124
|
+
if (reason === MOVING_TO_ANOTHER_ANCHOR) {
|
|
8125
|
+
// The control still has the same things to say, on another element:
|
|
8126
|
+
// the tokens stand and nothing was dismissed, so neither the token
|
|
8127
|
+
// callbacks nor the focus hand-back below apply.
|
|
8128
|
+
return;
|
|
8129
|
+
}
|
|
8130
|
+
// User dismissed the callout — notify all active tokens then clear.
|
|
8131
|
+
// Told what closed it: a token whose content is a popup of its own (a
|
|
8132
|
+
// picker in callout mode) closes that popup on the same event.
|
|
8133
|
+
for (const [, otherTokenData] of tokens) {
|
|
8134
|
+
otherTokenData.onClose?.({ event: closeEvent, reason });
|
|
8135
|
+
}
|
|
8136
|
+
tokens.clear();
|
|
8137
|
+
const element = controller.ref.current;
|
|
8138
|
+
if (
|
|
8139
|
+
shouldTransferFocusFromCallout &&
|
|
8140
|
+
element &&
|
|
8141
|
+
!element.closest('[aria-hidden="true"]')
|
|
8142
|
+
) {
|
|
8143
|
+
const focusTarget =
|
|
8144
|
+
findFocusDelegateTarget(anchorElement) || anchorElement;
|
|
8145
|
+
debugFocus(
|
|
8146
|
+
closeEvent,
|
|
8147
|
+
`callout is closing with focus, give focus back to the control ${getElementSignature(focusTarget)}.focus()`,
|
|
8148
|
+
);
|
|
8149
|
+
focusTarget.focus();
|
|
8150
|
+
}
|
|
8151
|
+
},
|
|
8152
|
+
});
|
|
8153
|
+
openResults = notifyCalloutOpen(event);
|
|
8154
|
+
};
|
|
8155
|
+
|
|
8156
|
+
// What a token has to say, drawn where that token points. A control has one
|
|
8157
|
+
// callout, so a token takes it over from whoever held it — and a token about
|
|
8158
|
+
// another element (an action refused about the row that was pressed, on a
|
|
8159
|
+
// list that owns the action) moves it there, because a sentence about a row
|
|
8160
|
+
// drawn on the list is a sentence pointing at the wrong thing.
|
|
8161
|
+
const showToken = (tokenData, event) => {
|
|
8162
|
+
if (tokenData.anchorElement && !tokenData.anchorElement.isConnected) {
|
|
8163
|
+
// What the token pointed at has left the page — a row removed while its
|
|
8164
|
+
// refusal was still waiting behind another one. There is nothing left to
|
|
8165
|
+
// draw on there, and the control holding the token is still here, so it
|
|
8166
|
+
// takes it (same rule as an action error whose requester is gone, see
|
|
8167
|
+
// use_execute_action.js).
|
|
8168
|
+
tokenData.anchorElement = controller.ref.current;
|
|
8169
|
+
}
|
|
8170
|
+
if (callout) {
|
|
8171
|
+
if (tokenData.anchorElement === calloutAnchorElement) {
|
|
8172
|
+
callout.update(tokenData.message, {
|
|
8173
|
+
status: tokenData.status,
|
|
8174
|
+
testId: tokenData.testId,
|
|
8175
|
+
icon: tokenData.icon,
|
|
8176
|
+
closeButton: tokenData.closeButton,
|
|
8177
|
+
});
|
|
8178
|
+
return;
|
|
8179
|
+
}
|
|
8180
|
+
callout.requestClose(event, MOVING_TO_ANOTHER_ANCHOR);
|
|
8181
|
+
}
|
|
8182
|
+
openCalloutForToken(tokenData, event);
|
|
8183
|
+
};
|
|
8184
|
+
|
|
8038
8185
|
// Remove a token. Closes the callout only when no tokens remain.
|
|
8039
8186
|
// If other tokens are still active, updates the callout to show the first remaining one.
|
|
8040
8187
|
const removeOpenToken = (token, event) => {
|
|
@@ -8045,12 +8192,7 @@ const createCalloutManager = (
|
|
|
8045
8192
|
if (tokens.size > 0) {
|
|
8046
8193
|
if (callout) {
|
|
8047
8194
|
const [, remainingTokenData] = tokens.entries().next().value;
|
|
8048
|
-
|
|
8049
|
-
status: remainingTokenData.status,
|
|
8050
|
-
testId: remainingTokenData.testId,
|
|
8051
|
-
icon: remainingTokenData.icon,
|
|
8052
|
-
closeButton: remainingTokenData.closeButton,
|
|
8053
|
-
});
|
|
8195
|
+
showToken(remainingTokenData, event);
|
|
8054
8196
|
}
|
|
8055
8197
|
return false;
|
|
8056
8198
|
}
|
|
@@ -8087,70 +8229,21 @@ const createCalloutManager = (
|
|
|
8087
8229
|
removeOpenToken(token, event);
|
|
8088
8230
|
return;
|
|
8089
8231
|
}
|
|
8090
|
-
const
|
|
8232
|
+
const tokenData = {
|
|
8233
|
+
message,
|
|
8091
8234
|
status,
|
|
8092
8235
|
testId,
|
|
8093
8236
|
icon,
|
|
8094
8237
|
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
8238
|
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);
|
|
8239
|
+
onClose,
|
|
8240
|
+
// Resolved as the token is added, and kept with it: a token shown later
|
|
8241
|
+
// (when the one covering it goes) must be drawn where it was pointing at
|
|
8242
|
+
// the time, and the control itself is where a token pointing nowhere goes.
|
|
8243
|
+
anchorElement: anchorElement || controller.ref.current,
|
|
8244
|
+
};
|
|
8245
|
+
tokens.set(token, tokenData);
|
|
8246
|
+
showToken(tokenData, event);
|
|
8154
8247
|
};
|
|
8155
8248
|
|
|
8156
8249
|
const calloutManager = {
|
|
@@ -8176,12 +8269,21 @@ const BUSY_CONSTRAINT = {
|
|
|
8176
8269
|
transient: true,
|
|
8177
8270
|
// Unlike readonly/disabled, a busy element DOES block its parent from
|
|
8178
8271
|
// submitting — the element is mid-operation and cannot safely participate.
|
|
8179
|
-
check: (field) => {
|
|
8272
|
+
check: (field, { intent } = {}) => {
|
|
8180
8273
|
const isBusy = isControlBusy(field);
|
|
8181
8274
|
if (!isBusy) {
|
|
8182
8275
|
return null;
|
|
8183
8276
|
}
|
|
8184
8277
|
|
|
8278
|
+
// Busy, and what it opens still opens: a picker's answer lives in a shape
|
|
8279
|
+
// only its popup draws, and refusing to open leaves it unreadable for as
|
|
8280
|
+
// long as the wait lasts. Opening reads and nothing more — the same
|
|
8281
|
+
// exemption read-only makes, on the same controls (see
|
|
8282
|
+
// READONLY_CONSTRAINT), so that "read" means one thing to both.
|
|
8283
|
+
if (intent === "read" && field.readOnlyOpens) {
|
|
8284
|
+
return null;
|
|
8285
|
+
}
|
|
8286
|
+
|
|
8185
8287
|
const isButton = field.controlType === "button";
|
|
8186
8288
|
const message = isButton
|
|
8187
8289
|
? naviI18n("constraint.busy.button")
|
|
@@ -8199,9 +8301,9 @@ CONSTRAINT_ATTRIBUTE_SET.add("data-busy");
|
|
|
8199
8301
|
// whether it may finally close, a slide whether it may move on).
|
|
8200
8302
|
//
|
|
8201
8303
|
// 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
|
-
//
|
|
8304
|
+
// control busy only because the group above is waiting has no state of its own
|
|
8305
|
+
// to read — the group's answer IS its answer, so it asks upward and inherits
|
|
8306
|
+
// the same live reading.
|
|
8205
8307
|
const isControlBusy = (field) => {
|
|
8206
8308
|
if (field.loadingFromOwnProp) {
|
|
8207
8309
|
return true;
|
|
@@ -8225,7 +8327,7 @@ const isControlBusy = (field) => {
|
|
|
8225
8327
|
return true;
|
|
8226
8328
|
}
|
|
8227
8329
|
}
|
|
8228
|
-
if (field.
|
|
8330
|
+
if (field.loadingFromAbove) {
|
|
8229
8331
|
const parent = field.parentUIStateController;
|
|
8230
8332
|
return parent ? isControlBusy(parent) : false;
|
|
8231
8333
|
}
|
|
@@ -10063,6 +10165,9 @@ const createControlInteraction = (
|
|
|
10063
10165
|
let failingManagedInteraction = null;
|
|
10064
10166
|
// The title this rule put on the element, if any (see checkInteractivity).
|
|
10065
10167
|
let titleWritten = null;
|
|
10168
|
+
// The constraint this control last refused a press with, while that refusal
|
|
10169
|
+
// is on screen — what refreshReport has to take back once it stops holding.
|
|
10170
|
+
let reportedConstraint = null;
|
|
10066
10171
|
|
|
10067
10172
|
const checkInteractivity = ({ event, intent = "write" } = {}) => {
|
|
10068
10173
|
interactionFailedConstraintInfo = null;
|
|
@@ -10153,20 +10258,50 @@ const createControlInteraction = (
|
|
|
10153
10258
|
|
|
10154
10259
|
const canInteract =
|
|
10155
10260
|
!interactionFailedConstraintInfo && !failingManagedInteraction;
|
|
10156
|
-
// When the control
|
|
10261
|
+
// When the control can be interacted with, remove the interaction token
|
|
10157
10262
|
// so the callout closes if no other tokens (e.g. validation) are active.
|
|
10158
10263
|
if (canInteract) {
|
|
10159
10264
|
callout.removeOpenToken(INTERACTION_TOKEN, event);
|
|
10265
|
+
reportedConstraint = null;
|
|
10160
10266
|
}
|
|
10161
10267
|
return canInteract;
|
|
10162
10268
|
};
|
|
10163
10269
|
|
|
10270
|
+
// What this rule says out loud is a LIVE reading, and nothing else comes back
|
|
10271
|
+
// to correct it: busy stops being true on its own (see BUSY_CONSTRAINT's
|
|
10272
|
+
// `transient`), and read-only inherited from a group goes with the run that
|
|
10273
|
+
// set it. Read again whenever the control's interactivity moves, so a refusal
|
|
10274
|
+
// is taken back once what it says stops being true — a callout left open over
|
|
10275
|
+
// a control it no longer describes also sits on top of it and swallows the
|
|
10276
|
+
// next press.
|
|
10277
|
+
//
|
|
10278
|
+
// Only when something WAS said: reading a control that has refused nothing
|
|
10279
|
+
// would start writing titles nobody asked for.
|
|
10280
|
+
const refreshReport = (event) => {
|
|
10281
|
+
const constraintReported = reportedConstraint;
|
|
10282
|
+
if (!constraintReported && titleWritten === null) {
|
|
10283
|
+
return;
|
|
10284
|
+
}
|
|
10285
|
+
checkInteractivity({ event });
|
|
10286
|
+
if (!reportedConstraint) {
|
|
10287
|
+
return;
|
|
10288
|
+
}
|
|
10289
|
+
if (interactionFailedConstraintInfo?.constraint === constraintReported) {
|
|
10290
|
+
return;
|
|
10291
|
+
}
|
|
10292
|
+
// Held still, but by something else — and the press that was answered is
|
|
10293
|
+
// long over, so it is taken back rather than answered a second time.
|
|
10294
|
+
callout.removeOpenToken(INTERACTION_TOKEN, event);
|
|
10295
|
+
reportedConstraint = null;
|
|
10296
|
+
};
|
|
10297
|
+
|
|
10164
10298
|
const reportInteractivity = ({ event } = {}) => {
|
|
10165
10299
|
if (failingManagedInteraction) {
|
|
10166
10300
|
// Report on the specific child that is blocking, not the parent.
|
|
10167
10301
|
failingManagedInteraction.reportInteractivity({ event });
|
|
10168
10302
|
return;
|
|
10169
10303
|
}
|
|
10304
|
+
reportedConstraint = interactionFailedConstraintInfo.constraint;
|
|
10170
10305
|
debugInteraction(
|
|
10171
10306
|
event,
|
|
10172
10307
|
`reportInteractivity (${interactionFailedConstraintInfo.name})`,
|
|
@@ -10189,6 +10324,7 @@ const createControlInteraction = (
|
|
|
10189
10324
|
const controlInteraction = {
|
|
10190
10325
|
checkInteractivity,
|
|
10191
10326
|
reportInteractivity,
|
|
10327
|
+
refreshReport,
|
|
10192
10328
|
};
|
|
10193
10329
|
Object.defineProperty(controlInteraction, "interactionFailedConstraintInfo", {
|
|
10194
10330
|
get: () => interactionFailedConstraintInfo,
|
|
@@ -10237,6 +10373,10 @@ const onRequestInteraction = (
|
|
|
10237
10373
|
// through (see READONLY_CONSTRAINT).
|
|
10238
10374
|
intent = "write",
|
|
10239
10375
|
bypassInteractivity = false,
|
|
10376
|
+
// navi continuing a gesture on its own rather than a user asking for
|
|
10377
|
+
// something: the decision still applies, the explanation does not (see
|
|
10378
|
+
// below).
|
|
10379
|
+
automatic = false,
|
|
10240
10380
|
// Who asked, when the control is not answering the gesture on its own —
|
|
10241
10381
|
// the source of a command (see the self-interactions step-back below).
|
|
10242
10382
|
requester,
|
|
@@ -10297,7 +10437,16 @@ const onRequestInteraction = (
|
|
|
10297
10437
|
const reason = failedInfo
|
|
10298
10438
|
? `failing interaction constraint "${failedInfo.name}"`
|
|
10299
10439
|
: "not interactable";
|
|
10300
|
-
|
|
10440
|
+
// A refusal is explained to whoever asked, and an automatic follow-up
|
|
10441
|
+
// has nobody waiting for one: a group whose action a part just asked
|
|
10442
|
+
// for is busy running exactly that (see auto_group_action in
|
|
10443
|
+
// control_hooks.jsx), so the follow-up is turned down — which is what
|
|
10444
|
+
// keeps the action from running twice — while the press that started
|
|
10445
|
+
// it went through. Saying "this element is busy" there answers an
|
|
10446
|
+
// accepted press with a refusal.
|
|
10447
|
+
if (!automatic) {
|
|
10448
|
+
ci.reportInteractivity({ event });
|
|
10449
|
+
}
|
|
10301
10450
|
onPrevented(reason);
|
|
10302
10451
|
return false;
|
|
10303
10452
|
}
|
|
@@ -10351,6 +10500,11 @@ const dispatchRequestAction = (
|
|
|
10351
10500
|
{
|
|
10352
10501
|
event,
|
|
10353
10502
|
name = "dispatchRequestAction",
|
|
10503
|
+
// The gate's own options, named here so they reach it: everything left in
|
|
10504
|
+
// actionOptions goes to the action alone, so a gate option arriving under
|
|
10505
|
+
// any other name is dropped without a word.
|
|
10506
|
+
automatic,
|
|
10507
|
+
bypassInteractivity,
|
|
10354
10508
|
prevented,
|
|
10355
10509
|
allowed,
|
|
10356
10510
|
always,
|
|
@@ -10360,6 +10514,8 @@ const dispatchRequestAction = (
|
|
|
10360
10514
|
return dispatchRequestInteraction(element, {
|
|
10361
10515
|
event,
|
|
10362
10516
|
name,
|
|
10517
|
+
automatic,
|
|
10518
|
+
bypassInteractivity,
|
|
10363
10519
|
// The gate needs it as much as the action does: the requester may be an
|
|
10364
10520
|
// affordance that claimed the press to ask for this very action, and the
|
|
10365
10521
|
// control must not read that claim as "this press was not for me" — see
|
|
@@ -12965,9 +13121,10 @@ const useExecuteAction = (
|
|
|
12965
13121
|
}, [error]);
|
|
12966
13122
|
|
|
12967
13123
|
const addErrorMessage = (error, { requester } = {}) => {
|
|
12968
|
-
// The error is stored on the element that owns the action (the form/element
|
|
12969
|
-
// The requester
|
|
12970
|
-
//
|
|
13124
|
+
// The error is stored on the element that owns the action (the form/element
|
|
13125
|
+
// itself). The requester — the submit button that sent, the row of a list
|
|
13126
|
+
// whose command sent — is stored as the callout display target, so the
|
|
13127
|
+
// message appears on whoever asked rather than on the whole control.
|
|
12971
13128
|
const element = elementRef.current;
|
|
12972
13129
|
let target = requester;
|
|
12973
13130
|
// A requester that is no longer on the page is not a place to show
|
|
@@ -31300,6 +31457,12 @@ const anyMatchingRouteSignal = (routes) => {
|
|
|
31300
31457
|
* @param {Element} [options.anchor] Where a popup this opens should be placed,
|
|
31301
31458
|
* when that is not the element asking: a menu opened by a press belongs at the
|
|
31302
31459
|
* point the press happened, and the row that was pressed is not that point.
|
|
31460
|
+
* @param {Element} [options.requester] Who asked, when that is not the source.
|
|
31461
|
+
* A group triggers its own command from itself — what the command aims at is
|
|
31462
|
+
* read around the group, not around the child — while the gesture was on one
|
|
31463
|
+
* of its children, and what a refusal is about is that child. Same role as
|
|
31464
|
+
* the submit button a form's send names: the action belongs to the control,
|
|
31465
|
+
* the answer is drawn on whoever asked for it.
|
|
31303
31466
|
*/
|
|
31304
31467
|
const triggerNaviCommand = (element, command, event, options) => {
|
|
31305
31468
|
const run = resolveNaviCommand(element, command, event, options);
|
|
@@ -31330,7 +31493,7 @@ const resolveNaviCommand = (
|
|
|
31330
31493
|
element,
|
|
31331
31494
|
command,
|
|
31332
31495
|
event,
|
|
31333
|
-
{ optional, value, anchor } = {},
|
|
31496
|
+
{ optional, value, anchor, requester } = {},
|
|
31334
31497
|
) => {
|
|
31335
31498
|
if (!event) {
|
|
31336
31499
|
throw new Error(
|
|
@@ -31359,6 +31522,7 @@ const resolveNaviCommand = (
|
|
|
31359
31522
|
// source: the attribute form has a `value` to read (`<Button value={id}>`),
|
|
31360
31523
|
// a JS decision has none, and both must be able to say the same thing.
|
|
31361
31524
|
value,
|
|
31525
|
+
requester,
|
|
31362
31526
|
});
|
|
31363
31527
|
if (!execute) {
|
|
31364
31528
|
if (optional) {
|
|
@@ -31542,7 +31706,8 @@ const onNaviCommand = (e, { debugCommand = () => {} } = {}) => {
|
|
|
31542
31706
|
};
|
|
31543
31707
|
|
|
31544
31708
|
const NAVI_COMMANDS = {};
|
|
31545
|
-
// commandHandler(source, event
|
|
31709
|
+
// commandHandler(source, event, { argument, anchor, value, requester })
|
|
31710
|
+
// → { target, implementation } | undefined
|
|
31546
31711
|
// - Each handler calls resolveExplicitTarget(source) first, then falls back to
|
|
31547
31712
|
// its own DOM resolution logic (closest expandable, parent control, etc.).
|
|
31548
31713
|
// - Returns undefined when no target can be found — this is a normal outcome for
|
|
@@ -31780,7 +31945,7 @@ const resolveAfterSend = (target, requester) => {
|
|
|
31780
31945
|
return undefined;
|
|
31781
31946
|
};
|
|
31782
31947
|
|
|
31783
|
-
registerNaviCommand("--navi-send", (source, event) => {
|
|
31948
|
+
registerNaviCommand("--navi-send", (source, event, { requester }) => {
|
|
31784
31949
|
const expandable = resolveExpandableAround(source);
|
|
31785
31950
|
const target =
|
|
31786
31951
|
resolveExplicitTarget(source) ||
|
|
@@ -31823,14 +31988,21 @@ registerNaviCommand("--navi-send", (source, event) => {
|
|
|
31823
31988
|
return {
|
|
31824
31989
|
target,
|
|
31825
31990
|
implementation: () => {
|
|
31826
|
-
|
|
31827
|
-
|
|
31828
|
-
|
|
31829
|
-
|
|
31830
|
-
|
|
31831
|
-
|
|
31832
|
-
|
|
31833
|
-
|
|
31991
|
+
// Who asked, taken from whoever triggered the command when it said so —
|
|
31992
|
+
// a group sends from itself and names the child the gesture was on (see
|
|
31993
|
+
// triggerNaviCommand's `requester`). Named means named: the stand-in
|
|
31994
|
+
// below is for a source nobody spoke for.
|
|
31995
|
+
let sendRequester = requester;
|
|
31996
|
+
if (!sendRequester) {
|
|
31997
|
+
sendRequester = source;
|
|
31998
|
+
if (!source.matches(submitSelector)) {
|
|
31999
|
+
// When present, use the first submit button as the requester, not the input.
|
|
32000
|
+
// This aligns with browser behavior where Enter in a text input triggers
|
|
32001
|
+
// the first submit button of the form, not the input itself.
|
|
32002
|
+
const firstButtonSubmitting = target.querySelector(submitSelector);
|
|
32003
|
+
if (firstButtonSubmitting) {
|
|
32004
|
+
sendRequester = firstButtonSubmitting;
|
|
32005
|
+
}
|
|
31834
32006
|
}
|
|
31835
32007
|
}
|
|
31836
32008
|
// Nothing is committed when a constraint fails, so nothing is decided
|
|
@@ -31844,7 +32016,7 @@ registerNaviCommand("--navi-send", (source, event) => {
|
|
|
31844
32016
|
// from the response writes it on the form while it runs
|
|
31845
32017
|
// (data-after-send), and this is what picks it up.
|
|
31846
32018
|
const runAfterSend = () => {
|
|
31847
|
-
const afterSend = resolveAfterSend(target,
|
|
32019
|
+
const afterSend = resolveAfterSend(target, sendRequester);
|
|
31848
32020
|
if (!afterSend) {
|
|
31849
32021
|
return;
|
|
31850
32022
|
}
|
|
@@ -31879,7 +32051,7 @@ registerNaviCommand("--navi-send", (source, event) => {
|
|
|
31879
32051
|
initiator.preventDefault();
|
|
31880
32052
|
}
|
|
31881
32053
|
},
|
|
31882
|
-
requester,
|
|
32054
|
+
requester: sendRequester,
|
|
31883
32055
|
}),
|
|
31884
32056
|
);
|
|
31885
32057
|
if (sent === false || invalid) {
|
|
@@ -34140,8 +34312,20 @@ const useUIStateController = (
|
|
|
34140
34312
|
resetUIState: (e) => {
|
|
34141
34313
|
controller.setUIState(controller.state, e);
|
|
34142
34314
|
},
|
|
34315
|
+
// What the control shows becomes what is acknowledged: the outside said
|
|
34316
|
+
// yes to it, so it is where a rollback goes back to. Left alone for a
|
|
34317
|
+
// control whose value is GIVEN — the prop is already its truth — and
|
|
34318
|
+
// while a newer request waits behind this one, whose own answer is what
|
|
34319
|
+
// will settle what is accepted.
|
|
34320
|
+
acknowledgeUIState: () => {
|
|
34321
|
+
if (controller.hasStateProp || controller.queuedActionAllowedEvent) {
|
|
34322
|
+
return;
|
|
34323
|
+
}
|
|
34324
|
+
controller.state = controller.uiState;
|
|
34325
|
+
},
|
|
34143
34326
|
onActionEnd: (e) => {
|
|
34144
34327
|
debugUIState(`"${controlType}" actionEnd called`);
|
|
34328
|
+
acknowledgeOwnAction(controller);
|
|
34145
34329
|
controller.rules.validation.syncValidity(e);
|
|
34146
34330
|
},
|
|
34147
34331
|
onActionError: (e) => {
|
|
@@ -34351,6 +34535,18 @@ const firstDefinedChildUIState = (children) => {
|
|
|
34351
34535
|
return undefined;
|
|
34352
34536
|
};
|
|
34353
34537
|
|
|
34538
|
+
// A run that came back with a yes settles what the control is worth — but only
|
|
34539
|
+
// for the control the run BELONGS to. Anything with a `command` still fires its
|
|
34540
|
+
// own navi_action_* events (it is given a placeholder action so they exist),
|
|
34541
|
+
// and taking those for an acknowledgement is how a row would remember its own
|
|
34542
|
+
// selection as accepted and hand it back on the group's rollback.
|
|
34543
|
+
const acknowledgeOwnAction = (controller) => {
|
|
34544
|
+
if (!controller.props.action) {
|
|
34545
|
+
return;
|
|
34546
|
+
}
|
|
34547
|
+
controller.acknowledgeUIState();
|
|
34548
|
+
};
|
|
34549
|
+
|
|
34354
34550
|
// Default aggregate/distribute implementations keyed by controlType or stateType.
|
|
34355
34551
|
// Looked up in useUIGroupStateController to fill in omitted aggregateChildStates /
|
|
34356
34552
|
// distributeChildUIState. If neither a default nor an explicit impl is found for a
|
|
@@ -34779,12 +34975,15 @@ const useUIGroupStateController = (
|
|
|
34779
34975
|
if (notifyExternal === true || notifyExternal === "if-it-moves") {
|
|
34780
34976
|
if (
|
|
34781
34977
|
notifyExternal === "if-it-moves" &&
|
|
34782
|
-
compareTwoJsValues(groupUIState, controller.
|
|
34978
|
+
compareTwoJsValues(groupUIState, controller.announcedUIState)
|
|
34783
34979
|
) {
|
|
34784
|
-
// A child confirming what it already shows, to a group
|
|
34785
|
-
//
|
|
34786
|
-
// group reacted then. One trip through a picker is one answer,
|
|
34787
|
-
// there is nothing left to say here.
|
|
34980
|
+
// A child confirming what it already shows, to a group that has
|
|
34981
|
+
// already said it: the value arrived while the popup was open and
|
|
34982
|
+
// the group reacted then. One trip through a picker is one answer,
|
|
34983
|
+
// so there is nothing left to say here. Measured against what was
|
|
34984
|
+
// announced rather than against what the group holds — a picker
|
|
34985
|
+
// mounting into a form folds its suggestion into that silently, and
|
|
34986
|
+
// a value nobody was ever told is not one that has been said.
|
|
34788
34987
|
return;
|
|
34789
34988
|
}
|
|
34790
34989
|
// Somebody answered: what the group is worth is what its children say
|
|
@@ -34815,7 +35014,7 @@ const useUIGroupStateController = (
|
|
|
34815
35014
|
{ except: actingChild },
|
|
34816
35015
|
);
|
|
34817
35016
|
}
|
|
34818
|
-
applyState(groupUIState, e);
|
|
35017
|
+
applyState(groupUIState, e, { actingChild });
|
|
34819
35018
|
} else if (notifyExternal === "silent") {
|
|
34820
35019
|
controller.syncInternalState(groupUIState);
|
|
34821
35020
|
s.parentUIStateController?.onChildUIAction(controller, e, {
|
|
@@ -34869,10 +35068,19 @@ const useUIGroupStateController = (
|
|
|
34869
35068
|
childUIStateController.setUIState(childNewState, e);
|
|
34870
35069
|
};
|
|
34871
35070
|
|
|
34872
|
-
const applyState = (
|
|
35071
|
+
const applyState = (
|
|
35072
|
+
newUIState,
|
|
35073
|
+
e,
|
|
35074
|
+
{ internalBehavior = false, actingChild } = {},
|
|
35075
|
+
) => {
|
|
34873
35076
|
const { controller } = s;
|
|
34874
35077
|
const currentUIState = controller.uiState;
|
|
34875
35078
|
controller.uiState = newUIState;
|
|
35079
|
+
// What the outside was last told, which is not what the group is worth:
|
|
35080
|
+
// a child mounting into a group that derived its own value moves that
|
|
35081
|
+
// value through syncInternalState, deliberately without telling anybody
|
|
35082
|
+
// (see onChange). Only what goes through here has been said out loud.
|
|
35083
|
+
controller.announcedUIState = newUIState;
|
|
34876
35084
|
uiStateSignal.value = newUIState;
|
|
34877
35085
|
debugUIGroup(
|
|
34878
35086
|
e,
|
|
@@ -34885,7 +35093,10 @@ const useUIGroupStateController = (
|
|
|
34885
35093
|
s.parentUIStateController?.onChildUIAction(controller, e, {
|
|
34886
35094
|
stateChanged: true,
|
|
34887
35095
|
});
|
|
34888
|
-
controller.onUIAction(e, {
|
|
35096
|
+
controller.onUIAction(e, {
|
|
35097
|
+
skipCommand: internalBehavior,
|
|
35098
|
+
actingChild,
|
|
35099
|
+
});
|
|
34889
35100
|
const el = controller.ref.current;
|
|
34890
35101
|
if (el) {
|
|
34891
35102
|
dispatchInternalCustomEvent(el, "navi_ui_state_change", {
|
|
@@ -34905,6 +35116,7 @@ const useUIGroupStateController = (
|
|
|
34905
35116
|
hasDefaultValueProp,
|
|
34906
35117
|
props,
|
|
34907
35118
|
uiState: stateInitial,
|
|
35119
|
+
announcedUIState: stateInitial,
|
|
34908
35120
|
// Whether what the group holds was HANDED to it (a parent distributing,
|
|
34909
35121
|
// a picker filling its popup, a value prop) rather than worked out from
|
|
34910
35122
|
// its children. What it protects is read in onChange.
|
|
@@ -35023,7 +35235,7 @@ const useUIGroupStateController = (
|
|
|
35023
35235
|
uiStateSignal.value = newUIState;
|
|
35024
35236
|
publishUIState(newUIState);
|
|
35025
35237
|
},
|
|
35026
|
-
onUIAction: (e, { skipCommand } = {}) => {
|
|
35238
|
+
onUIAction: (e, { skipCommand, actingChild } = {}) => {
|
|
35027
35239
|
const currentUIState = controller.uiState;
|
|
35028
35240
|
// The same write applyState/onChange make (see writeBoundSignal), for
|
|
35029
35241
|
// the case where the state did not move but the user acted all the
|
|
@@ -35037,7 +35249,14 @@ const useUIGroupStateController = (
|
|
|
35037
35249
|
if (!skipCommand && controller.props.command) {
|
|
35038
35250
|
const el = controller.ref.current;
|
|
35039
35251
|
if (el) {
|
|
35040
|
-
|
|
35252
|
+
// The command is the GROUP's — what it aims at is read around the
|
|
35253
|
+
// group, not around the child — but the child is who asked, and a
|
|
35254
|
+
// refusal is drawn on whoever asked (see triggerNaviCommand's
|
|
35255
|
+
// `requester`). Without it a list refusing a name says so at its
|
|
35256
|
+
// own foot, rows away from the name that was touched.
|
|
35257
|
+
triggerNaviCommand(el, controller.props.command, e, {
|
|
35258
|
+
requester: actingChild?.ref.current,
|
|
35259
|
+
});
|
|
35041
35260
|
}
|
|
35042
35261
|
}
|
|
35043
35262
|
},
|
|
@@ -35132,7 +35351,7 @@ const useUIGroupStateController = (
|
|
|
35132
35351
|
actingChild: childUIStateController,
|
|
35133
35352
|
});
|
|
35134
35353
|
} else {
|
|
35135
|
-
controller.onUIAction(e);
|
|
35354
|
+
controller.onUIAction(e, { actingChild: childUIStateController });
|
|
35136
35355
|
}
|
|
35137
35356
|
},
|
|
35138
35357
|
unregisterChild: (childUIStateController) => {
|
|
@@ -35194,7 +35413,19 @@ const useUIGroupStateController = (
|
|
|
35194
35413
|
}
|
|
35195
35414
|
onChange(e, { notifyExternal: true });
|
|
35196
35415
|
},
|
|
35416
|
+
// The group was told yes, so what each of its parts shows is accepted
|
|
35417
|
+
// too: a group holds no value of its own — its rollback puts the
|
|
35418
|
+
// children back where they were (see resetUIState), and they can only
|
|
35419
|
+
// go back to what was acknowledged if they were told.
|
|
35420
|
+
acknowledgeUIState: () => {
|
|
35421
|
+
for (const c of childUIStateControllerArray) {
|
|
35422
|
+
if (shouldPropagateStateToChild(c)) {
|
|
35423
|
+
c.acknowledgeUIState();
|
|
35424
|
+
}
|
|
35425
|
+
}
|
|
35426
|
+
},
|
|
35197
35427
|
onActionEnd: (e) => {
|
|
35428
|
+
acknowledgeOwnAction(controller);
|
|
35198
35429
|
controller.rules.validation.syncValidity(e);
|
|
35199
35430
|
},
|
|
35200
35431
|
onActionError: (e) => {
|
|
@@ -35282,7 +35513,21 @@ const useUIGroupStateController = (
|
|
|
35282
35513
|
// down last time. A popup reopened on another subject hands down the very
|
|
35283
35514
|
// same empty value it did before, and the selection left inside it from
|
|
35284
35515
|
// the previous opening is what has to go.
|
|
35285
|
-
|
|
35516
|
+
//
|
|
35517
|
+
// Except while the group is asking about that very difference: the run in
|
|
35518
|
+
// flight carries what the user just chose, and the value is what the
|
|
35519
|
+
// owner holds until it answers. Putting the children back on it here
|
|
35520
|
+
// would take the choice off the screen on the first re-render — which the
|
|
35521
|
+
// run itself causes, by making the group busy — so the press would appear
|
|
35522
|
+
// to do nothing until the answer arrived, and the rollback on a refusal
|
|
35523
|
+
// would have nothing left to undo.
|
|
35524
|
+
const askingAboutIt =
|
|
35525
|
+
controller.actionInFlight || controller.queuedActionAllowedEvent;
|
|
35526
|
+
if (
|
|
35527
|
+
hasValueProp &&
|
|
35528
|
+
!askingAboutIt &&
|
|
35529
|
+
!compareTwoJsValues(value, controller.uiState)
|
|
35530
|
+
) {
|
|
35286
35531
|
placeChildrenFrom(value);
|
|
35287
35532
|
}
|
|
35288
35533
|
if (
|
|
@@ -37179,10 +37424,19 @@ const useInteractiveProps = (props, {
|
|
|
37179
37424
|
// live source rather than from the rendered aria-busy, which conflates all
|
|
37180
37425
|
// three and is a frame behind. See its own comment.
|
|
37181
37426
|
uiStateController.loadingFromOwnProp = Boolean(loading);
|
|
37182
|
-
|
|
37427
|
+
// The group above waiting on something, which reaches every control in it:
|
|
37428
|
+
// each one is held for as long as that lasts, and "wait" is what they have
|
|
37429
|
+
// to say about it — not the read-only they wear to express it, which is a
|
|
37430
|
+
// settled word (see BUSY_CONSTRAINT and READONLY_CONSTRAINT).
|
|
37431
|
+
uiStateController.loadingFromAbove = Boolean(controlLoading);
|
|
37183
37432
|
// Read by BUSY_CONSTRAINT: an optimistic control stays interactive while
|
|
37184
37433
|
// its bound action runs (a new toggle replaces the run instead of waiting).
|
|
37185
37434
|
uiStateController.optimistic = Boolean(optimistic);
|
|
37435
|
+
// What the interaction rule last refused is only true while the control is
|
|
37436
|
+
// held; the state it was read from moves here (see refreshReport).
|
|
37437
|
+
useLayoutEffect(() => {
|
|
37438
|
+
uiStateController.rules.interaction.refreshReport(new CustomEvent("interactivity_change"));
|
|
37439
|
+
}, [disabledResolved, readOnlyResolved, loadingResolved, controlLoading, uiStateController]);
|
|
37186
37440
|
Object.assign(controlHostProps, {
|
|
37187
37441
|
"required": requiredResolved,
|
|
37188
37442
|
"aria-busy": loadingResolved ? "true" : "false",
|
|
@@ -37529,16 +37783,13 @@ const useInteractiveProps = (props, {
|
|
|
37529
37783
|
event: e.detail.eventChain[0],
|
|
37530
37784
|
name: "auto_group_action",
|
|
37531
37785
|
requester: e.detail.requester,
|
|
37532
|
-
//
|
|
37533
|
-
//
|
|
37534
|
-
//
|
|
37535
|
-
//
|
|
37536
|
-
//
|
|
37537
|
-
//
|
|
37538
|
-
|
|
37539
|
-
// busy constraint would refuse the group for an action that
|
|
37540
|
-
// is already over. The validity gate still applies.
|
|
37541
|
-
bypassInteractivity: true
|
|
37786
|
+
// Nobody asked for this one: it is the group's own continuation
|
|
37787
|
+
// of a press the child's gate already accepted. The gate still
|
|
37788
|
+
// decides — a group told to send by its own `command` is
|
|
37789
|
+
// already running that very action, and letting this through
|
|
37790
|
+
// would run it a second time — but a refusal here is not
|
|
37791
|
+
// reported, since the press it would answer went through.
|
|
37792
|
+
automatic: true
|
|
37542
37793
|
});
|
|
37543
37794
|
}
|
|
37544
37795
|
}
|
|
@@ -41940,10 +42191,8 @@ const css$W = /* css */`@layer navi {
|
|
|
41940
42191
|
}
|
|
41941
42192
|
}
|
|
41942
42193
|
|
|
41943
|
-
&[data-expand-x] {
|
|
41944
|
-
|
|
41945
|
-
|
|
41946
|
-
&:not([data-vertical]):not([data-scrollable]) .navi_link {
|
|
42194
|
+
&[data-expand-x]:not([data-vertical]):not([data-scrollable]) {
|
|
42195
|
+
& .navi_link {
|
|
41947
42196
|
flex: 1;
|
|
41948
42197
|
justify-content: center;
|
|
41949
42198
|
}
|
|
@@ -62004,6 +62253,42 @@ const ListItemFooter = props => {
|
|
|
62004
62253
|
});
|
|
62005
62254
|
};
|
|
62006
62255
|
|
|
62256
|
+
// What a row may say it is waiting on, all of them about the row as a thing
|
|
62257
|
+
// the LIST holds: joining it, leaving it, or being saved where it is. A
|
|
62258
|
+
// selectable row waiting on the list's own send is a different event with a
|
|
62259
|
+
// different subject — the selection, not the row (see list_selectable.jsx).
|
|
62260
|
+
const LOADING_REASON_SET = new Set(["adding", "removing", "updating"]);
|
|
62261
|
+
|
|
62262
|
+
/**
|
|
62263
|
+
* What a row waiting on something has to say when it is pressed. `loading` may
|
|
62264
|
+
* say WHAT it is waiting for: a row being created is not simply "busy", and
|
|
62265
|
+
* saying which one it is tells the user what to expect.
|
|
62266
|
+
*
|
|
62267
|
+
* Shared with the selectable row, which carries the same `loading` on the
|
|
62268
|
+
* hidden input holding its selection — the row and that control must answer a
|
|
62269
|
+
* press with one sentence, whichever of them catches it.
|
|
62270
|
+
*/
|
|
62271
|
+
const listItemBusyMessage = (loading, props) => {
|
|
62272
|
+
if (LOADING_REASON_SET.has(loading)) {
|
|
62273
|
+
return naviI18n(`constraint.busy.item.${loading}`, props);
|
|
62274
|
+
}
|
|
62275
|
+
return naviI18n("constraint.busy.item", props);
|
|
62276
|
+
};
|
|
62277
|
+
|
|
62278
|
+
/**
|
|
62279
|
+
* Why the row cannot be acted on, in the row's own terms — or in the caller's,
|
|
62280
|
+
* where it gave them. The same two props every navi control answers a refusal
|
|
62281
|
+
* with (`readOnlyMessage`, `busyMessage`), so a row and the control it carries
|
|
62282
|
+
* cannot end up saying two different things about one press: whichever of them
|
|
62283
|
+
* catches it, the sentence is the same.
|
|
62284
|
+
*/
|
|
62285
|
+
const listItemBlockedMessage = (loading, props) => {
|
|
62286
|
+
if (loading) {
|
|
62287
|
+
return props.busyMessage ?? listItemBusyMessage(loading, props);
|
|
62288
|
+
}
|
|
62289
|
+
return props.readOnlyMessage ?? naviI18n("constraint.readonly.item", props);
|
|
62290
|
+
};
|
|
62291
|
+
|
|
62007
62292
|
installImportMetaCssBuild(import.meta);const css$y = /* css */`@layer navi {
|
|
62008
62293
|
.navi_list_container[navi-selectable] {
|
|
62009
62294
|
--list-item-outline-width: var(--navi-focus-outline-width);
|
|
@@ -62219,7 +62504,17 @@ const ListSelectable = props => {
|
|
|
62219
62504
|
}
|
|
62220
62505
|
return kept;
|
|
62221
62506
|
};
|
|
62222
|
-
const [listControlRootProps, listControlProps, childrenWrapperProps] = useControlgroupProps(
|
|
62507
|
+
const [listControlRootProps, listControlProps, childrenWrapperProps] = useControlgroupProps({
|
|
62508
|
+
// The defaults of the group this IS (see CheckboxGroup / RadioGroup):
|
|
62509
|
+
// what a selectable list shows is a claim about what was accepted, so
|
|
62510
|
+
// a run that ends without an answer puts the rows back where they were
|
|
62511
|
+
// — a row left ticked over a refusal says the opposite of what
|
|
62512
|
+
// happened, and the error callout beside it does not untick it.
|
|
62513
|
+
resetOnCancel: true,
|
|
62514
|
+
resetOnAbort: true,
|
|
62515
|
+
resetOnError: true,
|
|
62516
|
+
...props
|
|
62517
|
+
}, {
|
|
62223
62518
|
stateType: multiple ? "array" : "",
|
|
62224
62519
|
controlType: multiple ? "checkbox_group" : "radio_group",
|
|
62225
62520
|
aggregateChildStates
|
|
@@ -62506,6 +62801,14 @@ const ListItemSelectable = props => {
|
|
|
62506
62801
|
selected,
|
|
62507
62802
|
pointed,
|
|
62508
62803
|
selectableArea = "all",
|
|
62804
|
+
// True of the ROW as much as of the control it carries, so both are told.
|
|
62805
|
+
// Left in `rest` they would reach the hidden input alone — the control
|
|
62806
|
+
// system claims them (they are control props) and the row would never see
|
|
62807
|
+
// them, which for a row means no outline, no dimming, no press swallowed
|
|
62808
|
+
// and none of the buttons it contains held back (see ListItemReal). Any
|
|
62809
|
+
// prop a row and a control both understand belongs here.
|
|
62810
|
+
loading,
|
|
62811
|
+
readOnly,
|
|
62509
62812
|
// The row's own click handler, kept out of the control props below: those
|
|
62510
62813
|
// describe the hidden input that carries the selection, and a caller
|
|
62511
62814
|
// writing onClick on a <List.Item> is talking about the row they see.
|
|
@@ -62516,23 +62819,33 @@ const ListItemSelectable = props => {
|
|
|
62516
62819
|
// A checkbox toggles on its own; only a radio has to be told it may let go.
|
|
62517
62820
|
const deselectable = useContext(SelectableListDeselectableContext) && !multiple;
|
|
62518
62821
|
// Whose reason it is that this row cannot be taken. Read-only reaching it
|
|
62519
|
-
// from above is the LIST's, and
|
|
62520
|
-
//
|
|
62521
|
-
//
|
|
62522
|
-
//
|
|
62523
|
-
//
|
|
62524
|
-
//
|
|
62525
|
-
//
|
|
62822
|
+
// from above is the LIST's, and that is the whole answer — said as the
|
|
62823
|
+
// selection where several things are taken, as the choice where one thing
|
|
62824
|
+
// is. Said of each row in turn, "this option is not available" describes
|
|
62825
|
+
// something else entirely: a list where each row happens to be unavailable
|
|
62826
|
+
// for its own reasons, which is what a row held on its own says.
|
|
62827
|
+
//
|
|
62828
|
+
// A list waiting on something is not unavailable, it is not answering yet —
|
|
62829
|
+
// busy rather than read-only (see BUSY_CONSTRAINT, which is asked first),
|
|
62830
|
+
// and the wait is about the same thing the read-only would be about.
|
|
62526
62831
|
const readOnlyFromAbove = useContext(ReadOnlyContext);
|
|
62527
|
-
const
|
|
62528
|
-
|
|
62529
|
-
|
|
62832
|
+
const readOnlyMessageKey = readOnlyFromAbove ? multiple ? `constraint.readonly.selection` : `constraint.readonly.choice` : `constraint.readonly.option`;
|
|
62833
|
+
// Handed to the row as well as to the control: whichever of the two catches
|
|
62834
|
+
// the press answers it, and they must not answer it differently.
|
|
62835
|
+
const readOnlyMessageResolved = props.readOnlyMessage ?? naviI18n(readOnlyMessageKey, props);
|
|
62836
|
+
const busyMessageResolved = props.busyMessage ?? (loading ? listItemBusyMessage(loading, props) : naviI18n(multiple ? `constraint.busy.selection` : `constraint.busy.choice`, props));
|
|
62530
62837
|
const inputRef = useRef();
|
|
62531
62838
|
const inputType = multiple ? "checkbox" : "radio";
|
|
62532
62839
|
const inputId = `${id}_input`;
|
|
62533
62840
|
inputRef.nullCanHappen = true; // virtualization
|
|
62534
62841
|
const [checkableRootProps, checkableProps, controlChildrenWrapperProps] = useCheckableProps({
|
|
62535
|
-
readOnlyMessage:
|
|
62842
|
+
readOnlyMessage: readOnlyMessageResolved,
|
|
62843
|
+
// What the row itself is waiting for when it says so, the wait of the
|
|
62844
|
+
// list around it otherwise — said as the selection where several things
|
|
62845
|
+
// are taken, as the choice where one thing is.
|
|
62846
|
+
busyMessage: busyMessageResolved,
|
|
62847
|
+
loading,
|
|
62848
|
+
readOnly,
|
|
62536
62849
|
...rest,
|
|
62537
62850
|
ref: inputRef,
|
|
62538
62851
|
id: inputId,
|
|
@@ -62549,17 +62862,19 @@ const ListItemSelectable = props => {
|
|
|
62549
62862
|
basePseudoState,
|
|
62550
62863
|
children
|
|
62551
62864
|
} = checkableProps;
|
|
62552
|
-
|
|
62865
|
+
// Everything holding the control back, not only the row's own `readOnly`
|
|
62866
|
+
// above: the list above it, a maxLengthGuard with no room left.
|
|
62867
|
+
const readOnlyResolved = basePseudoState[":read-only"];
|
|
62553
62868
|
const realInputContextValue = useMemo(() => {
|
|
62554
62869
|
return {
|
|
62555
62870
|
id: inputId,
|
|
62556
62871
|
type: inputType,
|
|
62557
62872
|
checked,
|
|
62558
|
-
readOnly,
|
|
62873
|
+
readOnly: readOnlyResolved,
|
|
62559
62874
|
value,
|
|
62560
62875
|
deselectable
|
|
62561
62876
|
};
|
|
62562
|
-
}, [inputId, inputType, checked,
|
|
62877
|
+
}, [inputId, inputType, checked, readOnlyResolved, value, deselectable]);
|
|
62563
62878
|
return jsxs(Next, {
|
|
62564
62879
|
id: id,
|
|
62565
62880
|
index: index,
|
|
@@ -62581,6 +62896,10 @@ const ListItemSelectable = props => {
|
|
|
62581
62896
|
},
|
|
62582
62897
|
ref: props.ref,
|
|
62583
62898
|
onClick: onClick,
|
|
62899
|
+
loading: loading,
|
|
62900
|
+
readOnly: readOnly,
|
|
62901
|
+
readOnlyMessage: readOnlyMessageResolved,
|
|
62902
|
+
busyMessage: busyMessageResolved,
|
|
62584
62903
|
selectable: undefined,
|
|
62585
62904
|
"navi-selectable-area-all": selectableArea === "all" ? "" : undefined,
|
|
62586
62905
|
children: [jsx(SelectableRealInput, {
|
|
@@ -65754,7 +66073,7 @@ const ListItemReal = props => {
|
|
|
65754
66073
|
if (calloutRef.current && calloutRef.current.opened) {
|
|
65755
66074
|
return;
|
|
65756
66075
|
}
|
|
65757
|
-
calloutRef.current = openCallout(
|
|
66076
|
+
calloutRef.current = openCallout(listItemBlockedMessage(loading, props), {
|
|
65758
66077
|
anchorElement: event.currentTarget,
|
|
65759
66078
|
status: "info",
|
|
65760
66079
|
openingEvent: event
|
|
@@ -65781,6 +66100,11 @@ const ListItemReal = props => {
|
|
|
65781
66100
|
...itemColumnsOverrideProps,
|
|
65782
66101
|
index: undefined,
|
|
65783
66102
|
selected: undefined
|
|
66103
|
+
// Read by listItemBlockedMessage, not by the element.
|
|
66104
|
+
,
|
|
66105
|
+
|
|
66106
|
+
readOnlyMessage: undefined,
|
|
66107
|
+
busyMessage: undefined
|
|
65784
66108
|
// We use aria-hidden and not hidden because hidden would be forced to
|
|
65785
66109
|
// display: none while here we want to keep it in the DOM to avoid layout shift
|
|
65786
66110
|
// but visually hidden
|
|
@@ -65831,18 +66155,6 @@ const ListItemReal = props => {
|
|
|
65831
66155
|
})]
|
|
65832
66156
|
});
|
|
65833
66157
|
};
|
|
65834
|
-
// Why the row cannot be acted on, in the row's own terms. `loading` may say
|
|
65835
|
-
// what it is waiting for ("adding", "removing"): a row being created is not
|
|
65836
|
-
// simply "busy", and saying which one it is tells the user what to expect.
|
|
65837
|
-
const blockedMessage = (loading, readOnly, props) => {
|
|
65838
|
-
if (!loading) {
|
|
65839
|
-
return naviI18n("constraint.readonly.item", props);
|
|
65840
|
-
}
|
|
65841
|
-
if (loading === "adding" || loading === "removing") {
|
|
65842
|
-
return naviI18n(`constraint.busy.item.${loading}`, props);
|
|
65843
|
-
}
|
|
65844
|
-
return naviI18n("constraint.busy.item", props);
|
|
65845
|
-
};
|
|
65846
66158
|
const LIST_ITEM_STYLE_CSS_VARS = {
|
|
65847
66159
|
"borderRadius": "--list-item-border-radius",
|
|
65848
66160
|
"borderWidth": "--list-item-border-width",
|
|
@@ -65897,7 +66209,7 @@ const LIST_ITEM_STYLE_CSS_VARS = {
|
|
|
65897
66209
|
* skeleton?: boolean,
|
|
65898
66210
|
* error?: boolean | import("ignore:preact").ComponentChildren,
|
|
65899
66211
|
* onErrorDismiss?: (event: Event) => void,
|
|
65900
|
-
* loading?: boolean | "adding" | "removing",
|
|
66212
|
+
* loading?: boolean | "adding" | "removing" | "updating",
|
|
65901
66213
|
* readOnly?: boolean,
|
|
65902
66214
|
* filtered?: boolean,
|
|
65903
66215
|
* hidden?: boolean,
|
|
@@ -65938,12 +66250,17 @@ const LIST_ITEM_STYLE_CSS_VARS = {
|
|
|
65938
66250
|
* like the list's own error. `true` shows a generic sentence. When
|
|
65939
66251
|
* `onErrorDismiss` is given, a dismiss button is drawn next to the message
|
|
65940
66252
|
* and calls it.
|
|
65941
|
-
* @param {boolean|"adding"|"removing"} [props.loading]
|
|
66253
|
+
* @param {boolean|"adding"|"removing"|"updating"} [props.loading]
|
|
65942
66254
|
* The row is waiting on something: it draws a loading outline and, like
|
|
65943
66255
|
* readOnly, stops taking clicks. Works on any item, not only a selectable
|
|
65944
|
-
* one — a list is edited row by row.
|
|
65945
|
-
*
|
|
65946
|
-
*
|
|
66256
|
+
* one — a list is edited row by row. Rather than true, say WHAT it is
|
|
66257
|
+
* waiting for, which is what a press on it then answers: "adding" (joining
|
|
66258
|
+
* the list), "removing" (leaving it), "updating" (being saved where it is).
|
|
66259
|
+
*
|
|
66260
|
+
* All three are about the row as a thing the LIST holds, never about the
|
|
66261
|
+
* selection: a selectable row taken while its list sends says so on its own
|
|
66262
|
+
* ("la sélection est en cours d'enregistrement"), and needs no `loading` for
|
|
66263
|
+
* that.
|
|
65947
66264
|
* @param {boolean} [props.readOnly]
|
|
65948
66265
|
* The row cannot be acted on: dimmed and click-through-proof, buttons inside
|
|
65949
66266
|
* it included.
|