@jsenv/navi 0.29.440 → 0.29.442

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.
@@ -12814,6 +12814,53 @@ const findControlProxy = (el) => {
12814
12814
  return null;
12815
12815
  };
12816
12816
 
12817
+ /**
12818
+ * Tells a typed field's control that the user gave up on what they typed:
12819
+ * `navi_cancel` with its reason — Escape, or leaving the field invalid. What
12820
+ * giving up means is the control's to decide (onnavi_cancel in
12821
+ * control_hooks.jsx: resetOnCancel, cancelOnEscape, cancelOnBlurInvalid).
12822
+ *
12823
+ * Leaving a field empty is not a cancel: the "change" of that same blur has
12824
+ * already sent the empty value. A field that must not be left empty says
12825
+ * `required`, and leaving it is then a blur_invalid.
12826
+ */
12827
+ const addInputCancel = (input, controller) => {
12828
+ const dispatchCancel = (detail) => {
12829
+ dispatchInternalCustomEvent(input, "navi_cancel", detail);
12830
+ };
12831
+ const onkeydown = (e) => {
12832
+ if (e.key !== "Escape") {
12833
+ return;
12834
+ }
12835
+ if (controller.rules.callout.callout) {
12836
+ // Escape closes the open callout first (callout.js listens on its anchor).
12837
+ return;
12838
+ }
12839
+ dispatchCancel({ event: e, reason: "escape_key" });
12840
+ };
12841
+ const onblur = (e) => {
12842
+ if (input.closest("[inert]")) {
12843
+ // The page took the field away (an edition ending, a side swapped out)
12844
+ // and the focus went with it: the user did not leave it.
12845
+ return;
12846
+ }
12847
+ const { failedConstraintInfo } = controller.rules.validation;
12848
+ if (failedConstraintInfo) {
12849
+ dispatchCancel({
12850
+ event: e,
12851
+ reason: "blur_invalid",
12852
+ failedConstraintInfo,
12853
+ });
12854
+ }
12855
+ };
12856
+ input.addEventListener("keydown", onkeydown);
12857
+ input.addEventListener("blur", onblur);
12858
+ return () => {
12859
+ input.removeEventListener("keydown", onkeydown);
12860
+ input.removeEventListener("blur", onblur);
12861
+ };
12862
+ };
12863
+
12817
12864
  const addInputEffect = (
12818
12865
  input,
12819
12866
  callback,
@@ -13002,11 +13049,15 @@ const listenInputStateChange = (
13002
13049
  callback(e);
13003
13050
  };
13004
13051
  input.addEventListener("input", oninput);
13005
- input.addEventListener("keydown", onkeydown);
13052
+ // Capture: the guard is set before the control reacts to the key. Its
13053
+ // reaction can end what the field is in (Enter sends, Escape cancels, an
13054
+ // edition closes and hands the focus away), and the "change" of that blur
13055
+ // arrives in the microtasks between two listeners of this keydown.
13056
+ input.addEventListener("keydown", onkeydown, { capture: true });
13006
13057
  input.addEventListener("change", onchange);
13007
13058
  addTeardown(() => {
13008
13059
  input.removeEventListener("input", oninput);
13009
- input.removeEventListener("keydown", onkeydown);
13060
+ input.removeEventListener("keydown", onkeydown, { capture: true });
13010
13061
  input.removeEventListener("change", onchange);
13011
13062
  });
13012
13063
 
@@ -45537,16 +45588,19 @@ const useControlProps = (props, {
45537
45588
  // ref so the effect always fires the current render's reaction.
45538
45589
  const applyEventReactionRef = useRef();
45539
45590
  applyEventReactionRef.current = applyEventReaction;
45591
+ const isTypedField = controlType === "input" && !isCheckable && props.type !== "range";
45540
45592
  const refCallback = useCallback(field => {
45541
- if (!hasNaviChangeEventReaction || actionEvent === "custom") {
45542
- return undefined;
45543
- }
45544
- return addInputEffect(field, e => applyEventReactionRef.current("naviChange", e), {
45593
+ const removeInputCancel = isTypedField ? addInputCancel(field, uiStateController) : undefined;
45594
+ const removeInputEffect = hasNaviChangeEventReaction && actionEvent !== "custom" ? addInputEffect(field, e => applyEventReactionRef.current("naviChange", e), {
45545
45595
  waitForChange: actionAfterChange,
45546
45596
  debounce: actionDebounce,
45547
45597
  debugInteraction
45548
- });
45549
- }, [actionEvent, actionAfterChange, actionDebounce, hasNaviChangeEventReaction, ref]);
45598
+ }) : undefined;
45599
+ return () => {
45600
+ removeInputCancel?.();
45601
+ removeInputEffect?.();
45602
+ };
45603
+ }, [isTypedField, actionEvent, actionAfterChange, actionDebounce, hasNaviChangeEventReaction, ref]);
45550
45604
  const refComposed = useComposeElementRef(refCallback, ref);
45551
45605
  const onPaste = e => {
45552
45606
  props.onPaste?.(e);
@@ -46488,7 +46542,7 @@ const useInteractiveProps = (props, {
46488
46542
  }
46489
46543
  if (
46490
46544
  // error prevent cancellation until the user closes it (or something closes it)
46491
- e.detail.failedConstraintInfo.level === "error" && e.detail.failedConstraintInfo.reportStatus !== "closed") {
46545
+ e.detail.failedConstraintInfo.status === "error" && e.detail.failedConstraintInfo.reportStatus !== "closed") {
46492
46546
  return;
46493
46547
  }
46494
46548
  }
@@ -60075,21 +60129,51 @@ const Editable = props => {
60075
60129
  editingPreviousRef.current = editing;
60076
60130
  }
60077
60131
 
60078
- // Simulate typing the initial value when editing starts with a custom value
60132
+ // The field is mounted the whole time, so edition starting is when it
60133
+ // arrives, and Editable places the focus itself: a mount-time autofocus
60134
+ // would only ever see the first `editing`. Starting edition is a press aimed
60135
+ // at typing, so the field takes the keyboard whatever asked for it — a
60136
+ // click, Enter, a letter key.
60137
+ const focusedBeforeEditionRef = useRef(null);
60079
60138
  useLayoutEffect(() => {
60139
+ const field = ref.current;
60080
60140
  if (!editing) {
60141
+ const focusedBeforeEdition = focusedBeforeEditionRef.current;
60142
+ focusedBeforeEditionRef.current = null;
60143
+ if (!focusedBeforeEdition || !focusedBeforeEdition.isConnected) {
60144
+ return;
60145
+ }
60146
+ // The field just went inert while holding the focus (Enter, Escape, the
60147
+ // action ending): the focus goes back to where edition was asked from.
60148
+ // Focus the user took away, the blur that ended edition, stays where
60149
+ // they put it — on the body included.
60150
+ if (document.activeElement !== field) {
60151
+ return;
60152
+ }
60153
+ moveFocusTo(focusedBeforeEdition);
60081
60154
  return;
60082
60155
  }
60083
- const editingEvent = editing.event;
60084
- if (editingEvent) {
60085
- const editingEventInitialValue = editingEvent.detail?.initialValue;
60086
- if (editingEventInitialValue !== undefined) {
60087
- const input = ref.current;
60088
- input.value = editingEventInitialValue;
60089
- input.dispatchEvent(new CustomEvent("input", {
60090
- bubbles: false
60091
- }));
60092
- }
60156
+ focusedBeforeEditionRef.current = document.activeElement;
60157
+ if (!valueSignal) {
60158
+ // An edition starts from the value, not from what a cancelled one left
60159
+ // typed in the field. Done before the focus: written into a focused
60160
+ // field, it would read as a change to send (see input_effect.js).
60161
+ dispatchRequestResetUIState(field, editing.event);
60162
+ }
60163
+ moveFocusTo(field);
60164
+ const initialValue = editing.event?.detail?.initialValue;
60165
+ if (initialValue !== undefined) {
60166
+ // The key that started edition is its first keystroke.
60167
+ field.value = initialValue;
60168
+ field.dispatchEvent(new CustomEvent("input", {
60169
+ bubbles: false
60170
+ }));
60171
+ return;
60172
+ }
60173
+ if (autoFocusSelect) {
60174
+ field.select();
60175
+ // Keep the beginning of the text visible instead of scrolling to the end
60176
+ field.scrollLeft = 0;
60093
60177
  }
60094
60178
  }, [editing]);
60095
60179
  const controlProps = {
@@ -60098,9 +60182,6 @@ const Editable = props => {
60098
60182
  name,
60099
60183
  value,
60100
60184
  valueSignal,
60101
- autoFocus: editing,
60102
- autoFocusVisible: true,
60103
- autoFocusSelect,
60104
60185
  cancelOnEscape: true,
60105
60186
  cancelOnBlurInvalid: true,
60106
60187
  constraints,
@@ -60120,6 +60201,11 @@ const Editable = props => {
60120
60201
  });
60121
60202
  },
60122
60203
  onBlur: e => {
60204
+ if (!editing) {
60205
+ // The focus handed back once edition ended (see the effect above):
60206
+ // the edition is already over, this blur is its consequence.
60207
+ return;
60208
+ }
60123
60209
  let inputValue;
60124
60210
  const valueWhenEditStart = valueWhenEditStartRef.current;
60125
60211
  let inputValueWhenEditStart;
@@ -60186,7 +60272,7 @@ const Editable = props => {
60186
60272
  // - input not focusable (via keyboard or anything)
60187
60273
  // - cannot be interacted with pointer (click, hover, etc)
60188
60274
  // - is ignored by screen readers
60189
- inert: editing ? undefined : "",
60275
+ inert: !editing,
60190
60276
  "data-editing": editing ? "" : undefined,
60191
60277
  children: control
60192
60278
  })]
@@ -90761,20 +90847,33 @@ const Table = props => {
90761
90847
  columns,
90762
90848
  canChangeColumnOrder: Boolean(onColumnOrderChange)
90763
90849
  });
90850
+
90851
+ // Edition starts from a cell holding the focus itself. Once started, the
90852
+ // focus is in the cell's field and its keys are the field's: an Enter there
90853
+ // validates, it must not start edition again.
90854
+ const canStartEditing = () => dragContextValue.grabTarget === null && document.activeElement.tagName === "TD";
90855
+ // The key starting edition is its first keystroke, typed by Editable
90856
+ // (initialValue). Its default is prevented: the browser would type it a
90857
+ // second time into the field that takes the focus during this keydown.
90858
+ const startEditingWithKey = e => {
90859
+ e.preventDefault();
90860
+ document.activeElement.dispatchEvent(new CustomEvent("editrequested", {
90861
+ bubbles: false,
90862
+ detail: {
90863
+ initialValue: e.key
90864
+ }
90865
+ }));
90866
+ return true;
90867
+ };
90764
90868
  useKeyboardShortcuts(ref, [...createSelectionKeyboardShortcuts(selectionController, {
90765
90869
  toggleEnabled: true,
90766
90870
  enabled: () => dragContextValue.grabTarget === null
90767
90871
  }), {
90768
90872
  key: "enter",
90769
90873
  description: "Edit table cell content",
90770
- enabled: () => dragContextValue.grabTarget === null,
90874
+ enabled: canStartEditing,
90771
90875
  handler: () => {
90772
- // Find the currently focused cell
90773
- const activeCell = document.activeElement.closest("td");
90774
- if (!activeCell) {
90775
- return false;
90776
- }
90777
- activeCell.dispatchEvent(new CustomEvent("editrequested", {
90876
+ document.activeElement.dispatchEvent(new CustomEvent("editrequested", {
90778
90877
  bubbles: false
90779
90878
  }));
90780
90879
  return true;
@@ -90782,37 +90881,13 @@ const Table = props => {
90782
90881
  }, {
90783
90882
  key: "a-z",
90784
90883
  description: "Start editing table cell content",
90785
- enabled: () => dragContextValue.grabTarget === null,
90786
- handler: e => {
90787
- const activeCell = document.activeElement.closest("td");
90788
- if (!activeCell) {
90789
- return false;
90790
- }
90791
- activeCell.dispatchEvent(new CustomEvent("editrequested", {
90792
- bubbles: false,
90793
- detail: {
90794
- initialValue: e.key
90795
- }
90796
- }));
90797
- return true;
90798
- }
90884
+ enabled: canStartEditing,
90885
+ handler: startEditingWithKey
90799
90886
  }, {
90800
90887
  key: "0-9",
90801
90888
  description: "Start editing table cell content",
90802
- enabled: () => dragContextValue.grabTarget === null,
90803
- handler: e => {
90804
- const activeCell = document.activeElement.closest("td");
90805
- if (!activeCell) {
90806
- return false;
90807
- }
90808
- activeCell.dispatchEvent(new CustomEvent("editrequested", {
90809
- bubbles: false,
90810
- detail: {
90811
- initialValue: e.key
90812
- }
90813
- }));
90814
- return true;
90815
- }
90889
+ enabled: canStartEditing,
90890
+ handler: startEditingWithKey
90816
90891
  }]);
90817
90892
 
90818
90893
  // resizing