@jsenv/navi 0.29.92 → 0.29.94

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.
@@ -24208,12 +24208,25 @@ const useUIStateController = (
24208
24208
  } = controlInfo;
24209
24209
  controller.value = value;
24210
24210
  controller.defaultValue = defaultValue;
24211
+ // An optimistic control with work in flight keeps the user's intent on
24212
+ // screen: an incoming external state is the echo of an intermediate
24213
+ // commit (the first of two queued toggles landing), and the queued
24214
+ // request about to go out was built on top of the UI state —
24215
+ // overwriting it would both flash the superseded value and send it.
24216
+ // `controller.state` is still taken below: it is the last known good
24217
+ // state, the rollback target if the chain fails.
24218
+ const optimisticWorkInFlight = Boolean(
24219
+ props.optimistic &&
24220
+ (controller.actionInFlight || controller.queuedActionAllowedEvent),
24221
+ );
24211
24222
  if (hasStateProp) {
24212
24223
  controller.hasStateProp = true;
24213
24224
  const currentState = controller.state;
24214
24225
  if (!compareTwoJsValues(state, currentState)) {
24215
24226
  controller.state = state;
24216
- controller.setUIState(state, new CustomEvent("state_prop_change"));
24227
+ if (!optimisticWorkInFlight) {
24228
+ controller.setUIState(state, new CustomEvent("state_prop_change"));
24229
+ }
24217
24230
  }
24218
24231
  } else {
24219
24232
  if (controller.hasStateProp) {
@@ -24230,10 +24243,12 @@ const useUIStateController = (
24230
24243
  const currentState = controller.state;
24231
24244
  if (!compareTwoJsValues(stateFromSignal, currentState)) {
24232
24245
  controller.state = stateFromSignal;
24233
- controller.setUIState(
24234
- stateFromSignal,
24235
- new CustomEvent("state_prop_change"),
24236
- );
24246
+ if (!optimisticWorkInFlight) {
24247
+ controller.setUIState(
24248
+ stateFromSignal,
24249
+ new CustomEvent("state_prop_change"),
24250
+ );
24251
+ }
24237
24252
  }
24238
24253
  }
24239
24254
  }
@@ -24664,7 +24679,7 @@ const useUIGroupStateController = (
24664
24679
 
24665
24680
  // onChange and applyState live inside init so they close over the stable
24666
24681
  // signals/pubsub without needing external refs.
24667
- const onChange = (e, { notifyExternal }) => {
24682
+ const onChange = (e, { notifyExternal, actingChild }) => {
24668
24683
  if (groupIsRenderingRef.current) {
24669
24684
  // Held until the layout effect below, WITH what it asked for: a child
24670
24685
  // whose bound signal was written from the outside changes during the
@@ -24677,6 +24692,7 @@ const useUIGroupStateController = (
24677
24692
  e,
24678
24693
  notifyExternal:
24679
24694
  pendingChange?.notifyExternal === true ? true : notifyExternal,
24695
+ actingChild,
24680
24696
  };
24681
24697
  return;
24682
24698
  }
@@ -24700,6 +24716,21 @@ const useUIGroupStateController = (
24700
24716
  // Somebody answered: what the group is worth is what its children say
24701
24717
  // between them, from here on.
24702
24718
  controller.stateGivenFromAbove = false;
24719
+ if (resolvedDistributeChildStates) {
24720
+ // A group answering for all its children at once holds ONE answer
24721
+ // its children are views OF, rather than a value that IS what they
24722
+ // said: a list saying who plays and four seats saying who sits
24723
+ // where. One view speaking moves the answer, so the others have to
24724
+ // show it again — the same rule as a child arriving after the value
24725
+ // did, for a child that was there when the value moved without it.
24726
+ // The one that just acted is left alone: it is where the user put
24727
+ // it.
24728
+ controller.placeChildrenUIState(
24729
+ groupUIState,
24730
+ new CustomEvent("propagate_down_set_ui_state", { detail: {} }),
24731
+ { except: actingChild },
24732
+ );
24733
+ }
24703
24734
  applyState(groupUIState, e);
24704
24735
  } else if (notifyExternal === "silent") {
24705
24736
  controller.syncInternalState(groupUIState);
@@ -24780,7 +24811,7 @@ const useUIGroupStateController = (
24780
24811
  // place — see warnChildAnswersForItself.
24781
24812
  // One pass over every child, which is what a plural distribute needs:
24782
24813
  // it is asked once, sees the whole group, and answers for all of them.
24783
- placeChildrenUIState: (groupUIState, e) => {
24814
+ placeChildrenUIState: (groupUIState, e, { except } = {}) => {
24784
24815
  if (!resolvedDistributeChildStates) {
24785
24816
  for (const childUIStateController of childUIStateControllerArray) {
24786
24817
  controller.placeChildUIState(
@@ -24802,6 +24833,9 @@ const useUIGroupStateController = (
24802
24833
  return;
24803
24834
  }
24804
24835
  for (const childUIStateController of monitoredChildren) {
24836
+ if (childUIStateController === except) {
24837
+ continue;
24838
+ }
24805
24839
  if (!stateByChild.has(childUIStateController)) {
24806
24840
  // Not named by the answer: left where it is, the way
24807
24841
  // CANNOT_DERIVE leaves a child a per-child distribute says
@@ -25001,7 +25035,10 @@ const useUIGroupStateController = (
25001
25035
  )}`,
25002
25036
  );
25003
25037
  if (stateChanged) {
25004
- onChange(e, { notifyExternal: silent ? "silent" : true });
25038
+ onChange(e, {
25039
+ notifyExternal: silent ? "silent" : true,
25040
+ actingChild: childUIStateController,
25041
+ });
25005
25042
  } else {
25006
25043
  controller.onUIAction(e);
25007
25044
  }
@@ -25214,6 +25251,7 @@ const useUIGroupStateController = (
25214
25251
  chainEvent(batchedEvent, pendingChange.e);
25215
25252
  scope._onChange(batchedEvent, {
25216
25253
  notifyExternal: pendingChange.notifyExternal,
25254
+ actingChild: pendingChange.actingChild,
25217
25255
  });
25218
25256
  }
25219
25257
  });
@@ -27219,7 +27257,6 @@ const useInteractiveProps = (props, {
27219
27257
  uiStateController.actionInFlight = false;
27220
27258
  uiStateController.runningAction = null;
27221
27259
  const queuedEvent = uiStateController.queuedActionAllowedEvent;
27222
- uiStateController.queuedActionAllowedEvent = null;
27223
27260
  if (!queuedEvent) {
27224
27261
  return;
27225
27262
  }
@@ -27227,12 +27264,20 @@ const useInteractiveProps = (props, {
27227
27264
  // A failure abandons the queue: the UI is rolled back to the last
27228
27265
  // known state (resetOnError above), and what was queued was built
27229
27266
  // on top of the state that just failed.
27267
+ uiStateController.queuedActionAllowedEvent = null;
27230
27268
  return;
27231
27269
  }
27232
27270
  // A microtask later, not right here: this runs inside the batch()
27233
27271
  // that settles the action (see watchActionCompletion for the same
27234
27272
  // constraint).
27235
27273
  queueMicrotask(() => {
27274
+ // Cleared here, right before the dispatch, never earlier: between
27275
+ // the outcome and this microtask a render can slip in (the echo
27276
+ // of what the settled run committed), and the external-state gate
27277
+ // in ui_state_controller.js must still see work in flight or it
27278
+ // would overwrite the UI state the queued request is about to
27279
+ // send.
27280
+ uiStateController.queuedActionAllowedEvent = null;
27236
27281
  executeAction(queuedEvent);
27237
27282
  });
27238
27283
  });