@jsenv/navi 0.27.46 → 0.27.48

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.
@@ -15447,7 +15447,12 @@ const asControlHostValue = (
15447
15447
  if (type === "datetime-local") {
15448
15448
  return asDatetimeLocalString(jsValue);
15449
15449
  }
15450
- if (type === "number" || type === "range" || inputMode === "numeric") {
15450
+ if (
15451
+ type === "number" ||
15452
+ type === "range" ||
15453
+ inputMode === "numeric" ||
15454
+ inputMode === "decimal"
15455
+ ) {
15451
15456
  return asNumberString(jsValue);
15452
15457
  }
15453
15458
  if (type === "color") {
@@ -15521,7 +15526,8 @@ const readControlValue = (controlHost) => {
15521
15526
  if (
15522
15527
  type === "number" ||
15523
15528
  type === "range" ||
15524
- controlHost.inputMode === "numeric"
15529
+ controlHost.inputMode === "numeric" ||
15530
+ controlHost.inputMode === "decimal"
15525
15531
  ) {
15526
15532
  return readNumberFromInput(controlHost);
15527
15533
  }
@@ -19426,7 +19432,11 @@ const REQUIRED_CONSTRAINT = {
19426
19432
  return naviI18n("constraint.required.time");
19427
19433
  }
19428
19434
  const inputMode = field.controlHostProps.inputMode;
19429
- if (type === "number" || inputMode === "numeric") {
19435
+ if (
19436
+ type === "number" ||
19437
+ inputMode === "numeric" ||
19438
+ inputMode === "decimal"
19439
+ ) {
19430
19440
  return naviI18n("constraint.required.number");
19431
19441
  }
19432
19442
  if (type === "datetime-local") {
@@ -19587,7 +19597,8 @@ const MAX_LENGTH_CONSTRAINT = {
19587
19597
  } else if (!isTextarea) {
19588
19598
  return null;
19589
19599
  }
19590
- const maxLength = field.controlHostProps.maxLength ?? field.props?.maxLengthGuard;
19600
+ const maxLength =
19601
+ field.controlHostProps.maxLength ?? field.props?.maxLengthGuard;
19591
19602
  if (maxLength === undefined) {
19592
19603
  return null;
19593
19604
  }
@@ -19630,7 +19641,8 @@ const TYPE_NUMBER_CONSTRAINT = {
19630
19641
  }
19631
19642
  const type = field.controlHostProps.type;
19632
19643
  const inputMode = field.controlHostProps.inputMode;
19633
- const isNumber = type === "number" || inputMode === "numeric";
19644
+ const isNumber =
19645
+ type === "number" || inputMode === "numeric" || inputMode === "decimal";
19634
19646
  if (!isNumber) {
19635
19647
  return null;
19636
19648
  }
@@ -19711,7 +19723,8 @@ const MIN_CONSTRAINT = {
19711
19723
  if (!valueAsString) {
19712
19724
  return null;
19713
19725
  }
19714
- const isNumber = type === "number" || inputMode === "numeric";
19726
+ const isNumber =
19727
+ type === "number" || inputMode === "numeric" || inputMode === "decimal";
19715
19728
  if (isNumber) {
19716
19729
  const minNumber = parseFloat(minString);
19717
19730
  if (isNaN(minNumber)) {
@@ -19818,7 +19831,8 @@ const MAX_CONSTRAINT = {
19818
19831
  if (!valueAsString) {
19819
19832
  return null;
19820
19833
  }
19821
- const isNumber = type === "number" || inputMode === "numeric";
19834
+ const isNumber =
19835
+ type === "number" || inputMode === "numeric" || inputMode === "decimal";
19822
19836
  if (isNumber) {
19823
19837
  const maxNumber = parseFloat(maxString);
19824
19838
  if (isNaN(maxNumber)) {
@@ -19940,7 +19954,8 @@ const STEP_CONSTRAINT = {
19940
19954
  }
19941
19955
  const type = field.controlHostProps.type;
19942
19956
  const inputMode = field.controlHostProps.inputMode;
19943
- const isNumericText = type === "text" && inputMode === "numeric";
19957
+ const isNumericText =
19958
+ type === "text" && (inputMode === "numeric" || inputMode === "decimal");
19944
19959
  if (!isNumericText && !STEP_SUPPORTED_TYPE_SET.has(type)) {
19945
19960
  return null;
19946
19961
  }
@@ -22598,13 +22613,17 @@ const useUIStateController = (
22598
22613
  );
22599
22614
  }
22600
22615
  }
22601
- // Still fire uiAction so external listeners (e.g. signals) stay in
22602
- // sync, but do NOT fire the command and do NOT notify the parent —
22603
- // both would cause an infinite loop when a parent cascades state
22604
- // down to its children (child command would re-trigger the cascade).
22605
- uiStateController.onUIAction(e, {
22606
- skipCommand: true,
22607
- });
22616
+ // initial_state_push is pure initialization (equivalent to defaultValue on the
22617
+ // child itself): skip uiAction entirely so no side effects fire on mount.
22618
+ if (e.type !== "initial_state_push") {
22619
+ // Still fire uiAction so external listeners (e.g. signals) stay in
22620
+ // sync, but do NOT fire the command and do NOT notify the parent —
22621
+ // both would cause an infinite loop when a parent cascades state
22622
+ // down to its children (child command would re-trigger the cascade).
22623
+ uiStateController.onUIAction(e, {
22624
+ skipCommand: true,
22625
+ });
22626
+ }
22608
22627
  // Exception: when the facade propagates a child state change up to the
22609
22628
  // real picker input, also notify the parent group (e.g. Form) so it
22610
22629
  // keeps its cached aggregated state in sync and fires its own uiAction.
@@ -22741,7 +22760,6 @@ const useUIStateController = (
22741
22760
  uiStateController.rules = rules;
22742
22761
  return uiStateController;
22743
22762
  };
22744
-
22745
22763
  const NO_PARENT = [() => {}, () => {}, () => {}];
22746
22764
  const useParentControllerNotifiers = (
22747
22765
  parentUIStateController,
@@ -22821,6 +22839,90 @@ const useParentControllerNotifiers = (
22821
22839
  * **Filtering**: `childControlFilter` can exclude certain child types from aggregation
22822
22840
  * (e.g. ignoring buttons inside a selectable list).
22823
22841
  */
22842
+ const CANNOT_DERIVE = Symbol("cannot_derive");
22843
+
22844
+ // Default aggregate/distribute implementations keyed by controlType or stateType.
22845
+ // Looked up in useUIGroupStateController to fill in omitted aggregateChildStates /
22846
+ // distributeChildUIState. If neither a default nor an explicit impl is found for a
22847
+ // group, creation throws so the caller knows it must supply them.
22848
+ const GROUP_DEFAULTS = {
22849
+ radio_group: {
22850
+ childControlFilter: (child) =>
22851
+ child.controlType === "input" && child.controlHostProps?.type === "radio",
22852
+ aggregateChildStates: (children) => {
22853
+ for (const child of children) {
22854
+ const childUIState = child.uiState;
22855
+ if (childUIState !== undefined) {
22856
+ return childUIState;
22857
+ }
22858
+ }
22859
+ return undefined;
22860
+ },
22861
+ distributeChildUIState: (newUIState, childUIStateController) => {
22862
+ const childSelected = childUIStateController.props.value === newUIState;
22863
+ if (childSelected) {
22864
+ return childUIStateController.props.value;
22865
+ }
22866
+ return undefined;
22867
+ },
22868
+ },
22869
+ checkbox_group: {
22870
+ childControlFilter: (child) =>
22871
+ child.controlType === "input" &&
22872
+ child.controlHostProps?.type === "checkbox",
22873
+ aggregateChildStates: (children) => {
22874
+ const values = [];
22875
+ for (const child of children) {
22876
+ const childUIState = child.uiState;
22877
+ if (childUIState !== undefined) {
22878
+ values.push(childUIState);
22879
+ }
22880
+ }
22881
+ return values.length === 0 ? undefined : values;
22882
+ },
22883
+ distributeChildUIState: (newUIState, childUIStateController) => {
22884
+ const childSelected =
22885
+ Array.isArray(newUIState) &&
22886
+ newUIState.includes(childUIStateController.props.value);
22887
+ if (childSelected) {
22888
+ return childUIStateController.props.value;
22889
+ }
22890
+ return undefined;
22891
+ },
22892
+ },
22893
+ object: {
22894
+ aggregateChildStates: (children) => {
22895
+ const groupValues = {};
22896
+ for (const child of children) {
22897
+ const { name, uiState, allowNameless } = child;
22898
+ if (!name) {
22899
+ if (!allowNameless) {
22900
+ console.warn(
22901
+ "A group child is missing a name property, its state won't be included in the group state",
22902
+ child,
22903
+ );
22904
+ }
22905
+ continue;
22906
+ }
22907
+ groupValues[name] = uiState;
22908
+ }
22909
+ return groupValues;
22910
+ },
22911
+ distributeChildUIState: (newUIState, child) => {
22912
+ const childName = child.name;
22913
+ if (
22914
+ childName &&
22915
+ newUIState !== null &&
22916
+ typeof newUIState === "object" &&
22917
+ Object.prototype.hasOwnProperty.call(newUIState, childName)
22918
+ ) {
22919
+ return newUIState[childName];
22920
+ }
22921
+ return CANNOT_DERIVE;
22922
+ },
22923
+ },
22924
+ };
22925
+
22824
22926
  const useUIGroupStateController = (
22825
22927
  props,
22826
22928
  controlType,
@@ -22840,11 +22942,26 @@ const useUIGroupStateController = (
22840
22942
  const debugUIGroup = useDebugUIState();
22841
22943
  const debugFocus = useDebugFocus();
22842
22944
 
22843
- if (typeof aggregateChildStates !== "function") {
22844
- throw new TypeError("aggregateChildStates must be a function");
22945
+ const defaults = GROUP_DEFAULTS[controlType] ?? GROUP_DEFAULTS[stateType];
22946
+ const resolvedChildControlFilter =
22947
+ childControlFilter ?? defaults?.childControlFilter ?? null;
22948
+ const resolvedAggregateChildStates =
22949
+ aggregateChildStates ?? defaults?.aggregateChildStates;
22950
+ const resolvedDistributeChildUIState =
22951
+ distributeChildUIState ?? defaults?.distributeChildUIState;
22952
+ if (
22953
+ typeof resolvedAggregateChildStates !== "function" ||
22954
+ typeof resolvedDistributeChildUIState !== "function"
22955
+ ) {
22956
+ throw new Error(
22957
+ `No aggregate/distribute implementation found for controlType="${controlType}" stateType="${stateType}". ` +
22958
+ `Either use a known controlType/stateType or provide aggregateChildStates and distributeChildUIState explicitly.`,
22959
+ );
22845
22960
  }
22846
22961
  const parentUIStateController = useContext(ParentUIStateControllerContext);
22847
- const { id, name, value, uiAction, command } = props;
22962
+ const hasValueProp = Object.hasOwn(props, "value");
22963
+ const hasDefaultValueProp = Object.hasOwn(props, "defaultValue");
22964
+ const { id, name, value, defaultValue, uiAction, command } = props;
22848
22965
  const ref = props.ref;
22849
22966
  const uiActionRef = useRef(uiAction);
22850
22967
  const fallbackState =
@@ -22893,7 +23010,7 @@ const useUIGroupStateController = (
22893
23010
  pendingChangeRef.current = true;
22894
23011
  return;
22895
23012
  }
22896
- const aggChildState = aggregateChildStates(
23013
+ const aggChildState = resolvedAggregateChildStates(
22897
23014
  childUIStateControllerArray,
22898
23015
  fallbackState,
22899
23016
  );
@@ -22958,13 +23075,68 @@ const useUIGroupStateController = (
22958
23075
  }
22959
23076
  });
22960
23077
 
23078
+ const isMonitoringChild = (childUIStateController) => {
23079
+ if (childUIStateController.isProxy) {
23080
+ return false;
23081
+ }
23082
+ if (
23083
+ resolvedChildControlFilter &&
23084
+ !resolvedChildControlFilter(childUIStateController)
23085
+ ) {
23086
+ return false;
23087
+ }
23088
+ return true;
23089
+ };
23090
+ const shouldPropagateStateToChild = (childUIStateController) => {
23091
+ if (!isMonitoringChild(childUIStateController)) {
23092
+ return false;
23093
+ }
23094
+ if (childUIStateController.controlType === "button") {
23095
+ return false;
23096
+ }
23097
+ return true;
23098
+ };
23099
+
22961
23100
  const existingController = controllerRef.current;
22962
23101
  if (existingController) {
23102
+ const prevValue = existingController.value;
23103
+ const prevHasValueProp = existingController.hasValueProp;
22963
23104
  existingController.props = props;
22964
23105
  existingController.id = id;
22965
23106
  existingController.name = name;
22966
23107
  existingController.value = value;
23108
+ existingController.defaultValue = defaultValue;
23109
+ existingController.hasValueProp = hasValueProp;
23110
+ existingController.hasDefaultValueProp = hasDefaultValueProp;
22967
23111
  uiActionRef.current = uiAction;
23112
+ // When the controlled value prop changes (or when becoming controlled for the
23113
+ // first time), silently cascade to children that have no individual state prop.
23114
+ if (
23115
+ hasValueProp &&
23116
+ (!prevHasValueProp || !compareTwoJsValues(value, prevValue))
23117
+ ) {
23118
+ const propagateDownEvent = new CustomEvent(
23119
+ "propagate_down_set_ui_state",
23120
+ { detail: {} },
23121
+ );
23122
+ for (const childUIStateController of childUIStateControllerArray) {
23123
+ if (!shouldPropagateStateToChild(childUIStateController)) {
23124
+ continue;
23125
+ }
23126
+ if (childUIStateController.hasStateProp) {
23127
+ continue;
23128
+ }
23129
+ const childNewState = existingController.distributeChildUIState(
23130
+ value,
23131
+ childUIStateController,
23132
+ );
23133
+ if (childNewState === CANNOT_DERIVE) {
23134
+ continue;
23135
+ }
23136
+ childUIStateController.setUIState(childNewState, propagateDownEvent);
23137
+ }
23138
+ existingController.syncInternalState(value);
23139
+ }
22968
23140
  return existingController;
22969
23141
  }
22970
23142
  debugUIGroup(
@@ -22973,32 +23145,23 @@ const useUIGroupStateController = (
22973
23145
 
22974
23146
  const [publishUIState, subscribeUIState] = createPubSub();
22975
23147
  const uiStateSignal = signal(fallbackState);
22976
- const isMonitoringChild = (childUIStateController) => {
22977
- if (childUIStateController.isProxy) {
22978
- return false;
22979
- }
22980
- if (childControlFilter && !childControlFilter(childUIStateController)) {
22981
- return false;
22982
- }
22983
- return true;
22984
- };
22985
23148
  const groupUIStateController = {
22986
23149
  controlType,
22987
23150
  id,
22988
23151
  name,
22989
23152
  value,
23153
+ defaultValue,
23154
+ hasValueProp,
23155
+ hasDefaultValueProp,
22990
23156
  props,
22991
23157
  uiState: fallbackState,
22992
23158
  uiStateSignal,
22993
23159
  wantRequesterButtonState,
22994
23160
  elementRef: ref,
22995
23161
  getPropFromState: (uiState) => uiState,
22996
- // Cascades the new value to each monitored child (fires each child's uiAction
22997
- // via internalBehavior), then re-aggregates and fires this group's own reactions.
22998
- // Cascade strategy depends on controlType:
22999
- // - "radio_group": child gets true/false based on whether its value matches the scalar state.
23000
- // - "checkbox_group": child gets true/false based on whether its value is in the state array.
23001
- // - default (ControlGroup): child gets the value at its named key in the state object.
23162
+ distributeChildUIState: resolvedDistributeChildUIState,
23163
+ // Cascades newUIState to each monitored child via resolvedDistributeChildUIState,
23164
+ // then re-aggregates and fires this group's own reactions.
23002
23165
  setUIState: (newUIState, e) => {
23003
23166
  if (
23004
23167
  stateType === "object" &&
@@ -23017,73 +23180,42 @@ const useUIGroupStateController = (
23017
23180
  );
23018
23181
  return;
23019
23182
  }
23020
- const propagateDownEvent = new CustomEvent(
23021
- "propagate_down_set_ui_state",
23022
- { detail: {} },
23023
- );
23183
+ // initial_state_push propagates silently (no uiAction anywhere in the chain);
23184
+ // regular updates use propagate_down_set_ui_state which fires uiAction on children.
23185
+ const propagateEventType =
23186
+ e.type === "initial_state_push"
23187
+ ? "initial_state_push"
23188
+ : "propagate_down_set_ui_state";
23189
+ const propagateDownEvent = new CustomEvent(propagateEventType, {
23190
+ detail: {},
23191
+ });
23024
23192
  chainEvent(propagateDownEvent, e);
23025
23193
  for (const childUIStateController of childUIStateControllerArray) {
23026
- if (!isMonitoringChild(childUIStateController)) {
23194
+ if (!shouldPropagateStateToChild(childUIStateController)) {
23027
23195
  continue;
23028
23196
  }
23029
- if (childUIStateController.controlType === "button") {
23197
+ const childNewState = resolvedDistributeChildUIState(
23198
+ newUIState,
23199
+ childUIStateController,
23200
+ );
23201
+ if (childNewState === CANNOT_DERIVE) {
23030
23202
  continue;
23031
23203
  }
23032
- if (controlType === "radio_group") {
23033
- const childSelected =
23034
- childUIStateController.props.value === newUIState;
23035
- // Pass the child's own value (not `true`) when selected, `undefined` (not `false`) when not.
23036
- // `toDomProps` uses `newUIState !== undefined` to determine `checked`, so passing `false`
23037
- // would incorrectly render the radio as checked.
23038
- const childNewState = childSelected
23039
- ? childUIStateController.props.value
23040
- : undefined;
23041
- childUIStateController.setUIState(childNewState, propagateDownEvent);
23042
- } else if (controlType === "checkbox_group") {
23043
- const childSelected =
23044
- Array.isArray(newUIState) &&
23045
- newUIState.includes(childUIStateController.props.value);
23046
- const childNewState = childSelected
23047
- ? childUIStateController.props.value
23048
- : undefined;
23049
- childUIStateController.setUIState(childNewState, propagateDownEvent);
23050
- } else if (distributeChildUIState) {
23051
- const childrenStateMap = distributeChildUIState(newUIState);
23052
- if (childrenStateMap && typeof childrenStateMap === "object") {
23053
- const childName = childUIStateController.name;
23054
- if (
23055
- childName &&
23056
- Object.prototype.hasOwnProperty.call(childrenStateMap, childName)
23057
- ) {
23058
- childUIStateController.setUIState(
23059
- childrenStateMap[childName],
23060
- propagateDownEvent,
23061
- );
23062
- }
23063
- }
23064
- } else {
23065
- const childName = childUIStateController.name;
23066
- if (
23067
- childName &&
23068
- newUIState !== null &&
23069
- typeof newUIState === "object" &&
23070
- Object.prototype.hasOwnProperty.call(newUIState, childName)
23071
- ) {
23072
- childUIStateController.setUIState(
23073
- newUIState[childName],
23074
- propagateDownEvent,
23075
- );
23076
- }
23077
- }
23204
+ childUIStateController.setUIState(childNewState, propagateDownEvent);
23078
23205
  }
23079
23206
  // Re-aggregate from children and apply — do NOT call onChange to avoid a loop
23080
23207
  // (onChange would call setUIState again, which would cascade again).
23081
- const aggChildState = aggregateChildStates(
23208
+ const aggChildState = resolvedAggregateChildStates(
23082
23209
  childUIStateControllerArray,
23083
23210
  fallbackState,
23084
23211
  );
23085
23212
  const groupUIState =
23086
23213
  aggChildState === undefined ? fallbackState : aggChildState;
23214
+ if (e.type === "initial_state_push") {
23215
+ // Silent initialization: update state without firing uiAction or notifying parent.
23216
+ groupUIStateController.syncInternalState(groupUIState);
23217
+ return;
23218
+ }
23087
23219
  applyState(groupUIState, e, { internalBehavior: true });
23088
23220
  },
23089
23221
  // Called on mount/unmount/render-batch: updates state silently with no external reactions.
@@ -23133,6 +23265,33 @@ const useUIGroupStateController = (
23133
23265
  debugUIGroup(
23134
23266
  `${controlType}.registerChild("${childControlType}") -> registered (total: ${childUIStateControllerArray.length})`,
23135
23267
  );
23268
+ // Auto-derive child's initial state from the group's value/defaultValue
23269
+ // when the child has no individually-controlled state prop.
23270
+ // Use initial_state_push so uiAction does not fire during mount initialization.
23271
+ if (!childUIStateController.hasStateProp) {
23272
+ const initialEvent = new CustomEvent("initial_state_push", {
23273
+ detail: {},
23274
+ });
23275
+ if (groupUIStateController.hasValueProp) {
23276
+ // Controlled: always cascade current group value (even undefined = deselect all).
23277
+ const childNewState = resolvedDistributeChildUIState(
23278
+ groupUIStateController.value,
23279
+ childUIStateController,
23280
+ );
23281
+ if (childNewState !== CANNOT_DERIVE) {
23282
+ childUIStateController.setUIState(childNewState, initialEvent);
23283
+ }
23284
+ } else if (groupUIStateController.hasDefaultValueProp) {
23285
+ // Uncontrolled: set initial state from defaultValue on mount.
23286
+ const childNewState = resolvedDistributeChildUIState(
23287
+ groupUIStateController.defaultValue,
23288
+ childUIStateController,
23289
+ );
23290
+ if (childNewState !== CANNOT_DERIVE) {
23291
+ childUIStateController.setUIState(childNewState, initialEvent);
23292
+ }
23293
+ }
23294
+ }
23136
23295
  onChange(new CustomEvent(`${childControlType}_mount`), {
23137
23296
  notifyExternal: "silent",
23138
23297
  // childUIStateController,
@@ -23211,10 +23370,7 @@ const useUIGroupStateController = (
23211
23370
  );
23212
23371
  chainEvent(propagateDownResetEvent, e);
23213
23372
  for (const childUIStateController of childUIStateControllerArray) {
23214
- if (!isMonitoringChild(childUIStateController)) {
23215
- continue;
23216
- }
23217
- if (childUIStateController.controlType === "button") {
23373
+ if (!shouldPropagateStateToChild(childUIStateController)) {
23218
23374
  continue;
23219
23375
  }
23220
23376
  childUIStateController.resetUIState(propagateDownResetEvent);
@@ -23387,6 +23543,18 @@ const useUIFacadeStateController = (props, realUIStateController) => {
23387
23543
  );
23388
23544
  firstChildControllerRef.current = child;
23389
23545
  realUIStateController.facadeChild = child;
23546
+ // If the picker already has a meaningful state (from value or defaultValue),
23547
+ // push it to the child on registration so it reflects the pre-set value
23548
+ // without firing uiAction (equivalent to defaultValue on the child itself).
23549
+ const initialState = realUIStateController.uiState;
23550
+ if (initialState !== undefined) {
23551
+ updatingRef.current = true;
23552
+ const initialEvent = new CustomEvent("initial_state_push", {
23553
+ detail: {},
23554
+ });
23555
+ child.setUIState(initialState, initialEvent);
23556
+ updatingRef.current = false;
23557
+ }
23390
23558
  }
23391
23559
  },
23392
23560
  unregisterChild: (child) => {
@@ -23479,6 +23647,10 @@ const INTERNAL_EVENT_SET = new Set([
23479
23647
  // so the picker knows the child's current value (e.g. for "store value at open"),
23480
23648
  // but no action pipeline, command, or synthetic input event should fire.
23481
23649
  "facade_child_mount_sync",
23650
+ // Facade pushing its current state (from value/defaultValue) down to the first child
23651
+ // on registration, and group pushing value/defaultValue to children on registerChild.
23652
+ // Equivalent to defaultValue initialization: no uiAction, no commands, no parent notification.
23653
+ "initial_state_push",
23482
23654
  ]);
23483
23655
  const isInternalEvent = (e) => {
23484
23656
  return INTERNAL_EVENT_SET.has(e.type);
@@ -24282,6 +24454,12 @@ const useControlgroupProps = (props, {
24282
24454
  cascadeValidationToChildren
24283
24455
  });
24284
24456
  const [boundAction] = useActionBoundToOneParam(action, uiGroupStateController.uiStateSignal);
24457
+ // Mirror single-input behaviour: a controlled value with no handler makes the
24458
+ // group read-only so children don't appear interactive when they can't change.
24459
+ const implicitReadOnly = uiGroupStateController.hasValueProp && !action && !props.uiAction;
24460
+ if (implicitReadOnly && !props.readOnly) {
24461
+ props.readOnly = true;
24462
+ }
24285
24463
  const [actionRequester, setActionRequester] = useState();
24286
24464
  const [controlRootProps, controlgroupProps] = useInteractiveProps(props, {
24287
24465
  uiStateController: uiGroupStateController,
@@ -30480,25 +30658,7 @@ const ControlGroup = props => {
30480
30658
  wantRequesterButtonState: true,
30481
30659
  controlType: props.type || "control_group",
30482
30660
  stateType: "object",
30483
- cascadeValidationToChildren: true,
30484
- aggregateChildStates: childUIStateControllers => {
30485
- const groupValues = {};
30486
- for (const childUIStateController of childUIStateControllers) {
30487
- const {
30488
- name,
30489
- uiState,
30490
- allowNameless
30491
- } = childUIStateController;
30492
- if (!name) {
30493
- if (!allowNameless) {
30494
- console.warn("A ControlGroup child is missing a name property, its state won't be included in the group state", childUIStateController);
30495
- }
30496
- continue;
30497
- }
30498
- groupValues[name] = uiState;
30499
- }
30500
- return groupValues;
30501
- }
30661
+ cascadeValidationToChildren: true
30502
30662
  });
30503
30663
  const {
30504
30664
  children
@@ -31979,7 +32139,6 @@ const NAVI_TYPE_DEFAULTS = {
31979
32139
  },
31980
32140
  navi_number: {
31981
32141
  type: "text",
31982
- inputMode: "numeric",
31983
32142
  autoCorrect: "off",
31984
32143
  spellcheck: false,
31985
32144
  autoComplete: "off",
@@ -32058,6 +32217,16 @@ const resolveInputProps = (props) => {
32058
32217
  if (!currentTypeDefaults) {
32059
32218
  return;
32060
32219
  }
32220
+ // For navi_number: choose inputMode based on whether step/min/max suggest decimals.
32221
+ // inputMode="numeric" (integer keyboard) vs "decimal" (keyboard with decimal separator).
32222
+ if (currentType === "navi_number" && props.inputMode === undefined) {
32223
+ props.inputMode =
32224
+ hasDecimalPlaces(props.step) ||
32225
+ hasDecimalPlaces(props.min) ||
32226
+ hasDecimalPlaces(props.max)
32227
+ ? "decimal"
32228
+ : "numeric";
32229
+ }
32061
32230
  for (const key of Object.keys(currentTypeDefaults)) {
32062
32231
  if (props[key] === undefined) {
32063
32232
  props[key] = currentTypeDefaults[key];
@@ -32152,6 +32321,14 @@ const STEP_FORMATTER_BY_TYPE = {
32152
32321
  "datetime": timeStringToSeconds,
32153
32322
  };
32154
32323
 
32324
+ const hasDecimalPlaces = (value) => {
32325
+ if (value === undefined || value === null) {
32326
+ return false;
32327
+ }
32328
+ const num = Number(value);
32329
+ return !isNaN(num) && !Number.isInteger(num);
32330
+ };
32331
+
32155
32332
  installImportMetaCssBuild(import.meta);const css$y = /* css */`
32156
32333
  @layer navi {
32157
32334
  .navi_input_range {
@@ -32422,7 +32599,8 @@ const InputRangeFieldInterface = props => {
32422
32599
  };
32423
32600
  resolveInputProps(props);
32424
32601
  const [rangeRootProps, rangeHostProps] = useControlProps(props, {
32425
- controlType: "input"});
32602
+ controlType: "input"
32603
+ });
32426
32604
  const {
32427
32605
  basePseudoState
32428
32606
  } = rangeHostProps;
@@ -32523,7 +32701,7 @@ const RangePseudoElements = ["::-navi-loader"];
32523
32701
 
32524
32702
  const InputModeResolver = props => {
32525
32703
  const Next = useNextResolver();
32526
- if (props.inputMode === "numeric") {
32704
+ if (props.inputMode === "numeric" || props.inputMode === "decimal") {
32527
32705
  return jsx(InputModeNumeric, {
32528
32706
  ...props
32529
32707
  });
@@ -33078,6 +33256,38 @@ const InputTextualWithSuggestions = props => {
33078
33256
  });
33079
33257
  };
33080
33258
 
33259
+ // When readonly focus and mousedown should select input content
33260
+ // (the only relevant interaction to perform on readonly is copying the value)
33261
+ // Nice side effect is that input_group.jsx will see all input is selected
33262
+ // and arrow left/right will always nav between inputs.
33263
+ // (Otherwise we would prevent left/right + show calllout about readonly)
33264
+ const useAutoSelectReadOnly = (props) => {
33265
+ const onFocus = (e) => {
33266
+ props.onFocus(e);
33267
+ if (e.defaultPrevented) {
33268
+ return;
33269
+ }
33270
+ if (!e.target.readOnly) {
33271
+ return;
33272
+ }
33273
+ e.preventDefault();
33274
+ e.target.select();
33275
+ };
33276
+ const onMouseDown = (e) => {
33277
+ props.onMouseDown(e);
33278
+ if (e.defaultPrevented) {
33279
+ return;
33280
+ }
33281
+ if (!e.target.readOnly) {
33282
+ return;
33283
+ }
33284
+ e.preventDefault();
33285
+ e.target.select();
33286
+ };
33287
+
33288
+ return { onFocus, onMouseDown };
33289
+ };
33290
+
33081
33291
  installImportMetaCssBuild(import.meta);/**
33082
33292
  * Input component for all textual input types.
33083
33293
  *
@@ -33513,35 +33723,14 @@ const InputTextualFirstResolver = props => {
33513
33723
  };
33514
33724
  const InputTextual = createComponentResolver([InputTextualFirstResolver, InputWithListResolver, InputWithSuggestionsResolver, InputTypeResolver, InputModeResolver, InputHeadlessResolver, InputTextualUI]);
33515
33725
  const RealInput = props => {
33726
+ const autoSelectReadOnlyProps = useAutoSelectReadOnly(props);
33516
33727
  return jsx(Box, {
33517
33728
  ...props,
33518
33729
  as: "input",
33519
- baseClassName: "navi_control_input"
33520
- // When readonly focus and mousedown should select input content
33521
- // (the only relevant interaction to perform on readonly is copying the value)
33522
- // Nice side effect is that input_group.jsx will see all input is selected
33523
- // and arrow left/right will always nav between inputs.
33524
- // (Otherwise we would prevent left/right + show calllout about readonly)
33525
- ,
33526
-
33527
- onFocus: e => {
33528
- props.onFocus(e);
33529
- if (!e.defaultPrevented && e.target.readOnly) {
33530
- e.preventDefault();
33531
- e.target.select();
33532
- }
33533
- },
33534
- onMouseDown: e => {
33535
- props.onMouseDown(e);
33536
- if (!e.defaultPrevented && e.target.readOnly) {
33537
- e.preventDefault();
33538
- e.target.select();
33539
- }
33540
- }
33730
+ baseClassName: "navi_control_input",
33731
+ ...autoSelectReadOnlyProps,
33541
33732
  // Never set native maxLength — our guard handles it. maxLength stays in
33542
33733
  // inputControlHostProps so form validation constraints still read it.
33543
- ,
33544
-
33545
33734
  maxLength: undefined
33546
33735
  });
33547
33736
  };
@@ -33856,25 +34045,7 @@ const FormControl = props => {
33856
34045
  wantRequesterButtonState: true,
33857
34046
  controlType: "form",
33858
34047
  stateType: "object",
33859
- cascadeValidationToChildren: true,
33860
- aggregateChildStates: childUIStateControllers => {
33861
- const formValues = {};
33862
- for (const childUIStateController of childUIStateControllers) {
33863
- const {
33864
- name,
33865
- uiState,
33866
- allowNameless
33867
- } = childUIStateController;
33868
- if (!name) {
33869
- if (!allowNameless) {
33870
- console.warn("A form child component is missing a name property, its state won't be included in the form state", childUIStateController);
33871
- }
33872
- continue;
33873
- }
33874
- formValues[name] = uiState;
33875
- }
33876
- return formValues;
33877
- }
34048
+ cascadeValidationToChildren: true
33878
34049
  });
33879
34050
  const {
33880
34051
  basePseudoState,
@@ -34100,19 +34271,7 @@ const CheckboxGroupInterface = props => {
34100
34271
  ...props
34101
34272
  }, {
34102
34273
  stateType: "array",
34103
- controlType: "checkbox_group",
34104
- childControlFilter: childUIStateController => {
34105
- return childUIStateController.controlType === "input" && childUIStateController.controlHostProps.type === "checkbox";
34106
- },
34107
- aggregateChildStates: childUIStateControllers => {
34108
- const values = [];
34109
- for (const childUIStateController of childUIStateControllers) {
34110
- if (childUIStateController.uiState) {
34111
- values.push(childUIStateController.uiState);
34112
- }
34113
- }
34114
- return values.length === 0 ? undefined : values;
34115
- }
34274
+ controlType: "checkbox_group"
34116
34275
  });
34117
34276
  useFocusGroup(ref, {
34118
34277
  wrap: "both"
@@ -34439,6 +34598,9 @@ const isTextInputElement = (el) => {
34439
34598
  installImportMetaCssBuild(import.meta);const css$t = /* css */`
34440
34599
  .navi_input_duration {
34441
34600
  --duration-separator-spacing: 4px;
34601
+ --loader-color: var(--navi-loader-color);
34602
+
34603
+ position: relative; /* For loading outline */
34442
34604
 
34443
34605
  .navi_label {
34444
34606
  &[data-separator] {
@@ -34540,13 +34702,8 @@ const InputDuration = props => {
34540
34702
  // Exception: if the current value already has minutes, show them read-only so
34541
34703
  // the stored precision is faithfully displayed.
34542
34704
  const showMinutes = maxSeconds >= 60 && (stepSeconds === undefined || stepSeconds % 3600 !== 0 || valueHasMinutes);
34543
-
34544
- // Mirror the single-input behaviour: a controlled value with no handler makes the field read-only.
34545
- const implicitReadOnly = hasValue && !uiAction && !action;
34546
- if (implicitReadOnly && !props.readOnly && undefined) ;
34547
34705
  const [groupRootProps, groupHostProps, childrenWrapperProps] = useControlgroupProps({
34548
34706
  ...props,
34549
- readOnly: props.readOnly || implicitReadOnly,
34550
34707
  uiAction: (groupState, event) => {
34551
34708
  const hiddenInput = ref.current;
34552
34709
  if (hiddenInput) {
@@ -34595,15 +34752,10 @@ const InputDuration = props => {
34595
34752
  // sub-inputs are correctly reset to their original raw string values.
34596
34753
  // ISO 8601 encodes milliseconds as fractional seconds (e.g. "PT0.5S" = 500ms),
34597
34754
  // so fractional seconds are split back into whole seconds + ms.
34598
- distributeChildUIState: groupState => {
34755
+ distributeChildUIState: (groupState, childUIStateController) => {
34599
34756
  const components = parseDuration(groupState);
34600
34757
  if (!components) {
34601
- return {
34602
- hour: undefined,
34603
- minute: undefined,
34604
- second: undefined,
34605
- millisecond: undefined
34606
- };
34758
+ return undefined;
34607
34759
  }
34608
34760
  const rawSeconds = components.seconds;
34609
34761
  let secondForField = rawSeconds;
@@ -34612,12 +34764,13 @@ const InputDuration = props => {
34612
34764
  secondForField = Math.floor(rawSeconds);
34613
34765
  millisecondForField = Math.round(rawSeconds % 1 * 1000);
34614
34766
  }
34615
- return {
34767
+ const fieldMap = {
34616
34768
  hour: components.hours,
34617
34769
  minute: components.minutes,
34618
34770
  second: secondForField,
34619
34771
  millisecond: millisecondForField
34620
34772
  };
34773
+ return fieldMap[childUIStateController.name];
34621
34774
  }
34622
34775
  });
34623
34776
  const {
@@ -34639,6 +34792,8 @@ const InputDuration = props => {
34639
34792
  secondValue = Math.floor(rawSecondValue);
34640
34793
  millisecondValue = Math.round(rawSecondValue % 1 * 1000);
34641
34794
  }
34795
+ const loading = basePseudoState[":-navi-loading"];
34796
+ delete basePseudoState[":-navi-loading"];
34642
34797
  return jsxs(Box, {
34643
34798
  ref: groupRef,
34644
34799
  className: "navi_input_duration",
@@ -34648,7 +34803,11 @@ const InputDuration = props => {
34648
34803
  unit: undefined,
34649
34804
  unitHour: undefined,
34650
34805
  ...clipboardProps,
34651
- children: [jsx("input", {
34806
+ children: [jsx(LoadingOutline, {
34807
+ loading: loading,
34808
+ color: "var(--loader-color)",
34809
+ inset: -1
34810
+ }), jsx("input", {
34652
34811
  ...groupHostProps,
34653
34812
  type: "hidden",
34654
34813
  basePseudoState: undefined // eslint-disable-line react/no-unknown-property
@@ -34960,7 +35119,11 @@ const InputDurationPart = ({
34960
35119
  return jsxs(Label, {
34961
35120
  flex: "y",
34962
35121
  "data-separator": separator || undefined,
34963
- children: [jsx(Input, {
35122
+ children: [jsx(Input
35123
+ // When autofocused this field should be selected
35124
+ // this help to modify the value on mobile
35125
+ , {
35126
+ autoSelect: true,
34964
35127
  type: "navi_number",
34965
35128
  "navi-input-type": unit,
34966
35129
  name: unit,
@@ -35006,20 +35169,7 @@ const RadioGroupInterface = props => {
35006
35169
  resetOnError: true,
35007
35170
  ...props
35008
35171
  }, {
35009
- controlType: "radio_group",
35010
- childControlFilter: childUIStateController => {
35011
- return childUIStateController.controlType === "input" && childUIStateController.controlHostProps.type === "radio";
35012
- },
35013
- aggregateChildStates: childUIStateControllers => {
35014
- let activeValue;
35015
- for (const childUIStateController of childUIStateControllers) {
35016
- if (childUIStateController.uiState) {
35017
- activeValue = childUIStateController.uiState;
35018
- break;
35019
- }
35020
- }
35021
- return activeValue;
35022
- }
35172
+ controlType: "radio_group"
35023
35173
  });
35024
35174
  useFocusGroup(ref, {
35025
35175
  wrap: "both"
@@ -37547,30 +37697,7 @@ const ListSelectable = props => {
37547
37697
  } = props;
37548
37698
  const [listControlRootProps, listControlProps, childrenWrapperProps] = useControlgroupProps(props, {
37549
37699
  stateType: multiple ? "array" : "",
37550
- controlType: multiple ? "checkbox_group" : "radio_group",
37551
- childControlFilter: multiple ? childUIStateController => {
37552
- return childUIStateController.controlType === "input" && childUIStateController.controlHostProps.type === "checkbox";
37553
- } : childUIStateController => {
37554
- return childUIStateController.controlType === "input" && childUIStateController.controlHostProps.type === "radio";
37555
- },
37556
- aggregateChildStates: multiple ? childUIStateControllers => {
37557
- const values = [];
37558
- for (const childUIStateController of childUIStateControllers) {
37559
- if (childUIStateController.uiState) {
37560
- values.push(childUIStateController.uiState);
37561
- }
37562
- }
37563
- return values.length === 0 ? undefined : values;
37564
- } : childUIStateControllers => {
37565
- let activeValue;
37566
- for (const childUIStateController of childUIStateControllers) {
37567
- if (childUIStateController.uiState) {
37568
- activeValue = childUIStateController.uiState;
37569
- break;
37570
- }
37571
- }
37572
- return activeValue;
37573
- }
37700
+ controlType: multiple ? "checkbox_group" : "radio_group"
37574
37701
  });
37575
37702
  const uiGroupStateController = getUIStateControllerById(listControlProps.id);
37576
37703
  useFocusGroup(ref, {
@@ -37655,6 +37782,8 @@ const ListSelectable = props => {
37655
37782
  ...listControlRootProps,
37656
37783
  ...listControlProps,
37657
37784
  name: undefined,
37785
+ value: undefined,
37786
+ defaultValue: undefined,
37658
37787
  selectedIndicator: undefined,
37659
37788
  selectable: undefined,
37660
37789
  multiple: undefined
@@ -38727,7 +38856,13 @@ const useListScrollSync = ({
38727
38856
  }
38728
38857
  hasBeenDisplayedRef.current = true;
38729
38858
  const items = tracker.itemsSignal.peek();
38730
- const firstSelected = items.find(i => i.selected);
38859
+ const firstSelected = items.find(i => {
38860
+ if (i.selected) {
38861
+ return true;
38862
+ }
38863
+ const inputController = getUIStateControllerById(`${i.id}_input`);
38864
+ return inputController ? inputController.uiStateSignal.peek() : false;
38865
+ });
38731
38866
  if (firstSelected) {
38732
38867
  scrollToItem(firstSelected, {
38733
38868
  event: new CustomEvent("navi_displayed", {
@@ -40605,19 +40740,20 @@ const isWithinPickerContent = (el, pickerEl) => {
40605
40740
  };
40606
40741
  const PickerInput = props => {
40607
40742
  const {
40608
- ui
40743
+ ui,
40744
+ readOnly
40609
40745
  } = props;
40610
40746
 
40611
40747
  // After type resolution: force readOnly when the input type would open the
40612
40748
  // mobile keyboard. We also suppress the visual ":read-only" state so the
40613
40749
  // picker still looks interactive (it is — just not keyboard-typeable).
40614
- if (MOBILE_KEYBOARD_TYPES.has(props.type) && !props.readOnly) {
40615
- props.readOnly = true;
40616
- props["data-readonly-forced"] = "";
40617
- }
40750
+ const readOnlyForced = readOnly ? false : MOBILE_KEYBOARD_TYPES.has(props.type || "text");
40751
+ const autoSelectReadOnlyProps = useAutoSelectReadOnly(props);
40618
40752
  return jsx(Box, {
40619
40753
  as: "input",
40620
40754
  ...props,
40755
+ readOnly: readOnlyForced ? true : readOnly,
40756
+ "data-readonly-forced": readOnlyForced ? "" : undefined,
40621
40757
  ui: undefined,
40622
40758
  className: "navi_picker_input",
40623
40759
  pseudoClasses: PickerInputPseudoClasses,
@@ -40630,7 +40766,8 @@ const PickerInput = props => {
40630
40766
  // Ensure tab does not tab through the browser picker elements (like in input date)
40631
40767
  performTabNavigation(e);
40632
40768
  }
40633
- }
40769
+ },
40770
+ ...autoSelectReadOnlyProps
40634
40771
  });
40635
40772
  };
40636
40773
  // Input types that open the software keyboard on mobile.