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