@jsenv/navi 0.29.440 → 0.29.441

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.
@@ -13381,6 +13381,53 @@ const findControlProxy = (el) => {
13381
13381
  return null;
13382
13382
  };
13383
13383
 
13384
+ /**
13385
+ * Tells a typed field's control that the user gave up on what they typed:
13386
+ * `navi_cancel` with its reason — Escape, or leaving the field invalid. What
13387
+ * giving up means is the control's to decide (onnavi_cancel in
13388
+ * control_hooks.jsx: resetOnCancel, cancelOnEscape, cancelOnBlurInvalid).
13389
+ *
13390
+ * Leaving a field empty is not a cancel: the "change" of that same blur has
13391
+ * already sent the empty value. A field that must not be left empty says
13392
+ * `required`, and leaving it is then a blur_invalid.
13393
+ */
13394
+ const addInputCancel = (input, controller) => {
13395
+ const dispatchCancel = (detail) => {
13396
+ dispatchInternalCustomEvent(input, "navi_cancel", detail);
13397
+ };
13398
+ const onkeydown = (e) => {
13399
+ if (e.key !== "Escape") {
13400
+ return;
13401
+ }
13402
+ if (controller.rules.callout.callout) {
13403
+ // Escape closes the open callout first (callout.js listens on its anchor).
13404
+ return;
13405
+ }
13406
+ dispatchCancel({ event: e, reason: "escape_key" });
13407
+ };
13408
+ const onblur = (e) => {
13409
+ if (input.closest("[inert]")) {
13410
+ // The page took the field away (an edition ending, a side swapped out)
13411
+ // and the focus went with it: the user did not leave it.
13412
+ return;
13413
+ }
13414
+ const { failedConstraintInfo } = controller.rules.validation;
13415
+ if (failedConstraintInfo) {
13416
+ dispatchCancel({
13417
+ event: e,
13418
+ reason: "blur_invalid",
13419
+ failedConstraintInfo,
13420
+ });
13421
+ }
13422
+ };
13423
+ input.addEventListener("keydown", onkeydown);
13424
+ input.addEventListener("blur", onblur);
13425
+ return () => {
13426
+ input.removeEventListener("keydown", onkeydown);
13427
+ input.removeEventListener("blur", onblur);
13428
+ };
13429
+ };
13430
+
13384
13431
  const addInputEffect = (
13385
13432
  input,
13386
13433
  callback,
@@ -13569,11 +13616,15 @@ const listenInputStateChange = (
13569
13616
  callback(e);
13570
13617
  };
13571
13618
  input.addEventListener("input", oninput);
13572
- input.addEventListener("keydown", onkeydown);
13619
+ // Capture: the guard is set before the control reacts to the key. Its
13620
+ // reaction can end what the field is in (Enter sends, Escape cancels, an
13621
+ // edition closes and hands the focus away), and the "change" of that blur
13622
+ // arrives in the microtasks between two listeners of this keydown.
13623
+ input.addEventListener("keydown", onkeydown, { capture: true });
13573
13624
  input.addEventListener("change", onchange);
13574
13625
  addTeardown(() => {
13575
13626
  input.removeEventListener("input", oninput);
13576
- input.removeEventListener("keydown", onkeydown);
13627
+ input.removeEventListener("keydown", onkeydown, { capture: true });
13577
13628
  input.removeEventListener("change", onchange);
13578
13629
  });
13579
13630
 
@@ -46704,16 +46755,19 @@ const useControlProps = (props, {
46704
46755
  // ref so the effect always fires the current render's reaction.
46705
46756
  const applyEventReactionRef = useRef();
46706
46757
  applyEventReactionRef.current = applyEventReaction;
46758
+ const isTypedField = controlType === "input" && !isCheckable && props.type !== "range";
46707
46759
  const refCallback = useCallback(field => {
46708
- if (!hasNaviChangeEventReaction || actionEvent === "custom") {
46709
- return undefined;
46710
- }
46711
- return addInputEffect(field, e => applyEventReactionRef.current("naviChange", e), {
46760
+ const removeInputCancel = isTypedField ? addInputCancel(field, uiStateController) : undefined;
46761
+ const removeInputEffect = hasNaviChangeEventReaction && actionEvent !== "custom" ? addInputEffect(field, e => applyEventReactionRef.current("naviChange", e), {
46712
46762
  waitForChange: actionAfterChange,
46713
46763
  debounce: actionDebounce,
46714
46764
  debugInteraction
46715
- });
46716
- }, [actionEvent, actionAfterChange, actionDebounce, hasNaviChangeEventReaction, ref]);
46765
+ }) : undefined;
46766
+ return () => {
46767
+ removeInputCancel?.();
46768
+ removeInputEffect?.();
46769
+ };
46770
+ }, [isTypedField, actionEvent, actionAfterChange, actionDebounce, hasNaviChangeEventReaction, ref]);
46717
46771
  const refComposed = useComposeElementRef(refCallback, ref);
46718
46772
  const onPaste = e => {
46719
46773
  props.onPaste?.(e);
@@ -47676,7 +47730,7 @@ const useInteractiveProps = (props, {
47676
47730
  }
47677
47731
  if (
47678
47732
  // error prevent cancellation until the user closes it (or something closes it)
47679
- e.detail.failedConstraintInfo.level === "error" && e.detail.failedConstraintInfo.reportStatus !== "closed") {
47733
+ e.detail.failedConstraintInfo.status === "error" && e.detail.failedConstraintInfo.reportStatus !== "closed") {
47680
47734
  return;
47681
47735
  }
47682
47736
  }
@@ -61287,21 +61341,51 @@ const Editable = props => {
61287
61341
  editingPreviousRef.current = editing;
61288
61342
  }
61289
61343
 
61290
- // Simulate typing the initial value when editing starts with a custom value
61344
+ // The field is mounted the whole time, so edition starting is when it
61345
+ // arrives, and Editable places the focus itself: a mount-time autofocus
61346
+ // would only ever see the first `editing`. Starting edition is a press aimed
61347
+ // at typing, so the field takes the keyboard whatever asked for it — a
61348
+ // click, Enter, a letter key.
61349
+ const focusedBeforeEditionRef = useRef(null);
61291
61350
  useLayoutEffect(() => {
61351
+ const field = ref.current;
61292
61352
  if (!editing) {
61353
+ const focusedBeforeEdition = focusedBeforeEditionRef.current;
61354
+ focusedBeforeEditionRef.current = null;
61355
+ if (!focusedBeforeEdition || !focusedBeforeEdition.isConnected) {
61356
+ return;
61357
+ }
61358
+ // The field just went inert while holding the focus (Enter, Escape, the
61359
+ // action ending): the focus goes back to where edition was asked from.
61360
+ // Focus the user took away, the blur that ended edition, stays where
61361
+ // they put it — on the body included.
61362
+ if (document.activeElement !== field) {
61363
+ return;
61364
+ }
61365
+ moveFocusTo(focusedBeforeEdition);
61293
61366
  return;
61294
61367
  }
61295
- const editingEvent = editing.event;
61296
- if (editingEvent) {
61297
- const editingEventInitialValue = editingEvent.detail?.initialValue;
61298
- if (editingEventInitialValue !== undefined) {
61299
- const input = ref.current;
61300
- input.value = editingEventInitialValue;
61301
- input.dispatchEvent(new CustomEvent("input", {
61302
- bubbles: false
61303
- }));
61304
- }
61368
+ focusedBeforeEditionRef.current = document.activeElement;
61369
+ if (!valueSignal) {
61370
+ // An edition starts from the value, not from what a cancelled one left
61371
+ // typed in the field. Done before the focus: written into a focused
61372
+ // field, it would read as a change to send (see input_effect.js).
61373
+ dispatchRequestResetUIState(field, editing.event);
61374
+ }
61375
+ moveFocusTo(field);
61376
+ const initialValue = editing.event?.detail?.initialValue;
61377
+ if (initialValue !== undefined) {
61378
+ // The key that started edition is its first keystroke.
61379
+ field.value = initialValue;
61380
+ field.dispatchEvent(new CustomEvent("input", {
61381
+ bubbles: false
61382
+ }));
61383
+ return;
61384
+ }
61385
+ if (autoFocusSelect) {
61386
+ field.select();
61387
+ // Keep the beginning of the text visible instead of scrolling to the end
61388
+ field.scrollLeft = 0;
61305
61389
  }
61306
61390
  }, [editing]);
61307
61391
  const controlProps = {
@@ -61310,9 +61394,6 @@ const Editable = props => {
61310
61394
  name,
61311
61395
  value,
61312
61396
  valueSignal,
61313
- autoFocus: editing,
61314
- autoFocusVisible: true,
61315
- autoFocusSelect,
61316
61397
  cancelOnEscape: true,
61317
61398
  cancelOnBlurInvalid: true,
61318
61399
  constraints,
@@ -61332,6 +61413,11 @@ const Editable = props => {
61332
61413
  });
61333
61414
  },
61334
61415
  onBlur: e => {
61416
+ if (!editing) {
61417
+ // The focus handed back once edition ended (see the effect above):
61418
+ // the edition is already over, this blur is its consequence.
61419
+ return;
61420
+ }
61335
61421
  let inputValue;
61336
61422
  const valueWhenEditStart = valueWhenEditStartRef.current;
61337
61423
  let inputValueWhenEditStart;
@@ -61398,7 +61484,7 @@ const Editable = props => {
61398
61484
  // - input not focusable (via keyboard or anything)
61399
61485
  // - cannot be interacted with pointer (click, hover, etc)
61400
61486
  // - is ignored by screen readers
61401
- inert: editing ? undefined : "",
61487
+ inert: !editing,
61402
61488
  "data-editing": editing ? "" : undefined,
61403
61489
  children: control
61404
61490
  })]
@@ -92979,20 +93065,33 @@ const Table = props => {
92979
93065
  columns,
92980
93066
  canChangeColumnOrder: Boolean(onColumnOrderChange)
92981
93067
  });
93068
+
93069
+ // Edition starts from a cell holding the focus itself. Once started, the
93070
+ // focus is in the cell's field and its keys are the field's: an Enter there
93071
+ // validates, it must not start edition again.
93072
+ const canStartEditing = () => dragContextValue.grabTarget === null && document.activeElement.tagName === "TD";
93073
+ // The key starting edition is its first keystroke, typed by Editable
93074
+ // (initialValue). Its default is prevented: the browser would type it a
93075
+ // second time into the field that takes the focus during this keydown.
93076
+ const startEditingWithKey = e => {
93077
+ e.preventDefault();
93078
+ document.activeElement.dispatchEvent(new CustomEvent("editrequested", {
93079
+ bubbles: false,
93080
+ detail: {
93081
+ initialValue: e.key
93082
+ }
93083
+ }));
93084
+ return true;
93085
+ };
92982
93086
  useKeyboardShortcuts(ref, [...createSelectionKeyboardShortcuts(selectionController, {
92983
93087
  toggleEnabled: true,
92984
93088
  enabled: () => dragContextValue.grabTarget === null
92985
93089
  }), {
92986
93090
  key: "enter",
92987
93091
  description: "Edit table cell content",
92988
- enabled: () => dragContextValue.grabTarget === null,
93092
+ enabled: canStartEditing,
92989
93093
  handler: () => {
92990
- // Find the currently focused cell
92991
- const activeCell = document.activeElement.closest("td");
92992
- if (!activeCell) {
92993
- return false;
92994
- }
92995
- activeCell.dispatchEvent(new CustomEvent("editrequested", {
93094
+ document.activeElement.dispatchEvent(new CustomEvent("editrequested", {
92996
93095
  bubbles: false
92997
93096
  }));
92998
93097
  return true;
@@ -93000,37 +93099,13 @@ const Table = props => {
93000
93099
  }, {
93001
93100
  key: "a-z",
93002
93101
  description: "Start editing table cell content",
93003
- enabled: () => dragContextValue.grabTarget === null,
93004
- handler: e => {
93005
- const activeCell = document.activeElement.closest("td");
93006
- if (!activeCell) {
93007
- return false;
93008
- }
93009
- activeCell.dispatchEvent(new CustomEvent("editrequested", {
93010
- bubbles: false,
93011
- detail: {
93012
- initialValue: e.key
93013
- }
93014
- }));
93015
- return true;
93016
- }
93102
+ enabled: canStartEditing,
93103
+ handler: startEditingWithKey
93017
93104
  }, {
93018
93105
  key: "0-9",
93019
93106
  description: "Start editing table cell content",
93020
- enabled: () => dragContextValue.grabTarget === null,
93021
- handler: e => {
93022
- const activeCell = document.activeElement.closest("td");
93023
- if (!activeCell) {
93024
- return false;
93025
- }
93026
- activeCell.dispatchEvent(new CustomEvent("editrequested", {
93027
- bubbles: false,
93028
- detail: {
93029
- initialValue: e.key
93030
- }
93031
- }));
93032
- return true;
93033
- }
93107
+ enabled: canStartEditing,
93108
+ handler: startEditingWithKey
93034
93109
  }]);
93035
93110
 
93036
93111
  // resizing