@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.
Files changed (127) 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 +1012 -471
  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/tokenfield/TokenFieldValue.d.ts +28 -4
  72. package/dist/tokenfield/TokenFieldValue.d.ts.map +1 -1
  73. package/dist/tokenfield/index.d.ts +1 -1
  74. package/dist/tokenfield/index.d.ts.map +1 -1
  75. package/dist/tree/createTreeState.d.ts +2 -2
  76. package/dist/tree/createTreeState.d.ts.map +1 -1
  77. package/dist/utils/index.d.ts +1 -1
  78. package/dist/utils/index.d.ts.map +1 -1
  79. package/dist/utils/reactivity.d.ts +5 -0
  80. package/dist/utils/reactivity.d.ts.map +1 -1
  81. package/package.json +2 -2
  82. package/src/calendar/createCalendarState.ts +139 -58
  83. package/src/calendar/createDatePickerState.ts +14 -11
  84. package/src/calendar/createRangeCalendarState.ts +43 -17
  85. package/src/calendar/createTimeFieldState.ts +1 -1
  86. package/src/checkbox/createCheckboxGroupState.ts +16 -0
  87. package/src/collections/createListState.ts +4 -4
  88. package/src/collections/createMenuState.ts +134 -4
  89. package/src/collections/createSelectionState.ts +2 -2
  90. package/src/collections/index.ts +4 -0
  91. package/src/color/createColorAreaState.ts +19 -0
  92. package/src/color/createColorFieldState.ts +76 -26
  93. package/src/color/createColorSliderState.ts +25 -2
  94. package/src/color/createColorWheelState.ts +9 -5
  95. package/src/combobox/createComboBoxState.ts +371 -121
  96. package/src/combobox/index.ts +4 -0
  97. package/src/data/createAsyncList.ts +19 -4
  98. package/src/data/createListData.ts +8 -2
  99. package/src/data/createTreeData.ts +11 -3
  100. package/src/dnd/createDraggableCollectionState.ts +8 -1
  101. package/src/dnd/createDroppableCollectionState.ts +10 -0
  102. package/src/form/createFormValidationState.ts +17 -3
  103. package/src/grid/createGridState.ts +15 -8
  104. package/src/index.ts +18 -2
  105. package/src/menu/index.ts +5 -0
  106. package/src/numberfield/createNumberFieldState.ts +97 -35
  107. package/src/radio/createRadioGroupState.ts +4 -1
  108. package/src/select/createSelectState.ts +253 -131
  109. package/src/select/index.ts +8 -1
  110. package/src/selection/SelectionManager.ts +3 -1
  111. package/src/selection/createMultipleSelectionState.ts +2 -2
  112. package/src/slider/createSliderState.ts +227 -103
  113. package/src/ssr/index.ts +11 -4
  114. package/src/steplist/createStepListState.ts +156 -123
  115. package/src/table/createTableColumnResizeState.ts +51 -11
  116. package/src/table/createTableState.ts +5 -4
  117. package/src/table/createTreeGridState.ts +3 -4
  118. package/src/table/types.ts +2 -2
  119. package/src/tabs/createTabListState.ts +8 -31
  120. package/src/tokenfield/TokenFieldValue.ts +75 -16
  121. package/src/tokenfield/index.ts +1 -0
  122. package/src/tree/createTreeState.ts +15 -8
  123. package/src/utils/index.ts +1 -0
  124. package/src/utils/reactivity.ts +8 -0
  125. package/dist/flags/flags.d.ts.map +0 -1
  126. /package/dist/{flags → private/flags}/flags.d.ts +0 -0
  127. /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
- import type { Key } from "../collections/types";
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: Array<{ key: Key; [key: string]: any }>;
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<Array<{ key: Key }>>;
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 items = () => props.items;
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 setLastCompletedStep = (key: Key) => {
97
- const currentIndex = indexMap().get(key);
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 === null) return false;
115
- const keyIndex = indexMap().get(key);
116
- const completedIndex = indexMap().get(completed);
117
- if (keyIndex === undefined || completedIndex === undefined) return false;
118
- return keyIndex <= completedIndex;
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 isStepDisabled = (key: Key): boolean => {
122
- if (isDisabled()) return true;
123
- return disabledKeysSet().has(key);
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 false;
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
- // Find the first selectable non-completed step
148
- const findDefaultSelectedKey = (): Key | null => {
149
- const currentItems = items();
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
- // All completed - select first non-disabled
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 (!isStepDisabled(item.key)) {
115
+ if (!disabled.has(item.key)) {
158
116
  return item.key;
159
117
  }
160
118
  }
161
- return currentItems.length > 0 ? currentItems[0].key : null;
119
+ return currentItems[0]?.key;
162
120
  };
163
121
 
164
- // Selected key signal (uncontrolled)
165
- const [selectedKeyInternal, setSelectedKeyInternal] = createInternalSignal<Key | null>(
166
- props.defaultSelectedKey ?? findDefaultSelectedKey(),
167
- );
168
-
169
- const selectedKey: Accessor<Key | null> = () => {
170
- if (props.selectedKey !== undefined) {
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
- return selectedKeyInternal();
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
- const setSelectedKey = (key: Key) => {
177
- if (isReadOnly() || isDisabled()) return;
178
- if (!isSelectable(key)) return;
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
- // Mark previous selected step as completed if advancing
181
- const currentSelected = selectedKey();
182
- if (currentSelected !== null) {
183
- const currentIndex = indexMap().get(currentSelected);
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
- if (props.selectedKey === undefined) {
196
- setSelectedKeyInternal(key);
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 `useStepListState`'s effect: whenever the selected step
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
- // `defaultSelectedKey` ahead of progress), auto-complete its immediate
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 selIdx = indexMap().get(selKey);
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().get(completed) ?? -1) : -1;
201
+ const lcs = completed !== null ? (indexMap.get(completed) ?? -1) : -1;
216
202
  if (selIdx <= lcs + 1) return null;
217
- return items()[selIdx - 1]?.key;
203
+ return keysLinkedList.get(selKey) ?? null;
218
204
  },
219
205
  (prevKey) => {
220
- if (prevKey !== undefined && prevKey !== null) setLastCompletedStep(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
- selectedKey,
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. Fixed-width columns get their declared width (clamped to min/max).
119
- * 2. Remaining space is distributed among fractional columns proportionally.
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 parsed = columns.map((c) => parseColumnDef(c, tableWidth));
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, tableWidth - usedSpace);
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 w: number;
170
+ let targetSize: number;
143
171
  if (p.fr > 0) {
144
- w = p.fr * perFr;
172
+ targetSize = p.fr * perFr;
145
173
  } else {
146
- w = p.fixedPx;
174
+ targetSize = p.fixedPx;
147
175
  }
148
176
  // Clamp
149
- w = Math.max(p.minWidth, Math.min(p.maxWidth, w));
150
- widths.set(p.key, Math.round(w));
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.round(Math.max(minW, Math.min(maxW, width)));
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 { Accessor } from "solid-js";
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: Accessor<TableStateOptions<T, C>>): TableState<T, C> {
45
- const getOptions = () => options();
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
- return gridState.isKeyboardNavigationDisabled;
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 { Accessor } from "solid-js";
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: Accessor<TreeGridStateOptions<T, C>>): TreeGridState<T, C> {
40
- const getOptions = () => options();
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`.
@@ -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` for nested rows). */
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, onCleanup } from "solid-js";
20
+ import { createEffect, createMemo } from "solid-js";
21
21
  import type { Accessor } from "solid-js";
22
- import { createInternalSignal, access, type MaybeAccessor } from "../utils";
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 = () => access(props);
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
- // If the tab list doesn't have focus and the selected key changes, or if there
230
- // isn't a focused key yet, move the focused key to the selected key so the
231
- // roving tabIndex stays on the selected tab. Predicate matches useTabListState.
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(