@proyecto-viviana/solid-stately 0.6.0-rc.0 → 0.6.0-rc.2

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.
Files changed (134) hide show
  1. package/README.md +59 -29
  2. package/dist/calendar/createCalendarState.d.ts +3 -1
  3. package/dist/calendar/createCalendarState.d.ts.map +1 -1
  4. package/dist/calendar/createDatePickerState.d.ts +2 -0
  5. package/dist/calendar/createDatePickerState.d.ts.map +1 -1
  6. package/dist/calendar/createRangeCalendarState.d.ts +2 -0
  7. package/dist/calendar/createRangeCalendarState.d.ts.map +1 -1
  8. package/dist/calendar/createTimeFieldState.d.ts +1 -1
  9. package/dist/calendar/createTimeFieldState.d.ts.map +1 -1
  10. package/dist/checkbox/createCheckboxGroupState.d.ts +2 -0
  11. package/dist/checkbox/createCheckboxGroupState.d.ts.map +1 -1
  12. package/dist/collections/createListState.d.ts.map +1 -1
  13. package/dist/collections/createMenuState.d.ts +46 -2
  14. package/dist/collections/createMenuState.d.ts.map +1 -1
  15. package/dist/collections/createSelectionState.d.ts.map +1 -1
  16. package/dist/collections/index.d.ts +1 -1
  17. package/dist/collections/index.d.ts.map +1 -1
  18. package/dist/color/createColorAreaState.d.ts +4 -0
  19. package/dist/color/createColorAreaState.d.ts.map +1 -1
  20. package/dist/color/createColorFieldState.d.ts +2 -0
  21. package/dist/color/createColorFieldState.d.ts.map +1 -1
  22. package/dist/color/createColorSliderState.d.ts +2 -0
  23. package/dist/color/createColorSliderState.d.ts.map +1 -1
  24. package/dist/color/createColorWheelState.d.ts.map +1 -1
  25. package/dist/combobox/createComboBoxState.d.ts +90 -23
  26. package/dist/combobox/createComboBoxState.d.ts.map +1 -1
  27. package/dist/combobox/index.d.ts +1 -1
  28. package/dist/combobox/index.d.ts.map +1 -1
  29. package/dist/data/createAsyncList.d.ts.map +1 -1
  30. package/dist/data/createListData.d.ts.map +1 -1
  31. package/dist/data/createTreeData.d.ts.map +1 -1
  32. package/dist/dnd/createDraggableCollectionState.d.ts +5 -1
  33. package/dist/dnd/createDraggableCollectionState.d.ts.map +1 -1
  34. package/dist/dnd/createDroppableCollectionState.d.ts +6 -0
  35. package/dist/dnd/createDroppableCollectionState.d.ts.map +1 -1
  36. package/dist/form/createFormValidationState.d.ts.map +1 -1
  37. package/dist/grid/createGridState.d.ts +2 -2
  38. package/dist/grid/createGridState.d.ts.map +1 -1
  39. package/dist/index.d.ts +5 -5
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +1044 -478
  42. package/dist/index.js.map +1 -1
  43. package/dist/menu/index.d.ts +2 -1
  44. package/dist/menu/index.d.ts.map +1 -1
  45. package/dist/numberfield/createNumberFieldState.d.ts +6 -3
  46. package/dist/numberfield/createNumberFieldState.d.ts.map +1 -1
  47. package/dist/private/flags/flags.d.ts.map +1 -0
  48. package/dist/private/flags/flags.js +1 -1
  49. package/dist/private/flags/flags.js.map +1 -1
  50. package/dist/radio/createRadioGroupState.d.ts.map +1 -1
  51. package/dist/select/createSelectState.d.ts +85 -29
  52. package/dist/select/createSelectState.d.ts.map +1 -1
  53. package/dist/select/index.d.ts +1 -1
  54. package/dist/select/index.d.ts.map +1 -1
  55. package/dist/selection/SelectionManager.d.ts.map +1 -1
  56. package/dist/selection/createMultipleSelectionState.d.ts.map +1 -1
  57. package/dist/slider/createSliderState.d.ts +35 -9
  58. package/dist/slider/createSliderState.d.ts.map +1 -1
  59. package/dist/ssr/index.d.ts +0 -6
  60. package/dist/ssr/index.d.ts.map +1 -1
  61. package/dist/steplist/createStepListState.d.ts +8 -13
  62. package/dist/steplist/createStepListState.d.ts.map +1 -1
  63. package/dist/table/createTableColumnResizeState.d.ts.map +1 -1
  64. package/dist/table/createTableState.d.ts +2 -2
  65. package/dist/table/createTableState.d.ts.map +1 -1
  66. package/dist/table/createTreeGridState.d.ts +2 -2
  67. package/dist/table/createTreeGridState.d.ts.map +1 -1
  68. package/dist/table/types.d.ts +2 -2
  69. package/dist/table/types.d.ts.map +1 -1
  70. package/dist/tabs/createTabListState.d.ts.map +1 -1
  71. package/dist/textfield/createTextFieldState.d.ts.map +1 -1
  72. package/dist/toggle/createToggleGroupState.d.ts +0 -8
  73. package/dist/toggle/createToggleGroupState.d.ts.map +1 -1
  74. package/dist/toggle/createToggleState.d.ts.map +1 -1
  75. package/dist/tokenfield/TokenFieldValue.d.ts +28 -4
  76. package/dist/tokenfield/TokenFieldValue.d.ts.map +1 -1
  77. package/dist/tokenfield/index.d.ts +1 -1
  78. package/dist/tokenfield/index.d.ts.map +1 -1
  79. package/dist/tree/createTreeState.d.ts +2 -2
  80. package/dist/tree/createTreeState.d.ts.map +1 -1
  81. package/dist/utils/index.d.ts +1 -1
  82. package/dist/utils/index.d.ts.map +1 -1
  83. package/dist/utils/reactivity.d.ts +5 -0
  84. package/dist/utils/reactivity.d.ts.map +1 -1
  85. package/package.json +2 -2
  86. package/src/calendar/createCalendarState.ts +139 -58
  87. package/src/calendar/createDatePickerState.ts +14 -11
  88. package/src/calendar/createRangeCalendarState.ts +43 -17
  89. package/src/calendar/createTimeFieldState.ts +1 -1
  90. package/src/checkbox/createCheckboxGroupState.ts +16 -0
  91. package/src/collections/createListState.ts +4 -4
  92. package/src/collections/createMenuState.ts +134 -4
  93. package/src/collections/createSelectionState.ts +2 -2
  94. package/src/collections/index.ts +4 -0
  95. package/src/color/createColorAreaState.ts +19 -0
  96. package/src/color/createColorFieldState.ts +76 -26
  97. package/src/color/createColorSliderState.ts +25 -2
  98. package/src/color/createColorWheelState.ts +9 -5
  99. package/src/combobox/createComboBoxState.ts +371 -121
  100. package/src/combobox/index.ts +4 -0
  101. package/src/data/createAsyncList.ts +19 -4
  102. package/src/data/createListData.ts +8 -2
  103. package/src/data/createTreeData.ts +11 -3
  104. package/src/dnd/createDraggableCollectionState.ts +8 -1
  105. package/src/dnd/createDroppableCollectionState.ts +10 -0
  106. package/src/form/createFormValidationState.ts +23 -5
  107. package/src/grid/createGridState.ts +15 -8
  108. package/src/index.ts +18 -2
  109. package/src/menu/index.ts +5 -0
  110. package/src/numberfield/createNumberFieldState.ts +97 -35
  111. package/src/radio/createRadioGroupState.ts +4 -1
  112. package/src/select/createSelectState.ts +253 -131
  113. package/src/select/index.ts +8 -1
  114. package/src/selection/SelectionManager.ts +3 -1
  115. package/src/selection/createMultipleSelectionState.ts +2 -2
  116. package/src/slider/createSliderState.ts +227 -103
  117. package/src/ssr/index.ts +11 -4
  118. package/src/steplist/createStepListState.ts +156 -123
  119. package/src/table/createTableColumnResizeState.ts +51 -11
  120. package/src/table/createTableState.ts +5 -4
  121. package/src/table/createTreeGridState.ts +3 -4
  122. package/src/table/types.ts +2 -2
  123. package/src/tabs/createTabListState.ts +8 -31
  124. package/src/textfield/createTextFieldState.ts +6 -3
  125. package/src/toggle/createToggleGroupState.ts +4 -2
  126. package/src/toggle/createToggleState.ts +5 -4
  127. package/src/tokenfield/TokenFieldValue.ts +75 -16
  128. package/src/tokenfield/index.ts +1 -0
  129. package/src/tree/createTreeState.ts +15 -8
  130. package/src/utils/index.ts +1 -0
  131. package/src/utils/reactivity.ts +8 -0
  132. package/dist/flags/flags.d.ts.map +0 -1
  133. /package/dist/{flags → private/flags}/flags.d.ts +0 -0
  134. /package/src/{flags → private/flags}/flags.ts +0 -0
@@ -7,6 +7,10 @@ export {
7
7
  defaultContainsFilter,
8
8
  type ComboBoxState,
9
9
  type ComboBoxStateProps,
10
+ type ComboBoxValidationValue,
11
+ type ComboBoxSelectionMode,
12
+ type ComboBoxValueType,
13
+ type ComboBoxChangeValueType,
10
14
  type FilterFn,
11
15
  type MenuTriggerAction,
12
16
  type FocusStrategy,
@@ -340,8 +340,14 @@ export function createAsyncList<T, C = string>(options: AsyncListOptions<T, C>):
340
340
 
341
341
  insertBefore(key: Key, ...values: T[]) {
342
342
  setState((s) => {
343
- const index = s.items.findIndex((item) => getKey(item) === key);
344
- if (index === -1) return s;
343
+ let index = s.items.findIndex((item) => getKey(item) === key);
344
+ if (index === -1) {
345
+ if (s.items.length === 0) {
346
+ index = 0;
347
+ } else {
348
+ return s;
349
+ }
350
+ }
345
351
  return {
346
352
  ...s,
347
353
  items: [...s.items.slice(0, index), ...values, ...s.items.slice(index)],
@@ -351,8 +357,14 @@ export function createAsyncList<T, C = string>(options: AsyncListOptions<T, C>):
351
357
 
352
358
  insertAfter(key: Key, ...values: T[]) {
353
359
  setState((s) => {
354
- const index = s.items.findIndex((item) => getKey(item) === key);
355
- if (index === -1) return s;
360
+ let index = s.items.findIndex((item) => getKey(item) === key);
361
+ if (index === -1) {
362
+ if (s.items.length === 0) {
363
+ index = 0;
364
+ } else {
365
+ return s;
366
+ }
367
+ }
356
368
  return {
357
369
  ...s,
358
370
  items: [...s.items.slice(0, index + 1), ...values, ...s.items.slice(index + 1)],
@@ -380,6 +392,9 @@ export function createAsyncList<T, C = string>(options: AsyncListOptions<T, C>):
380
392
  }
381
393
  selectedKeys = newSelection;
382
394
  }
395
+ if (s.cursor == null && items.length === 0) {
396
+ selectedKeys = new Set();
397
+ }
383
398
  return { ...s, items, selectedKeys };
384
399
  });
385
400
  },
@@ -165,8 +165,14 @@ export function createListData<T>(options: ListOptions<T>): ListData<T> {
165
165
 
166
166
  insertBefore(key: Key, ...values: T[]) {
167
167
  setState((s) => {
168
- const index = s.items.findIndex((item) => getKey(item) === key);
169
- if (index === -1) return s;
168
+ let index = s.items.findIndex((item) => getKey(item) === key);
169
+ if (index === -1) {
170
+ if (s.items.length === 0) {
171
+ index = 0;
172
+ } else {
173
+ return s;
174
+ }
175
+ }
170
176
  return insertItems(s, index, ...values);
171
177
  });
172
178
  },
@@ -345,6 +345,11 @@ export function createTreeData<T>(options: TreeOptions<T>): TreeData<T> {
345
345
  },
346
346
 
347
347
  remove(...keys: Key[]) {
348
+ if (keys.length === 0) {
349
+ return;
350
+ }
351
+
352
+ let nextMap: Map<Key, TreeNode<T>> | undefined;
348
353
  setTreeState((state) => {
349
354
  let { items, nodeMap } = state;
350
355
  let newMap = new Map(nodeMap);
@@ -356,14 +361,17 @@ export function createTreeData<T>(options: TreeOptions<T>): TreeData<T> {
356
361
  newMap = result.nodeMap;
357
362
  }
358
363
 
364
+ nextMap = newMap;
359
365
  return { items: newItems, nodeMap: newMap };
360
366
  });
361
367
 
362
368
  setSelectedKeys((sel) => {
363
- if (sel === "all") return sel;
369
+ if (sel === "all" || !nextMap) return sel;
364
370
  const newSel = new Set(sel);
365
- for (const key of keys) {
366
- newSel.delete(key);
371
+ for (const key of sel) {
372
+ if (!nextMap.has(key)) {
373
+ newSel.delete(key);
374
+ }
367
375
  }
368
376
  return newSel;
369
377
  });
@@ -36,14 +36,16 @@ type DragKey = string | number;
36
36
 
37
37
  export interface DraggableCollectionNode {
38
38
  parentKey?: DragKey | null;
39
+ textValue?: string;
39
40
  }
40
41
 
41
42
  export interface DraggableCollectionLike {
42
43
  getItem(key: DragKey): DraggableCollectionNode | null | undefined;
43
44
  getKeys?(): Iterable<DragKey>;
45
+ getTextValue?(key: DragKey): string;
44
46
  }
45
47
 
46
- export interface DraggableCollectionStateOptions<T = object> {
48
+ export interface DraggableCollectionStateOptions<_T = object> {
47
49
  /** A function that returns the items being dragged. */
48
50
  getItems: (keys: Set<DragKey>) => DragItem[];
49
51
  /** Function that returns the allowed drop operations. */
@@ -95,6 +97,8 @@ export interface DraggableCollectionState {
95
97
  getKeysForDrag(key: DragKey): Set<DragKey>;
96
98
  /** Returns whether `key` is selected. */
97
99
  isSelected(key: DragKey): boolean;
100
+ /** Collection passed to the drag. Item text is read from here. */
101
+ readonly collection?: DraggableCollectionLike;
98
102
  }
99
103
 
100
104
  function selectedKeySet(
@@ -281,5 +285,8 @@ export function createDraggableCollectionState<T = object>(
281
285
  const selected = selectedKeySet(p.selectedKeys, p.collection);
282
286
  return selected === "all" ? true : selected.has(key);
283
287
  },
288
+ get collection() {
289
+ return getProps().collection;
290
+ },
284
291
  };
285
292
  }
@@ -147,6 +147,12 @@ export interface DroppableCollectionState {
147
147
  * level boolean on this port.
148
148
  */
149
149
  isDropTargetFor(target: DropTarget | null): boolean;
150
+ /**
151
+ * The host collection from the options. RAC always has one
152
+ * (`useDroppableCollectionState.ts:46-48`); this port's option is optional,
153
+ * so the field is undefined when the host omits it.
154
+ */
155
+ readonly collection: DroppableCollectionLike | undefined;
150
156
  }
151
157
 
152
158
  /**
@@ -525,6 +531,10 @@ export function createDroppableCollectionState(
525
531
  getDropOperation,
526
532
  shouldAcceptItemDrop,
527
533
  isDropTargetFor,
534
+ // RAC `useDroppableCollectionState.ts:175-176`.
535
+ get collection() {
536
+ return getProps().collection;
537
+ },
528
538
  };
529
539
  }
530
540
 
@@ -21,7 +21,7 @@
21
21
  * Port of react-stately's useFormValidationState.
22
22
  */
23
23
 
24
- import { createContext, createEffect, createMemo } from "solid-js";
24
+ import { createContext, createEffect, createMemo, untrack } from "solid-js";
25
25
  import type { Accessor } from "solid-js";
26
26
  import { createInternalSignal, useContextOptional } from "../utils";
27
27
 
@@ -121,6 +121,18 @@ export const DEFAULT_VALIDATION_RESULT: ValidationResult = {
121
121
  /** Context for server-side validation errors. */
122
122
  export const FormValidationContext = createContext<ValidationErrors>({});
123
123
 
124
+ // Form publishes one proxy so field setup does not freeze the first map.
125
+ // The symbol reads the current validationErrors object. A new object resets
126
+ // the clear flag, matching the pin's context identity check.
127
+ const validationErrorsSource = Symbol.for("proyecto-viviana.validationErrorsSource");
128
+
129
+ function validationErrorsIdentity(errors: ValidationErrors): ValidationErrors {
130
+ const source = (errors as ValidationErrors & Record<symbol, ValidationErrors | undefined>)[
131
+ validationErrorsSource
132
+ ];
133
+ return source ?? errors;
134
+ }
135
+
124
136
  /** Private prop key for passing validation state to children. */
125
137
  export const privateValidationStateProp = "__formValidationState" + Date.now();
126
138
 
@@ -240,12 +252,14 @@ export function createFormValidationState<T>(props: FormValidationProps<T>): For
240
252
  return [];
241
253
  });
242
254
 
243
- // Track server errors clearing
244
- const [lastServerErrors, setLastServerErrors] = createInternalSignal(serverErrors);
255
+ // Track server errors clearing. Compare the map object, not the proxy.
256
+ const [lastServerErrors, setLastServerErrors] = createInternalSignal(
257
+ validationErrorsIdentity(serverErrors),
258
+ );
245
259
  const [isServerErrorCleared, setServerErrorCleared] = createInternalSignal(false);
246
260
 
247
261
  createEffect(
248
- () => ({ errors: serverErrors, last: lastServerErrors() }),
262
+ () => ({ errors: validationErrorsIdentity(serverErrors), last: lastServerErrors() }),
249
263
  ({ errors, last }) => {
250
264
  if (errors !== last) {
251
265
  setLastServerErrors(errors);
@@ -310,7 +324,11 @@ export function createFormValidationState<T>(props: FormValidationProps<T>): For
310
324
  displayValidation,
311
325
  updateValidation(value: ValidationResult) {
312
326
  // If validationBehavior is 'aria', update in realtime. Otherwise, store until commit.
313
- if (validationBehavior() === "aria" && !isEqualValidation(currentValidity(), value)) {
327
+ // An imperative write: it reads the behavior and validity as they stand.
328
+ if (
329
+ untrack(validationBehavior) === "aria" &&
330
+ !isEqualValidation(untrack(currentValidity), value)
331
+ ) {
314
332
  setCurrentValidity(value);
315
333
  } else {
316
334
  nextValidation = value;
@@ -4,19 +4,18 @@
4
4
  */
5
5
 
6
6
  import { createEffect, createMemo } from "solid-js";
7
- import type { Accessor } from "solid-js";
8
7
  import type { GridState, GridStateOptions, GridCollection, GridNode } from "./types";
9
8
  import type { Key, FocusStrategy, SelectionBehavior, Selection } from "../collections/types";
10
- import { createInternalSignal } from "../utils";
9
+ import { createInternalSignal, propsAccessor, type MaybeAccessor } from "../utils";
11
10
 
12
11
  /**
13
12
  * Creates state management for a grid component.
14
13
  * Handles row selection, focus management, and keyboard navigation state.
15
14
  */
16
15
  export function createGridState<T extends object, C extends GridCollection<T> = GridCollection<T>>(
17
- options: Accessor<GridStateOptions<T, C>>,
16
+ options: MaybeAccessor<GridStateOptions<T, C>>,
18
17
  ): GridState<T, C> {
19
- const getOptions = () => options();
18
+ const getOptions = propsAccessor(options);
20
19
 
21
20
  const disabledKeys = createMemo(() => {
22
21
  const keys = getOptions().disabledKeys;
@@ -53,18 +52,26 @@ export function createGridState<T extends object, C extends GridCollection<T> =
53
52
 
54
53
  const focusMode = createMemo(() => getOptions().focusMode ?? "row");
55
54
 
55
+ let lastSelectionBehavior = selectionBehaviorProp();
56
56
  createEffect(selectionBehaviorProp, (behavior) => {
57
- setSelectionBehaviorState(behavior);
57
+ if (behavior !== lastSelectionBehavior) {
58
+ setSelectionBehaviorState(behavior);
59
+ lastSelectionBehavior = behavior;
60
+ }
58
61
  });
59
62
 
60
63
  createEffect(
61
64
  () => ({
62
65
  prop: selectionBehaviorProp(),
63
- state: selectionBehaviorState(),
64
66
  keys: selectedKeys(),
65
67
  }),
66
- ({ prop, state, keys }) => {
67
- if (prop === "replace" && state === "toggle" && keys !== "all" && keys.size === 0) {
68
+ ({ prop, keys }) => {
69
+ if (
70
+ prop === "replace" &&
71
+ selectionBehaviorState() === "toggle" &&
72
+ keys !== "all" &&
73
+ keys.size === 0
74
+ ) {
68
75
  setSelectionBehaviorState("replace");
69
76
  }
70
77
  },
package/src/index.ts CHANGED
@@ -24,6 +24,7 @@ export {
24
24
  type TextSegment,
25
25
  type TokenSegment,
26
26
  type Position,
27
+ type SelectedRange,
27
28
  type TokenFieldValueOptions,
28
29
  } from "./tokenfield";
29
30
 
@@ -77,12 +78,16 @@ export {
77
78
  type SingleSelectListState,
78
79
  createMenuState,
79
80
  createMenuTriggerState,
81
+ createSubmenuTriggerState,
80
82
  type MenuStateProps,
81
83
  type MenuState,
82
84
  type MenuTriggerType,
83
85
  type MenuTriggerProps,
84
86
  type MenuTriggerStateProps,
85
87
  type MenuTriggerState,
88
+ type RootMenuTriggerState,
89
+ type SubmenuTriggerProps,
90
+ type SubmenuTriggerState,
86
91
  } from "./collections";
87
92
 
88
93
  // Selection (module-compat aliases)
@@ -96,7 +101,7 @@ export {
96
101
  } from "./selection";
97
102
 
98
103
  // Menu (module-compat aliases)
99
- export { useMenuTriggerState } from "./menu";
104
+ export { useMenuTriggerState, useSubmenuTriggerState } from "./menu";
100
105
 
101
106
  // Datepicker (module-compat aliases)
102
107
  export {
@@ -111,13 +116,24 @@ export {
111
116
  type SegmentType,
112
117
  } from "./datepicker";
113
118
 
114
- export { createSelectState, type SelectStateProps, type SelectState } from "./select";
119
+ export {
120
+ createSelectState,
121
+ type SelectStateProps,
122
+ type SelectState,
123
+ type SelectSelectionMode,
124
+ type SelectValueType,
125
+ type SelectChangeValueType,
126
+ } from "./select";
115
127
 
116
128
  export {
117
129
  createComboBoxState,
118
130
  defaultContainsFilter,
119
131
  type ComboBoxState,
120
132
  type ComboBoxStateProps,
133
+ type ComboBoxValidationValue,
134
+ type ComboBoxSelectionMode,
135
+ type ComboBoxValueType,
136
+ type ComboBoxChangeValueType,
121
137
  type FilterFn,
122
138
  type MenuTriggerAction,
123
139
  } from "./combobox";
package/src/menu/index.ts CHANGED
@@ -8,12 +8,17 @@
8
8
  export {
9
9
  createMenuState,
10
10
  createMenuTriggerState,
11
+ createSubmenuTriggerState,
11
12
  type MenuStateProps,
12
13
  type MenuState,
13
14
  type MenuTriggerType,
14
15
  type MenuTriggerProps,
15
16
  type MenuTriggerStateProps,
16
17
  type MenuTriggerState,
18
+ type RootMenuTriggerState,
19
+ type SubmenuTriggerProps,
20
+ type SubmenuTriggerState,
17
21
  } from "../collections/createMenuState";
18
22
 
19
23
  export { createMenuTriggerState as useMenuTriggerState } from "../collections/createMenuState";
24
+ export { createSubmenuTriggerState as useSubmenuTriggerState } from "../collections/createMenuState";
@@ -74,7 +74,10 @@ export interface NumberFieldStateProps {
74
74
  export interface NumberFieldState extends FormValidationState {
75
75
  /** The current input value as a string. */
76
76
  inputValue: Accessor<string>;
77
- /** The current numeric value. */
77
+ /**
78
+ * The currently parsed number value, or NaN if the input could not be parsed.
79
+ * Updated from the input text as the user types.
80
+ */
78
81
  numberValue: Accessor<number>;
79
82
  /** Whether the value can be incremented. */
80
83
  canIncrement: Accessor<boolean>;
@@ -98,8 +101,8 @@ export interface NumberFieldState extends FormValidationState {
98
101
  setInputValue: (value: string) => void;
99
102
  /** Validate a partial input value. */
100
103
  validate: (value: string) => boolean;
101
- /** Commit the current input value. */
102
- commit: () => void;
104
+ /** Commit the current input value, or an override when a paste replaces it. */
105
+ commit: (value?: string) => void;
103
106
  /** Increment the value by step. */
104
107
  increment: () => void;
105
108
  /** Decrement the value by step. */
@@ -142,13 +145,60 @@ function clamp(value: number, min?: number, max?: number): number {
142
145
  }
143
146
 
144
147
  /**
145
- * Snaps a value to the nearest step.
148
+ * Rounds to the decimal precision implied by a step, including exponential steps.
149
+ */
150
+ function roundToStepPrecision(value: number, step: number): number {
151
+ let roundedValue = value;
152
+ let precision = 0;
153
+ const stepString = step.toString();
154
+ const eIndex = stepString.toLowerCase().indexOf("e-");
155
+ if (eIndex > 0) {
156
+ precision = Math.abs(Math.floor(Math.log10(Math.abs(step)))) + eIndex;
157
+ } else {
158
+ const pointIndex = stepString.indexOf(".");
159
+ if (pointIndex >= 0) {
160
+ precision = stepString.length - pointIndex;
161
+ }
162
+ }
163
+ if (precision > 0) {
164
+ const pow = Math.pow(10, precision);
165
+ roundedValue = Math.round(roundedValue * pow) / pow;
166
+ }
167
+ return roundedValue;
168
+ }
169
+
170
+ /**
171
+ * Snaps to the nearest step, then to the last in-range step when that exceeds max.
172
+ * A negative halfway rounds away from the step base.
146
173
  */
147
- function snapToStep(value: number, step: number, min?: number): number {
148
- const base = min ?? 0;
149
- const diff = value - base;
150
- const steps = Math.round(diff / step);
151
- return handleDecimalOperation("+", base, steps * step);
174
+ function snapValueToStep(
175
+ value: number,
176
+ min: number | undefined,
177
+ max: number | undefined,
178
+ step: number,
179
+ ): number {
180
+ const minNumber = Number(min);
181
+ const maxNumber = Number(max);
182
+ const remainder = (value - (isNaN(minNumber) ? 0 : minNumber)) % step;
183
+ let snappedValue = roundToStepPrecision(
184
+ Math.abs(remainder) * 2 >= step
185
+ ? value + Math.sign(remainder) * (step - Math.abs(remainder))
186
+ : value - remainder,
187
+ step,
188
+ );
189
+
190
+ if (!isNaN(minNumber)) {
191
+ if (snappedValue < minNumber) {
192
+ snappedValue = minNumber;
193
+ } else if (!isNaN(maxNumber) && snappedValue > maxNumber) {
194
+ snappedValue =
195
+ minNumber + Math.floor(roundToStepPrecision((maxNumber - minNumber) / step, step)) * step;
196
+ }
197
+ } else if (!isNaN(maxNumber) && snappedValue > maxNumber) {
198
+ snappedValue = Math.floor(roundToStepPrecision(maxNumber / step, step)) * step;
199
+ }
200
+
201
+ return roundToStepPrecision(snappedValue, step);
152
202
  }
153
203
 
154
204
  function isValidStep(step: number | undefined): step is number {
@@ -213,7 +263,7 @@ export function createNumberFieldState(
213
263
  if (isNaN(value)) return NaN;
214
264
 
215
265
  if (hasCustomStep()) {
216
- return clamp(snapToStep(value, step(), p.minValue), p.minValue, p.maxValue);
266
+ return snapValueToStep(value, p.minValue, p.maxValue, step());
217
267
  }
218
268
 
219
269
  return clamp(value, p.minValue, p.maxValue);
@@ -320,13 +370,13 @@ export function createNumberFieldState(
320
370
  setInputValueInternal(value);
321
371
  };
322
372
 
323
- // Commit the current input value
324
- const commit = () => {
373
+ // Commit the current input value, or an override when a paste replaces it.
374
+ const commit = (override?: string) => {
325
375
  ensureInitialized();
326
376
  const p = getProps();
327
- const input = readNow(inputValue);
377
+ const input = override === undefined ? readNow(inputValue) : override;
328
378
 
329
- if (input === "" || input === "-") {
379
+ if (input === "") {
330
380
  // Clear value
331
381
  setNumberValue(NaN);
332
382
  setInputValueInternal(p.value === undefined ? "" : formatNumber(readNow(actualNumberValue)));
@@ -348,7 +398,8 @@ export function createNumberFieldState(
348
398
  parsed = numberParser().parse(formatNumber(parsed));
349
399
 
350
400
  setNumberValue(parsed);
351
- setInputValueInternal(formatNumber(parsed));
401
+ // A controlled field keeps the current number until its value prop updates.
402
+ setInputValueInternal(formatNumber(p.value === undefined ? parsed : previous));
352
403
 
353
404
  p.onChange?.(parsed);
354
405
  if (parsed !== previous) {
@@ -367,7 +418,7 @@ export function createNumberFieldState(
367
418
 
368
419
  if (p.maxValue == null) return true;
369
420
  return (
370
- snapToStep(current, step(), p.minValue) > current ||
421
+ snapValueToStep(current, p.minValue, p.maxValue, step()) > current ||
371
422
  handleDecimalOperation("+", current, step()) <= p.maxValue
372
423
  );
373
424
  });
@@ -383,7 +434,7 @@ export function createNumberFieldState(
383
434
 
384
435
  if (p.minValue == null) return true;
385
436
  return (
386
- snapToStep(current, step(), p.minValue) < current ||
437
+ snapValueToStep(current, p.minValue, p.maxValue, step()) < current ||
387
438
  handleDecimalOperation("-", current, step()) >= p.minValue
388
439
  );
389
440
  });
@@ -394,16 +445,31 @@ export function createNumberFieldState(
394
445
 
395
446
  if (isNaN(parsed)) {
396
447
  const base = isNaN(minOrMaxValue) ? 0 : minOrMaxValue;
397
- return clamp(snapToStep(base, step(), p.minValue), p.minValue, p.maxValue);
448
+ return snapValueToStep(base, p.minValue, p.maxValue, step());
398
449
  }
399
450
 
400
- const snapped = snapToStep(parsed, step(), p.minValue);
451
+ const snapped = snapValueToStep(parsed, p.minValue, p.maxValue, step());
401
452
  if ((operation === "+" && snapped > parsed) || (operation === "-" && snapped < parsed)) {
402
- return clamp(snapped, p.minValue, p.maxValue);
453
+ return snapped;
403
454
  }
404
455
 
405
- const next = handleDecimalOperation(operation, parsed, step());
406
- return clamp(snapToStep(next, step(), p.minValue), p.minValue, p.maxValue);
456
+ return snapValueToStep(
457
+ handleDecimalOperation(operation, parsed, step()),
458
+ p.minValue,
459
+ p.maxValue,
460
+ step(),
461
+ );
462
+ };
463
+
464
+ // Pin reformats the text when the committed number changes. A controlled
465
+ // step leaves the text alone until `value` updates, unless the step lands
466
+ // on that same number and has to replace what the user typed.
467
+ const commitSteppedNumber = (next: number, refreshWhenUnchanged: boolean) => {
468
+ const committed = readNow(actualNumberValue);
469
+ setNumberValue(next);
470
+ if (getProps().value === undefined || (refreshWhenUnchanged && next === committed)) {
471
+ setInputValueInternal(formatNumber(next));
472
+ }
407
473
  };
408
474
 
409
475
  // Increment by step
@@ -414,8 +480,7 @@ export function createNumberFieldState(
414
480
  if (p.isDisabled || p.isReadOnly) return;
415
481
 
416
482
  const current = safeNextStep("+", p.minValue);
417
- setNumberValue(current);
418
- setInputValueInternal(formatNumber(current));
483
+ commitSteppedNumber(current, true);
419
484
  p.onChange?.(current);
420
485
  validation.commitValidation();
421
486
  };
@@ -428,8 +493,7 @@ export function createNumberFieldState(
428
493
  if (p.isDisabled || p.isReadOnly) return;
429
494
 
430
495
  const current = safeNextStep("-", p.maxValue);
431
- setNumberValue(current);
432
- setInputValueInternal(formatNumber(current));
496
+ commitSteppedNumber(current, true);
433
497
  p.onChange?.(current);
434
498
  validation.commitValidation();
435
499
  };
@@ -443,14 +507,13 @@ export function createNumberFieldState(
443
507
 
444
508
  if (p.maxValue == null) return;
445
509
 
446
- const snapped = snapToStep(p.maxValue, step(), p.minValue);
447
- setNumberValue(snapped);
448
- setInputValueInternal(formatNumber(snapped));
510
+ const snapped = snapValueToStep(p.maxValue, p.minValue, p.maxValue, step());
511
+ commitSteppedNumber(snapped, false);
449
512
  p.onChange?.(snapped);
450
513
  validation.commitValidation();
451
514
  };
452
515
 
453
- // Set to min
516
+ // Set to min. Pin sets the raw minimum, not a snapped step.
454
517
  const decrementToMin = () => {
455
518
  ensureInitialized();
456
519
  syncControlledValue();
@@ -459,16 +522,14 @@ export function createNumberFieldState(
459
522
 
460
523
  if (p.minValue == null) return;
461
524
 
462
- setNumberValue(p.minValue);
463
- setInputValueInternal(formatNumber(p.minValue));
525
+ commitSteppedNumber(p.minValue, false);
464
526
  p.onChange?.(p.minValue);
465
527
  validation.commitValidation();
466
528
  };
467
529
 
468
530
  const setNumberValuePublic = (value: number) => {
469
531
  ensureInitialized();
470
- setNumberValue(value);
471
- setInputValueInternal(formatNumber(value));
532
+ commitSteppedNumber(value, false);
472
533
  getProps().onChange?.(value);
473
534
  };
474
535
 
@@ -483,8 +544,9 @@ export function createNumberFieldState(
483
544
  syncControlledValue();
484
545
  return inputValue;
485
546
  },
547
+ // Parsed text. Form validation reads the committed number separately.
486
548
  get numberValue() {
487
- return actualNumberValue;
549
+ return parsedInputValue;
488
550
  },
489
551
  canIncrement,
490
552
  canDecrement,
@@ -220,7 +220,10 @@ export function createRadioGroupState(props: MaybeAccessor<RadioGroupProps> = {}
220
220
  const state: RadioGroupState = {
221
221
  name,
222
222
  selectedValue,
223
- defaultSelectedValue: initialProps.defaultValue ?? null,
223
+ // useRadioGroupState.ts:125 — a controlled value, including null, is the
224
+ // reset default. defaultValue applies only while uncontrolled.
225
+ defaultSelectedValue:
226
+ initialProps.value !== undefined ? initialProps.value : (initialProps.defaultValue ?? null),
224
227
  setSelectedValue,
225
228
  lastFocusedValue,
226
229
  setLastFocusedValue,