@jsenv/navi 0.27.35 → 0.27.37

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.
@@ -20761,11 +20761,9 @@ const checkConstraints = ({
20761
20761
  fromRequestAction,
20762
20762
  } = {}) => {
20763
20763
  let elementToValidate = event.currentTarget;
20764
- if (!elementToValidate.__validationInterface__) {
20765
- const controlHost = findControlHost(requester);
20766
- if (controlHost) {
20767
- elementToValidate = controlHost;
20768
- }
20764
+ const controlHost = findControlHost(elementToValidate);
20765
+ if (controlHost) {
20766
+ elementToValidate = controlHost;
20769
20767
  }
20770
20768
 
20771
20769
  let validationInterface = elementToValidate.__validationInterface__;
@@ -22858,7 +22856,7 @@ const createOnKeyDownForShortcutArray = (shortcuts, busyRef) => {
22858
22856
  }
22859
22857
 
22860
22858
  return (keyboardEvent) => {
22861
- applyKeyboardShortcuts(shortcutsCopy, keyboardEvent);
22859
+ return applyKeyboardShortcuts(shortcutsCopy, keyboardEvent);
22862
22860
  };
22863
22861
  };
22864
22862
  const createOnKeyDownForShortcuts = (shortcuts) => {
@@ -22943,7 +22941,7 @@ const applyKeyboardShortcuts = (shortcuts, keyboardEvent) => {
22943
22941
  if (returnValue === false) {
22944
22942
  keyboardEvent.preventDefault();
22945
22943
  }
22946
- return shortcutCandidate;
22944
+ return returnValue;
22947
22945
  }
22948
22946
  return null;
22949
22947
  };
@@ -23173,8 +23171,15 @@ const css$L = /* css */`
23173
23171
  &[data-skeleton] {
23174
23172
  border-radius: 0.2em;
23175
23173
  }
23174
+
23176
23175
  &[data-capitalize] {
23177
- display: inline-block; /* We need inline-block to match the pseudo element */
23176
+ text-transform: capitalize;
23177
+
23178
+ .navi_text_sizer {
23179
+ .navi_text {
23180
+ display: inline-block; /* We need inline-block to match the pseudo element */
23181
+ }
23182
+ }
23178
23183
  }
23179
23184
  &[data-shrinkwrap] {
23180
23185
  display: inline-block;
@@ -23220,25 +23225,11 @@ const css$L = /* css */`
23220
23225
  }
23221
23226
 
23222
23227
  &[data-text-overflow] {
23228
+ display: block;
23223
23229
  min-width: 0;
23224
- flex-wrap: wrap;
23225
23230
  text-overflow: ellipsis;
23226
23231
  overflow: hidden;
23227
23232
  overflow-wrap: normal;
23228
-
23229
- .navi_text_overflow_wrapper {
23230
- display: flex;
23231
- width: 100%;
23232
- flex-grow: 1;
23233
- justify-content: inherit;
23234
- gap: 0.3em;
23235
-
23236
- .navi_text_overflow_text {
23237
- max-width: 100%;
23238
- text-overflow: ellipsis;
23239
- overflow: hidden;
23240
- }
23241
- }
23242
23233
  }
23243
23234
 
23244
23235
  &[data-skeleton] {
@@ -23469,9 +23460,6 @@ const shouldInjectSpacingBetween = (left, right) => {
23469
23460
  if (rightIsNode && isMarkedAsOutsideTextFlow(right)) {
23470
23461
  return false;
23471
23462
  }
23472
- if (rightIsNode && right.props && right.props.overflowPinned) {
23473
- return false;
23474
- }
23475
23463
  if (typeof left === "string" && /\s$/.test(left)) {
23476
23464
  return false;
23477
23465
  }
@@ -23480,7 +23468,7 @@ const shouldInjectSpacingBetween = (left, right) => {
23480
23468
  }
23481
23469
  return true;
23482
23470
  };
23483
- const OverflowPinnedContext = createContext(null);
23471
+
23484
23472
  /**
23485
23473
  * Text component for rendering inline or block text with layout-stable style changes.
23486
23474
  *
@@ -23494,10 +23482,6 @@ const OverflowPinnedContext = createContext(null);
23494
23482
  * `maxLines={1}` truncates after one line (single-line ellipsis).
23495
23483
  * `maxLines={n}` (n > 1) truncates after n lines (multi-line clamp).
23496
23484
  *
23497
- * @param {boolean} [props.overflowPinned]
23498
- * Must be used inside a `<Text maxLines>` parent.
23499
- * Pins this element outside the truncated text flow (e.g. a badge or icon).
23500
- *
23501
23485
  * @param {string} [props.spacing]
23502
23486
  * Controls the separator injected between child nodes.
23503
23487
  * Accepts a size/spacing scale key, a CSS length, or `"pre"` / `0` to disable.
@@ -23562,11 +23546,6 @@ const TextDispatcher = props => {
23562
23546
  ...props
23563
23547
  });
23564
23548
  }
23565
- if (props.overflowPinned) {
23566
- return jsx(TextOverflowPinned, {
23567
- ...props
23568
- });
23569
- }
23570
23549
  if (props.selectRange) {
23571
23550
  return jsx(TextWithSelectRange, {
23572
23551
  ...props
@@ -23740,9 +23719,7 @@ const TextOverflow = ({
23740
23719
  children,
23741
23720
  ...rest
23742
23721
  }) => {
23743
- const [overflowPinned, setOverflowPinned] = useState(null);
23744
23722
  return jsx(TextDispatcher, {
23745
- flex: true,
23746
23723
  block: true,
23747
23724
  as: "div",
23748
23725
  pre: noWrap === undefined ? true : undefined
@@ -23754,44 +23731,10 @@ const TextOverflow = ({
23754
23731
  ...rest,
23755
23732
  maxLines: undefined,
23756
23733
  "data-text-overflow": "",
23757
- spacing: "pre",
23758
- children: jsxs("span", {
23759
- className: "navi_text_overflow_wrapper",
23760
- children: [overflowPinned && overflowPinned.position === "start" ? overflowPinned.vnode : null, jsx(OverflowPinnedContext.Provider, {
23761
- value: setOverflowPinned,
23762
- children: jsx(Text, {
23763
- className: "navi_text_overflow_text",
23764
- spacing: spacing,
23765
- capitalize: capitalize,
23766
- children: children
23767
- })
23768
- }), overflowPinned && overflowPinned.position === "end" ? overflowPinned.vnode : null]
23769
- })
23770
- });
23771
- };
23772
- const TextOverflowPinned = props => {
23773
- const {
23774
- overflowPinned
23775
- } = props;
23776
- const setOverflowPinned = useContext(OverflowPinnedContext);
23777
- const text = jsx(Text, {
23778
- ...props,
23779
- "data-overflow-pinned": "",
23780
- overflowPinned: undefined
23734
+ spacing: spacing,
23735
+ capitalize: capitalize,
23736
+ children: children
23781
23737
  });
23782
- if (!setOverflowPinned) {
23783
- console.warn(`<Text overflowPinned> declared outside a <Text maxLines="1">`);
23784
- return text;
23785
- }
23786
- if (overflowPinned) {
23787
- setOverflowPinned({
23788
- vnode: text,
23789
- position: overflowPinned === true ? "end" : overflowPinned
23790
- });
23791
- return null;
23792
- }
23793
- setOverflowPinned(null);
23794
- return text;
23795
23738
  };
23796
23739
  const TextWithSelectRange = ({
23797
23740
  ref,
@@ -24552,7 +24495,15 @@ const useAutoFocus = (
24552
24495
  // }
24553
24496
  // }, []);
24554
24497
 
24555
- return triggerAutofocus;
24498
+ return {
24499
+ "autoFocus": undefined,
24500
+ "navi-autofocus": autoFocus
24501
+ ? autoFocus === true
24502
+ ? ""
24503
+ : autoFocus
24504
+ : undefined,
24505
+ "navi-autofocus-select": autoFocus && autoSelect ? "" : undefined,
24506
+ };
24556
24507
  };
24557
24508
 
24558
24509
  let blurEvent = null;
@@ -24576,7 +24527,189 @@ document.body.addEventListener(
24576
24527
  { capture: true },
24577
24528
  );
24578
24529
 
24579
- const dispatchNaviCommand = (
24530
+ /**
24531
+ * Converts a JS value into the form expected by the browser DOM property for a
24532
+ * given control type/input type combination.
24533
+ *
24534
+ * For example:
24535
+ * - `datetime-local` inputs expect a local datetime string without timezone
24536
+ * - `number`/`range` inputs expect a numeric string or number
24537
+ * - `color` inputs require a non-empty hex string (falls back to `#000000`)
24538
+ * - All other inputs receive the value as-is (undefined → "")
24539
+ *
24540
+ * Returns either the converted value directly, or a converter function when the
24541
+ * conversion depends on the runtime value (e.g. plain inputs return `asInputValue`).
24542
+ *
24543
+ * @param {any} value - The JS value to convert.
24544
+ * @param {{ controlType: string, type: string }} options
24545
+ * @returns {any} The DOM-compatible value or a converter function.
24546
+ */
24547
+ const asControlHostValue = (
24548
+ jsValue,
24549
+ { controlType, type, inputMode },
24550
+ ) => {
24551
+ if (controlType === "input" || controlType === "picker") {
24552
+ if (type === "datetime-local") {
24553
+ return asDatetimeLocalString(jsValue);
24554
+ }
24555
+ if (type === "number" || type === "range" || inputMode === "numeric") {
24556
+ return asNumberString(jsValue);
24557
+ }
24558
+ if (type === "color") {
24559
+ return asColorString(jsValue);
24560
+ }
24561
+ return asInputValue(jsValue);
24562
+ }
24563
+ return jsValue;
24564
+ };
24565
+ // As explained in https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/datetime-local#setting_timezones
24566
+ // datetime-local does not support timezones
24567
+ const asDatetimeLocalString = (dateTimeString) => {
24568
+ const date = new Date(dateTimeString);
24569
+ if (isNaN(date.getTime())) {
24570
+ return dateTimeString;
24571
+ }
24572
+ const year = date.getFullYear();
24573
+ const month = String(date.getMonth() + 1).padStart(2, "0");
24574
+ const day = String(date.getDate()).padStart(2, "0");
24575
+ const hours = String(date.getHours()).padStart(2, "0");
24576
+ const minutes = String(date.getMinutes()).padStart(2, "0");
24577
+ const seconds = String(date.getSeconds()).padStart(2, "0");
24578
+ return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
24579
+ };
24580
+ const asNumberString = (jsValue) => {
24581
+ if (jsValue === undefined) {
24582
+ return "";
24583
+ }
24584
+ return jsValue;
24585
+ };
24586
+ // Browser requires a non-empty value for <input type="color">.
24587
+ // When our logical value is empty we give it #000000 so it doesn't choke.
24588
+ // The UI uses the original (possibly empty) value to show the checkerboard.
24589
+ const asColorString = (jsValue) => {
24590
+ return jsValue || "#000000";
24591
+ };
24592
+ const asInputValue = (jsValue) => {
24593
+ if (jsValue === undefined) {
24594
+ return "";
24595
+ }
24596
+ return jsValue;
24597
+ };
24598
+
24599
+ /**
24600
+ * Reads the current logical JS value from a control host DOM element.
24601
+ *
24602
+ * Handles all navi control host element types:
24603
+ * - `<button>` — reads via `navi_get_value` custom event, falls back to `button.value`
24604
+ * - `<input type="number|range">` — parses as a number, returns `undefined` when empty
24605
+ * - `<input type="checkbox|radio">` — returns `undefined` when unchecked, otherwise reads
24606
+ * via `navi_get_value` custom event (to preserve the original JS type of the value prop)
24607
+ * - `<input type="datetime-local">` — converts the local datetime string to an ISO 8601 string
24608
+ * - `<input type="navi_picker">` — delegates to the controller via `navi_get_ui_state`
24609
+ * - All other inputs — returns `input.value` as a string
24610
+ *
24611
+ * @param {HTMLElement} controlHost - The control host DOM element to read from.
24612
+ * @returns {any} The current logical value of the control.
24613
+ */
24614
+ const readControlValue = (controlHost) => {
24615
+ if (
24616
+ controlHost.tagName === "BUTTON" ||
24617
+ controlHost.getAttribute("role") === "button"
24618
+ ) {
24619
+ return readValueFromButton(controlHost);
24620
+ }
24621
+ if (controlHost.tagName === "INPUT") {
24622
+ // important: input.type = "navi_js"; followed by input.type; returns "text"
24623
+ // so use getAttribute
24624
+ const type = controlHost.getAttribute("type");
24625
+
24626
+ if (
24627
+ type === "number" ||
24628
+ type === "range" ||
24629
+ controlHost.inputMode === "numeric"
24630
+ ) {
24631
+ return readNumberFromInput(controlHost);
24632
+ }
24633
+ if (type === "color") {
24634
+ return readValueFromControlHost(controlHost);
24635
+ }
24636
+ if (type === "checkbox" || type === "radio") {
24637
+ return readValueFromCheckableInput(controlHost);
24638
+ }
24639
+ if (type === "datetime-local") {
24640
+ return readDatetimeLocalFromInput(controlHost);
24641
+ }
24642
+ if (type === "navi_js") {
24643
+ return getUIStateFromElement(controlHost);
24644
+ }
24645
+ return readValueFromInput(controlHost);
24646
+ }
24647
+ if (controlHost.hasAttribute("navi-control-host")) {
24648
+ // Non-button, non-input navi controls (e.g. Badge.Button rendered as span)
24649
+ return readValueFromControlHost(controlHost);
24650
+ }
24651
+ return readValueFromElement(controlHost);
24652
+ };
24653
+ const readValueFromControlHost = (controlHost) => {
24654
+ return readValueFromNaviCustomEvent(controlHost, controlHost.value);
24655
+ };
24656
+ const readValueFromButton = (button) => {
24657
+ return readValueFromControlHost(button);
24658
+ };
24659
+ const readDatetimeLocalFromInput = (input) => {
24660
+ const localDateTimeString = input.value;
24661
+ if (localDateTimeString === "") {
24662
+ return "";
24663
+ }
24664
+ const localDate = new Date(localDateTimeString);
24665
+ if (isNaN(localDate.getTime())) {
24666
+ return localDateTimeString;
24667
+ }
24668
+ return localDate.toISOString();
24669
+ };
24670
+ const readNumberFromInput = (input) => {
24671
+ const numberString = input.value;
24672
+ if (numberString === "") {
24673
+ return "";
24674
+ }
24675
+ const asNumber = Number(numberString);
24676
+ if (isNaN(asNumber)) {
24677
+ return numberString;
24678
+ }
24679
+ return asNumber;
24680
+ };
24681
+ const readValueFromCheckableInput = (input) => {
24682
+ const checked = input.checked;
24683
+ if (!checked) {
24684
+ return undefined;
24685
+ }
24686
+ return readValueFromControlHost(input);
24687
+ };
24688
+ const readValueFromInput = (input) => {
24689
+ const value = input.value;
24690
+ return value;
24691
+ };
24692
+ const readValueFromElement = (element) => {
24693
+ const value = element.value;
24694
+ return value;
24695
+ };
24696
+ const readValueFromNaviCustomEvent = (field, fallback) => {
24697
+ // prefer the value given as prop (respect original type, browser would convert to string)
24698
+ let responded;
24699
+ let value;
24700
+ dispatchCustomEvent(field, "navi_get_value", {
24701
+ respondWith: (jsValue) => {
24702
+ responded = true;
24703
+ value = jsValue;
24704
+ },
24705
+ });
24706
+ if (responded) {
24707
+ return value;
24708
+ }
24709
+ return fallback;
24710
+ };
24711
+
24712
+ const triggerNaviCommand = (
24580
24713
  element,
24581
24714
  command,
24582
24715
  event,
@@ -24615,6 +24748,7 @@ const dispatchNaviCommand = (
24615
24748
  value,
24616
24749
  });
24617
24750
  };
24751
+
24618
24752
  // Returns the target explicitly declared via HTML attributes (commandfor / navi-command-target),
24619
24753
  // or undefined when no such attribute is present.
24620
24754
  // Returns null when the attribute is present but the target element was not found (already warned).
@@ -24680,6 +24814,13 @@ const resolveCommandValue = (source, event) => {
24680
24814
  if (source.hasAttribute("command-value")) {
24681
24815
  return source.getAttribute("command-value");
24682
24816
  }
24817
+ if (source.type === "radio" || source.type === "checkbox") {
24818
+ // Use readControlValue so that radio/checkbox sources return their `value`
24819
+ // attribute (e.g. "Cherry") rather than the boolean checked state (true).
24820
+ // getUIStateFromElement would return true for a checked radio, which is
24821
+ // wrong when the command needs to propagate the selected item's identity.
24822
+ return readControlValue(source);
24823
+ }
24683
24824
  return getUIStateFromElement(source);
24684
24825
  };
24685
24826
 
@@ -24772,7 +24913,7 @@ registerNaviCommand("--navi-clear", (source, event) => {
24772
24913
  target,
24773
24914
  implementation: () => {
24774
24915
  if (fromInput) ; else {
24775
- dispatchNaviCommand(source, "--navi-close", event, {
24916
+ triggerNaviCommand(source, "--navi-close", event, {
24776
24917
  optional: true,
24777
24918
  });
24778
24919
  }
@@ -24927,11 +25068,11 @@ const executeNaviDefine = (source, event, target) => {
24927
25068
  // send button's undefined value would corrupt the picker state.
24928
25069
  const skipUpdate = resolvePickerInnerControl(target) !== null;
24929
25070
  if (!skipUpdate) {
24930
- dispatchNaviCommand(source, "--navi-update", event);
25071
+ triggerNaviCommand(source, "--navi-update", event);
24931
25072
  }
24932
25073
  // The picker's onClose already dispatches the action with the final value.
24933
25074
  // Dispatching again here would fire the action twice.
24934
- return dispatchNaviCommand(target, "--navi-close", event);
25075
+ return triggerNaviCommand(target, "--navi-close", event);
24935
25076
  };
24936
25077
 
24937
25078
  registerNaviCommand("--navi-scroll", (source, event) => {
@@ -24950,6 +25091,36 @@ registerNaviCommand("--navi-scroll", (source, event) => {
24950
25091
  },
24951
25092
  };
24952
25093
  });
25094
+ registerNaviCommand("--navi-check", (source, event) => {
25095
+ const target =
25096
+ resolveExplicitTarget(source) || resolveFirstParentControl(source);
25097
+ if (!target) {
25098
+ return undefined;
25099
+ }
25100
+ return {
25101
+ target,
25102
+ implementation: () => {
25103
+ return dispatchCustomEvent(target, "navi_request_check", {
25104
+ event,
25105
+ });
25106
+ },
25107
+ };
25108
+ });
25109
+ registerNaviCommand("--navi-uncheck", (source, event) => {
25110
+ const target =
25111
+ resolveExplicitTarget(source) || resolveFirstParentControl(source);
25112
+ if (!target) {
25113
+ return undefined;
25114
+ }
25115
+ return {
25116
+ target,
25117
+ implementation: () => {
25118
+ return dispatchCustomEvent(target, "navi_request_uncheck", {
25119
+ event,
25120
+ });
25121
+ },
25122
+ };
25123
+ });
24953
25124
  registerNaviCommand("--navi-select", (source, event) => {
24954
25125
  const target =
24955
25126
  resolveExplicitTarget(source) || resolveFirstParentControl(source);
@@ -25040,223 +25211,42 @@ const CONTROL_PROP_SET = new Set([
25040
25211
  "autoFocusVisible",
25041
25212
  "autoSelect",
25042
25213
 
25043
- "onMouseDown",
25044
- "onClick",
25045
- "onKeyDown",
25046
- "onPaste",
25047
- "onInput",
25048
-
25049
- "onCancel",
25050
- "cancelOnBlurInvalid",
25051
- "cancelOnEscape",
25052
- "onActionPrevented",
25053
- "onActionStart",
25054
- "onActionAborted",
25055
- "onActionError",
25056
- "actionErrorEffect",
25057
- "errorMapping",
25058
- "onActionEnd",
25059
-
25060
- "resetOnCancel",
25061
- "resetOnAbort",
25062
- "resetOnError",
25063
- ]);
25064
-
25065
- const ControlToInterfaceContext = createContext(null);
25066
- const MessagePropsRefContext = createContext();
25067
-
25068
- const ControlNameContext = createContext();
25069
- const DisabledContext = createContext();
25070
- const ReadOnlyContext = createContext();
25071
- const RequiredContext = createContext();
25072
- const LoadingContext = createContext();
25073
- createContext();
25074
-
25075
- const ActionContext = createContext();
25076
- const ActionRequesterContext = createContext();
25077
-
25078
- /**
25079
- * Converts a JS value into the form expected by the browser DOM property for a
25080
- * given control type/input type combination.
25081
- *
25082
- * For example:
25083
- * - `datetime-local` inputs expect a local datetime string without timezone
25084
- * - `number`/`range` inputs expect a numeric string or number
25085
- * - `color` inputs require a non-empty hex string (falls back to `#000000`)
25086
- * - All other inputs receive the value as-is (undefined → "")
25087
- *
25088
- * Returns either the converted value directly, or a converter function when the
25089
- * conversion depends on the runtime value (e.g. plain inputs return `asInputValue`).
25090
- *
25091
- * @param {any} value - The JS value to convert.
25092
- * @param {{ controlType: string, type: string }} options
25093
- * @returns {any} The DOM-compatible value or a converter function.
25094
- */
25095
- const asControlHostValue = (
25096
- jsValue,
25097
- { controlType, type, inputMode },
25098
- ) => {
25099
- if (controlType === "input" || controlType === "picker") {
25100
- if (type === "datetime-local") {
25101
- return asDatetimeLocalString(jsValue);
25102
- }
25103
- if (type === "number" || type === "range" || inputMode === "numeric") {
25104
- return asNumberString(jsValue);
25105
- }
25106
- if (type === "color") {
25107
- return asColorString(jsValue);
25108
- }
25109
- return asInputValue(jsValue);
25110
- }
25111
- return jsValue;
25112
- };
25113
- // As explained in https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/datetime-local#setting_timezones
25114
- // datetime-local does not support timezones
25115
- const asDatetimeLocalString = (dateTimeString) => {
25116
- const date = new Date(dateTimeString);
25117
- if (isNaN(date.getTime())) {
25118
- return dateTimeString;
25119
- }
25120
- const year = date.getFullYear();
25121
- const month = String(date.getMonth() + 1).padStart(2, "0");
25122
- const day = String(date.getDate()).padStart(2, "0");
25123
- const hours = String(date.getHours()).padStart(2, "0");
25124
- const minutes = String(date.getMinutes()).padStart(2, "0");
25125
- const seconds = String(date.getSeconds()).padStart(2, "0");
25126
- return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
25127
- };
25128
- const asNumberString = (jsValue) => {
25129
- if (jsValue === undefined) {
25130
- return "";
25131
- }
25132
- return jsValue;
25133
- };
25134
- // Browser requires a non-empty value for <input type="color">.
25135
- // When our logical value is empty we give it #000000 so it doesn't choke.
25136
- // The UI uses the original (possibly empty) value to show the checkerboard.
25137
- const asColorString = (jsValue) => {
25138
- return jsValue || "#000000";
25139
- };
25140
- const asInputValue = (jsValue) => {
25141
- if (jsValue === undefined) {
25142
- return "";
25143
- }
25144
- return jsValue;
25145
- };
25146
-
25147
- /**
25148
- * Reads the current logical JS value from a control host DOM element.
25149
- *
25150
- * Handles all navi control host element types:
25151
- * - `<button>` — reads via `navi_get_value` custom event, falls back to `button.value`
25152
- * - `<input type="number|range">` — parses as a number, returns `undefined` when empty
25153
- * - `<input type="checkbox|radio">` — returns `undefined` when unchecked, otherwise reads
25154
- * via `navi_get_value` custom event (to preserve the original JS type of the value prop)
25155
- * - `<input type="datetime-local">` — converts the local datetime string to an ISO 8601 string
25156
- * - `<input type="navi_picker">` — delegates to the controller via `navi_get_ui_state`
25157
- * - All other inputs — returns `input.value` as a string
25158
- *
25159
- * @param {HTMLElement} controlHost - The control host DOM element to read from.
25160
- * @returns {any} The current logical value of the control.
25161
- */
25162
- const readControlValue = (controlHost) => {
25163
- if (
25164
- controlHost.tagName === "BUTTON" ||
25165
- controlHost.getAttribute("role") === "button"
25166
- ) {
25167
- return readValueFromButton(controlHost);
25168
- }
25169
- if (controlHost.tagName === "INPUT") {
25170
- // important: input.type = "navi_js"; followed by input.type; returns "text"
25171
- // so use getAttribute
25172
- const type = controlHost.getAttribute("type");
25173
-
25174
- if (
25175
- type === "number" ||
25176
- type === "range" ||
25177
- controlHost.inputMode === "numeric"
25178
- ) {
25179
- return readNumberFromInput(controlHost);
25180
- }
25181
- if (type === "color") {
25182
- return readValueFromControlHost(controlHost);
25183
- }
25184
- if (type === "checkbox" || type === "radio") {
25185
- return readValueFromCheckableInput(controlHost);
25186
- }
25187
- if (type === "datetime-local") {
25188
- return readDatetimeLocalFromInput(controlHost);
25189
- }
25190
- if (type === "navi_js") {
25191
- return getUIStateFromElement(controlHost);
25192
- }
25193
- return readValueFromInput(controlHost);
25194
- }
25195
- if (controlHost.hasAttribute("navi-control-host")) {
25196
- // Non-button, non-input navi controls (e.g. Badge.Button rendered as span)
25197
- return readValueFromControlHost(controlHost);
25198
- }
25199
- return readValueFromElement(controlHost);
25200
- };
25201
- const readValueFromControlHost = (controlHost) => {
25202
- return readValueFromNaviCustomEvent(controlHost, controlHost.value);
25203
- };
25204
- const readValueFromButton = (button) => {
25205
- return readValueFromControlHost(button);
25206
- };
25207
- const readDatetimeLocalFromInput = (input) => {
25208
- const localDateTimeString = input.value;
25209
- if (localDateTimeString === "") {
25210
- return "";
25211
- }
25212
- const localDate = new Date(localDateTimeString);
25213
- if (isNaN(localDate.getTime())) {
25214
- return localDateTimeString;
25215
- }
25216
- return localDate.toISOString();
25217
- };
25218
- const readNumberFromInput = (input) => {
25219
- const numberString = input.value;
25220
- if (numberString === "") {
25221
- return "";
25222
- }
25223
- const asNumber = Number(numberString);
25224
- if (isNaN(asNumber)) {
25225
- return numberString;
25226
- }
25227
- return asNumber;
25228
- };
25229
- const readValueFromCheckableInput = (input) => {
25230
- const checked = input.checked;
25231
- if (!checked) {
25232
- return undefined;
25233
- }
25234
- return readValueFromControlHost(input);
25235
- };
25236
- const readValueFromInput = (input) => {
25237
- const value = input.value;
25238
- return value;
25239
- };
25240
- const readValueFromElement = (element) => {
25241
- const value = element.value;
25242
- return value;
25243
- };
25244
- const readValueFromNaviCustomEvent = (field, fallback) => {
25245
- // prefer the value given as prop (respect original type, browser would convert to string)
25246
- let responded;
25247
- let value;
25248
- dispatchCustomEvent(field, "navi_get_value", {
25249
- respondWith: (jsValue) => {
25250
- responded = true;
25251
- value = jsValue;
25252
- },
25253
- });
25254
- if (responded) {
25255
- return value;
25256
- }
25257
- return fallback;
25258
- };
25259
-
25214
+ "onMouseDown",
25215
+ "onClick",
25216
+ "onKeyDown",
25217
+ "onPaste",
25218
+ "onInput",
25219
+ "interactionDefinitions",
25220
+
25221
+ "onCancel",
25222
+ "cancelOnBlurInvalid",
25223
+ "cancelOnEscape",
25224
+ "onActionPrevented",
25225
+ "onActionStart",
25226
+ "onActionAborted",
25227
+ "onActionError",
25228
+ "actionErrorEffect",
25229
+ "errorMapping",
25230
+ "onActionEnd",
25231
+
25232
+ "resetOnCancel",
25233
+ "resetOnAbort",
25234
+ "resetOnError",
25235
+ ]);
25236
+
25237
+ const ControlToInterfaceContext = createContext(null);
25238
+ const MessagePropsRefContext = createContext();
25239
+
25240
+ const ControlNameContext = createContext();
25241
+ const DisabledContext = createContext();
25242
+ const ReadOnlyContext = createContext();
25243
+ const RequiredContext = createContext();
25244
+ const LoadingContext = createContext();
25245
+ createContext();
25246
+
25247
+ const ActionContext = createContext();
25248
+ const ActionRequesterContext = createContext();
25249
+
25260
25250
  // In-memory registry of all mounted ui state controllers keyed by their id.
25261
25251
  // Allows direct controller access without dispatching DOM events — used by external
25262
25252
  // callers (e.g. selectable_list) to call setUIState by id instead of via the DOM.
@@ -25352,6 +25342,7 @@ const onUIStateControllerDestroyed = (uiStateController) => {
25352
25342
  * registerChild(child): void; // Called on child mount
25353
25343
  * onChildInteraction(child, e, { stateChanged: boolean }): void; // Called on user interaction
25354
25344
  * unregisterChild(child): void; // Called on child unmount
25345
+ * props: Object;
25355
25346
  * }
25356
25347
  * ```
25357
25348
  */
@@ -25575,7 +25566,7 @@ const useUIStateController = (
25575
25566
  if (command) {
25576
25567
  const element = uiStateController.elementRef.current;
25577
25568
  if (element) {
25578
- dispatchNaviCommand(element, command, e);
25569
+ triggerNaviCommand(element, command, e);
25579
25570
  }
25580
25571
  }
25581
25572
  };
@@ -25615,8 +25606,8 @@ const useUIStateController = (
25615
25606
  uiStateController.props.type === "radio" &&
25616
25607
  shouldNotifyParent(e)
25617
25608
  ) {
25618
- onUIAction();
25619
25609
  notifyParentAboutChildInteraction(e, { stateChanged: false });
25610
+ onUIAction();
25620
25611
  }
25621
25612
  return false;
25622
25613
  }
@@ -25941,6 +25932,10 @@ const useUIGroupStateController = (
25941
25932
  `${controlType}.applyState(${JSON.stringify(newUIState)}, "${e.type}") -> updates from ${JSON.stringify(currentUIState)} to ${JSON.stringify(newUIState)}`,
25942
25933
  );
25943
25934
  publishUIState(newUIState);
25935
+ // Notify the parent (facade) BEFORE firing the command so that when a
25936
+ // command like --navi-send closes the picker, the picker input already
25937
+ // holds the new value.
25938
+ notifyParentAboutChildInteraction(e, { stateChanged: true });
25944
25939
  if (internalBehavior) {
25945
25940
  // Fire uiAction only — skip command to avoid re-triggering the same command
25946
25941
  // that caused this setUIState call in the first place.
@@ -25957,7 +25952,6 @@ const useUIGroupStateController = (
25957
25952
  value: newUIState,
25958
25953
  });
25959
25954
  }
25960
- notifyParentAboutChildInteraction(e, { stateChanged: true });
25961
25955
  };
25962
25956
 
25963
25957
  useLayoutEffect(() => {
@@ -25975,6 +25969,7 @@ const useUIGroupStateController = (
25975
25969
  existingUIStateController.id = id;
25976
25970
  existingUIStateController.name = name;
25977
25971
  existingUIStateController.value = value;
25972
+ existingUIStateController.props = props;
25978
25973
  uiActionRef.current = uiAction;
25979
25974
  return existingUIStateController;
25980
25975
  }
@@ -25998,6 +25993,7 @@ const useUIGroupStateController = (
25998
25993
  id,
25999
25994
  name,
26000
25995
  value,
25996
+ props,
26001
25997
  uiState: fallbackState,
26002
25998
  uiStateSignal,
26003
25999
  wantRequesterButtonState,
@@ -26093,7 +26089,7 @@ const useUIGroupStateController = (
26093
26089
  if (command) {
26094
26090
  const el = ref.current;
26095
26091
  if (el) {
26096
- dispatchNaviCommand(el, command, e);
26092
+ triggerNaviCommand(el, command, e);
26097
26093
  }
26098
26094
  }
26099
26095
  },
@@ -26234,7 +26230,7 @@ const EMPTY_OBJECT = {};
26234
26230
  * inside the picker popup. It also means `commands.js` no longer has to
26235
26231
  * manually re-dispatch to inner controls.
26236
26232
  */
26237
- const useUIFacadeStateController = (uiStateController) => {
26233
+ const useUIFacadeStateController = (props, uiStateController) => {
26238
26234
  const firstChildControllerRef = useRef(null);
26239
26235
  const updatingRef = useRef(false);
26240
26236
 
@@ -26258,11 +26254,24 @@ const useUIFacadeStateController = (uiStateController) => {
26258
26254
  });
26259
26255
  }, []);
26260
26256
 
26257
+ const includeChildController = (childController) => {
26258
+ if (childController.props["navi-list"]) {
26259
+ // Controls with navi-list act as standalone list navigators and should
26260
+ // not be treated as the picker's synced child.
26261
+ return false;
26262
+ }
26263
+ return true;
26264
+ };
26265
+
26261
26266
  return useMemo(
26262
26267
  () => ({
26263
26268
  controlType: "facade",
26269
+ props,
26264
26270
  uiStateSignal: uiStateController.uiStateSignal,
26265
26271
  registerChild: (child) => {
26272
+ if (!includeChildController(child)) {
26273
+ return;
26274
+ }
26266
26275
  if (!firstChildControllerRef.current) {
26267
26276
  firstChildControllerRef.current = child;
26268
26277
  } else {
@@ -26479,18 +26488,19 @@ const useControlProps = (props, {
26479
26488
  uiActionInternal,
26480
26489
  readOnlySupported
26481
26490
  }) => {
26491
+ const idDefault = useId();
26482
26492
  const debugInteraction = useDebugInteraction();
26483
26493
  const controlName = useContext(ControlNameContext);
26494
+ const controlToInterface = useContext(ControlToInterfaceContext);
26484
26495
  const state = props[statePropName];
26496
+ props.name = props.name || controlName;
26497
+ props.id = props.id || controlToInterface?.id || idDefault;
26485
26498
  if (isSignal(state)) {
26486
26499
  props = {
26487
26500
  ...props,
26488
26501
  [statePropName]: state.value
26489
26502
  };
26490
26503
  }
26491
- if (!props.name && controlName) {
26492
- props.name = controlName;
26493
- }
26494
26504
  const uiStateController = useUIStateController(props, controlType, {
26495
26505
  statePropName,
26496
26506
  defaultStatePropName,
@@ -26516,13 +26526,6 @@ const useControlProps = (props, {
26516
26526
  actionAfterChange = actionInteraction === "change",
26517
26527
  actionDebounce
26518
26528
  } = props;
26519
- let isCheckable = false;
26520
- // Effect to run when the Enter key is pressed.
26521
- // For most inputs Enter submits the surrounding form; for checkables Enter
26522
- // synthesizes a click so the browser's native checkbox/radio activation runs
26523
- // (which then fires input -> goes through the action pipeline).
26524
- let enterEffect;
26525
- let spaceEffect;
26526
26529
  const updateUIState = e => {
26527
26530
  const value = readControlValue(ref.current);
26528
26531
  uiStateController.setUIState(value, e);
@@ -26558,9 +26561,6 @@ const useControlProps = (props, {
26558
26561
  interaction.effect?.(e);
26559
26562
  return true;
26560
26563
  };
26561
- const asBrowserAction = (interaction, e) => {
26562
- return asInteraction(interaction, e);
26563
- };
26564
26564
  const lastEventRequestingActionRef = useRef();
26565
26565
  const lastActionValueRef = useRef(NO_ACTION_YET);
26566
26566
  const wasCheckedAtMousedownRef = useRef(false);
@@ -26578,12 +26578,7 @@ const useControlProps = (props, {
26578
26578
  // because at that point asAction hasn't run yet and we must not pre-empt its dedup.
26579
26579
  controlHostProps.onnavi_ui_state_change = e => {
26580
26580
  const originatingEvent = e.detail.event;
26581
- if (isCheckable) {
26582
- const sourceIsOwnInput = originatingEvent?.type === "input" && originatingEvent?.target === ref.current;
26583
- if (!sourceIsOwnInput) {
26584
- lastActionValueRef.current = e.detail.value;
26585
- }
26586
- } else if (originatingEvent?.type === "radio_sibling_uncheck") {
26581
+ if (originatingEvent?.type === "radio_sibling_uncheck") {
26587
26582
  lastActionValueRef.current = e.detail.value;
26588
26583
  }
26589
26584
  };
@@ -26621,181 +26616,238 @@ const useControlProps = (props, {
26621
26616
  interaction.effect?.(e);
26622
26617
  return true;
26623
26618
  };
26624
- const applyInteraction = (interaction, e, {
26625
- ifValueModified
26626
- } = {}) => {
26627
- if (!interaction) {
26628
- return false;
26629
- }
26630
- return interaction.callback(interaction, e, {
26631
- ifValueModified
26632
- });
26633
- };
26634
- let mousedownInteraction;
26635
- let clickInteraction;
26636
- let inputInteraction;
26637
- let keydownInteraction = {
26638
- name: "keydown",
26639
- callback: asBrowserAction
26640
- };
26641
- // a custom concept being combination of "input", "change" and may other events
26642
- // this even if trigerred when value changes and can be controlled by actionDebounce and actionAfterChange
26643
- let naviChangeInteraction;
26644
- if (controlType === "button") {
26645
- mousedownInteraction = {
26646
- name: "mousedown",
26647
- callback: actionOnMouseDown ? asAction : asInteraction
26648
- // effect: updateUIState,
26649
- };
26650
- clickInteraction = {
26651
- name: "click",
26652
- callback: actionOnMouseDown ? asInteraction : asAction
26653
- // effect: updateUIState,
26654
- };
26655
- } else if (controlType === "input" || controlType === "picker") {
26656
- isCheckable = props.type === "radio" || props.type === "checkbox";
26657
- // On input, gate the interaction (readonly check) and update UI state to reflect the new value.
26658
- inputInteraction = {
26659
- name: "input",
26660
- callback: asInteraction,
26661
- effect: updateUIState
26662
- };
26663
- naviChangeInteraction = {
26664
- name: "navi_change",
26665
- callback: asAction
26666
- };
26667
- enterEffect = e => {
26668
- const input = e.currentTarget;
26669
- dispatchNaviCommand(input, "--navi-send", e);
26670
- };
26671
- if (controlType === "picker") {
26672
- mousedownInteraction = {
26673
- name: "mousedown to open picker",
26674
- callback: asInteraction
26675
- };
26676
- clickInteraction = {
26677
- name: "click to open picker",
26678
- callback: asInteraction
26679
- };
26680
- }
26681
- if (isCheckable) {
26682
- inputInteraction = {
26683
- name: "input",
26684
- callback: asAction
26619
+ const getDefaultInteractionDefinitions = () => {
26620
+ const keyDownDefault = () => {
26621
+ return {
26622
+ name: "keydown",
26623
+ type: "interaction"
26685
26624
  };
26686
- // For checkables, click does NOT update state — it only gates the
26687
- // browser's native check/uncheck via interaction constraints (e.g.
26688
- // readOnly). State actually changes via the "input" event that the
26689
- // browser fires right after, which routes through asAction so the
26690
- // full action pipeline (constraints, navi_action_allowed, sibling
26691
- // uncheck for radios…) runs in one place.
26692
- clickInteraction = {
26693
- name: "click",
26694
- callback: asBrowserAction,
26695
- // When a radio is already checked and gets clicked, the browser does NOT
26696
- // fire an input event (state doesn't change), so asAction never runs.
26697
- // We still want uiAction + command to fire. We can tell whether the click
26698
- // is on an already-checked radio by looking at wasCheckedAtMousedownRef:
26699
- // if it was checked at mousedown, the input event won't come, so we do it here.
26700
- effect: e => {
26701
- const checkable = e.currentTarget;
26702
- if (checkable.type === "radio" && wasCheckedAtMousedownRef.current) {
26703
- updateUIState(e);
26704
- }
26625
+ };
26626
+ if (controlType === "button") {
26627
+ return {
26628
+ keyDown: keyDownDefault,
26629
+ mouseDown: () => {
26630
+ return {
26631
+ name: "mousedown",
26632
+ type: actionOnMouseDown ? "action" : "interaction"
26633
+ };
26634
+ },
26635
+ click: () => {
26636
+ return {
26637
+ name: "click",
26638
+ type: actionOnMouseDown ? "interaction" : "action",
26639
+ always: e => {
26640
+ const button = e.currentTarget;
26641
+ if (button.form) {
26642
+ e.preventDefault(); // prevent form submission
26643
+ }
26644
+ }
26645
+ };
26705
26646
  }
26706
26647
  };
26707
- naviChangeInteraction = undefined;
26708
- enterEffect = e => {
26709
- const checkable = ref.current;
26710
- const interactionAllowed = dispatchRequestInteraction(checkable, e, "enter");
26711
- if (!interactionAllowed) {
26712
- if (checkable.form) {
26713
- e.preventDefault();
26648
+ }
26649
+ if (controlType === "input") {
26650
+ if (props.type === "radio" || props.type === "checkbox") {
26651
+ const isRadio = props.type === "radio";
26652
+ return {
26653
+ keyDown: e => {
26654
+ if (e.key === "Enter") {
26655
+ const inputEl = ref.current;
26656
+ const isRadio = props.type === "radio";
26657
+ const always = () => {
26658
+ if (inputEl.form) {
26659
+ e.preventDefault();
26660
+ }
26661
+ };
26662
+ if (isRadio) {
26663
+ return {
26664
+ name: "enter to check radio",
26665
+ effect: e => {
26666
+ dispatchRequestSetUIState(inputEl, true, {
26667
+ event: e
26668
+ });
26669
+ },
26670
+ always
26671
+ };
26672
+ }
26673
+ const checked = inputEl.checked;
26674
+ if (checked) {
26675
+ return {
26676
+ name: "enter to uncheck checkbox",
26677
+ effect: e => {
26678
+ dispatchRequestSetUIState(inputEl, undefined, {
26679
+ event: e
26680
+ });
26681
+ },
26682
+ always
26683
+ };
26684
+ }
26685
+ return {
26686
+ name: "enter to check",
26687
+ effect: e => {
26688
+ dispatchRequestSetUIState(inputEl, true, {
26689
+ event: e
26690
+ });
26691
+ },
26692
+ always
26693
+ };
26694
+ }
26695
+ if (isRadio && e.key === " ") {
26696
+ const inputEl = e.currentTarget;
26697
+ if (inputEl.checked) {
26698
+ wasCheckedAtMousedownRef.current = true;
26699
+ onClick(e);
26700
+ }
26701
+ }
26702
+ return keyDownDefault();
26703
+ },
26704
+ mouseDown: e => {
26705
+ if (isRadio) {
26706
+ wasCheckedAtMousedownRef.current = e.currentTarget.checked;
26707
+ }
26708
+ },
26709
+ // For checkables, click does NOT update state — it only gates the
26710
+ // browser's native check/uncheck via interaction constraints (e.g.
26711
+ // readOnly). State actually changes via the "input" event that the
26712
+ // browser fires right after, which routes through asAction so the
26713
+ // full action pipeline (constraints, navi_action_allowed, sibling
26714
+ // uncheck for radios…) runs in one place.
26715
+ click: () => {
26716
+ return {
26717
+ name: `click on ${props.type}`,
26718
+ type: "interaction",
26719
+ // When a radio is already checked and gets clicked, the browser does NOT
26720
+ // fire an input event (state doesn't change), so asAction never runs.
26721
+ // We still want uiAction + command to fire. We can tell whether the click
26722
+ // is on an already-checked radio by looking at wasCheckedAtMousedownRef:
26723
+ // if it was checked at mousedown, the input event won't come, so we do it here.
26724
+ effect: isRadio ? e => {
26725
+ if (wasCheckedAtMousedownRef.current) {
26726
+ updateUIState(e);
26727
+ }
26728
+ } : undefined
26729
+ };
26730
+ },
26731
+ input: () => {
26732
+ return {
26733
+ name: "input",
26734
+ type: "action"
26735
+ };
26714
26736
  }
26715
- return;
26716
- }
26717
- let newState;
26718
- if (checkable.type === "checkbox") {
26719
- // toggle: if checked → uncheck (undefined), if unchecked → check (value)
26720
- newState = checkable.checked ? undefined : checkable.value;
26721
- } else {
26722
- // radio: always check
26723
- newState = checkable.value;
26724
- }
26725
- dispatchRequestSetUIState(checkable, newState, {
26726
- event: e
26727
- });
26728
- if (checkable.form) {
26729
- e.preventDefault();
26737
+ };
26738
+ }
26739
+ const keyDownDefaultOnInput = e => {
26740
+ if (e.key === "Enter") {
26741
+ const input = e.currentTarget;
26742
+ return {
26743
+ name: "enter to --navi-send",
26744
+ effect: () => {
26745
+ triggerNaviCommand(input, "--navi-send", e);
26746
+ }
26747
+ };
26730
26748
  }
26749
+ return keyDownDefault();
26731
26750
  };
26732
- if (props.type === "radio") {
26733
- spaceEffect = e => {
26734
- const radio = e.currentTarget;
26735
- if (radio.checked) {
26736
- wasCheckedAtMousedownRef.current = true;
26737
- onClick(e);
26751
+ if (props.type === "range") {
26752
+ return {
26753
+ keyDown: keyDownDefaultOnInput,
26754
+ mouseDown: () => {
26755
+ return {
26756
+ name: "mousedown",
26757
+ type: "interaction",
26758
+ effect: updateUIState
26759
+ };
26760
+ },
26761
+ input: {
26762
+ name: "input",
26763
+ type: "interaction",
26764
+ effect: updateUIState
26765
+ },
26766
+ naviChange: () => {
26767
+ return {
26768
+ name: "navi_change",
26769
+ type: "action"
26770
+ };
26738
26771
  }
26739
26772
  };
26740
26773
  }
26741
- } else if (props.type === "range") {
26742
- mousedownInteraction = {
26743
- name: "mousedown",
26744
- callback: asBrowserAction,
26745
- effect: updateUIState
26774
+ return {
26775
+ keyDown: keyDownDefaultOnInput,
26776
+ input: () => {
26777
+ return {
26778
+ name: "input",
26779
+ type: "interaction",
26780
+ effect: updateUIState
26781
+ };
26782
+ },
26783
+ naviChange: () => {
26784
+ return {
26785
+ name: "navi_change",
26786
+ type: "action"
26787
+ };
26788
+ }
26746
26789
  };
26747
- } else if (props.type === "color") ;
26748
- }
26790
+ }
26791
+ return null;
26792
+ };
26793
+ const defaultInteractionDefinitions = getDefaultInteractionDefinitions();
26794
+ const {
26795
+ interactionDefinitions
26796
+ } = props;
26797
+ const applyInteraction = (interactionName, e, {
26798
+ ifValueModified
26799
+ } = {}) => {
26800
+ const defaultInteractionDefinition = defaultInteractionDefinitions?.[interactionName];
26801
+ const customInteractionDefinition = interactionDefinitions?.[interactionName];
26802
+ const interaction = customInteractionDefinition?.(e) ?? defaultInteractionDefinition?.(e);
26803
+ if (!interaction) {
26804
+ return false;
26805
+ }
26806
+ const {
26807
+ name,
26808
+ effect,
26809
+ type = "interaction",
26810
+ always
26811
+ } = interaction;
26812
+ const dispatchFn = type === "action" ? asAction : asInteraction;
26813
+ const applied = dispatchFn({
26814
+ name,
26815
+ effect
26816
+ }, e, {
26817
+ ifValueModified
26818
+ });
26819
+ always?.(e);
26820
+ return applied;
26821
+ };
26749
26822
  const onMouseDown = e => {
26750
26823
  props.onMouseDown?.(e);
26751
- if (isCheckable && props.type === "radio") {
26752
- wasCheckedAtMousedownRef.current = e.currentTarget.checked;
26753
- }
26754
- applyInteraction(mousedownInteraction, e);
26824
+ applyInteraction("mouseDown", e);
26755
26825
  transferFocusToTarget(e);
26756
26826
  };
26757
26827
  const onClick = e => {
26758
26828
  props.onClick?.(e);
26759
- applyInteraction(clickInteraction, e);
26829
+ applyInteraction("click", e);
26760
26830
  transferFocusToTarget(e);
26761
- const controlHost = e.currentTarget;
26762
- if (controlHost.form) {
26763
- if (controlType === "button") {
26764
- // prevent form submission
26765
- e.preventDefault();
26766
- } else if (controlHost.closest("button")) {
26767
- // prevent button form submission by click
26768
- // (When an input is inside a <button> like for a picker)
26769
- // any click in the picker could trigger form submission as browser see this as click on button inside form
26770
- e.preventDefault();
26771
- }
26772
- }
26773
26831
  };
26774
26832
  const onKeyDown = e => {
26775
26833
  props.onKeyDown?.(e);
26776
- if (e.key === "Enter" && enterEffect) {
26777
- enterEffect(e);
26778
- return;
26779
- }
26780
- if (e.key === " " && spaceEffect) {
26781
- spaceEffect(e);
26782
- return;
26783
- }
26784
- applyInteraction(keydownInteraction, e);
26834
+ applyInteraction("keyDown", e);
26785
26835
  };
26786
26836
  const onInput = e => {
26787
26837
  props.onInput?.(e);
26788
- applyInteraction(inputInteraction, e, {
26838
+ applyInteraction("input", e, {
26789
26839
  ifValueModified: true
26790
26840
  });
26791
26841
  };
26792
- const hasNaviChangeInteraction = Boolean(naviChangeInteraction);
26842
+ // a custom concept being combination of "input", "change" and may other events
26843
+ // this even if trigerred when value changes and can be controlled by actionDebounce and actionAfterChange
26844
+ const hasNaviChangeInteractionDefinition = Boolean(interactionDefinitions?.naviChange || defaultInteractionDefinitions?.naviChange);
26793
26845
  const refCallback = useCallback(field => {
26794
- if (!hasNaviChangeInteraction || actionInteraction === "custom") {
26846
+ if (!hasNaviChangeInteractionDefinition || actionInteraction === "custom") {
26795
26847
  return undefined;
26796
26848
  }
26797
26849
  return addInputEffect(field, e => {
26798
- applyInteraction(naviChangeInteraction, e, {
26850
+ applyInteraction("naviChange", e, {
26799
26851
  ifValueModified: true
26800
26852
  });
26801
26853
  }, {
@@ -26803,7 +26855,7 @@ const useControlProps = (props, {
26803
26855
  debounce: actionDebounce,
26804
26856
  debugInteraction
26805
26857
  });
26806
- }, [actionInteraction, actionAfterChange, actionDebounce, hasNaviChangeInteraction]);
26858
+ }, [actionInteraction, actionAfterChange, actionDebounce, hasNaviChangeInteractionDefinition]);
26807
26859
  const refComposed = useComposeElementRef(refCallback, ref);
26808
26860
  const onPaste = e => {
26809
26861
  props.onPaste?.(e);
@@ -26936,7 +26988,7 @@ const useControlgroupProps = (props, {
26936
26988
  */
26937
26989
  const useControlFacadeProps = (props, options) => {
26938
26990
  const [controlRootProps, controlHostProps, uiStateController] = useControlProps(props, options);
26939
- const facadeController = useUIFacadeStateController(uiStateController);
26991
+ const facadeController = useUIFacadeStateController(props, uiStateController);
26940
26992
  return [controlRootProps, controlHostProps, {
26941
26993
  value: facadeController
26942
26994
  }];
@@ -27017,14 +27069,11 @@ const useInteractiveProps = (props, {
27017
27069
  autoFocusVisible,
27018
27070
  autoSelect
27019
27071
  } = props;
27020
- useAutoFocus(ref, autoFocus, {
27072
+ const autoFocusProps = useAutoFocus(ref, autoFocus, {
27021
27073
  focusVisible: autoFocusVisible,
27022
27074
  autoSelect
27023
27075
  });
27024
- Object.assign(controlHostProps, {
27025
- "navi-autofocus": autoFocus ? autoFocus === true ? "" : autoFocus : undefined,
27026
- "navi-autofocus-select": autoFocus && autoSelect ? "" : undefined
27027
- });
27076
+ Object.assign(controlHostProps, autoFocusProps);
27028
27077
  }
27029
27078
  {
27030
27079
  const {
@@ -27037,13 +27086,12 @@ const useInteractiveProps = (props, {
27037
27086
  readOnly,
27038
27087
  loading
27039
27088
  } = props;
27040
- const idResolved = id || controlToInterfaceContext?.id;
27041
27089
  const disabledResolved = disabled || controlDisabled;
27042
27090
  const requiredResolved = required || controlRequired;
27043
27091
  const loadingResolved = loading || actionStatus.loading || controlLoading && parentActionRequester === ref.current;
27044
27092
  const readOnlyResolved = readOnly || controlReadOnly || loadingResolved || uiStateController.readOnly;
27045
27093
  Object.assign(controlHostProps, {
27046
- "id": idResolved,
27094
+ id,
27047
27095
  "navi-control-proxy-for": naviProxyFor,
27048
27096
  name,
27049
27097
  type,
@@ -27078,6 +27126,7 @@ const useInteractiveProps = (props, {
27078
27126
  }
27079
27127
  {
27080
27128
  const uiState = uiStateController.uiStateSignal.value;
27129
+ const isCheckable = uiStateController.controlType === "input" && (props.type === "radio" || props.type === "checkbox");
27081
27130
  Object.assign(controlHostProps, {
27082
27131
  onnavi_clear_ui_state: e => {
27083
27132
  uiStateController.clearUIState(e);
@@ -27090,6 +27139,16 @@ const useInteractiveProps = (props, {
27090
27139
  },
27091
27140
  onnavi_set_ui_state: e => {
27092
27141
  uiStateController.setUIState(e.detail.value, e);
27142
+ },
27143
+ onnavi_request_check: e => {
27144
+ if (isCheckable) {
27145
+ uiStateController.setUIState(true, e);
27146
+ }
27147
+ },
27148
+ onnavi_request_uncheck: e => {
27149
+ if (isCheckable) {
27150
+ uiStateController.setUIState(false, e);
27151
+ }
27093
27152
  }
27094
27153
  });
27095
27154
  // Mirror ui state handlers on the root so events dispatched on the root element
@@ -27098,7 +27157,9 @@ const useInteractiveProps = (props, {
27098
27157
  onnavi_clear_ui_state: controlHostProps.onnavi_clear_ui_state,
27099
27158
  onnavi_reset_ui_state: controlHostProps.onnavi_reset_ui_state,
27100
27159
  onnavi_get_ui_state: controlHostProps.onnavi_get_ui_state,
27101
- onnavi_set_ui_state: controlHostProps.onnavi_set_ui_state
27160
+ onnavi_set_ui_state: controlHostProps.onnavi_set_ui_state,
27161
+ onnavi_request_check: controlHostProps.onnavi_request_check,
27162
+ onnavi_request_uncheck: controlHostProps.onnavi_request_uncheck
27102
27163
  });
27103
27164
  const {
27104
27165
  statePropName
@@ -28552,7 +28613,7 @@ const LinkPlain = props => {
28552
28613
  selection,
28553
28614
  selectionController
28554
28615
  });
28555
- useAutoFocus(ref, autoFocus);
28616
+ const autoFocusProps = useAutoFocus(ref, autoFocus);
28556
28617
  useConstraints(ref, constraints);
28557
28618
  const shouldDimColor = readOnly || disabled;
28558
28619
  useDimColorWhen(ref, shouldDimColor);
@@ -28604,10 +28665,7 @@ const LinkPlain = props => {
28604
28665
  color: anchor && !innerChildren ? "inherit" : undefined,
28605
28666
  id: anchor ? href.slice(1) : undefined,
28606
28667
  ...remainingProps,
28607
- autoFocus: undefined // See use_auto_focus.js
28608
- ,
28609
-
28610
- "navi-autofocus": autoFocus ? "" : undefined,
28668
+ ...autoFocusProps,
28611
28669
  ref: ref,
28612
28670
  href: href,
28613
28671
  rel: innerRel,
@@ -29919,7 +29977,7 @@ const InputCheckboxFieldInterface = props => {
29919
29977
  color: "var(--loader-color)"
29920
29978
  }), visualVnode, jsx(RealInputCheckbox, {
29921
29979
  ...checkboxHostProps,
29922
- switch: switchProp
29980
+ switch: switchProp ? "" : undefined
29923
29981
  })]
29924
29982
  });
29925
29983
  };
@@ -30055,23 +30113,20 @@ const Field = props => {
30055
30113
  const refDefault = useRef();
30056
30114
  props.ref = props.ref || refDefault;
30057
30115
  const {
30058
- as,
30059
- vertical
30116
+ ownLabel
30060
30117
  } = props;
30061
- if (as === undefined && !vertical) {
30062
- props.as = "label";
30063
- }
30064
- if (props.as === "label") {
30065
- return jsx(FieldAsLabel, {
30118
+ if (ownLabel) {
30119
+ return jsx(FieldAsContainer, {
30066
30120
  ...props
30067
30121
  });
30068
30122
  }
30069
- return jsx(FieldAsContainer, {
30123
+ return jsx(FieldAsLabel, {
30070
30124
  ...props
30071
30125
  });
30072
30126
  };
30073
30127
  const FieldAsLabel = props => {
30074
30128
  return jsx(FieldUI, {
30129
+ as: "label",
30075
30130
  ...props
30076
30131
  });
30077
30132
  };
@@ -32485,8 +32540,6 @@ const InputTextualFirstResolver = props => {
32485
32540
  const Next = useNextResolver();
32486
32541
  const defaultRef = useRef(null);
32487
32542
  props.ref = props.ref || defaultRef;
32488
- const idDefault = useId(); // needed by ui state controller and slot labels
32489
- props.id = props.id || idDefault;
32490
32543
  resolveInputProps(props);
32491
32544
  return jsx(Next, {
32492
32545
  ...props
@@ -32852,11 +32905,10 @@ const FormControl = props => {
32852
32905
  onSubmit: e => {
32853
32906
  const form = e.currentTarget;
32854
32907
  e.preventDefault();
32855
- dispatchInternalCustomEvent(form, "navi_action_allowed", {
32908
+ dispatchRequestAction(form, {
32856
32909
  event: e,
32857
- action: "auto",
32858
- method: "rerun",
32859
- requester: form,
32910
+ requester: e.submitter || form,
32911
+ actionOrigin: "form_submit",
32860
32912
  meta: {
32861
32913
  isSubmit: true
32862
32914
  }
@@ -32891,7 +32943,7 @@ const getFormManagedControls = form => {
32891
32943
  // Exclude inputs that are inside any control group (radio, checkbox, control_group…)
32892
32944
  // — the group host is the managed control for the form, not its individual child inputs.
32893
32945
  const controlGroupHost = element.closest("[navi-control-group]");
32894
- if (controlGroupHost && form.contains(controlGroupHost)) {
32946
+ if (controlGroupHost && controlGroupHost !== form && form.contains(controlGroupHost)) {
32895
32947
  continue;
32896
32948
  }
32897
32949
  managedControls.push(element);
@@ -33313,7 +33365,7 @@ const markAutofocusRestoreOnClose = (containerEl) => {
33313
33365
  // as a result document.activeElement is not up-to-date (can be document.body for instance)
33314
33366
  const getFocusedBeforeTransfer = (e) => {
33315
33367
  const initiator = e.detail.eventChain[0];
33316
- if (initiator.type === "mousedown" && initiator.defaultPrevented) {
33368
+ if (initiator.type === "mousedown") {
33317
33369
  // if we we had let browser give focus, the element would be the one that would be focused
33318
33370
  return initiator.currentTarget;
33319
33371
  }
@@ -33324,7 +33376,7 @@ const getFocusedBeforeTransfer = (e) => {
33324
33376
  return document.activeElement;
33325
33377
  };
33326
33378
 
33327
- const transferFocus = (containerEl, debugFocus, e) => {
33379
+ const transferFocus = (containerEl, debugFocus, e, fallback) => {
33328
33380
  let target;
33329
33381
  let reason;
33330
33382
  if (containerEl.hasAttribute("navi-autofocus-restore")) {
@@ -33364,6 +33416,11 @@ const transferFocus = (containerEl, debugFocus, e) => {
33364
33416
  target = naviAutoFocusFallback;
33365
33417
  }
33366
33418
  }
33419
+ if (!target) {
33420
+ if (fallback) {
33421
+ target = fallback;
33422
+ }
33423
+ }
33367
33424
  if (!target) {
33368
33425
  return;
33369
33426
  }
@@ -33431,6 +33488,7 @@ const Dialog = props => {
33431
33488
  const ref = rest.ref || defaultRef;
33432
33489
  const debugPopup = useDebugPopup();
33433
33490
  const debugFocus = useDebugFocus();
33491
+ const autoFocusProps = useAutoFocus(ref, props.autoFocus);
33434
33492
  const openedRef = useRef(false);
33435
33493
  const [addCleanup, cleanup] = useCleanup();
33436
33494
  const open = (e, {
@@ -33447,7 +33505,7 @@ const Dialog = props => {
33447
33505
  dialogEl.style.setProperty("--anchor-height", `${snapToPixel(height)}px`);
33448
33506
  const focusedBeforeOpen = getFocusedBeforeTransfer(e);
33449
33507
  dialogEl.showModal();
33450
- transferFocus(dialogEl, debugFocus, e);
33508
+ transferFocus(dialogEl, debugFocus, e, focusedBeforeOpen);
33451
33509
  if (scrollTrap) {
33452
33510
  addCleanup(trapScrollInside(dialogEl));
33453
33511
  }
@@ -33512,9 +33570,11 @@ const Dialog = props => {
33512
33570
  };
33513
33571
  return jsx(Box, {
33514
33572
  ...rest,
33573
+ ...autoFocusProps,
33515
33574
  as: "dialog",
33516
33575
  ref: ref,
33517
33576
  baseClassName: "navi_dialog",
33577
+ pseudoClasses: DIALOG_PSEUDO_CLASSES,
33518
33578
  onMouseDown: e => {
33519
33579
  rest.onMouseDown?.(e);
33520
33580
  // The <dialog> element covers the full viewport; clicking the backdrop
@@ -33543,6 +33603,7 @@ const Dialog = props => {
33543
33603
  children: children
33544
33604
  });
33545
33605
  };
33606
+ const DIALOG_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-visible", ":focus-within"];
33546
33607
 
33547
33608
  installImportMetaCssBuild(import.meta);const css$q = /* css */`
33548
33609
  .navi_popover {
@@ -33587,6 +33648,7 @@ const Popover = props => {
33587
33648
  const id = rest.id || defaultId;
33588
33649
  const debugPopup = useDebugPopup();
33589
33650
  const debugFocus = useDebugFocus();
33651
+ const autoFocusProps = useAutoFocus(ref, props.autoFocus);
33590
33652
  const [opened, setOpened] = useState(false);
33591
33653
  const openedRef = useRef(opened);
33592
33654
  openedRef.current = opened;
@@ -33598,7 +33660,7 @@ const Popover = props => {
33598
33660
  const popoverEl = ref.current;
33599
33661
  const focusedBeforeOpen = getFocusedBeforeTransfer(e);
33600
33662
  popoverEl.showPopover();
33601
- transferFocus(popoverEl, debugFocus, e);
33663
+ transferFocus(popoverEl, debugFocus, e, focusedBeforeOpen);
33602
33664
  const effectiveAnchor = anchor || document.documentElement;
33603
33665
  const positionPopover = positionEvent => {
33604
33666
  const {
@@ -33717,9 +33779,10 @@ const Popover = props => {
33717
33779
  id: id,
33718
33780
  popover: "manual",
33719
33781
  ...rest,
33782
+ ...autoFocusProps,
33720
33783
  ref: ref,
33721
33784
  baseClassName: "navi_popover",
33722
- pseudoClasses: PopoverPseudoClasses,
33785
+ pseudoClasses: POPOVER_PSEUDO_CLASSES,
33723
33786
  onnavi_request_open: e => {
33724
33787
  const {
33725
33788
  anchor
@@ -33747,7 +33810,7 @@ const Popover = props => {
33747
33810
  }), children]
33748
33811
  });
33749
33812
  };
33750
- const PopoverPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":focus-within", ":read-only", ":disabled"];
33813
+ const POPOVER_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-visible", ":focus-within"];
33751
33814
 
33752
33815
  installImportMetaCssBuild(import.meta);const css$p = /* css */`
33753
33816
  .navi_picker {
@@ -33769,7 +33832,7 @@ installImportMetaCssBuild(import.meta);const css$p = /* css */`
33769
33832
  border-radius: var(--picker-border-radius);
33770
33833
  outline-width: var(--picker-outline-width);
33771
33834
  outline-color: var(--picker-outline-color);
33772
- outline-offset: var(--picker-outline-offset);
33835
+ outline-offset: 0px;
33773
33836
  box-shadow:
33774
33837
  0 4px 8px rgba(0, 0, 0, 0.08),
33775
33838
  0 12px 40px rgba(0, 0, 0, 0.22);
@@ -33827,6 +33890,10 @@ installImportMetaCssBuild(import.meta);const css$p = /* css */`
33827
33890
  overflow: auto;
33828
33891
  overscroll-behavior: none;
33829
33892
  }
33893
+
33894
+ &[data-focus-visible] {
33895
+ outline-style: solid;
33896
+ }
33830
33897
  }
33831
33898
 
33832
33899
  &[aria-expanded="true"] {
@@ -33855,7 +33922,7 @@ installImportMetaCssBuild(import.meta);const css$p = /* css */`
33855
33922
  border-radius: var(--picker-border-radius);
33856
33923
  outline-width: var(--picker-outline-width);
33857
33924
  outline-color: var(--picker-outline-color);
33858
- outline-offset: var(--picker-outline-offset);
33925
+ outline-offset: 0;
33859
33926
  box-shadow:
33860
33927
  0 4px 8px rgba(0, 0, 0, 0.08),
33861
33928
  0 12px 40px rgba(0, 0, 0, 0.22);
@@ -33867,6 +33934,10 @@ installImportMetaCssBuild(import.meta);const css$p = /* css */`
33867
33934
  flex-direction: column;
33868
33935
  }
33869
33936
 
33937
+ &[data-focus-visible] {
33938
+ outline-style: solid;
33939
+ }
33940
+
33870
33941
  &::backdrop {
33871
33942
  background: rgba(0, 0, 0, 0.4);
33872
33943
  }
@@ -33974,10 +34045,14 @@ const PickerCustom = props => {
33974
34045
  const valueAtOpenRef = useRef(null);
33975
34046
  const activeElementAtOpenRef = useRef(null);
33976
34047
  const onOpen = e => {
33977
- activeElementAtOpenRef.current = e.detail.focusedBeforeOpen;
33978
34048
  expandedRef.current = true;
33979
34049
  setExpanded(true);
33980
- valueAtOpenRef.current = getPickerInputUIState(ref.current);
34050
+ const focusedBeforeOpen = e.detail.focusedBeforeOpen;
34051
+ activeElementAtOpenRef.current = focusedBeforeOpen;
34052
+ debugFocus(e, "picked opened, store element focused", focusedBeforeOpen);
34053
+ const valueAtOpen = getPickerInputUIState(ref.current);
34054
+ valueAtOpenRef.current = valueAtOpen;
34055
+ debugPopup(e, `picker opened, store value at open (${valueAtOpen})`);
33981
34056
  };
33982
34057
  const restoreFocus = e => {
33983
34058
  const activeElementAtOpen = activeElementAtOpenRef.current;
@@ -34001,6 +34076,11 @@ const PickerCustom = props => {
34001
34076
  const onClose = e => {
34002
34077
  const cancelEvent = findEvent(e, eInChain => eInChain.type === "navi_request_close" && eInChain.detail.isCancel);
34003
34078
  const isCancel = Boolean(cancelEvent);
34079
+ const mousedownEvent = findEvent(e, "mousedown");
34080
+ if (mousedownEvent) {
34081
+ debugPopup(e, `closed by mousedown -> disable next click`);
34082
+ disableClickFor();
34083
+ }
34004
34084
  expandedRef.current = false;
34005
34085
  setExpanded(false);
34006
34086
  // Reset so the next opening re-evaluates screen size
@@ -34046,17 +34126,12 @@ const PickerCustom = props => {
34046
34126
  });
34047
34127
  };
34048
34128
  const requestClose = (e = new CustomEvent("programmatic"), {
34049
- cancel = false
34129
+ isCancel = false
34050
34130
  } = {}) => {
34051
- const mousedownEvent = findEvent(e, "mousedown");
34052
- if (mousedownEvent) {
34053
- debugPopup(e, `disable click`);
34054
- disableClickFor();
34055
- }
34056
34131
  const popupEl = popupRef.current;
34057
34132
  return dispatchCustomEvent(popupEl, "navi_request_close", {
34058
34133
  event: e,
34059
- cancel
34134
+ isCancel
34060
34135
  });
34061
34136
  };
34062
34137
  const onInteraction = (e, {
@@ -34108,91 +34183,97 @@ const PickerCustom = props => {
34108
34183
  }
34109
34184
  });
34110
34185
  {
34111
- const {
34112
- onMouseDown,
34113
- onClick,
34114
- onKeyDown
34115
- } = props;
34116
34186
  const onKeyDownShortcuts = createOnKeyDownForShortcuts({
34117
- arrowdown: e => {
34118
- onInteraction(e, {
34187
+ "a-z": e => {
34188
+ return {
34189
+ name: "letter key to open",
34190
+ effect: () => {
34191
+ requestOpen(e);
34192
+ }
34193
+ };
34194
+ },
34195
+ "0-9": e => {
34196
+ return {
34197
+ name: "numeric key to open",
34198
+ effect: () => {
34199
+ requestOpen(e);
34200
+ }
34201
+ };
34202
+ },
34203
+ "arrowdown": e => {
34204
+ return {
34119
34205
  name: "arrow_down_to_open",
34120
34206
  effect: () => {
34121
- e.preventDefault(); // prevent container scroll
34122
34207
  requestOpen(e);
34208
+ e.preventDefault(); // prevent container scroll
34123
34209
  }
34124
- });
34210
+ };
34125
34211
  },
34126
- arrowup: e => {
34127
- onInteraction(e, {
34212
+ "arrowup": e => {
34213
+ return {
34128
34214
  name: "arrow_up_to_open",
34129
34215
  effect: () => {
34130
- e.preventDefault(); // prevent container scroll
34131
34216
  requestOpen(e);
34217
+ e.preventDefault(); // prevent container scroll
34132
34218
  }
34133
- });
34219
+ };
34134
34220
  },
34135
- space: e => {
34136
- onInteraction(e, {
34221
+ "space": e => {
34222
+ return {
34137
34223
  name: "space_to_open",
34138
34224
  effect: () => {
34139
- e.preventDefault(); // prevent scroll
34140
34225
  requestOpen(e);
34226
+ e.preventDefault(); // prevent scroll
34141
34227
  }
34142
- });
34228
+ };
34143
34229
  },
34144
- escape: e => {
34230
+ "escape": e => {
34145
34231
  if (!expandedRef.current) {
34146
- return;
34232
+ return null;
34147
34233
  }
34148
- onInteraction(e, {
34234
+ return {
34149
34235
  name: "escape_to_cancel",
34150
34236
  effect: () => {
34151
- e.preventDefault(); // prevent browser from closing the dialog (if any)
34152
34237
  requestClose(e, {
34153
- cancel: true
34238
+ isCancel: true
34154
34239
  });
34240
+ e.preventDefault(); // prevent browser from closing the dialog (if any)
34155
34241
  }
34156
- });
34242
+ };
34157
34243
  }
34158
34244
  });
34159
34245
  Object.assign(pickerProps, {
34160
- onMouseDown: e => {
34161
- onMouseDown?.(e);
34162
- onInteraction(e, {
34163
- name: "mousedown to open picker",
34164
- effect: () => {
34165
- if (expandedRef.current) {
34166
- requestClose(e);
34167
- } else {
34168
- e.preventDefault(); // prevent browser trying to give focus to the select (popover will take focus)
34246
+ interactionDefinitions: {
34247
+ mouseDown: e => {
34248
+ if (expandedRef.current) {
34249
+ return {
34250
+ name: "mousedown to close picker",
34251
+ effect: () => requestClose(e)
34252
+ };
34253
+ }
34254
+ return {
34255
+ name: "mousedown to open picker",
34256
+ effect: () => {
34169
34257
  debugFocus(e, `prevent browser giving focus to button (mousedown.preventDefault())`);
34170
34258
  requestOpen(e);
34259
+ e.preventDefault(); // prevent browser trying to give focus to the select (popover will take focus)
34171
34260
  }
34172
- }
34173
- });
34174
- },
34175
- onClick: e => {
34176
- // if (e.detail === 0) {
34177
- // disable enter to open that would happen because it's a <button>
34178
- // but we want to keep the input behavior here
34179
- // (space to open, enter to submit)
34180
- // return;
34181
- // }
34182
- onClick?.(e);
34183
- onInteraction(e, {
34184
- name: e.detail === 0 ? "keyboard click to open picker" : "click to open picker",
34185
- effect: () => {
34186
- // When a label is clicked it transfers focus to the select
34187
- // in that case we want to open it (otherwise we have already opened on mousedown interaction)
34188
- e.preventDefault();
34189
- requestOpen(e);
34190
- }
34191
- });
34192
- },
34193
- onKeyDown: e => {
34194
- onKeyDown?.(e);
34195
- onKeyDownShortcuts(e);
34261
+ };
34262
+ },
34263
+ click: e => {
34264
+ // When a label is clicked it transfers focus to the select
34265
+ // in that case we want to open it (otherwise we have already opened on mousedown interaction)
34266
+ return {
34267
+ name: e.detail === 0 ? "click (keyboard or progammatic) to open picker" : "click to open picker",
34268
+ effect: () => {
34269
+ requestOpen(e);
34270
+ e.preventDefault();
34271
+ }
34272
+ };
34273
+ },
34274
+ keyDown: e => {
34275
+ return onKeyDownShortcuts(e);
34276
+ }
34196
34277
  }
34197
34278
  });
34198
34279
  Object.assign(popupProps, {
@@ -34201,7 +34282,7 @@ const PickerCustom = props => {
34201
34282
  return;
34202
34283
  }
34203
34284
  // mousedown inside popover should not bubble to the select (would re-open it if that mousedown closes it)
34204
- debugPopup(e, `popover mouseDown stopPropagation`);
34285
+ debugPopup(e, `"mousedown" received on popup -> prevent bubbling to picker (e.stopPropagation())`);
34205
34286
  e.stopPropagation();
34206
34287
  },
34207
34288
  onClick: e => {
@@ -34209,18 +34290,21 @@ const PickerCustom = props => {
34209
34290
  return;
34210
34291
  }
34211
34292
  // click inside popover should not bubble to the picker (would re-open it if that click closes it)
34212
- // preventDefault also prevents a form submit that would otherwise be triggered when
34213
- // the picker is inside a <form> and the click lands on a non-button element
34214
- debugPopup(e, `popover click stopPropagation + preventDefault`);
34293
+ debugPopup(e, `"click" received on popup -> prevent bubbling to picker (e.stopPropagation())`);
34215
34294
  e.stopPropagation();
34216
- e.preventDefault();
34295
+ // Here we can't preventDefault because the click might be needed to check a radio for instance.
34296
+ // As a result we have to let it go through which means it could trigger form submission
34297
+ // but we've put type="button" on the picker to ensure it can't submit the form
34298
+ // so browser won't submit eventual form for clicks inside the popover/dialog
34299
+ // e.preventDefault();
34217
34300
  },
34218
34301
  onKeyDown: e => {
34219
34302
  // some keys pressed inside popover should not reach the picker button
34220
34303
  // (like enter that would try to request action of closest form otherwise for instance)
34221
34304
  if (e.key === "Enter") {
34305
+ debugPopup(e, `"enter" received on popup -> prevent bubbling to picker (e.stopPropagation())`);
34222
34306
  e.stopPropagation();
34223
- // preventDefault prevents the browser from synthesising a click on the
34307
+ // preventDefault prevents the browser from dispatching a "click" on the
34224
34308
  // picker button when focus moves to it synchronously during enterEffect
34225
34309
  e.preventDefault();
34226
34310
  }
@@ -34291,7 +34375,10 @@ const PickerContentInsidePopover = props => {
34291
34375
  viewportSpacing: viewportSpacing,
34292
34376
  scrollTrap: scrollTrap,
34293
34377
  pointerTrap: pointerTrap,
34294
- focusTrap: focusTrap,
34378
+ focusTrap: focusTrap
34379
+ /* make popover focusable so it can be the first focus target when opening */,
34380
+ tabIndex: -1,
34381
+ autoFocus: "fallback",
34295
34382
  children: [popoverMode === "attached" ? jsx("div", {
34296
34383
  className: "navi_picker_anchor_clone",
34297
34384
  onMouseDown: e => {
@@ -34327,6 +34414,7 @@ const PickerContentInsideDialog = props => {
34327
34414
  scrollTrap: scrollTrap,
34328
34415
  pointerTrap: pointerTrap,
34329
34416
  centerInVisualViewport: true,
34417
+ autoFocus: "fallback",
34330
34418
  children: children
34331
34419
  })
34332
34420
  });
@@ -34473,6 +34561,7 @@ const TimeDate = ({
34473
34561
  if (children === undefined) {
34474
34562
  return jsx(TimeText, {
34475
34563
  ...props,
34564
+ capitalize: false,
34476
34565
  children: formatDatePlaceholder(lang)
34477
34566
  });
34478
34567
  }
@@ -34579,6 +34668,7 @@ const TimeDatetime = ({
34579
34668
  if (children === undefined) {
34580
34669
  return jsx(TimeText, {
34581
34670
  ...props,
34671
+ capitalize: false,
34582
34672
  children: formatDatetimePlaceholder(lang)
34583
34673
  });
34584
34674
  }
@@ -37764,7 +37854,6 @@ const PickerDateUI = props => {
37764
37854
  type: "date",
37765
37855
  color: "var(--picker-placeholder-color",
37766
37856
  capitalize: true,
37767
- maxLines: "1",
37768
37857
  ...props
37769
37858
  });
37770
37859
  }
@@ -37773,7 +37862,6 @@ const PickerDateUI = props => {
37773
37862
  return jsx(Time, {
37774
37863
  type: "date",
37775
37864
  capitalize: true,
37776
- maxLines: "1",
37777
37865
  ...props,
37778
37866
  children: value
37779
37867
  });
@@ -37797,7 +37885,6 @@ const PickerMonthUI = props => {
37797
37885
  return jsx(Time, {
37798
37886
  type: "month",
37799
37887
  color: "var(--picker-placeholder-color",
37800
- maxLines: "1",
37801
37888
  ...props
37802
37889
  });
37803
37890
  }
@@ -37805,7 +37892,6 @@ const PickerMonthUI = props => {
37805
37892
  }
37806
37893
  return jsx(Time, {
37807
37894
  type: "month",
37808
- maxLines: "1",
37809
37895
  capitalize: true,
37810
37896
  ...props,
37811
37897
  children: value
@@ -37830,7 +37916,6 @@ const PickerWeekUI = props => {
37830
37916
  return jsx(Time, {
37831
37917
  type: "week",
37832
37918
  color: "var(--picker-placeholder-color",
37833
- maxLines: "1",
37834
37919
  ...props
37835
37920
  });
37836
37921
  }
@@ -37839,7 +37924,6 @@ const PickerWeekUI = props => {
37839
37924
  return jsx(Time, {
37840
37925
  type: "week",
37841
37926
  capitalize: true,
37842
- maxLines: "1",
37843
37927
  ...props,
37844
37928
  children: value
37845
37929
  });
@@ -37863,7 +37947,6 @@ const PickerTimeUI = props => {
37863
37947
  return jsx(Time, {
37864
37948
  type: "time",
37865
37949
  color: "var(--picker-placeholder-color",
37866
- maxLines: "1",
37867
37950
  ...props
37868
37951
  });
37869
37952
  }
@@ -37871,7 +37954,6 @@ const PickerTimeUI = props => {
37871
37954
  }
37872
37955
  return jsx(Time, {
37873
37956
  type: "time",
37874
- maxLines: "1",
37875
37957
  ...props,
37876
37958
  children: value
37877
37959
  });
@@ -37895,7 +37977,6 @@ const PickerDatetimeUI = props => {
37895
37977
  return jsx(Time, {
37896
37978
  type: "datetime",
37897
37979
  color: "var(--picker-placeholder-color",
37898
- maxLines: "1",
37899
37980
  ...props
37900
37981
  });
37901
37982
  }
@@ -37903,7 +37984,6 @@ const PickerDatetimeUI = props => {
37903
37984
  }
37904
37985
  return jsx(Time, {
37905
37986
  type: "datetime",
37906
- maxLines: "1",
37907
37987
  children: value
37908
37988
  });
37909
37989
  };
@@ -38094,7 +38174,6 @@ installImportMetaCssBuild(import.meta);const css$i = /* css */`
38094
38174
  font-size: var(--picker-font-size);
38095
38175
  font-family: var(--picker-font-family);
38096
38176
  text-align: inherit;
38097
- text-overflow: ellipsis;
38098
38177
  background-color: var(--x-picker-background-color);
38099
38178
  border-width: var(--picker-border-width);
38100
38179
  border-style: solid;
@@ -38107,7 +38186,6 @@ installImportMetaCssBuild(import.meta);const css$i = /* css */`
38107
38186
  cursor: var(--x-picker-cursor, pointer);
38108
38187
  pointer-events: auto;
38109
38188
  user-select: none;
38110
- overflow: hidden;
38111
38189
  -webkit-tap-highlight-color: var(--navi-control-tap-highlight-color);
38112
38190
 
38113
38191
  .navi_picker_value {
@@ -38121,9 +38199,7 @@ installImportMetaCssBuild(import.meta);const css$i = /* css */`
38121
38199
  padding-bottom: var(--x-picker-padding-bottom);
38122
38200
  padding-left: var(--x-picker-padding-left);
38123
38201
  flex-grow: 1;
38124
- text-overflow: ellipsis;
38125
- white-space: nowrap;
38126
- overflow: hidden;
38202
+ justify-content: inherit;
38127
38203
 
38128
38204
  &[navi-placeholder] {
38129
38205
  color: var(--picker-placeholder-color);
@@ -38148,6 +38224,11 @@ installImportMetaCssBuild(import.meta);const css$i = /* css */`
38148
38224
  max-height: 100%;
38149
38225
  }
38150
38226
  }
38227
+ &[navi-single-line] {
38228
+ .navi_picker_right_slot {
38229
+ align-self: center;
38230
+ }
38231
+ }
38151
38232
  .navi_picker_input {
38152
38233
  position: absolute;
38153
38234
  inset: 0;
@@ -38168,16 +38249,6 @@ installImportMetaCssBuild(import.meta);const css$i = /* css */`
38168
38249
  text-align: initial; /* Don't inherit picker text align */
38169
38250
  }
38170
38251
 
38171
- &[data-line-clamp] {
38172
- overflow-wrap: anywhere;
38173
- .navi_picker_value {
38174
- display: -webkit-box;
38175
- white-space: normal;
38176
- -webkit-box-orient: vertical;
38177
- -webkit-line-clamp: var(--picker-max-lines);
38178
- }
38179
- }
38180
-
38181
38252
  /* Hover */
38182
38253
  &[data-hover] {
38183
38254
  --x-picker-background-color: var(--picker-background-color-hover);
@@ -38264,9 +38335,10 @@ const PickerButton = props => {
38264
38335
  icon,
38265
38336
  placeholder,
38266
38337
  ui,
38267
- maxLines,
38338
+ maxLines = 1,
38268
38339
  headless
38269
38340
  } = props;
38341
+ const isSingleLine = maxLines === 1;
38270
38342
  const inputRef = useRef(null);
38271
38343
  const [pickerRemainingProps, inputProps, facadeChildrenProps] = useControlFacadeProps({
38272
38344
  ...props,
@@ -38286,20 +38358,17 @@ const PickerButton = props => {
38286
38358
  children
38287
38359
  } = inputProps;
38288
38360
  const loading = basePseudoState[":-navi-loading"];
38289
- const hasLineClamp = maxLines && maxLines > 1;
38290
38361
  return jsxs(Box, {
38291
38362
  as: headless ? "div" : "button",
38363
+ type: "button" /* ensure click inside the picker cannot submit ancestor form if any */,
38292
38364
  ref: ref,
38293
38365
  baseClassName: "navi_picker",
38294
38366
  pseudoClasses: PICKER_BUTTON_PSEUDO_CLASSES,
38295
38367
  disabled: disabled,
38296
- "data-line-clamp": hasLineClamp ? "" : undefined,
38297
38368
  "data-variant": variant,
38298
- style: {
38299
- "--picker-max-lines": maxLines
38300
- },
38301
38369
  "navi-visually-hidden": headless ? "" : undefined,
38302
38370
  "navi-picker": "",
38371
+ "navi-single-line": isSingleLine ? "" : undefined,
38303
38372
  ...pickerRemainingProps,
38304
38373
  basePseudoState: basePseudoState,
38305
38374
  styleCSSVars: PickerStyleCSSVars
@@ -38337,7 +38406,7 @@ const PickerButton = props => {
38337
38406
  children: [jsx(LoadingOutline, {
38338
38407
  loading: loading,
38339
38408
  color: "var(--picker-loader-color)",
38340
- inset: -1
38409
+ inset: -2
38341
38410
  }), jsx(PickerInput, {
38342
38411
  ...inputProps,
38343
38412
  // eslint-disable-next-line react/no-children-prop
@@ -38351,6 +38420,7 @@ const PickerButton = props => {
38351
38420
  }), variant === "icon" || headless ? null : jsx(Text, {
38352
38421
  className: "navi_picker_value",
38353
38422
  "navi-placeholder": value === undefined || value === "" ? "" : undefined,
38423
+ maxLines: maxLines,
38354
38424
  children: jsx(PickerContext.Provider, {
38355
38425
  value: {
38356
38426
  value,
@@ -38434,8 +38504,6 @@ const PickerFirstResolver = props => {
38434
38504
  const Next = useNextResolver();
38435
38505
  const defaultRef = useRef(null);
38436
38506
  props.ref = props.ref || defaultRef;
38437
- const idDefault = useId(); // needed by ui state controller
38438
- props.id = props.id || idDefault;
38439
38507
  resolveInputProps(props);
38440
38508
  return jsx(Next, {
38441
38509
  ...props