@jsenv/navi 0.29.170 → 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.
@@ -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 suppression.",
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 that visually represents it.
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
- * Returns `null` when no proxy exists for `el`.
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 [firstProxy = null] = findControlProxies(el);
6223
- return firstProxy;
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
- callout.update(remainingTokenData.message, {
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 calloutOptions = {
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
- debug: debugPopup,
8115
- onClose: ({
8116
- event: closeEvent,
8117
- reason,
8118
- shouldTransferFocusFromCallout,
8119
- }) => {
8120
- removeCloseOnCleanup?.();
8121
- for (const result of openResults) {
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 running the action it asked for
8203
- // has no state of its own to read — the group's answer IS its answer, so it
8204
- // asks upward and inherits the same live reading.
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.loadingFromParent) {
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 is now interactable, remove the interaction token
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
- ci.reportInteractivity({ event });
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 itself).
12969
- // The requester (e.g. submit button) is stored as the callout display target
12970
- // so the validation message appears on the button, not the form.
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) → { target, implementation } | undefined
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
- let requester = source;
31827
- if (!source.matches(submitSelector)) {
31828
- // When present, use the first submit button as the requester, not the input.
31829
- // This aligns with browser behavior where Enter in a text input triggers
31830
- // the first submit button of the form, not the input itself.
31831
- const firstButtonSubmitting = target.querySelector(submitSelector);
31832
- if (firstButtonSubmitting) {
31833
- requester = firstButtonSubmitting;
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, requester);
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
@@ -34818,7 +35014,7 @@ const useUIGroupStateController = (
34818
35014
  { except: actingChild },
34819
35015
  );
34820
35016
  }
34821
- applyState(groupUIState, e);
35017
+ applyState(groupUIState, e, { actingChild });
34822
35018
  } else if (notifyExternal === "silent") {
34823
35019
  controller.syncInternalState(groupUIState);
34824
35020
  s.parentUIStateController?.onChildUIAction(controller, e, {
@@ -34872,7 +35068,11 @@ const useUIGroupStateController = (
34872
35068
  childUIStateController.setUIState(childNewState, e);
34873
35069
  };
34874
35070
 
34875
- const applyState = (newUIState, e, { internalBehavior = false } = {}) => {
35071
+ const applyState = (
35072
+ newUIState,
35073
+ e,
35074
+ { internalBehavior = false, actingChild } = {},
35075
+ ) => {
34876
35076
  const { controller } = s;
34877
35077
  const currentUIState = controller.uiState;
34878
35078
  controller.uiState = newUIState;
@@ -34893,7 +35093,10 @@ const useUIGroupStateController = (
34893
35093
  s.parentUIStateController?.onChildUIAction(controller, e, {
34894
35094
  stateChanged: true,
34895
35095
  });
34896
- controller.onUIAction(e, { skipCommand: internalBehavior });
35096
+ controller.onUIAction(e, {
35097
+ skipCommand: internalBehavior,
35098
+ actingChild,
35099
+ });
34897
35100
  const el = controller.ref.current;
34898
35101
  if (el) {
34899
35102
  dispatchInternalCustomEvent(el, "navi_ui_state_change", {
@@ -35032,7 +35235,7 @@ const useUIGroupStateController = (
35032
35235
  uiStateSignal.value = newUIState;
35033
35236
  publishUIState(newUIState);
35034
35237
  },
35035
- onUIAction: (e, { skipCommand } = {}) => {
35238
+ onUIAction: (e, { skipCommand, actingChild } = {}) => {
35036
35239
  const currentUIState = controller.uiState;
35037
35240
  // The same write applyState/onChange make (see writeBoundSignal), for
35038
35241
  // the case where the state did not move but the user acted all the
@@ -35046,7 +35249,14 @@ const useUIGroupStateController = (
35046
35249
  if (!skipCommand && controller.props.command) {
35047
35250
  const el = controller.ref.current;
35048
35251
  if (el) {
35049
- triggerNaviCommand(el, controller.props.command, e);
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
+ });
35050
35260
  }
35051
35261
  }
35052
35262
  },
@@ -35141,7 +35351,7 @@ const useUIGroupStateController = (
35141
35351
  actingChild: childUIStateController,
35142
35352
  });
35143
35353
  } else {
35144
- controller.onUIAction(e);
35354
+ controller.onUIAction(e, { actingChild: childUIStateController });
35145
35355
  }
35146
35356
  },
35147
35357
  unregisterChild: (childUIStateController) => {
@@ -35203,7 +35413,19 @@ const useUIGroupStateController = (
35203
35413
  }
35204
35414
  onChange(e, { notifyExternal: true });
35205
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
+ },
35206
35427
  onActionEnd: (e) => {
35428
+ acknowledgeOwnAction(controller);
35207
35429
  controller.rules.validation.syncValidity(e);
35208
35430
  },
35209
35431
  onActionError: (e) => {
@@ -35291,7 +35513,21 @@ const useUIGroupStateController = (
35291
35513
  // down last time. A popup reopened on another subject hands down the very
35292
35514
  // same empty value it did before, and the selection left inside it from
35293
35515
  // the previous opening is what has to go.
35294
- if (hasValueProp && !compareTwoJsValues(value, controller.uiState)) {
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
+ ) {
35295
35531
  placeChildrenFrom(value);
35296
35532
  }
35297
35533
  if (
@@ -37188,10 +37424,19 @@ const useInteractiveProps = (props, {
37188
37424
  // live source rather than from the rendered aria-busy, which conflates all
37189
37425
  // three and is a frame behind. See its own comment.
37190
37426
  uiStateController.loadingFromOwnProp = Boolean(loading);
37191
- uiStateController.loadingFromParent = loadingFromParent;
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);
37192
37432
  // Read by BUSY_CONSTRAINT: an optimistic control stays interactive while
37193
37433
  // its bound action runs (a new toggle replaces the run instead of waiting).
37194
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]);
37195
37440
  Object.assign(controlHostProps, {
37196
37441
  "required": requiredResolved,
37197
37442
  "aria-busy": loadingResolved ? "true" : "false",
@@ -37538,16 +37783,13 @@ const useInteractiveProps = (props, {
37538
37783
  event: e.detail.eventChain[0],
37539
37784
  name: "auto_group_action",
37540
37785
  requester: e.detail.requester,
37541
- // The interactivity gate is not re-asked: the user already
37542
- // interacted — with the child, whose gate said yes — and this
37543
- // follow-up is automatic. Asking again would also answer
37544
- // wrong: this event is dispatched inside the batch() that
37545
- // settles the child's action, where a bound action still
37546
- // READS as running (its state is mirrored through a signal
37547
- // effect the batch defers, see watchActionCompletion) — the
37548
- // busy constraint would refuse the group for an action that
37549
- // is already over. The validity gate still applies.
37550
- 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
37551
37793
  });
37552
37794
  }
37553
37795
  }
@@ -62011,6 +62253,42 @@ const ListItemFooter = props => {
62011
62253
  });
62012
62254
  };
62013
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
+
62014
62292
  installImportMetaCssBuild(import.meta);const css$y = /* css */`@layer navi {
62015
62293
  .navi_list_container[navi-selectable] {
62016
62294
  --list-item-outline-width: var(--navi-focus-outline-width);
@@ -62226,7 +62504,17 @@ const ListSelectable = props => {
62226
62504
  }
62227
62505
  return kept;
62228
62506
  };
62229
- const [listControlRootProps, listControlProps, childrenWrapperProps] = useControlgroupProps(props, {
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
+ }, {
62230
62518
  stateType: multiple ? "array" : "",
62231
62519
  controlType: multiple ? "checkbox_group" : "radio_group",
62232
62520
  aggregateChildStates
@@ -62513,6 +62801,14 @@ const ListItemSelectable = props => {
62513
62801
  selected,
62514
62802
  pointed,
62515
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,
62516
62812
  // The row's own click handler, kept out of the control props below: those
62517
62813
  // describe the hidden input that carries the selection, and a caller
62518
62814
  // writing onClick on a <List.Item> is talking about the row they see.
@@ -62523,23 +62819,33 @@ const ListItemSelectable = props => {
62523
62819
  // A checkbox toggles on its own; only a radio has to be told it may let go.
62524
62820
  const deselectable = useContext(SelectableListDeselectableContext) && !multiple;
62525
62821
  // Whose reason it is that this row cannot be taken. Read-only reaching it
62526
- // from above is the LIST's, and what is settled is then the whole answer —
62527
- // said as the selection where several things are taken, as the choice where
62528
- // one thing is. Said of each row in turn, "this option is not available"
62529
- // describes something else entirely: a list where each row happens to be
62530
- // unavailable for its own reasons, which goes on being said that way. Busy
62531
- // is not settled either: a list waiting on something says nothing about what
62532
- // will be possible once it is done, so the row keeps its own words.
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.
62533
62831
  const readOnlyFromAbove = useContext(ReadOnlyContext);
62534
- const loadingFromAbove = useContext(LoadingContext$1);
62535
- const answerIsSettled = Boolean(readOnlyFromAbove) && !loadingFromAbove;
62536
- const readOnlyMessageKey = answerIsSettled ? multiple ? `constraint.readonly.selection` : `constraint.readonly.choice` : `constraint.readonly.option`;
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));
62537
62837
  const inputRef = useRef();
62538
62838
  const inputType = multiple ? "checkbox" : "radio";
62539
62839
  const inputId = `${id}_input`;
62540
62840
  inputRef.nullCanHappen = true; // virtualization
62541
62841
  const [checkableRootProps, checkableProps, controlChildrenWrapperProps] = useCheckableProps({
62542
- readOnlyMessage: naviI18n(readOnlyMessageKey, props),
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,
62543
62849
  ...rest,
62544
62850
  ref: inputRef,
62545
62851
  id: inputId,
@@ -62556,17 +62862,19 @@ const ListItemSelectable = props => {
62556
62862
  basePseudoState,
62557
62863
  children
62558
62864
  } = checkableProps;
62559
- const readOnly = basePseudoState[":read-only"];
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"];
62560
62868
  const realInputContextValue = useMemo(() => {
62561
62869
  return {
62562
62870
  id: inputId,
62563
62871
  type: inputType,
62564
62872
  checked,
62565
- readOnly,
62873
+ readOnly: readOnlyResolved,
62566
62874
  value,
62567
62875
  deselectable
62568
62876
  };
62569
- }, [inputId, inputType, checked, readOnly, value, deselectable]);
62877
+ }, [inputId, inputType, checked, readOnlyResolved, value, deselectable]);
62570
62878
  return jsxs(Next, {
62571
62879
  id: id,
62572
62880
  index: index,
@@ -62588,6 +62896,10 @@ const ListItemSelectable = props => {
62588
62896
  },
62589
62897
  ref: props.ref,
62590
62898
  onClick: onClick,
62899
+ loading: loading,
62900
+ readOnly: readOnly,
62901
+ readOnlyMessage: readOnlyMessageResolved,
62902
+ busyMessage: busyMessageResolved,
62591
62903
  selectable: undefined,
62592
62904
  "navi-selectable-area-all": selectableArea === "all" ? "" : undefined,
62593
62905
  children: [jsx(SelectableRealInput, {
@@ -65761,7 +66073,7 @@ const ListItemReal = props => {
65761
66073
  if (calloutRef.current && calloutRef.current.opened) {
65762
66074
  return;
65763
66075
  }
65764
- calloutRef.current = openCallout(blockedMessage(loading, readOnly, props), {
66076
+ calloutRef.current = openCallout(listItemBlockedMessage(loading, props), {
65765
66077
  anchorElement: event.currentTarget,
65766
66078
  status: "info",
65767
66079
  openingEvent: event
@@ -65788,6 +66100,11 @@ const ListItemReal = props => {
65788
66100
  ...itemColumnsOverrideProps,
65789
66101
  index: undefined,
65790
66102
  selected: undefined
66103
+ // Read by listItemBlockedMessage, not by the element.
66104
+ ,
66105
+
66106
+ readOnlyMessage: undefined,
66107
+ busyMessage: undefined
65791
66108
  // We use aria-hidden and not hidden because hidden would be forced to
65792
66109
  // display: none while here we want to keep it in the DOM to avoid layout shift
65793
66110
  // but visually hidden
@@ -65838,18 +66155,6 @@ const ListItemReal = props => {
65838
66155
  })]
65839
66156
  });
65840
66157
  };
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
66158
  const LIST_ITEM_STYLE_CSS_VARS = {
65854
66159
  "borderRadius": "--list-item-border-radius",
65855
66160
  "borderWidth": "--list-item-border-width",
@@ -65904,7 +66209,7 @@ const LIST_ITEM_STYLE_CSS_VARS = {
65904
66209
  * skeleton?: boolean,
65905
66210
  * error?: boolean | import("ignore:preact").ComponentChildren,
65906
66211
  * onErrorDismiss?: (event: Event) => void,
65907
- * loading?: boolean | "adding" | "removing",
66212
+ * loading?: boolean | "adding" | "removing" | "updating",
65908
66213
  * readOnly?: boolean,
65909
66214
  * filtered?: boolean,
65910
66215
  * hidden?: boolean,
@@ -65945,12 +66250,17 @@ const LIST_ITEM_STYLE_CSS_VARS = {
65945
66250
  * like the list's own error. `true` shows a generic sentence. When
65946
66251
  * `onErrorDismiss` is given, a dismiss button is drawn next to the message
65947
66252
  * and calls it.
65948
- * @param {boolean|"adding"|"removing"} [props.loading]
66253
+ * @param {boolean|"adding"|"removing"|"updating"} [props.loading]
65949
66254
  * The row is waiting on something: it draws a loading outline and, like
65950
66255
  * readOnly, stops taking clicks. Works on any item, not only a selectable
65951
- * one — a list is edited row by row. Pass "adding" or "removing" rather than
65952
- * true to say WHAT it is waiting for, which is what a press on it then
65953
- * answers.
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.
65954
66264
  * @param {boolean} [props.readOnly]
65955
66265
  * The row cannot be acted on: dimmed and click-through-proof, buttons inside
65956
66266
  * it included.