@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.
- 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 +1044 -478
- 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/textfield/createTextFieldState.d.ts.map +1 -1
- package/dist/toggle/createToggleGroupState.d.ts +0 -8
- package/dist/toggle/createToggleGroupState.d.ts.map +1 -1
- package/dist/toggle/createToggleState.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 +23 -5
- 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/textfield/createTextFieldState.ts +6 -3
- package/src/toggle/createToggleGroupState.ts +4 -2
- package/src/toggle/createToggleState.ts +5 -4
- 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
|
@@ -22,10 +22,13 @@
|
|
|
22
22
|
import { createEffect, createMemo } from "solid-js";
|
|
23
23
|
import type { Accessor } from "solid-js";
|
|
24
24
|
import { createInternalSignal } from "../utils";
|
|
25
|
+
import type { CollectionNode, Key } from "../collections/types";
|
|
26
|
+
import {
|
|
27
|
+
createSingleSelectListState,
|
|
28
|
+
type SingleSelectListState,
|
|
29
|
+
} from "../collections/createListState";
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
export interface StepListStateProps {
|
|
31
|
+
export interface StepListStateProps<T = unknown> {
|
|
29
32
|
/** The currently selected step key (controlled). */
|
|
30
33
|
selectedKey?: Key;
|
|
31
34
|
/** The default selected step key (uncontrolled). */
|
|
@@ -45,44 +48,29 @@ export interface StepListStateProps {
|
|
|
45
48
|
/** Keys of individually disabled steps. */
|
|
46
49
|
disabledKeys?: Iterable<Key>;
|
|
47
50
|
/** The step items. */
|
|
48
|
-
items:
|
|
51
|
+
items: T[];
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
export interface StepListState {
|
|
52
|
-
readonly selectedKey: Accessor<Key | null>;
|
|
54
|
+
export interface StepListState<T = unknown> extends SingleSelectListState<T> {
|
|
53
55
|
readonly lastCompletedStep: Accessor<Key | null>;
|
|
54
|
-
readonly items: Accessor<
|
|
56
|
+
readonly items: Accessor<T[]>;
|
|
55
57
|
setSelectedKey(key: Key): void;
|
|
56
|
-
setLastCompletedStep(key: Key): void;
|
|
58
|
+
setLastCompletedStep(key: Key | null): void;
|
|
57
59
|
isCompleted(key: Key): boolean;
|
|
58
60
|
isSelectable(key: Key): boolean;
|
|
59
|
-
isDisabled: Accessor<boolean
|
|
61
|
+
isDisabled: Accessor<boolean> & ((key?: Key) => boolean);
|
|
60
62
|
isReadOnly: Accessor<boolean>;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
/**
|
|
64
66
|
* Creates state for a step list component.
|
|
65
67
|
*/
|
|
66
|
-
export function createStepListState(props: StepListStateProps): StepListState {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
// Build an index map: Key -> index
|
|
70
|
-
const indexMap = createMemo(() => {
|
|
71
|
-
const map = new Map<Key, number>();
|
|
72
|
-
const currentItems = items();
|
|
73
|
-
for (let i = 0; i < currentItems.length; i++) {
|
|
74
|
-
map.set(currentItems[i].key, i);
|
|
75
|
-
}
|
|
76
|
-
return map;
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// Disabled keys set
|
|
80
|
-
const disabledKeysSet = createMemo(() => new Set<Key>(props.disabledKeys ?? []));
|
|
81
|
-
|
|
82
|
-
const isDisabled: Accessor<boolean> = () => props.isDisabled ?? false;
|
|
68
|
+
export function createStepListState<T = unknown>(props: StepListStateProps<T>): StepListState<T> {
|
|
69
|
+
const isListDisabled: Accessor<boolean> = () => props.isDisabled ?? false;
|
|
83
70
|
const isReadOnly: Accessor<boolean> = () => props.isReadOnly ?? false;
|
|
71
|
+
const items: Accessor<T[]> = () => props.items ?? [];
|
|
84
72
|
|
|
85
|
-
// Last completed step signal (uncontrolled)
|
|
73
|
+
// Last completed step signal (uncontrolled / controlled sync)
|
|
86
74
|
const [lastCompletedStepInternal, setLastCompletedStepInternal] =
|
|
87
75
|
createInternalSignal<Key | null>(props.defaultLastCompletedStep ?? null);
|
|
88
76
|
|
|
@@ -93,143 +81,188 @@ export function createStepListState(props: StepListStateProps): StepListState {
|
|
|
93
81
|
return lastCompletedStepInternal();
|
|
94
82
|
};
|
|
95
83
|
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
const prevIndex = lastCompletedStep() !== null ? indexMap().get(lastCompletedStep()!) : -1;
|
|
99
|
-
|
|
100
|
-
// Only advance completion, never go back
|
|
101
|
-
if (
|
|
102
|
-
currentIndex !== undefined &&
|
|
103
|
-
(prevIndex === undefined || currentIndex > (prevIndex ?? -1))
|
|
104
|
-
) {
|
|
105
|
-
if (props.lastCompletedStep === undefined) {
|
|
106
|
-
setLastCompletedStepInternal(key);
|
|
107
|
-
}
|
|
108
|
-
props.onLastCompletedStepChange?.(key);
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const isCompleted = (key: Key): boolean => {
|
|
84
|
+
const isCompleted = (step: Key): boolean => {
|
|
85
|
+
if (step == null) return false;
|
|
113
86
|
const completed = lastCompletedStep();
|
|
114
|
-
if (completed
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
87
|
+
if (completed == null) return false;
|
|
88
|
+
const { indexMap } = keyMaps();
|
|
89
|
+
const stepIdx = indexMap.get(step);
|
|
90
|
+
const completedIdx = indexMap.get(completed);
|
|
91
|
+
if (stepIdx === undefined || completedIdx === undefined) return false;
|
|
92
|
+
return stepIdx <= completedIdx;
|
|
119
93
|
};
|
|
120
94
|
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
const isSelectable = (key: Key): boolean => {
|
|
127
|
-
if (isDisabled() || isReadOnly() || isStepDisabled(key)) return false;
|
|
128
|
-
// Completed steps are always selectable
|
|
129
|
-
if (isCompleted(key)) return true;
|
|
130
|
-
// First step is always selectable
|
|
131
|
-
const keyIndex = indexMap().get(key);
|
|
132
|
-
if (keyIndex === 0) return true;
|
|
133
|
-
// Otherwise a step is selectable only if the PREVIOUS step is completed —
|
|
134
|
-
// mirrors react-stately `useStepListState.isSelectable`
|
|
135
|
-
// (`isCompleted(prevStep) || step === firstKey`). There is no "step after the
|
|
136
|
-
// currently selected step" clause upstream: a fresh list exposes only the
|
|
137
|
-
// first step, and the immediate-next step becomes selectable when its
|
|
138
|
-
// predecessor is *completed*, not merely selected.
|
|
139
|
-
const currentItems = items();
|
|
140
|
-
if (keyIndex !== undefined && keyIndex > 0) {
|
|
141
|
-
const prevKey = currentItems[keyIndex - 1].key;
|
|
142
|
-
if (isCompleted(prevKey)) return true;
|
|
95
|
+
const isSelectable = (step: Key): boolean => {
|
|
96
|
+
if (isListDisabled() || singleSelectListState.disabledKeys().has(step) || isReadOnly()) {
|
|
97
|
+
return false;
|
|
143
98
|
}
|
|
144
|
-
return
|
|
99
|
+
if (isCompleted(step)) return true;
|
|
100
|
+
const { keysLinkedList } = keyMaps();
|
|
101
|
+
const prevStep = keysLinkedList.get(step);
|
|
102
|
+
return (
|
|
103
|
+
(prevStep !== undefined && isCompleted(prevStep)) ||
|
|
104
|
+
step === singleSelectListState.collection().getFirstKey()
|
|
105
|
+
);
|
|
145
106
|
};
|
|
146
107
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
for (const item of currentItems) {
|
|
151
|
-
if (!isCompleted(item.key) && !isStepDisabled(item.key)) {
|
|
152
|
-
return item.key;
|
|
153
|
-
}
|
|
108
|
+
const findInitialSelectedKey = (): Key | undefined => {
|
|
109
|
+
if (props.defaultSelectedKey !== undefined) {
|
|
110
|
+
return props.defaultSelectedKey;
|
|
154
111
|
}
|
|
155
|
-
|
|
112
|
+
const currentItems = (props.items ?? []) as Array<{ key: Key }>;
|
|
113
|
+
const disabled = new Set<Key>(props.disabledKeys ?? []);
|
|
156
114
|
for (const item of currentItems) {
|
|
157
|
-
if (!
|
|
115
|
+
if (!disabled.has(item.key)) {
|
|
158
116
|
return item.key;
|
|
159
117
|
}
|
|
160
118
|
}
|
|
161
|
-
return currentItems
|
|
119
|
+
return currentItems[0]?.key;
|
|
162
120
|
};
|
|
163
121
|
|
|
164
|
-
//
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
122
|
+
// Route through the shared SingleSelectListState collection spine
|
|
123
|
+
const singleSelectListState = createSingleSelectListState<T>({
|
|
124
|
+
get items() {
|
|
125
|
+
return (props.items ?? []) as T[];
|
|
126
|
+
},
|
|
127
|
+
getKey(item: any) {
|
|
128
|
+
return item.key;
|
|
129
|
+
},
|
|
130
|
+
getTextValue(item: any) {
|
|
131
|
+
return item.label ?? item.textValue ?? (item.key != null ? String(item.key) : "");
|
|
132
|
+
},
|
|
133
|
+
get disabledKeys() {
|
|
134
|
+
return props.disabledKeys;
|
|
135
|
+
},
|
|
136
|
+
get selectedKey() {
|
|
171
137
|
return props.selectedKey;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
138
|
+
},
|
|
139
|
+
get defaultSelectedKey() {
|
|
140
|
+
return findInitialSelectedKey();
|
|
141
|
+
},
|
|
142
|
+
onSelectionChange(key) {
|
|
143
|
+
if (key != null) {
|
|
144
|
+
props.onSelectionChange?.(key);
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
});
|
|
175
148
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
149
|
+
// Build indexMap and keysLinkedList from the collection
|
|
150
|
+
const keyMaps = createMemo(() => {
|
|
151
|
+
const coll = singleSelectListState.collection();
|
|
152
|
+
const indexMap = new Map<Key, number>();
|
|
153
|
+
const keysLinkedList = new Map<Key, Key | undefined>();
|
|
154
|
+
let i = 0;
|
|
155
|
+
let prev: CollectionNode<T> | undefined;
|
|
156
|
+
for (const item of coll) {
|
|
157
|
+
indexMap.set(item.key, i);
|
|
158
|
+
keysLinkedList.set(item.key, prev?.key);
|
|
159
|
+
prev = item;
|
|
160
|
+
i++;
|
|
161
|
+
}
|
|
162
|
+
return { indexMap, keysLinkedList };
|
|
163
|
+
});
|
|
179
164
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const newIndex = indexMap().get(key);
|
|
185
|
-
if (
|
|
186
|
-
currentIndex !== undefined &&
|
|
187
|
-
newIndex !== undefined &&
|
|
188
|
-
newIndex > currentIndex &&
|
|
189
|
-
!isCompleted(currentSelected)
|
|
190
|
-
) {
|
|
191
|
-
setLastCompletedStep(currentSelected);
|
|
165
|
+
const setLastCompletedStep = (key: Key | null) => {
|
|
166
|
+
if (key == null) {
|
|
167
|
+
if (props.lastCompletedStep === undefined) {
|
|
168
|
+
setLastCompletedStepInternal(null);
|
|
192
169
|
}
|
|
170
|
+
props.onLastCompletedStepChange?.(null);
|
|
171
|
+
return;
|
|
193
172
|
}
|
|
173
|
+
const { indexMap } = keyMaps();
|
|
174
|
+
const currentIndex = indexMap.get(key);
|
|
175
|
+
const completed = lastCompletedStep();
|
|
176
|
+
const prevIndex = completed !== null ? indexMap.get(completed) : -1;
|
|
194
177
|
|
|
195
|
-
|
|
196
|
-
|
|
178
|
+
// Only advance completion, never go back
|
|
179
|
+
if (
|
|
180
|
+
currentIndex !== undefined &&
|
|
181
|
+
(prevIndex === undefined || currentIndex > (prevIndex ?? -1))
|
|
182
|
+
) {
|
|
183
|
+
if (props.lastCompletedStep === undefined) {
|
|
184
|
+
setLastCompletedStepInternal(key);
|
|
185
|
+
}
|
|
186
|
+
props.onLastCompletedStepChange?.(key);
|
|
197
187
|
}
|
|
198
|
-
props.onSelectionChange?.(key);
|
|
199
188
|
};
|
|
200
189
|
|
|
201
|
-
// Mirror react-stately
|
|
190
|
+
// Mirror react-stately useStepListState's effect: whenever the selected step
|
|
202
191
|
// sits more than one past the last completed step (e.g. mounted with a
|
|
203
|
-
//
|
|
204
|
-
// predecessor. Because completion is cumulative (`isCompleted` = index ≤
|
|
205
|
-
// lastCompleted index), advancing `lastCompletedStep` to `selectedIdx - 1`
|
|
206
|
-
// marks every intermediate step complete. This runs regardless of
|
|
207
|
-
// `isDisabled` / `isReadOnly`, exactly as upstream (the effect is ungated).
|
|
192
|
+
// defaultSelectedKey ahead of progress), auto-complete its immediate predecessor.
|
|
208
193
|
createEffect(
|
|
209
194
|
() => {
|
|
210
|
-
const selKey = selectedKey();
|
|
195
|
+
const selKey = singleSelectListState.selectedKey();
|
|
211
196
|
if (selKey === null) return null;
|
|
212
|
-
const
|
|
197
|
+
const { indexMap, keysLinkedList } = keyMaps();
|
|
198
|
+
const selIdx = indexMap.get(selKey);
|
|
213
199
|
if (selIdx === undefined || selIdx <= 0) return null;
|
|
214
200
|
const completed = lastCompletedStep();
|
|
215
|
-
const lcs = completed !== null ? (indexMap
|
|
201
|
+
const lcs = completed !== null ? (indexMap.get(completed) ?? -1) : -1;
|
|
216
202
|
if (selIdx <= lcs + 1) return null;
|
|
217
|
-
return
|
|
203
|
+
return keysLinkedList.get(selKey) ?? null;
|
|
218
204
|
},
|
|
219
205
|
(prevKey) => {
|
|
220
|
-
if (prevKey
|
|
206
|
+
if (prevKey != null) {
|
|
207
|
+
setLastCompletedStep(prevKey);
|
|
208
|
+
}
|
|
221
209
|
},
|
|
222
210
|
);
|
|
223
211
|
|
|
212
|
+
// Sync initial focus to selectedKey if focusedKey is unset
|
|
213
|
+
createEffect(
|
|
214
|
+
() => singleSelectListState.selectedKey(),
|
|
215
|
+
(selKey) => {
|
|
216
|
+
if (singleSelectListState.focusedKey() == null && selKey != null) {
|
|
217
|
+
singleSelectListState.setFocusedKey(selKey);
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const setSelectedKey = (key: Key) => {
|
|
223
|
+
if (isListDisabled() || isReadOnly()) return;
|
|
224
|
+
if (!isSelectable(key)) return;
|
|
225
|
+
const { keysLinkedList } = keyMaps();
|
|
226
|
+
const prevKey = keysLinkedList.get(key);
|
|
227
|
+
if (prevKey && !isCompleted(prevKey)) {
|
|
228
|
+
setLastCompletedStep(prevKey);
|
|
229
|
+
}
|
|
230
|
+
singleSelectListState.setSelectedKey(key);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Bridge selectionManager with step list selectability
|
|
234
|
+
singleSelectListState.selectionManager.canSelectItem = (key: Key) => isSelectable(key);
|
|
235
|
+
const origReplaceSelection = singleSelectListState.selectionManager.replaceSelection.bind(
|
|
236
|
+
singleSelectListState.selectionManager,
|
|
237
|
+
);
|
|
238
|
+
singleSelectListState.selectionManager.replaceSelection = (key: Key) => {
|
|
239
|
+
if (isListDisabled() || isReadOnly()) return;
|
|
240
|
+
if (!isSelectable(key)) return;
|
|
241
|
+
const { keysLinkedList } = keyMaps();
|
|
242
|
+
const prevKey = keysLinkedList.get(key);
|
|
243
|
+
if (prevKey && !isCompleted(prevKey)) {
|
|
244
|
+
setLastCompletedStep(prevKey);
|
|
245
|
+
}
|
|
246
|
+
origReplaceSelection(key);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const isDisabledFn = ((key?: Key): boolean => {
|
|
250
|
+
if (key === undefined) {
|
|
251
|
+
return isListDisabled();
|
|
252
|
+
}
|
|
253
|
+
return !isSelectable(key);
|
|
254
|
+
}) as Accessor<boolean> & ((key?: Key) => boolean);
|
|
255
|
+
|
|
224
256
|
return {
|
|
225
|
-
|
|
257
|
+
...singleSelectListState,
|
|
258
|
+
selectedKey: singleSelectListState.selectedKey,
|
|
226
259
|
lastCompletedStep,
|
|
227
260
|
items,
|
|
228
261
|
setSelectedKey,
|
|
229
262
|
setLastCompletedStep,
|
|
230
263
|
isCompleted,
|
|
231
264
|
isSelectable,
|
|
232
|
-
isDisabled,
|
|
265
|
+
isDisabled: isDisabledFn,
|
|
233
266
|
isReadOnly,
|
|
234
267
|
};
|
|
235
268
|
}
|
|
@@ -113,13 +113,38 @@ function parseColumnDef(col: ColumnResizeDefinition, tableWidth: number): Parsed
|
|
|
113
113
|
return { key: col.key, fr: 0, fixedPx: isNaN(px) ? 0 : px, minWidth, maxWidth };
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
function applyFractionalRemainder(
|
|
117
|
+
widths: Map<Key, number>,
|
|
118
|
+
columns: ColumnResizeDefinition[],
|
|
119
|
+
originalWidth: number,
|
|
120
|
+
): void {
|
|
121
|
+
const flooredWidth = Math.floor(originalWidth);
|
|
122
|
+
const hasFractionalWidth = originalWidth - flooredWidth > 0;
|
|
123
|
+
if (!hasFractionalWidth || columns.length === 0) return;
|
|
124
|
+
|
|
125
|
+
const lastKey = columns[columns.length - 1].key;
|
|
126
|
+
if (widths.has(lastKey)) {
|
|
127
|
+
const tableFractionalWidth = originalWidth.toString().split(".")[1];
|
|
128
|
+
if (tableFractionalWidth) {
|
|
129
|
+
const columnWidth = Math.floor(widths.get(lastKey)!).toString();
|
|
130
|
+
widths.set(lastKey, Number(columnWidth + "." + tableFractionalWidth));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
116
135
|
/**
|
|
117
136
|
* Distribute `tableWidth` among columns:
|
|
118
|
-
* 1.
|
|
119
|
-
* 2.
|
|
137
|
+
* 1. Floor tableWidth to integer before column sizing, matching upstream TableUtils.calculateColumnSizes.
|
|
138
|
+
* 2. Fixed-width columns get their declared width (clamped to min/max).
|
|
139
|
+
* 3. Remaining space is distributed among fractional columns proportionally with cascading rounding.
|
|
140
|
+
* 4. Leftover sub-pixel width is added to the last column so widths sum exactly to tableWidth.
|
|
120
141
|
*/
|
|
121
142
|
function distributeWidths(columns: ColumnResizeDefinition[], tableWidth: number): Map<Key, number> {
|
|
122
|
-
const
|
|
143
|
+
const originalWidth = tableWidth;
|
|
144
|
+
const flooredWidth = Math.floor(tableWidth);
|
|
145
|
+
const availableWidth = Math.max(0, flooredWidth);
|
|
146
|
+
|
|
147
|
+
const parsed = columns.map((c) => parseColumnDef(c, availableWidth));
|
|
123
148
|
|
|
124
149
|
// Pass 1 — allocate fixed columns
|
|
125
150
|
let usedSpace = 0;
|
|
@@ -133,23 +158,36 @@ function distributeWidths(columns: ColumnResizeDefinition[], tableWidth: number)
|
|
|
133
158
|
}
|
|
134
159
|
}
|
|
135
160
|
|
|
136
|
-
const remainingSpace = Math.max(0,
|
|
161
|
+
const remainingSpace = Math.max(0, availableWidth - usedSpace);
|
|
137
162
|
const perFr = totalFr > 0 ? remainingSpace / totalFr : 0;
|
|
138
163
|
|
|
139
|
-
// Pass 2 — build map
|
|
164
|
+
// Pass 2 — build map with cascading rounding
|
|
140
165
|
const widths = new Map<Key, number>();
|
|
166
|
+
let fpTotal = 0;
|
|
167
|
+
let intTotal = 0;
|
|
168
|
+
|
|
141
169
|
for (const p of parsed) {
|
|
142
|
-
let
|
|
170
|
+
let targetSize: number;
|
|
143
171
|
if (p.fr > 0) {
|
|
144
|
-
|
|
172
|
+
targetSize = p.fr * perFr;
|
|
145
173
|
} else {
|
|
146
|
-
|
|
174
|
+
targetSize = p.fixedPx;
|
|
147
175
|
}
|
|
148
176
|
// Clamp
|
|
149
|
-
|
|
150
|
-
|
|
177
|
+
targetSize = Math.max(p.minWidth, Math.min(p.maxWidth, targetSize));
|
|
178
|
+
|
|
179
|
+
// Cascade rounding to ensure sum of integers matches availableWidth
|
|
180
|
+
const rounded = Math.round(targetSize + fpTotal) - intTotal;
|
|
181
|
+
fpTotal += targetSize;
|
|
182
|
+
intTotal += rounded;
|
|
183
|
+
|
|
184
|
+
widths.set(p.key, rounded);
|
|
151
185
|
}
|
|
152
186
|
|
|
187
|
+
// Give the leftover sub-pixel width to the last column so the columns sum
|
|
188
|
+
// exactly to the (possibly fractional) available width.
|
|
189
|
+
applyFractionalRemainder(widths, columns, originalWidth);
|
|
190
|
+
|
|
153
191
|
return widths;
|
|
154
192
|
}
|
|
155
193
|
|
|
@@ -186,6 +224,7 @@ export function createTableColumnResizeState(
|
|
|
186
224
|
merged.set(key, width);
|
|
187
225
|
}
|
|
188
226
|
}
|
|
227
|
+
applyFractionalRemainder(merged, getProps().columns, getProps().tableWidth);
|
|
189
228
|
return merged;
|
|
190
229
|
});
|
|
191
230
|
|
|
@@ -229,7 +268,7 @@ export function createTableColumnResizeState(
|
|
|
229
268
|
const updateResizedColumns = (key: Key, width: number): Map<Key, number> => {
|
|
230
269
|
const minW = getColumnMinWidth(key);
|
|
231
270
|
const maxW = getColumnMaxWidth(key);
|
|
232
|
-
const clampedWidth = Math.
|
|
271
|
+
const clampedWidth = Math.max(minW, Math.min(maxW, Math.floor(width)));
|
|
233
272
|
|
|
234
273
|
const newOverrides = new Map(overrides());
|
|
235
274
|
newOverrides.set(key, clampedWidth);
|
|
@@ -243,6 +282,7 @@ export function createTableColumnResizeState(
|
|
|
243
282
|
merged.set(k, w);
|
|
244
283
|
}
|
|
245
284
|
}
|
|
285
|
+
applyFractionalRemainder(merged, getProps().columns, getProps().tableWidth);
|
|
246
286
|
return merged;
|
|
247
287
|
};
|
|
248
288
|
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { createMemo } from "solid-js";
|
|
21
|
-
import type
|
|
21
|
+
import { propsAccessor, type MaybeAccessor } from "../utils";
|
|
22
22
|
import { createGridState } from "../grid/createGridState";
|
|
23
23
|
import type { Key } from "../collections/types";
|
|
24
24
|
import type {
|
|
@@ -41,8 +41,8 @@ const OPPOSITE_SORT_DIRECTION: Record<SortDirection, SortDirection> = {
|
|
|
41
41
|
export function createTableState<
|
|
42
42
|
T extends object,
|
|
43
43
|
C extends TableCollection<T> = TableCollection<T>,
|
|
44
|
-
>(options:
|
|
45
|
-
const getOptions = (
|
|
44
|
+
>(options: MaybeAccessor<TableStateOptions<T, C>>): TableState<T, C> {
|
|
45
|
+
const getOptions = propsAccessor(options);
|
|
46
46
|
|
|
47
47
|
// Create the underlying grid state
|
|
48
48
|
const gridState = createGridState<T, C>(() => ({
|
|
@@ -103,7 +103,8 @@ export function createTableState<
|
|
|
103
103
|
return gridState.disabledBehavior;
|
|
104
104
|
},
|
|
105
105
|
get isKeyboardNavigationDisabled() {
|
|
106
|
-
|
|
106
|
+
// An empty body keeps navigation disabled after the flag is cleared.
|
|
107
|
+
return getOptions().collection.size === 0 || gridState.isKeyboardNavigationDisabled;
|
|
107
108
|
},
|
|
108
109
|
get focusedKey() {
|
|
109
110
|
return gridState.focusedKey;
|
|
@@ -21,8 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { createMemo } from "solid-js";
|
|
24
|
-
import type
|
|
25
|
-
import { createInternalSignal } from "../utils";
|
|
24
|
+
import { createInternalSignal, propsAccessor, type MaybeAccessor } from "../utils";
|
|
26
25
|
|
|
27
26
|
import { createTableState } from "./createTableState";
|
|
28
27
|
import { createTableCollection, TableCollection } from "./TableCollection";
|
|
@@ -36,8 +35,8 @@ import type { TreeGridState, TreeGridStateOptions } from "./types";
|
|
|
36
35
|
export function createTreeGridState<
|
|
37
36
|
T extends object,
|
|
38
37
|
C extends TableCollection<T> = TableCollection<T>,
|
|
39
|
-
>(options:
|
|
40
|
-
const getOptions = (
|
|
38
|
+
>(options: MaybeAccessor<TreeGridStateOptions<T, C>>): TreeGridState<T, C> {
|
|
39
|
+
const getOptions = propsAccessor(options);
|
|
41
40
|
|
|
42
41
|
// Expanded keys: controlled via `UNSTABLE_expandedKeys`, otherwise the internal signal
|
|
43
42
|
// seeded from `UNSTABLE_defaultExpandedKeys`.
|
package/src/table/types.ts
CHANGED
|
@@ -138,8 +138,8 @@ export interface TreeGridStateOptions<
|
|
|
138
138
|
> extends Omit<TableStateOptions<T, C>, "collection"> {
|
|
139
139
|
/** Column definitions. */
|
|
140
140
|
columns: ColumnDefinition<T>[];
|
|
141
|
-
/** Row definitions (may contain `childRows`
|
|
142
|
-
rows: RowDefinition<T>[];
|
|
141
|
+
/** Row definitions (may contain `childRows`) or raw data items keyed by `getKey`. */
|
|
142
|
+
rows: RowDefinition<T>[] | T[];
|
|
143
143
|
/** Function to get the key from a data item. */
|
|
144
144
|
getKey?: (item: T) => Key;
|
|
145
145
|
/** Function to get the text value from a data item. */
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
* Ported from packages/react-stately/src/tabs/useTabListState.ts.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { createEffect, createMemo
|
|
20
|
+
import { createEffect, createMemo } from "solid-js";
|
|
21
21
|
import type { Accessor } from "solid-js";
|
|
22
|
-
import { createInternalSignal,
|
|
22
|
+
import { createInternalSignal, propsAccessor, type MaybeAccessor } from "../utils";
|
|
23
23
|
import { ListCollection } from "../collections/ListCollection";
|
|
24
24
|
import type {
|
|
25
25
|
Collection,
|
|
@@ -94,7 +94,7 @@ export interface TabListState<T = unknown> {
|
|
|
94
94
|
export function createTabListState<T = unknown>(
|
|
95
95
|
props: MaybeAccessor<TabListStateProps<T>>,
|
|
96
96
|
): TabListState<T> {
|
|
97
|
-
const getProps = (
|
|
97
|
+
const getProps = propsAccessor(props);
|
|
98
98
|
|
|
99
99
|
const collection: Accessor<Collection<T>> = createMemo(() => {
|
|
100
100
|
const p = getProps();
|
|
@@ -226,42 +226,19 @@ export function createTabListState<T = unknown>(
|
|
|
226
226
|
setChildFocusStrategy(childStrategy ?? null);
|
|
227
227
|
};
|
|
228
228
|
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
// RAC writes in useEffect (after paint). A tracking-phase write would copy
|
|
233
|
-
// inside setSelectedKey, so D4 capture of pointerup/click would already see
|
|
234
|
-
// tabindex 0. Subscribe in compute; write on the next animation frame in apply.
|
|
229
|
+
// Match useTabListState's passive effect without adding a frame delay. Mouse
|
|
230
|
+
// press-start selection must update the roving target before native focusin;
|
|
231
|
+
// touch selects on press-up, so its earlier focusin retains the old tabindex.
|
|
235
232
|
let lastSelectedKey: Key | null = selectedKey();
|
|
236
|
-
let selectedToFocusedFrame: number | null = null;
|
|
237
|
-
const copySelectedToFocusedKey = () => {
|
|
238
|
-
selectedToFocusedFrame = null;
|
|
239
|
-
const sel = selectedKey();
|
|
240
|
-
if ((sel !== null && focusedKey() === null) || (!isFocused() && sel !== lastSelectedKey)) {
|
|
241
|
-
setFocusedKey(sel);
|
|
242
|
-
}
|
|
243
|
-
lastSelectedKey = sel;
|
|
244
|
-
};
|
|
245
233
|
createEffect(
|
|
246
234
|
() => ({ sel: selectedKey(), foc: focusedKey(), focused: isFocused() }),
|
|
247
235
|
({ sel, foc, focused }) => {
|
|
248
|
-
if (sel !== null && foc === null) {
|
|
236
|
+
if ((sel !== null && foc === null) || (!focused && sel !== lastSelectedKey)) {
|
|
249
237
|
setFocusedKey(sel);
|
|
250
|
-
lastSelectedKey = sel;
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (!focused && sel !== lastSelectedKey) {
|
|
255
|
-
if (selectedToFocusedFrame != null) return;
|
|
256
|
-
selectedToFocusedFrame = requestAnimationFrame(copySelectedToFocusedKey);
|
|
257
238
|
}
|
|
239
|
+
lastSelectedKey = sel;
|
|
258
240
|
},
|
|
259
241
|
);
|
|
260
|
-
onCleanup(() => {
|
|
261
|
-
if (selectedToFocusedFrame == null) return;
|
|
262
|
-
cancelAnimationFrame(selectedToFocusedFrame);
|
|
263
|
-
selectedToFocusedFrame = null;
|
|
264
|
-
});
|
|
265
242
|
|
|
266
243
|
// Keep uncontrolled selection valid as items/disabled keys change.
|
|
267
244
|
createEffect(
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* as used by @react-aria/textfield.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { Accessor } from "solid-js";
|
|
24
|
+
import { Accessor, untrack } from "solid-js";
|
|
25
25
|
import { createInternalSignal, type MaybeAccessor, access } from "../utils";
|
|
26
26
|
|
|
27
27
|
export interface TextFieldStateOptions {
|
|
@@ -49,8 +49,11 @@ export function createTextFieldState(
|
|
|
49
49
|
): TextFieldState {
|
|
50
50
|
const getProps = () => access(props);
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
const initialValue =
|
|
52
|
+
// Seeds the uncontrolled state once, by definition.
|
|
53
|
+
const initialValue = untrack(() => {
|
|
54
|
+
const initialProps = getProps();
|
|
55
|
+
return initialProps.value ?? initialProps.defaultValue ?? "";
|
|
56
|
+
});
|
|
54
57
|
|
|
55
58
|
const [internalValue, setInternalValue] = createInternalSignal(initialValue);
|
|
56
59
|
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* This is a port of @react-stately/toggle's useToggleGroupState.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import { untrack } from "solid-js";
|
|
24
25
|
import type { Key } from "../collections";
|
|
25
26
|
import { access, createInternalSignal, readNow, type MaybeAccessor } from "../utils";
|
|
26
27
|
|
|
@@ -69,9 +70,10 @@ export function createToggleGroupState(
|
|
|
69
70
|
): ToggleGroupState {
|
|
70
71
|
const getProps = () => access(props);
|
|
71
72
|
|
|
72
|
-
|
|
73
|
+
// Seeds the uncontrolled state once, by definition.
|
|
74
|
+
const initialDefaultSelectedKeys = untrack(() => getProps().defaultSelectedKeys);
|
|
73
75
|
const [internalSelectedKeys, setInternalSelectedKeys] = createInternalSignal<Set<Key>>(
|
|
74
|
-
toKeySet(
|
|
76
|
+
toKeySet(initialDefaultSelectedKeys),
|
|
75
77
|
);
|
|
76
78
|
|
|
77
79
|
const isControlled = () => getProps().selectedKeys !== undefined;
|