@jsenv/navi 0.29.475 → 0.29.477

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.
@@ -6566,11 +6566,6 @@ const CONTROL_PROP_SET = new Set([
6566
6566
  "autoFocusVisible",
6567
6567
  "autoFocusSelect",
6568
6568
 
6569
- "onMouseDown",
6570
- "onClick",
6571
- "onKeyDown",
6572
- "onPaste",
6573
- "onInput",
6574
6569
  "eventReactionDefinitions",
6575
6570
 
6576
6571
  "onCancel",
@@ -46903,6 +46898,9 @@ const ControlgroupChildrenWrapper = ({
46903
46898
  })
46904
46899
  });
46905
46900
 
46901
+ // What the caller writes for these is called from the control's own reactions
46902
+ // (see `reactions` below), so the split leaves them out of both sides.
46903
+ const LISTENERS_CALLED_BY_REACTIONS = new Set(["onMouseDown", "onClick", "onKeyDown", "onPaste", "onInput"]);
46906
46904
  /**
46907
46905
  * Core hook for interactive field components (InputTextual, InputCheckbox, etc.).
46908
46906
  *
@@ -46917,12 +46915,22 @@ const ControlgroupChildrenWrapper = ({
46917
46915
  * All state changes route through DOM events on the field element so that
46918
46916
  * external subscribers (e.g. useControlUIState, Selectable) receive every update.
46919
46917
  *
46918
+ * @param {object} options
46919
+ * @param {"host"|"root"} [options.listenersOn] Where the caller's DOM listeners
46920
+ * (`onPointerDown`, `onFocus`…, never navi's own `onnavi_*`) land. `"host"`,
46921
+ * the default, is right wherever the host is what the person presses and
46922
+ * focuses: the field inside its box, or one element for both. `"root"` is for
46923
+ * a control whose host is a hidden carrier, or whose root also holds what
46924
+ * the control opens, so that a listener hears the control as a whole.
46925
+ * `onMouseDown`, `onClick`, `onKeyDown`, `onPaste` and `onInput` stay the
46926
+ * host's either way: the control's own reactions to them call them.
46920
46927
  * @returns {[controlRootProps, controlHostProps, { uiStateController }]}
46921
46928
  */
46922
46929
  const useControlProps = (props, {
46923
46930
  controlType,
46924
46931
  persists,
46925
- uiActionInternal
46932
+ uiActionInternal,
46933
+ listenersOn = "host"
46926
46934
  }) => {
46927
46935
  const debugUIState = useDebugUIState();
46928
46936
  const debugAction = useDebugAction();
@@ -47009,7 +47017,9 @@ const useControlProps = (props, {
47009
47017
  const [controlRootProps, controlHostProps] = useInteractiveProps(props, {
47010
47018
  uiStateController,
47011
47019
  boundAction,
47012
- controlInfo
47020
+ controlInfo,
47021
+ listenersOn,
47022
+ listenersCalledByReactions: LISTENERS_CALLED_BY_REACTIONS
47013
47023
  });
47014
47024
  {
47015
47025
  const debugInteraction = useDebugInteraction();
@@ -48062,7 +48072,9 @@ const useControlgroupProps = (props, {
48062
48072
  wantRequesterButtonState,
48063
48073
  uiActionInternal,
48064
48074
  allowCapture = false,
48065
- cascadeValidationToChildren = false
48075
+ cascadeValidationToChildren = false,
48076
+ // See useControlProps.
48077
+ listenersOn = "host"
48066
48078
  }) => {
48067
48079
  const {
48068
48080
  action,
@@ -48116,7 +48128,8 @@ const useControlgroupProps = (props, {
48116
48128
  // so we don't have a value concept, nor readonly etc
48117
48129
  controlInfo: {
48118
48130
  controlType
48119
- }
48131
+ },
48132
+ listenersOn
48120
48133
  });
48121
48134
  const {
48122
48135
  basePseudoState
@@ -48255,12 +48268,17 @@ const ControlFacadeChildrenWrapper = ({
48255
48268
  const useInteractiveProps = (props, {
48256
48269
  uiStateController,
48257
48270
  boundAction,
48258
- controlInfo
48271
+ controlInfo,
48272
+ listenersOn,
48273
+ listenersCalledByReactions
48259
48274
  }) => {
48260
48275
  const {
48261
48276
  ref
48262
48277
  } = props;
48263
- const [controlRootProps, controlHostProps] = splitControlProps(props);
48278
+ const [controlRootProps, controlHostProps] = splitControlProps(props, {
48279
+ listenersOn,
48280
+ listenersCalledByReactions
48281
+ });
48264
48282
  controlRootProps["navi-control"] = controlInfo.controlType;
48265
48283
  if (props.selfInteractions) {
48266
48284
  // One attribute, in the DOM, because that is where the claim is read from —
@@ -48637,8 +48655,10 @@ const useInteractiveProps = (props, {
48637
48655
  }
48638
48656
  }
48639
48657
  };
48658
+ const onFocusFromProps = controlHostProps.onFocus;
48640
48659
  Object.assign(controlHostProps, {
48641
48660
  onFocus: e => {
48661
+ onFocusFromProps?.(e);
48642
48662
  // Transfer programmatic focus to the delegate target (navi-focus-delegate or navi-control-proxy-for)
48643
48663
  const focusProxyTarget = findFocusDelegateTarget(e.currentTarget) || findControlProxyTarget(e.currentTarget);
48644
48664
  if (focusProxyTarget) {
@@ -48870,7 +48890,10 @@ const useInteractiveProps = (props, {
48870
48890
  }
48871
48891
  return [controlRootProps, controlHostProps];
48872
48892
  };
48873
- const splitControlProps = props => {
48893
+ const splitControlProps = (props, {
48894
+ listenersOn,
48895
+ listenersCalledByReactions
48896
+ }) => {
48874
48897
  const {
48875
48898
  ref
48876
48899
  } = props;
@@ -48887,10 +48910,20 @@ const splitControlProps = props => {
48887
48910
  if (isControlProp(key)) {
48888
48911
  continue;
48889
48912
  }
48913
+ if (listenersCalledByReactions?.has(key)) {
48914
+ continue;
48915
+ }
48916
+ if (listenersOn === "host" && isDomListenerProp(key)) {
48917
+ controlHostProps[key] = props[key];
48918
+ continue;
48919
+ }
48890
48920
  controlRootProps[key] = props[key];
48891
48921
  }
48892
48922
  return [controlRootProps, controlHostProps];
48893
48923
  };
48924
+ // `onPointerDown`, `onFocusOut`… but not navi's own events (`onnavi_command`),
48925
+ // which keep the element they are dispatched on.
48926
+ const isDomListenerProp = key => /^on[A-Z]/.test(key);
48894
48927
  const isCheckableInput = (controlType, typeProp) => controlType === "input" && (typeProp === "radio" || typeProp === "checkbox");
48895
48928
 
48896
48929
  // The labels the DOM itself can hand over: a wrapping <label>, or a label[for]
@@ -61469,6 +61502,86 @@ const useAutoSelectReadOnly = (props) => {
61469
61502
  return { onFocus, onMouseDown, onPointerDown };
61470
61503
  };
61471
61504
 
61505
+ // selectOnPress: a field whose value is usually replaced rather than edited (an
61506
+ // address, a place to search) behaves like the address bar of a browser. The
61507
+ // first gesture that engages it after it took the focus selects its whole text,
61508
+ // so typing replaces it; a second press places the caret, to fix one part.
61509
+ //
61510
+ // Engaged by a gesture, never by the focus itself: a dialog opening hands the
61511
+ // focus to its first field (docs/autofocus.md), and a field selected on focus
61512
+ // would greet whoever opened the dialog already half-way into an edit. So a
61513
+ // focus that came without a press waits for the first key that types.
61514
+ //
61515
+ // The press selects once it is over, on `click`: the browser places the caret
61516
+ // on mousedown and may collapse a selection on mouseup, so anything selected
61517
+ // before the release is undone by it. `mousedown` decides whether a press
61518
+ // engages because a finger only produces one after a tap — a finger that
61519
+ // scrolled the page never reaches the field. Touch is included on purpose (see
61520
+ // use_autoselect_read_only.js for why a read-only field leaves it out): on a
61521
+ // field meant to be replaced, the native selection handles are expected.
61522
+ const useSelectOnPress = (props, selectOnPress) => {
61523
+ const engagedRef = useRef(false);
61524
+ const pressEngagesRef = useRef(false);
61525
+
61526
+ if (!selectOnPress) {
61527
+ return null;
61528
+ }
61529
+ const onMouseDown = (e) => {
61530
+ props.onMouseDown(e);
61531
+ if (e.defaultPrevented) {
61532
+ return;
61533
+ }
61534
+ if (e.button !== 0) {
61535
+ return;
61536
+ }
61537
+ pressEngagesRef.current = !engagedRef.current;
61538
+ engagedRef.current = true;
61539
+ };
61540
+ const onClick = (e) => {
61541
+ props.onClick(e);
61542
+ if (!pressEngagesRef.current) {
61543
+ return;
61544
+ }
61545
+ pressEngagesRef.current = false;
61546
+ if (e.defaultPrevented) {
61547
+ return;
61548
+ }
61549
+ const input = e.currentTarget;
61550
+ if (input.selectionStart !== input.selectionEnd) {
61551
+ // The press dragged across a part of the text: that part is what was meant.
61552
+ return;
61553
+ }
61554
+ input.select();
61555
+ // Keep the beginning of the text visible instead of scrolling to the end
61556
+ input.scrollLeft = 0;
61557
+ };
61558
+ const onKeyDown = (e) => {
61559
+ if (!engagedRef.current) {
61560
+ const defaultAction = getKeyboardEventDefaultAction(e);
61561
+ if (defaultAction === "type") {
61562
+ engagedRef.current = true;
61563
+ // Before the control's own keydown: maxLengthGuard measures the room
61564
+ // left against the selection the key is about to replace.
61565
+ e.currentTarget.select();
61566
+ } else if (defaultAction === "cursor_move") {
61567
+ engagedRef.current = true;
61568
+ }
61569
+ }
61570
+ props.onKeyDown(e);
61571
+ };
61572
+ const onBlur = (e) => {
61573
+ props.onBlur(e);
61574
+ if (document.activeElement === e.currentTarget) {
61575
+ // The window went to the background and the field still holds the focus:
61576
+ // coming back is not a new arrival.
61577
+ return;
61578
+ }
61579
+ engagedRef.current = false;
61580
+ };
61581
+
61582
+ return { onMouseDown, onClick, onKeyDown, onBlur };
61583
+ };
61584
+
61472
61585
  /**
61473
61586
  * Input component for all textual input types.
61474
61587
  *
@@ -61576,7 +61689,8 @@ const InputTextualUI = props => {
61576
61689
  ui,
61577
61690
  variant,
61578
61691
  backgroundColor,
61579
- width = "maxLength"
61692
+ width = "maxLength",
61693
+ selectOnPress
61580
61694
  } = props;
61581
61695
  const [inputControlRootProps, inputControlHostProps, controlChildrenWrapperProps] = useInputTextualProps(props);
61582
61696
  const {
@@ -61618,6 +61732,8 @@ const InputTextualUI = props => {
61618
61732
  } else {
61619
61733
  inputControlHostProps.width = width;
61620
61734
  }
61735
+ delete inputControlRootProps.selectOnPress;
61736
+ inputControlHostProps.selectOnPress = selectOnPress;
61621
61737
  return jsxs(Box, {
61622
61738
  as: "span",
61623
61739
  inline: true,
@@ -61690,7 +61806,7 @@ const resolveWidthFromMaxLength = (maxLength, inputMode) => {
61690
61806
  // rest — the guards, the slots, the constraints — which describe an edition
61691
61807
  // that does not happen here. What survives is what a Box understands: width,
61692
61808
  // spacing, colors, className, style.
61693
- const INPUT_ONLY_PROPS = ["value", "defaultValue", "signal", "id", "maxLength", "inputMode", "width", "variant", "type", "name", "placeholder", "required", "readOnly", "disabled", "loading", "error", "min", "max", "step", "pattern", "autoComplete", "autoCorrect", "spellcheck", "charGuard", "maxLengthGuard", "list", "suggestions", "headless", "fieldSizing", "action", "uiAction", "ui", "children"];
61809
+ const INPUT_ONLY_PROPS = ["value", "defaultValue", "signal", "id", "maxLength", "inputMode", "width", "variant", "type", "name", "placeholder", "required", "readOnly", "disabled", "loading", "error", "min", "max", "step", "pattern", "autoComplete", "autoCorrect", "spellcheck", "charGuard", "maxLengthGuard", "list", "suggestions", "headless", "fieldSizing", "selectOnPress", "action", "uiAction", "ui", "children"];
61694
61810
  const InputTextualAsText = props => {
61695
61811
  installInputCss();
61696
61812
  // The id a Field handed down, which is what its Label points at.
@@ -61754,14 +61870,20 @@ const InputTextualFirstResolver = props => {
61754
61870
  const InputTextual = /*#__PURE__*/createComponentResolver([InputTextualAsTextResolver, InputTextualFirstResolver, InputWithListResolver, InputWithSuggestionsResolver, InputTypeResolver, InputModeResolver, InputHeadlessResolver, InputTextualUI]);
61755
61871
  const RealInput = ({
61756
61872
  maxLength,
61873
+ selectOnPress,
61757
61874
  ...domProps
61758
61875
  }) => {
61759
61876
  const autoSelectReadOnlyProps = useAutoSelectReadOnly(domProps);
61877
+ const selectOnPressProps = useSelectOnPress({
61878
+ ...domProps,
61879
+ ...autoSelectReadOnlyProps
61880
+ }, selectOnPress);
61760
61881
  return jsx(Box, {
61761
61882
  ...domProps,
61762
61883
  as: "input",
61763
61884
  baseClassName: "navi_control_input",
61764
61885
  ...autoSelectReadOnlyProps,
61886
+ ...selectOnPressProps,
61765
61887
  // Never set native maxLength — our guard handles it. Omitting it entirely
61766
61888
  // avoids a Preact quirk: setting maxLength={undefined} on a fresh DOM element
61767
61889
  // (e.g. after a Suspense remount) causes Preact to run `el.maxLength = ""`
@@ -77592,6 +77714,11 @@ const ListItemSelectable = props => {
77592
77714
  ...(hasSelectedProp ? {
77593
77715
  checked: selected
77594
77716
  } : null)
77717
+ },
77718
+ // The host is the visually hidden input carrying the selection: the row
77719
+ // is what the person sees and presses (see onClick above).
77720
+ {
77721
+ listenersOn: "root"
77595
77722
  });
77596
77723
  const {
77597
77724
  checked,
@@ -86530,7 +86657,11 @@ const PickerButton = props => {
86530
86657
  ...props,
86531
86658
  ref: inputRef
86532
86659
  }, {
86533
- controlType: "picker"
86660
+ controlType: "picker",
86661
+ // The root holds what the picker opens as well as its trigger: a
86662
+ // listener on the picker hears what bubbles out of the popup (the focus
86663
+ // leaving it, see picker_custom.jsx).
86664
+ listenersOn: "root"
86534
86665
  });
86535
86666
  const uiStateController = getUIStateControllerById(inputProps.id);
86536
86667
  const value = uiStateController.uiState;
@@ -89739,6 +89870,8 @@ const InputDuration = props => {
89739
89870
  }, {
89740
89871
  controlType: "duration_group",
89741
89872
  cascadeValidationToChildren: true,
89873
+ // The host is an <input type="hidden"> carrying the value.
89874
+ listenersOn: "root",
89742
89875
  // Aggregates sub-input values into an ISO 8601-like duration string.
89743
89876
  // durationToISOString preserves non-numeric mid-edit values
89744
89877
  // (e.g. hours="ab") between their unit markers ("PTabH30M"),
@@ -89805,7 +89938,7 @@ const InputDuration = props => {
89805
89938
  } = groupHostProps;
89806
89939
  const groupRef = useRef();
89807
89940
  useInputGroup(groupRef);
89808
- const clipboardProps = useClipboardProps(groupRef);
89941
+ const clipboardProps = useClipboardProps(groupRef, groupRootProps);
89809
89942
  const hourValue = components?.hours;
89810
89943
  const minuteValue = components?.minutes;
89811
89944
  const rawSecondValue = components?.seconds;
@@ -89878,7 +90011,11 @@ const InputDuration = props => {
89878
90011
  const getVisibleFields = container => {
89879
90012
  return ["hour", "minute", "second", "millisecond"].filter(f => container.querySelector(`[navi-input-type="${f}"]`) !== null);
89880
90013
  };
89881
- const useClipboardProps = groupRef => {
90014
+
90015
+ // Spread after the group's props, so the caller's listeners for these events
90016
+ // are called from here. Whatever they do, the group's own reading runs too, as
90017
+ // it does after a field's paste guard prevented the paste.
90018
+ const useClipboardProps = (groupRef, callerProps) => {
89882
90019
  const getClipboardPayload = () => {
89883
90020
  const groupEl = groupRef.current;
89884
90021
  if (!groupEl) {
@@ -89944,6 +90081,7 @@ const useClipboardProps = groupRef => {
89944
90081
  return true;
89945
90082
  };
89946
90083
  const onCopy = e => {
90084
+ callerProps.onCopy?.(e);
89947
90085
  if (!isFromDurationField(e)) {
89948
90086
  return;
89949
90087
  }
@@ -89956,6 +90094,7 @@ const useClipboardProps = groupRef => {
89956
90094
  e.preventDefault();
89957
90095
  };
89958
90096
  const onCut = e => {
90097
+ callerProps.onCut?.(e);
89959
90098
  if (!isFromDurationField(e)) {
89960
90099
  return;
89961
90100
  }
@@ -89968,6 +90107,7 @@ const useClipboardProps = groupRef => {
89968
90107
  applyToGroup("", e);
89969
90108
  };
89970
90109
  const onPaste = e => {
90110
+ callerProps.onPaste?.(e);
89971
90111
  if (!isFromDurationField(e)) {
89972
90112
  return;
89973
90113
  }
@@ -92537,7 +92677,10 @@ function WheelUI(props) {
92537
92677
  // action explicitly on settle. A caller can still override actionEvent.
92538
92678
  actionEvent: props.actionEvent ?? "custom"
92539
92679
  }, {
92540
- controlType: "input"
92680
+ controlType: "input",
92681
+ // The host is the hidden input carrying the value: the container is
92682
+ // what is focused and spun.
92683
+ listenersOn: "root"
92541
92684
  });
92542
92685
  const uiStateController = getUIStateControllerById(controlHostProps.id);
92543
92686
  const {