@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.
@@ -40608,6 +40608,59 @@ const callLiftOverCallbacks = () => {
40608
40608
  });
40609
40609
  };
40610
40610
 
40611
+ /**
40612
+ * What a gesture changes in a closed picker is sent at once.
40613
+ *
40614
+ * A picker's `action` waits for its popup to close: what is done in there is
40615
+ * one answer, given when the person is done. A change made while the popup is
40616
+ * closed — the clear cross, a chip's cross, a paste, a cut, a value proposed
40617
+ * from beside it (`--navi-update`) — has no close coming, so nothing would ever
40618
+ * send it: the value moves on screen and the record behind it stays. Each of
40619
+ * those gestures calls this right after setting the value; a failing action
40620
+ * puts the value back (resetOnError), as it does after a close.
40621
+ *
40622
+ * Called by gestures only. A value set without one — a bound signal followed, a
40623
+ * cancel or a failure rolled back — is the picker being told, not asked, which
40624
+ * is why this is not a reaction to every value set on a closed picker.
40625
+ *
40626
+ * The action is requested the way the close requests it, not through
40627
+ * `--navi-send`: a send also answers the surface around the control (a slide
40628
+ * moves on, an open dialog closes — see resolveAfterSend in commands.js), and a
40629
+ * cross pressed on a picker in a dialog is not the dialog being answered.
40630
+ */
40631
+
40632
+
40633
+ const sendClosedPickerChange = (element, { event, name, requester }) => {
40634
+ const controlHost = findControlHost(element) || element;
40635
+ const controlRoot = findControlRoot(controlHost);
40636
+ if (!controlRoot || controlRoot.getAttribute("navi-control") !== "picker") {
40637
+ return;
40638
+ }
40639
+ if (controlRoot.getAttribute("aria-expanded") === "true") {
40640
+ return;
40641
+ }
40642
+ const { props } = controlHost.__uiStateController__;
40643
+ if (!props.action) {
40644
+ return;
40645
+ }
40646
+ // A picker that picks nothing (`type="confirm"`, a menu of actions) holds no
40647
+ // value its action would be sending: that action is the yes of its popup,
40648
+ // and a paste landing on the trigger must not say it.
40649
+ if (props.picksNothing) {
40650
+ return;
40651
+ }
40652
+ // A source with an action of its own made the request already: `<Button
40653
+ // action={remove} command="--navi-clear">` runs the removal, and the command
40654
+ // that follows only brings the picker in line with it. Sending the picker's
40655
+ // action on top would ask twice.
40656
+ if (requester?.__uiStateController__?.props.action) {
40657
+ return;
40658
+ }
40659
+ // `requester`: the source of a command may have claimed the press to send
40660
+ // this very change (see isAimedAtSelfInteractionsBelow).
40661
+ dispatchRequestAction(controlHost, { event, name, requester });
40662
+ };
40663
+
40611
40664
  /**
40612
40665
  * Runs a navi command from JS. It is the LAST RESORT: the attributes say the
40613
40666
  * same thing declaratively and go through this very function, so a press that
@@ -40999,6 +41052,11 @@ registerNaviCommand("--navi-update", (source, event, { argument }) => {
40999
41052
  event,
41000
41053
  behavior: argument,
41001
41054
  });
41055
+ sendClosedPickerChange(target, {
41056
+ event,
41057
+ name: "--navi-update",
41058
+ requester: source,
41059
+ });
41002
41060
  },
41003
41061
  });
41004
41062
  },
@@ -41011,20 +41069,6 @@ registerNaviCommand("--navi-clear", (source, event) => {
41011
41069
  return undefined;
41012
41070
  }
41013
41071
  const fromInput = source.closest(`[navi-control="input"]`);
41014
- // A control that commits on an explicit send — a picker, whose list sends the
41015
- // moment a value is chosen — has nothing that would commit a clear: its
41016
- // action never runs on a ui state change. Left alone, the field goes empty
41017
- // while the caller still holds the value it gave, and renders it right back.
41018
- //
41019
- // Unless the source committed the clear itself: a clear button given its own
41020
- // action (`<Button action={remove} command="--navi-clear">`) already made the
41021
- // request — the ui state is being brought in line with what just happened, and
41022
- // sending the picker's own action on top would ask twice.
41023
- const sourceController = source.__uiStateController__;
41024
- const fromSendOnlyControl = Boolean(
41025
- source.closest?.(`[navi-control=picker]`) &&
41026
- !sourceController?.props.action,
41027
- );
41028
41072
 
41029
41073
  const performClear = (clearEvent) => {
41030
41074
  dispatchRequestInteraction(target, {
@@ -41035,46 +41079,11 @@ registerNaviCommand("--navi-clear", (source, event) => {
41035
41079
  requester: source,
41036
41080
  prevented: () => clearEvent.preventDefault(),
41037
41081
  allowed: () => {
41038
- // What the control holds, before it holds nothing: the clear is
41039
- // optimistic — the field empties now and the send that commits it may
41040
- // still fail — so what it emptied has to be kept to be put back.
41041
- const uiStateBefore = getUIStateFromElement(target);
41042
41082
  dispatchRequestClearUIState(target, clearEvent);
41043
- if (!fromSendOnlyControl) {
41044
- return;
41045
- }
41046
- // After the clear, never before: the action is bound to the ui
41047
- // state signal, so this sends the value the control now holds.
41048
- const actionHost = findControlHost(target) || target;
41049
- const completion = watchActionCompletion(actionHost, () => {
41050
- triggerNaviCommand(source, "--navi-send", clearEvent, {
41051
- optional: true,
41052
- });
41053
- });
41054
- completion.whenSettled(({ error, aborted }) => {
41055
- if (!error && !aborted) {
41056
- return;
41057
- }
41058
- // The removal did not happen, so the field must stop saying it did.
41059
- // The error itself stays where the action put it — on the control,
41060
- // which is still there, unlike the cross that has just gone with the
41061
- // value it cleared (see addErrorMessage in use_execute_action.js).
41062
- //
41063
- // Put back from the inside ("clear_rollback" is an internal event
41064
- // type, see ui_state_controller.js) rather than asked for the way a
41065
- // user would: nobody acted, the control is being returned to the
41066
- // state its caller still holds. Asked from the outside it would be
41067
- // answered with "this element is busy" — the action is still
41068
- // settling — over the very error that explains why the value is back.
41069
- const controller = actionHost.__uiStateController__;
41070
- if (!controller) {
41071
- return;
41072
- }
41073
- const rollbackEvent = new CustomEvent("clear_rollback", {
41074
- detail: {},
41075
- });
41076
- chainEvent(rollbackEvent, clearEvent);
41077
- controller.setUIState(uiStateBefore, rollbackEvent);
41083
+ sendClosedPickerChange(target, {
41084
+ event: clearEvent,
41085
+ name: "--navi-clear",
41086
+ requester: source,
41078
41087
  });
41079
41088
  },
41080
41089
  });
@@ -41814,6 +41823,7 @@ registerNaviCommand("--navi-select", (source, event) => {
41814
41823
  return dispatchCustomEvent(target, "navi_request_select", {
41815
41824
  event,
41816
41825
  id: resolveCommandValue(source, event),
41826
+ requester: source,
41817
41827
  });
41818
41828
  },
41819
41829
  };
@@ -41830,6 +41840,7 @@ registerNaviCommand("--navi-unselect", (source, event) => {
41830
41840
  return dispatchCustomEvent(target, "navi_request_unselect", {
41831
41841
  event,
41832
41842
  id: resolveCommandValue(source, event),
41843
+ requester: source,
41833
41844
  });
41834
41845
  },
41835
41846
  };
@@ -45733,12 +45744,6 @@ const INTERNAL_EVENT_SET = new Set([
45733
45744
  // on registration, and group pushing value/defaultValue to children on registerChild.
45734
45745
  // Equivalent to defaultValue initialization: no uiAction, no commands, no parent notification.
45735
45746
  "initial_state_push",
45736
- // navi undoing its own optimistic write: the clear cross emptied the control
45737
- // before the send that commits it, the send failed, and the value it emptied
45738
- // goes back where it was (see the --navi-clear command). Nothing acted — the
45739
- // control is being put back on the state the caller still holds — so this
45740
- // must not fire a uiAction, a command, or a report on the way.
45741
- "clear_rollback",
45742
45747
  // A picker's popup closed on a cancel (the no button, Escape, a click
45743
45748
  // outside): what the picker held at open goes back. Nobody acted — no
45744
45749
  // command — but the value really did move back, so uiAction and the parent
@@ -52062,11 +52067,15 @@ const LinkWithRoute = ({
52062
52067
  also: currentAlso
52063
52068
  });
52064
52069
  const innerCurrent = current || linkMatching;
52070
+
52071
+ // What a childless link shows is LinkPlain's call (hrefFallback); a route
52072
+ // only says it the way its pattern writes it, not as the resolved href.
52065
52073
  return jsx(Link, {
52066
52074
  href: url,
52075
+ hrefText: route.buildRelativeUrl(routeParams),
52067
52076
  current: innerCurrent,
52068
52077
  ...rest,
52069
- children: children || route.buildRelativeUrl(routeParams)
52078
+ children: children
52070
52079
  });
52071
52080
  };
52072
52081
  const LinkPlain = props => {
@@ -52092,6 +52101,7 @@ const LinkPlain = props => {
52092
52101
  revealOnInteraction = false,
52093
52102
  stretch,
52094
52103
  hrefFallback = !anchor,
52104
+ hrefText = href,
52095
52105
  routeTransition,
52096
52106
  pressableDuringRouteTransition,
52097
52107
  replace,
@@ -52227,7 +52237,7 @@ const LinkPlain = props => {
52227
52237
  const documentRequest = isDocument ? {
52228
52238
  [LINK_DOCUMENT_ATTRIBUTE]: ""
52229
52239
  } : null;
52230
- const innerChildren = children || (hrefFallback ? href : children);
52240
+ const innerChildren = children || (hrefFallback ? hrefText : children);
52231
52241
  const startIconEl = startIcon;
52232
52242
  const endIconEl = innerEndIcon;
52233
52243
 
@@ -52278,6 +52288,7 @@ const LinkPlain = props => {
52278
52288
  startIcon: undefined,
52279
52289
  endIcon: undefined,
52280
52290
  hrefFallback: undefined,
52291
+ hrefText: undefined,
52281
52292
  routeTransition: undefined,
52282
52293
  pressableDuringRouteTransition: undefined,
52283
52294
  replace: undefined,
@@ -82760,7 +82771,9 @@ const PickerArrayUI = /*#__PURE__*/asPickerOwnUI(() => {
82760
82771
  /**
82761
82772
  * One value the picker holds, drawn as a chip with a cross that takes it back
82762
82773
  * out. Sits wherever the application draws what was picked — on the picker's
82763
- * façade (`ui`) or inside its popup — and both behave the same.
82774
+ * façade (`ui`) or inside its popup. Inside the open popup the removal waits
82775
+ * for the close like any change made there; pressed while the popup is closed
82776
+ * it is the whole gesture, and the picker's `action` runs at once.
82764
82777
  *
82765
82778
  * The cross asks with `--navi-unselect` rather than writing a new list, and it
82766
82779
  * asks the picker — which holds what was picked, and hands it down to whatever
@@ -83792,15 +83805,25 @@ const PickerButton = props => {
83792
83805
  const displayText = pickerEl.querySelector(".navi_picker_value")?.textContent ?? String(uiState);
83793
83806
  e.clipboardData.setData("text/plain", displayText);
83794
83807
  e.clipboardData.setData("application/x-navi", JSON.stringify(uiState));
83808
+ e.preventDefault();
83809
+ // A picker that asks before its cross clears anything is not
83810
+ // emptied by a keystroke without the question: the cut copies,
83811
+ // and the value stays.
83812
+ if (clearConfirm !== undefined) {
83813
+ return;
83814
+ }
83795
83815
  // the clear ui state part need control to be interactable
83796
83816
  dispatchRequestInteraction(pickerEl, {
83797
83817
  event: e,
83798
83818
  name: "cut",
83799
83819
  allowed: () => {
83800
83820
  dispatchRequestClearUIState(inputRef.current, e);
83821
+ sendClosedPickerChange(inputRef.current, {
83822
+ event: e,
83823
+ name: "cut"
83824
+ });
83801
83825
  }
83802
83826
  });
83803
- e.preventDefault();
83804
83827
  },
83805
83828
  onPaste: e => {
83806
83829
  const pickerEl = ref.current;
@@ -83826,6 +83849,10 @@ const PickerButton = props => {
83826
83849
  dispatchRequestSetUIState(inputRef.current, pasteValue, {
83827
83850
  event: e
83828
83851
  });
83852
+ sendClosedPickerChange(inputRef.current, {
83853
+ event: e,
83854
+ name: "paste"
83855
+ });
83829
83856
  }
83830
83857
  });
83831
83858
  e.preventDefault();
@@ -84056,7 +84083,8 @@ const requestPickerListEntry = (pickerEl, pickerInputEl, e, goal) => {
84056
84083
  return;
84057
84084
  }
84058
84085
  const {
84059
- id: entry
84086
+ id: entry,
84087
+ requester
84060
84088
  } = e.detail;
84061
84089
  const isThere = uiState.some(item => compareTwoJsValues(item, entry));
84062
84090
  if (goal === "select" ? isThere : !isThere) {
@@ -84071,6 +84099,11 @@ const requestPickerListEntry = (pickerEl, pickerInputEl, e, goal) => {
84071
84099
  dispatchRequestSetUIState(pickerInputEl, uiStateNext, {
84072
84100
  event: e
84073
84101
  });
84102
+ sendClosedPickerChange(pickerInputEl, {
84103
+ event: e,
84104
+ name: goal,
84105
+ requester
84106
+ });
84074
84107
  }
84075
84108
  });
84076
84109
  };
@@ -84543,7 +84576,9 @@ const PickerFirstResolver = props => {
84543
84576
  * the `<Picker type="confirm">` the cross then is, plain text or JSX. Asked
84544
84577
  * BEFORE the ui state is emptied, so answering no leaves the field exactly
84545
84578
  * as it was; answering yes clears and sends, and the picker's own action
84546
- * receives the cleared value like any other choice.
84579
+ * receives the cleared value like any other choice. A cut (Ctrl+X) on such a
84580
+ * picker copies the value and leaves it in place: no clear without the
84581
+ * question.
84547
84582
  * @param {"icon"|"headless"|"discrete"|"button"|"text"|"bare"|"picker"} [variant]
84548
84583
  * How the trigger is drawn. `"button"` is a Button's drawing — surface,
84549
84584
  * padding, a centered label, no chevron — and what a `type="confirm"` picker
@@ -96218,6 +96253,10 @@ const css$5 = /* css */`@keyframes navi_image_shimmer {
96218
96253
  `;
96219
96254
  const DEFAULT_PLACEHOLDER_LIGHT = "#d4d8dd";
96220
96255
  const DEFAULT_PLACEHOLDER_DARK = "#374151";
96256
+ // On an <img>, width/height describe the image: they stay HTML attributes.
96257
+ // Box would write them as inline styles, which beat every stylesheet rule, so
96258
+ // an image given its file's size (3840×2160) would overflow the box sizing it.
96259
+ const IMAGE_ATTRIBUTE_PROP_SET = new Set(["width", "height"]);
96221
96260
 
96222
96261
  /**
96223
96262
  * The props may come in any order: `src` and `srcset` reach the element after
@@ -96226,6 +96265,12 @@ const DEFAULT_PLACEHOLDER_DARK = "#374151";
96226
96265
  * decides whether to load when `src` is set, and an image whose `src` came
96227
96266
  * first loads, lazy or not.
96228
96267
  *
96268
+ * @param {number} [props.width] - The image's width in pixels, written as the
96269
+ * `<img>` attribute, like `height`. Given both, the browser reserves the
96270
+ * aspect ratio before the bytes arrive. They size the element only while no
96271
+ * stylesheet rule does: a box that holds the image sizes it with CSS (or
96272
+ * `style`) and keeps the ratio.
96273
+ * @param {number} [props.height] - The image's height in pixels, see `width`.
96229
96274
  * @param {string|false} [props.placeholderColor] - Background color shown while the image loads.
96230
96275
  * Defaults to a light gray. Pass `false` to disable.
96231
96276
  * @param {boolean} [props.placeholderDark] - Use a dark default placeholder color,
@@ -96250,6 +96295,7 @@ const Image = ({
96250
96295
  ...rest,
96251
96296
  as: "img",
96252
96297
  baseClassName: "navi_image",
96298
+ childPropSet: IMAGE_ATTRIBUTE_PROP_SET,
96253
96299
  "navi-placeholder": loadedRef.current ? undefined : "",
96254
96300
  style: {
96255
96301
  "--placeholder-color": resolvedPlaceholder || undefined,