@jsenv/navi 0.29.453 → 0.29.455

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.
@@ -39577,6 +39577,59 @@ const callLiftOverCallbacks = () => {
39577
39577
  });
39578
39578
  };
39579
39579
 
39580
+ /**
39581
+ * What a gesture changes in a closed picker is sent at once.
39582
+ *
39583
+ * A picker's `action` waits for its popup to close: what is done in there is
39584
+ * one answer, given when the person is done. A change made while the popup is
39585
+ * closed — the clear cross, a chip's cross, a paste, a cut, a value proposed
39586
+ * from beside it (`--navi-update`) — has no close coming, so nothing would ever
39587
+ * send it: the value moves on screen and the record behind it stays. Each of
39588
+ * those gestures calls this right after setting the value; a failing action
39589
+ * puts the value back (resetOnError), as it does after a close.
39590
+ *
39591
+ * Called by gestures only. A value set without one — a bound signal followed, a
39592
+ * cancel or a failure rolled back — is the picker being told, not asked, which
39593
+ * is why this is not a reaction to every value set on a closed picker.
39594
+ *
39595
+ * The action is requested the way the close requests it, not through
39596
+ * `--navi-send`: a send also answers the surface around the control (a slide
39597
+ * moves on, an open dialog closes — see resolveAfterSend in commands.js), and a
39598
+ * cross pressed on a picker in a dialog is not the dialog being answered.
39599
+ */
39600
+
39601
+
39602
+ const sendClosedPickerChange = (element, { event, name, requester }) => {
39603
+ const controlHost = findControlHost(element) || element;
39604
+ const controlRoot = findControlRoot(controlHost);
39605
+ if (!controlRoot || controlRoot.getAttribute("navi-control") !== "picker") {
39606
+ return;
39607
+ }
39608
+ if (controlRoot.getAttribute("aria-expanded") === "true") {
39609
+ return;
39610
+ }
39611
+ const { props } = controlHost.__uiStateController__;
39612
+ if (!props.action) {
39613
+ return;
39614
+ }
39615
+ // A picker that picks nothing (`type="confirm"`, a menu of actions) holds no
39616
+ // value its action would be sending: that action is the yes of its popup,
39617
+ // and a paste landing on the trigger must not say it.
39618
+ if (props.picksNothing) {
39619
+ return;
39620
+ }
39621
+ // A source with an action of its own made the request already: `<Button
39622
+ // action={remove} command="--navi-clear">` runs the removal, and the command
39623
+ // that follows only brings the picker in line with it. Sending the picker's
39624
+ // action on top would ask twice.
39625
+ if (requester?.__uiStateController__?.props.action) {
39626
+ return;
39627
+ }
39628
+ // `requester`: the source of a command may have claimed the press to send
39629
+ // this very change (see isAimedAtSelfInteractionsBelow).
39630
+ dispatchRequestAction(controlHost, { event, name, requester });
39631
+ };
39632
+
39580
39633
  /**
39581
39634
  * Runs a navi command from JS. It is the LAST RESORT: the attributes say the
39582
39635
  * same thing declaratively and go through this very function, so a press that
@@ -39952,6 +40005,11 @@ registerNaviCommand("--navi-update", (source, event, { argument }) => {
39952
40005
  event,
39953
40006
  behavior: argument,
39954
40007
  });
40008
+ sendClosedPickerChange(target, {
40009
+ event,
40010
+ name: "--navi-update",
40011
+ requester: source,
40012
+ });
39955
40013
  },
39956
40014
  });
39957
40015
  },
@@ -39964,20 +40022,6 @@ registerNaviCommand("--navi-clear", (source, event) => {
39964
40022
  return undefined;
39965
40023
  }
39966
40024
  const fromInput = source.closest(`[navi-control="input"]`);
39967
- // A control that commits on an explicit send — a picker, whose list sends the
39968
- // moment a value is chosen — has nothing that would commit a clear: its
39969
- // action never runs on a ui state change. Left alone, the field goes empty
39970
- // while the caller still holds the value it gave, and renders it right back.
39971
- //
39972
- // Unless the source committed the clear itself: a clear button given its own
39973
- // action (`<Button action={remove} command="--navi-clear">`) already made the
39974
- // request — the ui state is being brought in line with what just happened, and
39975
- // sending the picker's own action on top would ask twice.
39976
- const sourceController = source.__uiStateController__;
39977
- const fromSendOnlyControl = Boolean(
39978
- source.closest?.(`[navi-control=picker]`) &&
39979
- !sourceController?.props.action,
39980
- );
39981
40025
 
39982
40026
  const performClear = (clearEvent) => {
39983
40027
  dispatchRequestInteraction(target, {
@@ -39988,46 +40032,11 @@ registerNaviCommand("--navi-clear", (source, event) => {
39988
40032
  requester: source,
39989
40033
  prevented: () => clearEvent.preventDefault(),
39990
40034
  allowed: () => {
39991
- // What the control holds, before it holds nothing: the clear is
39992
- // optimistic — the field empties now and the send that commits it may
39993
- // still fail — so what it emptied has to be kept to be put back.
39994
- const uiStateBefore = getUIStateFromElement(target);
39995
40035
  dispatchRequestClearUIState(target, clearEvent);
39996
- if (!fromSendOnlyControl) {
39997
- return;
39998
- }
39999
- // After the clear, never before: the action is bound to the ui
40000
- // state signal, so this sends the value the control now holds.
40001
- const actionHost = findControlHost(target) || target;
40002
- const completion = watchActionCompletion(actionHost, () => {
40003
- triggerNaviCommand(source, "--navi-send", clearEvent, {
40004
- optional: true,
40005
- });
40006
- });
40007
- completion.whenSettled(({ error, aborted }) => {
40008
- if (!error && !aborted) {
40009
- return;
40010
- }
40011
- // The removal did not happen, so the field must stop saying it did.
40012
- // The error itself stays where the action put it — on the control,
40013
- // which is still there, unlike the cross that has just gone with the
40014
- // value it cleared (see addErrorMessage in use_execute_action.js).
40015
- //
40016
- // Put back from the inside ("clear_rollback" is an internal event
40017
- // type, see ui_state_controller.js) rather than asked for the way a
40018
- // user would: nobody acted, the control is being returned to the
40019
- // state its caller still holds. Asked from the outside it would be
40020
- // answered with "this element is busy" — the action is still
40021
- // settling — over the very error that explains why the value is back.
40022
- const controller = actionHost.__uiStateController__;
40023
- if (!controller) {
40024
- return;
40025
- }
40026
- const rollbackEvent = new CustomEvent("clear_rollback", {
40027
- detail: {},
40028
- });
40029
- chainEvent(rollbackEvent, clearEvent);
40030
- controller.setUIState(uiStateBefore, rollbackEvent);
40036
+ sendClosedPickerChange(target, {
40037
+ event: clearEvent,
40038
+ name: "--navi-clear",
40039
+ requester: source,
40031
40040
  });
40032
40041
  },
40033
40042
  });
@@ -40767,6 +40776,7 @@ registerNaviCommand("--navi-select", (source, event) => {
40767
40776
  return dispatchCustomEvent(target, "navi_request_select", {
40768
40777
  event,
40769
40778
  id: resolveCommandValue(source, event),
40779
+ requester: source,
40770
40780
  });
40771
40781
  },
40772
40782
  };
@@ -40783,6 +40793,7 @@ registerNaviCommand("--navi-unselect", (source, event) => {
40783
40793
  return dispatchCustomEvent(target, "navi_request_unselect", {
40784
40794
  event,
40785
40795
  id: resolveCommandValue(source, event),
40796
+ requester: source,
40786
40797
  });
40787
40798
  },
40788
40799
  };
@@ -44623,12 +44634,6 @@ const INTERNAL_EVENT_SET = new Set([
44623
44634
  // on registration, and group pushing value/defaultValue to children on registerChild.
44624
44635
  // Equivalent to defaultValue initialization: no uiAction, no commands, no parent notification.
44625
44636
  "initial_state_push",
44626
- // navi undoing its own optimistic write: the clear cross emptied the control
44627
- // before the send that commits it, the send failed, and the value it emptied
44628
- // goes back where it was (see the --navi-clear command). Nothing acted — the
44629
- // control is being put back on the state the caller still holds — so this
44630
- // must not fire a uiAction, a command, or a report on the way.
44631
- "clear_rollback",
44632
44637
  // A picker's popup closed on a cancel (the no button, Escape, a click
44633
44638
  // outside): what the picker held at open goes back. Nobody acted — no
44634
44639
  // command — but the value really did move back, so uiAction and the parent
@@ -50859,11 +50864,15 @@ const LinkWithRoute = ({
50859
50864
  also: currentAlso
50860
50865
  });
50861
50866
  const innerCurrent = current || linkMatching;
50867
+
50868
+ // What a childless link shows is LinkPlain's call (hrefFallback); a route
50869
+ // only says it the way its pattern writes it, not as the resolved href.
50862
50870
  return jsx(Link, {
50863
50871
  href: url,
50872
+ hrefText: route.buildRelativeUrl(routeParams),
50864
50873
  current: innerCurrent,
50865
50874
  ...rest,
50866
- children: children || route.buildRelativeUrl(routeParams)
50875
+ children: children
50867
50876
  });
50868
50877
  };
50869
50878
  const LinkPlain = props => {
@@ -50889,6 +50898,7 @@ const LinkPlain = props => {
50889
50898
  revealOnInteraction = false,
50890
50899
  stretch,
50891
50900
  hrefFallback = !anchor,
50901
+ hrefText = href,
50892
50902
  routeTransition,
50893
50903
  pressableDuringRouteTransition,
50894
50904
  replace,
@@ -51024,7 +51034,7 @@ const LinkPlain = props => {
51024
51034
  const documentRequest = isDocument ? {
51025
51035
  [LINK_DOCUMENT_ATTRIBUTE]: ""
51026
51036
  } : null;
51027
- const innerChildren = children || (hrefFallback ? href : children);
51037
+ const innerChildren = children || (hrefFallback ? hrefText : children);
51028
51038
  const startIconEl = startIcon;
51029
51039
  const endIconEl = innerEndIcon;
51030
51040
 
@@ -51075,6 +51085,7 @@ const LinkPlain = props => {
51075
51085
  startIcon: undefined,
51076
51086
  endIcon: undefined,
51077
51087
  hrefFallback: undefined,
51088
+ hrefText: undefined,
51078
51089
  routeTransition: undefined,
51079
51090
  pressableDuringRouteTransition: undefined,
51080
51091
  replace: undefined,
@@ -81179,7 +81190,9 @@ const PickerArrayUI = /*#__PURE__*/asPickerOwnUI(() => {
81179
81190
  /**
81180
81191
  * One value the picker holds, drawn as a chip with a cross that takes it back
81181
81192
  * out. Sits wherever the application draws what was picked — on the picker's
81182
- * façade (`ui`) or inside its popup — and both behave the same.
81193
+ * façade (`ui`) or inside its popup. Inside the open popup the removal waits
81194
+ * for the close like any change made there; pressed while the popup is closed
81195
+ * it is the whole gesture, and the picker's `action` runs at once.
81183
81196
  *
81184
81197
  * The cross asks with `--navi-unselect` rather than writing a new list, and it
81185
81198
  * asks the picker — which holds what was picked, and hands it down to whatever
@@ -82207,15 +82220,25 @@ const PickerButton = props => {
82207
82220
  const displayText = pickerEl.querySelector(".navi_picker_value")?.textContent ?? String(uiState);
82208
82221
  e.clipboardData.setData("text/plain", displayText);
82209
82222
  e.clipboardData.setData("application/x-navi", JSON.stringify(uiState));
82223
+ e.preventDefault();
82224
+ // A picker that asks before its cross clears anything is not
82225
+ // emptied by a keystroke without the question: the cut copies,
82226
+ // and the value stays.
82227
+ if (clearConfirm !== undefined) {
82228
+ return;
82229
+ }
82210
82230
  // the clear ui state part need control to be interactable
82211
82231
  dispatchRequestInteraction(pickerEl, {
82212
82232
  event: e,
82213
82233
  name: "cut",
82214
82234
  allowed: () => {
82215
82235
  dispatchRequestClearUIState(inputRef.current, e);
82236
+ sendClosedPickerChange(inputRef.current, {
82237
+ event: e,
82238
+ name: "cut"
82239
+ });
82216
82240
  }
82217
82241
  });
82218
- e.preventDefault();
82219
82242
  },
82220
82243
  onPaste: e => {
82221
82244
  const pickerEl = ref.current;
@@ -82241,6 +82264,10 @@ const PickerButton = props => {
82241
82264
  dispatchRequestSetUIState(inputRef.current, pasteValue, {
82242
82265
  event: e
82243
82266
  });
82267
+ sendClosedPickerChange(inputRef.current, {
82268
+ event: e,
82269
+ name: "paste"
82270
+ });
82244
82271
  }
82245
82272
  });
82246
82273
  e.preventDefault();
@@ -82459,7 +82486,8 @@ const requestPickerListEntry = (pickerEl, pickerInputEl, e, goal) => {
82459
82486
  return;
82460
82487
  }
82461
82488
  const {
82462
- id: entry
82489
+ id: entry,
82490
+ requester
82463
82491
  } = e.detail;
82464
82492
  const isThere = uiState.some(item => compareTwoJsValues(item, entry));
82465
82493
  if (goal === "select" ? isThere : !isThere) {
@@ -82474,6 +82502,11 @@ const requestPickerListEntry = (pickerEl, pickerInputEl, e, goal) => {
82474
82502
  dispatchRequestSetUIState(pickerInputEl, uiStateNext, {
82475
82503
  event: e
82476
82504
  });
82505
+ sendClosedPickerChange(pickerInputEl, {
82506
+ event: e,
82507
+ name: goal,
82508
+ requester
82509
+ });
82477
82510
  }
82478
82511
  });
82479
82512
  };
@@ -93963,6 +93996,10 @@ const css$5 = /* css */`@keyframes navi_image_shimmer {
93963
93996
  `;
93964
93997
  const DEFAULT_PLACEHOLDER_LIGHT = "#d4d8dd";
93965
93998
  const DEFAULT_PLACEHOLDER_DARK = "#374151";
93999
+ // On an <img>, width/height describe the image: they stay HTML attributes.
94000
+ // Box would write them as inline styles, which beat every stylesheet rule, so
94001
+ // an image given its file's size (3840×2160) would overflow the box sizing it.
94002
+ const IMAGE_ATTRIBUTE_PROP_SET = new Set(["width", "height"]);
93966
94003
 
93967
94004
  /**
93968
94005
  * The props may come in any order: `src` and `srcset` reach the element after
@@ -93971,6 +94008,12 @@ const DEFAULT_PLACEHOLDER_DARK = "#374151";
93971
94008
  * decides whether to load when `src` is set, and an image whose `src` came
93972
94009
  * first loads, lazy or not.
93973
94010
  *
94011
+ * @param {number} [props.width] - The image's width in pixels, written as the
94012
+ * `<img>` attribute, like `height`. Given both, the browser reserves the
94013
+ * aspect ratio before the bytes arrive. They size the element only while no
94014
+ * stylesheet rule does: a box that holds the image sizes it with CSS (or
94015
+ * `style`) and keeps the ratio.
94016
+ * @param {number} [props.height] - The image's height in pixels, see `width`.
93974
94017
  * @param {string|false} [props.placeholderColor] - Background color shown while the image loads.
93975
94018
  * Defaults to a light gray. Pass `false` to disable.
93976
94019
  * @param {boolean} [props.placeholderDark] - Use a dark default placeholder color,
@@ -93995,6 +94038,7 @@ const Image = ({
93995
94038
  ...rest,
93996
94039
  as: "img",
93997
94040
  baseClassName: "navi_image",
94041
+ childPropSet: IMAGE_ATTRIBUTE_PROP_SET,
93998
94042
  "navi-placeholder": loadedRef.current ? undefined : "",
93999
94043
  style: {
94000
94044
  "--placeholder-color": resolvedPlaceholder || undefined,