@proyecto-viviana/solid-stately 0.6.0-rc.0 → 0.6.0-rc.1
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.
- package/README.md +59 -29
- package/dist/calendar/createCalendarState.d.ts +3 -1
- package/dist/calendar/createCalendarState.d.ts.map +1 -1
- package/dist/calendar/createDatePickerState.d.ts +2 -0
- package/dist/calendar/createDatePickerState.d.ts.map +1 -1
- package/dist/calendar/createRangeCalendarState.d.ts +2 -0
- package/dist/calendar/createRangeCalendarState.d.ts.map +1 -1
- package/dist/calendar/createTimeFieldState.d.ts +1 -1
- package/dist/calendar/createTimeFieldState.d.ts.map +1 -1
- package/dist/checkbox/createCheckboxGroupState.d.ts +2 -0
- package/dist/checkbox/createCheckboxGroupState.d.ts.map +1 -1
- package/dist/collections/createListState.d.ts.map +1 -1
- package/dist/collections/createMenuState.d.ts +46 -2
- package/dist/collections/createMenuState.d.ts.map +1 -1
- package/dist/collections/createSelectionState.d.ts.map +1 -1
- package/dist/collections/index.d.ts +1 -1
- package/dist/collections/index.d.ts.map +1 -1
- package/dist/color/createColorAreaState.d.ts +4 -0
- package/dist/color/createColorAreaState.d.ts.map +1 -1
- package/dist/color/createColorFieldState.d.ts +2 -0
- package/dist/color/createColorFieldState.d.ts.map +1 -1
- package/dist/color/createColorSliderState.d.ts +2 -0
- package/dist/color/createColorSliderState.d.ts.map +1 -1
- package/dist/color/createColorWheelState.d.ts.map +1 -1
- package/dist/combobox/createComboBoxState.d.ts +90 -23
- package/dist/combobox/createComboBoxState.d.ts.map +1 -1
- package/dist/combobox/index.d.ts +1 -1
- package/dist/combobox/index.d.ts.map +1 -1
- package/dist/data/createAsyncList.d.ts.map +1 -1
- package/dist/data/createListData.d.ts.map +1 -1
- package/dist/data/createTreeData.d.ts.map +1 -1
- package/dist/dnd/createDraggableCollectionState.d.ts +5 -1
- package/dist/dnd/createDraggableCollectionState.d.ts.map +1 -1
- package/dist/dnd/createDroppableCollectionState.d.ts +6 -0
- package/dist/dnd/createDroppableCollectionState.d.ts.map +1 -1
- package/dist/form/createFormValidationState.d.ts.map +1 -1
- package/dist/grid/createGridState.d.ts +2 -2
- package/dist/grid/createGridState.d.ts.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1012 -471
- package/dist/index.js.map +1 -1
- package/dist/menu/index.d.ts +2 -1
- package/dist/menu/index.d.ts.map +1 -1
- package/dist/numberfield/createNumberFieldState.d.ts +6 -3
- package/dist/numberfield/createNumberFieldState.d.ts.map +1 -1
- package/dist/private/flags/flags.d.ts.map +1 -0
- package/dist/private/flags/flags.js +1 -1
- package/dist/private/flags/flags.js.map +1 -1
- package/dist/radio/createRadioGroupState.d.ts.map +1 -1
- package/dist/select/createSelectState.d.ts +85 -29
- package/dist/select/createSelectState.d.ts.map +1 -1
- package/dist/select/index.d.ts +1 -1
- package/dist/select/index.d.ts.map +1 -1
- package/dist/selection/SelectionManager.d.ts.map +1 -1
- package/dist/selection/createMultipleSelectionState.d.ts.map +1 -1
- package/dist/slider/createSliderState.d.ts +35 -9
- package/dist/slider/createSliderState.d.ts.map +1 -1
- package/dist/ssr/index.d.ts +0 -6
- package/dist/ssr/index.d.ts.map +1 -1
- package/dist/steplist/createStepListState.d.ts +8 -13
- package/dist/steplist/createStepListState.d.ts.map +1 -1
- package/dist/table/createTableColumnResizeState.d.ts.map +1 -1
- package/dist/table/createTableState.d.ts +2 -2
- package/dist/table/createTableState.d.ts.map +1 -1
- package/dist/table/createTreeGridState.d.ts +2 -2
- package/dist/table/createTreeGridState.d.ts.map +1 -1
- package/dist/table/types.d.ts +2 -2
- package/dist/table/types.d.ts.map +1 -1
- package/dist/tabs/createTabListState.d.ts.map +1 -1
- package/dist/tokenfield/TokenFieldValue.d.ts +28 -4
- package/dist/tokenfield/TokenFieldValue.d.ts.map +1 -1
- package/dist/tokenfield/index.d.ts +1 -1
- package/dist/tokenfield/index.d.ts.map +1 -1
- package/dist/tree/createTreeState.d.ts +2 -2
- package/dist/tree/createTreeState.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/reactivity.d.ts +5 -0
- package/dist/utils/reactivity.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/calendar/createCalendarState.ts +139 -58
- package/src/calendar/createDatePickerState.ts +14 -11
- package/src/calendar/createRangeCalendarState.ts +43 -17
- package/src/calendar/createTimeFieldState.ts +1 -1
- package/src/checkbox/createCheckboxGroupState.ts +16 -0
- package/src/collections/createListState.ts +4 -4
- package/src/collections/createMenuState.ts +134 -4
- package/src/collections/createSelectionState.ts +2 -2
- package/src/collections/index.ts +4 -0
- package/src/color/createColorAreaState.ts +19 -0
- package/src/color/createColorFieldState.ts +76 -26
- package/src/color/createColorSliderState.ts +25 -2
- package/src/color/createColorWheelState.ts +9 -5
- package/src/combobox/createComboBoxState.ts +371 -121
- package/src/combobox/index.ts +4 -0
- package/src/data/createAsyncList.ts +19 -4
- package/src/data/createListData.ts +8 -2
- package/src/data/createTreeData.ts +11 -3
- package/src/dnd/createDraggableCollectionState.ts +8 -1
- package/src/dnd/createDroppableCollectionState.ts +10 -0
- package/src/form/createFormValidationState.ts +17 -3
- package/src/grid/createGridState.ts +15 -8
- package/src/index.ts +18 -2
- package/src/menu/index.ts +5 -0
- package/src/numberfield/createNumberFieldState.ts +97 -35
- package/src/radio/createRadioGroupState.ts +4 -1
- package/src/select/createSelectState.ts +253 -131
- package/src/select/index.ts +8 -1
- package/src/selection/SelectionManager.ts +3 -1
- package/src/selection/createMultipleSelectionState.ts +2 -2
- package/src/slider/createSliderState.ts +227 -103
- package/src/ssr/index.ts +11 -4
- package/src/steplist/createStepListState.ts +156 -123
- package/src/table/createTableColumnResizeState.ts +51 -11
- package/src/table/createTableState.ts +5 -4
- package/src/table/createTreeGridState.ts +3 -4
- package/src/table/types.ts +2 -2
- package/src/tabs/createTabListState.ts +8 -31
- package/src/tokenfield/TokenFieldValue.ts +75 -16
- package/src/tokenfield/index.ts +1 -0
- package/src/tree/createTreeState.ts +15 -8
- package/src/utils/index.ts +1 -0
- package/src/utils/reactivity.ts +8 -0
- package/dist/flags/flags.d.ts.map +0 -1
- /package/dist/{flags → private/flags}/flags.d.ts +0 -0
- /package/src/{flags → private/flags}/flags.ts +0 -0
package/src/combobox/index.ts
CHANGED
|
@@ -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
|
-
|
|
344
|
-
if (index === -1)
|
|
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
|
-
|
|
355
|
-
if (index === -1)
|
|
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
|
-
|
|
169
|
-
if (index === -1)
|
|
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
|
|
366
|
-
|
|
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<
|
|
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
|
|
|
@@ -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(
|
|
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);
|
|
@@ -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:
|
|
16
|
+
options: MaybeAccessor<GridStateOptions<T, C>>,
|
|
18
17
|
): GridState<T, C> {
|
|
19
|
-
const getOptions = (
|
|
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
|
-
|
|
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,
|
|
67
|
-
if (
|
|
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 {
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
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 === ""
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
448
|
+
return snapValueToStep(base, p.minValue, p.maxValue, step());
|
|
398
449
|
}
|
|
399
450
|
|
|
400
|
-
const snapped =
|
|
451
|
+
const snapped = snapValueToStep(parsed, p.minValue, p.maxValue, step());
|
|
401
452
|
if ((operation === "+" && snapped > parsed) || (operation === "-" && snapped < parsed)) {
|
|
402
|
-
return
|
|
453
|
+
return snapped;
|
|
403
454
|
}
|
|
404
455
|
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
447
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|