@proyecto-viviana/solid-stately 0.2.4 → 0.3.0

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 (243) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -283
  3. package/dist/autocomplete/createAutocompleteState.d.ts +3 -2
  4. package/dist/autocomplete/index.d.ts +1 -1
  5. package/dist/calendar/createCalendarState.d.ts +22 -7
  6. package/dist/calendar/createDateFieldState.d.ts +30 -7
  7. package/dist/calendar/createDatePickerState.d.ts +60 -0
  8. package/dist/calendar/createRangeCalendarState.d.ts +27 -8
  9. package/dist/calendar/createTimeFieldState.d.ts +34 -7
  10. package/dist/calendar/index.d.ts +7 -6
  11. package/dist/checkbox/createCheckboxGroupState.d.ts +12 -3
  12. package/dist/checkbox/index.d.ts +1 -1
  13. package/dist/collections/ListCollection.d.ts +1 -1
  14. package/dist/collections/createListState.d.ts +8 -8
  15. package/dist/collections/createMenuState.d.ts +7 -7
  16. package/dist/collections/createSelectionState.d.ts +5 -5
  17. package/dist/collections/index.d.ts +5 -5
  18. package/dist/collections/types.d.ts +23 -12
  19. package/dist/color/Color.d.ts +1 -1
  20. package/dist/color/createColorAreaState.d.ts +4 -2
  21. package/dist/color/createColorFieldState.d.ts +38 -12
  22. package/dist/color/createColorSliderState.d.ts +9 -2
  23. package/dist/color/createColorWheelState.d.ts +13 -2
  24. package/dist/color/getColorChannels.d.ts +20 -0
  25. package/dist/color/index.d.ts +6 -6
  26. package/dist/color/types.d.ts +4 -4
  27. package/dist/combobox/createComboBoxState.d.ts +27 -13
  28. package/dist/combobox/index.d.ts +1 -1
  29. package/dist/data/createAsyncList.d.ts +111 -0
  30. package/dist/data/createListData.d.ts +65 -0
  31. package/dist/data/createTreeData.d.ts +65 -0
  32. package/dist/data/index.d.ts +3 -0
  33. package/dist/datepicker/index.d.ts +8 -0
  34. package/dist/disclosure/createDisclosureState.d.ts +2 -2
  35. package/dist/disclosure/index.d.ts +1 -1
  36. package/dist/dnd/createDragState.d.ts +2 -2
  37. package/dist/dnd/createDraggableCollectionState.d.ts +2 -2
  38. package/dist/dnd/createDropState.d.ts +2 -2
  39. package/dist/dnd/createDroppableCollectionState.d.ts +3 -3
  40. package/dist/dnd/index.d.ts +5 -5
  41. package/dist/dnd/types.d.ts +16 -16
  42. package/dist/form/createFormValidationState.d.ts +3 -3
  43. package/dist/form/index.d.ts +1 -1
  44. package/dist/grid/createGridState.d.ts +2 -2
  45. package/dist/grid/index.d.ts +2 -2
  46. package/dist/grid/types.d.ts +17 -11
  47. package/dist/index.d.ts +31 -25
  48. package/dist/index.js +7990 -6264
  49. package/dist/index.js.map +1 -7
  50. package/dist/menu/index.d.ts +8 -0
  51. package/dist/numberfield/createNumberFieldState.d.ts +2 -2
  52. package/dist/numberfield/index.d.ts +1 -1
  53. package/dist/overlays/createOverlayTriggerState.d.ts +2 -2
  54. package/dist/overlays/index.d.ts +1 -1
  55. package/dist/radio/createRadioGroupState.d.ts +13 -4
  56. package/dist/radio/index.d.ts +1 -1
  57. package/dist/searchfield/createSearchFieldState.d.ts +2 -2
  58. package/dist/searchfield/index.d.ts +2 -2
  59. package/dist/select/createSelectState.d.ts +20 -3
  60. package/dist/select/index.d.ts +1 -1
  61. package/dist/selection/index.d.ts +11 -0
  62. package/dist/slider/createSliderState.d.ts +3 -3
  63. package/dist/slider/index.d.ts +2 -2
  64. package/dist/steplist/createStepListState.d.ts +48 -0
  65. package/dist/steplist/index.d.ts +1 -0
  66. package/dist/table/TableCollection.d.ts +3 -3
  67. package/dist/table/createTableColumnResizeState.d.ts +54 -0
  68. package/dist/table/createTableState.d.ts +2 -2
  69. package/dist/table/index.d.ts +5 -3
  70. package/dist/table/types.d.ts +10 -10
  71. package/dist/tabs/createTabListState.d.ts +5 -5
  72. package/dist/tabs/index.d.ts +1 -1
  73. package/dist/textfield/createTextFieldState.d.ts +2 -2
  74. package/dist/textfield/index.d.ts +1 -1
  75. package/dist/toast/createToastState.d.ts +15 -3
  76. package/dist/toast/index.d.ts +1 -1
  77. package/dist/toggle/createToggleGroupState.d.ts +45 -0
  78. package/dist/toggle/createToggleState.d.ts +2 -2
  79. package/dist/toggle/index.d.ts +2 -1
  80. package/dist/tooltip/createTooltipTriggerState.d.ts +2 -2
  81. package/dist/tooltip/index.d.ts +1 -1
  82. package/dist/tree/TreeCollection.d.ts +5 -4
  83. package/dist/tree/createTreeState.d.ts +2 -2
  84. package/dist/tree/index.d.ts +3 -3
  85. package/dist/tree/types.d.ts +19 -13
  86. package/dist/utils/index.d.ts +1 -1
  87. package/dist/utils/reactivity.d.ts +1 -1
  88. package/package.json +30 -30
  89. package/src/autocomplete/createAutocompleteState.d.ts +19 -17
  90. package/src/autocomplete/createAutocompleteState.ts +25 -32
  91. package/src/autocomplete/index.d.ts +6 -2
  92. package/src/autocomplete/index.ts +1 -1
  93. package/src/calendar/createCalendarState.d.ts +139 -117
  94. package/src/calendar/createCalendarState.ts +221 -74
  95. package/src/calendar/createDateFieldState.d.ts +130 -95
  96. package/src/calendar/createDateFieldState.ts +286 -151
  97. package/src/calendar/createDatePickerState.ts +347 -0
  98. package/src/calendar/createRangeCalendarState.d.ts +154 -129
  99. package/src/calendar/createRangeCalendarState.ts +293 -79
  100. package/src/calendar/createTimeFieldState.d.ts +81 -79
  101. package/src/calendar/createTimeFieldState.ts +187 -108
  102. package/src/calendar/index.d.ts +80 -7
  103. package/src/calendar/index.ts +21 -12
  104. package/src/checkbox/createCheckboxGroupState.d.ts +57 -55
  105. package/src/checkbox/createCheckboxGroupState.ts +57 -30
  106. package/src/checkbox/index.d.ts +6 -2
  107. package/src/checkbox/index.ts +1 -1
  108. package/src/collections/ListCollection.d.ts +25 -22
  109. package/src/collections/ListCollection.ts +152 -146
  110. package/src/collections/createListState.d.ts +73 -58
  111. package/src/collections/createListState.ts +267 -264
  112. package/src/collections/createMenuState.d.ts +36 -31
  113. package/src/collections/createMenuState.ts +112 -106
  114. package/src/collections/createSelectionState.d.ts +78 -65
  115. package/src/collections/createSelectionState.ts +340 -336
  116. package/src/collections/index.d.ts +38 -6
  117. package/src/collections/index.ts +41 -46
  118. package/src/collections/types.d.ts +101 -101
  119. package/src/collections/types.ts +181 -169
  120. package/src/color/Color.d.ts +20 -5
  121. package/src/color/Color.ts +1054 -951
  122. package/src/color/createColorAreaState.d.ts +67 -65
  123. package/src/color/createColorAreaState.ts +301 -293
  124. package/src/color/createColorFieldState.d.ts +46 -44
  125. package/src/color/createColorFieldState.ts +428 -292
  126. package/src/color/createColorSliderState.d.ts +58 -56
  127. package/src/color/createColorSliderState.ts +272 -241
  128. package/src/color/createColorWheelState.d.ts +42 -40
  129. package/src/color/createColorWheelState.ts +248 -211
  130. package/src/color/getColorChannels.ts +36 -0
  131. package/src/color/index.d.ts +36 -7
  132. package/src/color/index.ts +44 -47
  133. package/src/color/types.d.ts +68 -63
  134. package/src/color/types.ts +119 -127
  135. package/src/combobox/createComboBoxState.d.ts +107 -105
  136. package/src/combobox/createComboBoxState.ts +790 -703
  137. package/src/combobox/index.d.ts +10 -2
  138. package/src/combobox/index.ts +13 -13
  139. package/src/data/createAsyncList.ts +403 -0
  140. package/src/data/createListData.ts +298 -0
  141. package/src/data/createTreeData.ts +646 -0
  142. package/src/data/index.ts +25 -0
  143. package/src/datepicker/index.ts +32 -0
  144. package/src/disclosure/createDisclosureState.d.ts +47 -43
  145. package/src/disclosure/createDisclosureState.ts +9 -23
  146. package/src/disclosure/index.d.ts +10 -2
  147. package/src/disclosure/index.ts +1 -1
  148. package/src/dnd/createDragState.d.ts +51 -42
  149. package/src/dnd/createDragState.ts +151 -153
  150. package/src/dnd/createDraggableCollectionState.d.ts +52 -41
  151. package/src/dnd/createDraggableCollectionState.ts +158 -165
  152. package/src/dnd/createDropState.d.ts +64 -45
  153. package/src/dnd/createDropState.ts +202 -212
  154. package/src/dnd/createDroppableCollectionState.d.ts +88 -59
  155. package/src/dnd/createDroppableCollectionState.ts +354 -357
  156. package/src/dnd/index.d.ts +54 -6
  157. package/src/dnd/index.ts +63 -76
  158. package/src/dnd/types.d.ts +167 -156
  159. package/src/dnd/types.ts +316 -317
  160. package/src/form/createFormValidationState.d.ts +47 -45
  161. package/src/form/createFormValidationState.ts +356 -389
  162. package/src/form/index.d.ts +16 -2
  163. package/src/form/index.ts +15 -15
  164. package/src/grid/createGridState.d.ts +7 -4
  165. package/src/grid/createGridState.ts +44 -51
  166. package/src/grid/index.d.ts +3 -3
  167. package/src/grid/index.ts +2 -8
  168. package/src/grid/types.d.ts +136 -129
  169. package/src/grid/types.ts +24 -17
  170. package/src/index.d.ts +310 -26
  171. package/src/index.ts +87 -79
  172. package/src/menu/index.ts +17 -0
  173. package/src/numberfield/createNumberFieldState.d.ts +56 -54
  174. package/src/numberfield/createNumberFieldState.ts +422 -383
  175. package/src/numberfield/index.d.ts +6 -2
  176. package/src/numberfield/index.ts +5 -5
  177. package/src/overlays/createOverlayTriggerState.d.ts +22 -20
  178. package/src/overlays/createOverlayTriggerState.ts +66 -67
  179. package/src/overlays/index.d.ts +6 -2
  180. package/src/overlays/index.ts +5 -5
  181. package/src/radio/createRadioGroupState.d.ts +54 -52
  182. package/src/radio/createRadioGroupState.ts +66 -30
  183. package/src/radio/index.d.ts +7 -2
  184. package/src/radio/index.ts +1 -1
  185. package/src/searchfield/createSearchFieldState.d.ts +16 -14
  186. package/src/searchfield/createSearchFieldState.ts +56 -62
  187. package/src/searchfield/index.d.ts +3 -3
  188. package/src/searchfield/index.ts +2 -5
  189. package/src/select/createSelectState.d.ts +63 -61
  190. package/src/select/createSelectState.ts +290 -181
  191. package/src/select/index.d.ts +2 -2
  192. package/src/select/index.ts +1 -5
  193. package/src/selection/index.ts +27 -0
  194. package/src/slider/createSliderState.d.ts +60 -60
  195. package/src/slider/createSliderState.ts +233 -211
  196. package/src/slider/index.d.ts +3 -3
  197. package/src/slider/index.ts +2 -6
  198. package/src/ssr/index.d.ts +1 -1
  199. package/src/ssr/index.ts +2 -2
  200. package/src/steplist/createStepListState.ts +191 -0
  201. package/src/steplist/index.ts +5 -0
  202. package/src/table/TableCollection.d.ts +42 -40
  203. package/src/table/TableCollection.ts +61 -64
  204. package/src/table/createTableColumnResizeState.ts +242 -0
  205. package/src/table/createTableState.d.ts +7 -4
  206. package/src/table/createTableState.ts +10 -7
  207. package/src/table/index.d.ts +14 -4
  208. package/src/table/index.ts +10 -3
  209. package/src/table/types.d.ts +98 -92
  210. package/src/table/types.ts +17 -13
  211. package/src/tabs/createTabListState.d.ts +59 -57
  212. package/src/tabs/createTabListState.ts +58 -39
  213. package/src/tabs/index.d.ts +8 -2
  214. package/src/tabs/index.ts +1 -1
  215. package/src/textfield/createTextFieldState.d.ts +16 -14
  216. package/src/textfield/createTextFieldState.ts +7 -18
  217. package/src/textfield/index.d.ts +6 -2
  218. package/src/textfield/index.ts +1 -1
  219. package/src/toast/createToastState.d.ts +92 -81
  220. package/src/toast/createToastState.ts +56 -44
  221. package/src/toast/index.d.ts +12 -2
  222. package/src/toast/index.ts +1 -1
  223. package/src/toggle/createToggleGroupState.ts +117 -0
  224. package/src/toggle/createToggleState.d.ts +19 -19
  225. package/src/toggle/createToggleState.ts +2 -16
  226. package/src/toggle/index.d.ts +2 -2
  227. package/src/toggle/index.ts +6 -4
  228. package/src/tooltip/createTooltipTriggerState.d.ts +17 -17
  229. package/src/tooltip/createTooltipTriggerState.ts +184 -183
  230. package/src/tooltip/index.d.ts +7 -2
  231. package/src/tooltip/index.ts +6 -6
  232. package/src/tree/TreeCollection.d.ts +26 -23
  233. package/src/tree/TreeCollection.ts +215 -175
  234. package/src/tree/createTreeState.d.ts +7 -4
  235. package/src/tree/createTreeState.ts +390 -392
  236. package/src/tree/index.d.ts +10 -4
  237. package/src/tree/index.ts +13 -13
  238. package/src/tree/types.d.ts +131 -126
  239. package/src/tree/types.ts +183 -174
  240. package/src/utils/index.d.ts +2 -2
  241. package/src/utils/index.ts +1 -1
  242. package/src/utils/reactivity.d.ts +2 -2
  243. package/src/utils/reactivity.ts +3 -3
@@ -1,392 +1,390 @@
1
- /**
2
- * Tree state management for Tree components.
3
- * Based on @react-stately/tree/useTreeState.
4
- *
5
- * Manages expansion state, selection, and focus for hierarchical tree data.
6
- */
7
-
8
- import { createSignal, createEffect, createMemo, on, type Accessor } from 'solid-js';
9
- import type {
10
- TreeState,
11
- TreeStateOptions,
12
- TreeCollection,
13
- TreeNode,
14
- } from './types';
15
- import type { Key, FocusStrategy } from '../collections/types';
16
-
17
- /**
18
- * Creates state management for a tree component.
19
- * Handles expansion, selection, focus management, and keyboard navigation state.
20
- */
21
- export function createTreeState<T extends object, C extends TreeCollection<T> = TreeCollection<T>>(
22
- options: Accessor<TreeStateOptions<T, C>>
23
- ): TreeState<T, C> {
24
- const getOptions = () => options();
25
-
26
- // Disabled keys as a Set
27
- const disabledKeys = createMemo(() => {
28
- const keys = getOptions().disabledKeys;
29
- return keys ? new Set(keys) : new Set<Key>();
30
- });
31
-
32
- // Expansion state (uncontrolled)
33
- const [internalExpandedKeys, setInternalExpandedKeys] = createSignal<Set<Key>>(
34
- getInitialExpandedKeys(getOptions().defaultExpandedKeys)
35
- );
36
-
37
- // Computed expanded keys (controlled or uncontrolled)
38
- const expandedKeys = createMemo(() => {
39
- const opts = getOptions();
40
- if (opts.expandedKeys !== undefined) {
41
- return new Set(opts.expandedKeys);
42
- }
43
- return internalExpandedKeys();
44
- });
45
-
46
- // Collection - rebuilt when expanded keys change
47
- const collection = createMemo(() => {
48
- const opts = getOptions();
49
- return opts.collectionFactory(expandedKeys());
50
- });
51
-
52
- // Focus state
53
- const [isFocused, setIsFocused] = createSignal(false);
54
- const [focusedKey, setFocusedKeyInternal] = createSignal<Key | null>(null);
55
- const [childFocusStrategy, setChildFocusStrategy] = createSignal<FocusStrategy | null>(null);
56
- const [isKeyboardNavigationDisabled, setKeyboardNavigationDisabled] = createSignal(false);
57
-
58
- // Selection state
59
- const [internalSelectedKeys, setInternalSelectedKeys] = createSignal<'all' | Set<Key>>(
60
- getInitialSelection(getOptions().defaultSelectedKeys)
61
- );
62
- const [anchorKey, setAnchorKey] = createSignal<Key | null>(null);
63
-
64
- // Computed selection
65
- const selectedKeys = createMemo(() => {
66
- const opts = getOptions();
67
- if (opts.selectedKeys !== undefined) {
68
- return normalizeSelection(opts.selectedKeys);
69
- }
70
- return internalSelectedKeys();
71
- });
72
-
73
- const selectionMode = createMemo(() => getOptions().selectionMode ?? 'none');
74
- const selectionBehavior = createMemo(() => getOptions().selectionBehavior ?? 'toggle');
75
- const disallowEmptySelection = createMemo(() => getOptions().disallowEmptySelection ?? false);
76
- const disabledBehavior = createMemo(() => getOptions().disabledBehavior ?? 'all');
77
-
78
- // Set focused key
79
- const setFocusedKey = (key: Key | null, strategy: FocusStrategy = 'first') => {
80
- setFocusedKeyInternal(key);
81
- setChildFocusStrategy(strategy);
82
- };
83
-
84
- // Reset focused key if the item is removed from visible collection
85
- let cachedCollection: C | null = null;
86
-
87
- createEffect(
88
- on(collection, (coll) => {
89
- const currentFocusedKey = focusedKey();
90
-
91
- if (currentFocusedKey != null && cachedCollection) {
92
- // Check if the focused item is still visible
93
- const visibleKeys = [...coll.getKeys()];
94
- if (!visibleKeys.includes(currentFocusedKey)) {
95
- // The focused item was collapsed or removed, find a new one to focus
96
- // Try to find the parent and focus it
97
- const node = cachedCollection.getItem(currentFocusedKey);
98
- if (node?.parentKey != null) {
99
- const parent = coll.getItem(node.parentKey);
100
- if (parent && visibleKeys.includes(parent.key)) {
101
- setFocusedKeyInternal(parent.key);
102
- cachedCollection = coll;
103
- return;
104
- }
105
- }
106
-
107
- // Otherwise, find the nearest visible sibling or previous item
108
- const rows = coll.rows;
109
- if (rows.length > 0) {
110
- // Find the closest item to where the focused item was
111
- const cachedNode = cachedCollection.getItem(currentFocusedKey);
112
- if (cachedNode?.rowIndex !== undefined) {
113
- const newIndex = Math.min(cachedNode.rowIndex, rows.length - 1);
114
- const newNode = rows[newIndex];
115
- if (newNode && !disabledKeys().has(newNode.key)) {
116
- setFocusedKeyInternal(newNode.key);
117
- cachedCollection = coll;
118
- return;
119
- }
120
- }
121
- // Fall back to first non-disabled item
122
- for (const row of rows) {
123
- if (!disabledKeys().has(row.key)) {
124
- setFocusedKeyInternal(row.key);
125
- break;
126
- }
127
- }
128
- } else {
129
- setFocusedKeyInternal(null);
130
- }
131
- }
132
- }
133
-
134
- cachedCollection = coll;
135
- })
136
- );
137
-
138
- // Selection methods
139
- const isSelected = (key: Key): boolean => {
140
- const keys = selectedKeys();
141
- if (keys === 'all') return true;
142
- return keys.has(key);
143
- };
144
-
145
- const isDisabled = (key: Key): boolean => {
146
- return disabledKeys().has(key);
147
- };
148
-
149
- const isExpanded = (key: Key): boolean => {
150
- return expandedKeys().has(key);
151
- };
152
-
153
- const updateSelection = (newSelection: 'all' | Set<Key>) => {
154
- const opts = getOptions();
155
-
156
- // Controlled mode
157
- if (opts.selectedKeys !== undefined) {
158
- opts.onSelectionChange?.(newSelection);
159
- return;
160
- }
161
-
162
- // Uncontrolled mode
163
- const current = internalSelectedKeys();
164
- const isDifferent =
165
- current === 'all' ||
166
- newSelection === 'all' ||
167
- current.size !== (newSelection as Set<Key>).size ||
168
- ![...current].every((k) => (newSelection as Set<Key>).has(k));
169
-
170
- if (isDifferent) {
171
- setInternalSelectedKeys(newSelection);
172
- opts.onSelectionChange?.(newSelection);
173
- }
174
- };
175
-
176
- const toggleSelection = (key: Key) => {
177
- if (isDisabled(key) && disabledBehavior() === 'all') return;
178
- if (selectionMode() === 'none') return;
179
-
180
- const current = selectedKeys();
181
-
182
- if (selectionMode() === 'single') {
183
- if (isSelected(key) && !disallowEmptySelection()) {
184
- updateSelection(new Set());
185
- } else {
186
- updateSelection(new Set([key]));
187
- }
188
- return;
189
- }
190
-
191
- // Multiple selection
192
- if (current === 'all') {
193
- // Can't toggle when all selected without knowing all keys
194
- return;
195
- }
196
-
197
- const newSelection = new Set(current);
198
- if (newSelection.has(key)) {
199
- if (newSelection.size > 1 || !disallowEmptySelection()) {
200
- newSelection.delete(key);
201
- }
202
- } else {
203
- newSelection.add(key);
204
- }
205
-
206
- updateSelection(newSelection);
207
- setAnchorKey(key);
208
- };
209
-
210
- const replaceSelection = (key: Key) => {
211
- if (isDisabled(key) && disabledBehavior() === 'all') return;
212
- if (selectionMode() === 'none') return;
213
-
214
- updateSelection(new Set([key]));
215
- setAnchorKey(key);
216
- };
217
-
218
- const extendSelection = (toKey: Key) => {
219
- if (isDisabled(toKey) && disabledBehavior() === 'all') return;
220
- if (selectionMode() !== 'multiple') {
221
- replaceSelection(toKey);
222
- return;
223
- }
224
-
225
- const anchor = anchorKey();
226
- if (!anchor) {
227
- replaceSelection(toKey);
228
- return;
229
- }
230
-
231
- const coll = collection();
232
- const rows = coll.rows.filter((r) => r.type === 'item');
233
- const keys = rows.map((r) => r.key);
234
- const anchorIndex = keys.indexOf(anchor);
235
- const toIndex = keys.indexOf(toKey);
236
-
237
- if (anchorIndex === -1 || toIndex === -1) {
238
- replaceSelection(toKey);
239
- return;
240
- }
241
-
242
- const start = Math.min(anchorIndex, toIndex);
243
- const end = Math.max(anchorIndex, toIndex);
244
- const rangeKeys = keys.slice(start, end + 1).filter((k) => !isDisabled(k));
245
-
246
- updateSelection(new Set(rangeKeys));
247
- };
248
-
249
- const selectAll = () => {
250
- if (selectionMode() !== 'multiple') return;
251
- updateSelection('all');
252
- };
253
-
254
- const clearSelection = () => {
255
- if (disallowEmptySelection()) return;
256
- updateSelection(new Set());
257
- };
258
-
259
- const toggleSelectAll = () => {
260
- if (selectionMode() !== 'multiple') return;
261
-
262
- if (selectedKeys() === 'all') {
263
- clearSelection();
264
- } else {
265
- selectAll();
266
- }
267
- };
268
-
269
- // Expansion methods
270
- const updateExpandedKeys = (newKeys: Set<Key>) => {
271
- const opts = getOptions();
272
-
273
- // Controlled mode
274
- if (opts.expandedKeys !== undefined) {
275
- opts.onExpandedChange?.(newKeys);
276
- return;
277
- }
278
-
279
- // Uncontrolled mode
280
- setInternalExpandedKeys(newKeys);
281
- opts.onExpandedChange?.(newKeys);
282
- };
283
-
284
- const toggleKey = (key: Key) => {
285
- const node = collection().getItem(key);
286
- if (!node || !node.isExpandable) return;
287
- if (isDisabled(key) && disabledBehavior() === 'all') return;
288
-
289
- const current = expandedKeys();
290
- const newKeys = new Set(current);
291
-
292
- if (newKeys.has(key)) {
293
- newKeys.delete(key);
294
- } else {
295
- newKeys.add(key);
296
- }
297
-
298
- updateExpandedKeys(newKeys);
299
- };
300
-
301
- const expandKey = (key: Key) => {
302
- const node = collection().getItem(key);
303
- if (!node || !node.isExpandable) return;
304
- if (isDisabled(key) && disabledBehavior() === 'all') return;
305
-
306
- const current = expandedKeys();
307
- if (current.has(key)) return;
308
-
309
- const newKeys = new Set(current);
310
- newKeys.add(key);
311
- updateExpandedKeys(newKeys);
312
- };
313
-
314
- const collapseKey = (key: Key) => {
315
- const node = collection().getItem(key);
316
- if (!node || !node.isExpandable) return;
317
- if (isDisabled(key) && disabledBehavior() === 'all') return;
318
-
319
- const current = expandedKeys();
320
- if (!current.has(key)) return;
321
-
322
- const newKeys = new Set(current);
323
- newKeys.delete(key);
324
- updateExpandedKeys(newKeys);
325
- };
326
-
327
- const setExpandedKeys = (keys: Set<Key>) => {
328
- updateExpandedKeys(keys);
329
- };
330
-
331
- return {
332
- get collection() {
333
- return collection();
334
- },
335
- get disabledKeys() {
336
- return disabledKeys();
337
- },
338
- get expandedKeys() {
339
- return expandedKeys();
340
- },
341
- get isKeyboardNavigationDisabled() {
342
- return isKeyboardNavigationDisabled();
343
- },
344
- get focusedKey() {
345
- return focusedKey();
346
- },
347
- get childFocusStrategy() {
348
- return childFocusStrategy();
349
- },
350
- get isFocused() {
351
- return isFocused();
352
- },
353
- get selectionMode() {
354
- return selectionMode();
355
- },
356
- get selectedKeys() {
357
- return selectedKeys();
358
- },
359
- isSelected,
360
- isDisabled,
361
- isExpanded,
362
- setFocusedKey,
363
- setFocused: setIsFocused,
364
- toggleSelection,
365
- replaceSelection,
366
- extendSelection,
367
- selectAll,
368
- clearSelection,
369
- toggleSelectAll,
370
- toggleKey,
371
- expandKey,
372
- collapseKey,
373
- setExpandedKeys,
374
- setKeyboardNavigationDisabled,
375
- };
376
- }
377
-
378
- // Helper functions
379
- function getInitialSelection(defaultKeys?: 'all' | Iterable<Key>): 'all' | Set<Key> {
380
- if (defaultKeys === undefined) return new Set();
381
- return normalizeSelection(defaultKeys);
382
- }
383
-
384
- function normalizeSelection(keys: 'all' | Iterable<Key>): 'all' | Set<Key> {
385
- if (keys === 'all') return 'all';
386
- return new Set(keys);
387
- }
388
-
389
- function getInitialExpandedKeys(defaultKeys?: Iterable<Key>): Set<Key> {
390
- if (defaultKeys === undefined) return new Set();
391
- return new Set(defaultKeys);
392
- }
1
+ /**
2
+ * Tree state management for Tree components.
3
+ * Based on @react-stately/tree/useTreeState.
4
+ *
5
+ * Manages expansion state, selection, and focus for hierarchical tree data.
6
+ */
7
+
8
+ import { createSignal, createEffect, createMemo, on, type Accessor } from "solid-js";
9
+ import type { TreeState, TreeStateOptions, TreeCollection, TreeNode } from "./types";
10
+ import type { Key, FocusStrategy } from "../collections/types";
11
+
12
+ /**
13
+ * Creates state management for a tree component.
14
+ * Handles expansion, selection, focus management, and keyboard navigation state.
15
+ */
16
+ export function createTreeState<T extends object, C extends TreeCollection<T> = TreeCollection<T>>(
17
+ options: Accessor<TreeStateOptions<T, C>>,
18
+ ): TreeState<T, C> {
19
+ const getOptions = () => options();
20
+
21
+ // Disabled keys as a Set
22
+ const disabledKeys = createMemo(() => {
23
+ const keys = getOptions().disabledKeys;
24
+ return keys ? new Set(keys) : new Set<Key>();
25
+ });
26
+
27
+ // Expansion state (uncontrolled)
28
+ const [internalExpandedKeys, setInternalExpandedKeys] = createSignal<Set<Key>>(
29
+ getInitialExpandedKeys(getOptions().defaultExpandedKeys),
30
+ );
31
+
32
+ // Computed expanded keys (controlled or uncontrolled)
33
+ const expandedKeys = createMemo(() => {
34
+ const opts = getOptions();
35
+ if (opts.expandedKeys !== undefined) {
36
+ return new Set(opts.expandedKeys);
37
+ }
38
+ return internalExpandedKeys();
39
+ });
40
+
41
+ // Collection - rebuilt when expanded keys change
42
+ const collection = createMemo(() => {
43
+ const opts = getOptions();
44
+ return opts.collectionFactory(expandedKeys());
45
+ });
46
+
47
+ // Focus state
48
+ const [isFocused, setIsFocused] = createSignal(false);
49
+ const [focusedKey, setFocusedKeyInternal] = createSignal<Key | null>(null);
50
+ const [childFocusStrategy, setChildFocusStrategy] = createSignal<FocusStrategy | null>(null);
51
+ const [isKeyboardNavigationDisabled, setKeyboardNavigationDisabled] = createSignal(false);
52
+
53
+ // Selection state
54
+ const [internalSelectedKeys, setInternalSelectedKeys] = createSignal<"all" | Set<Key>>(
55
+ getInitialSelection(getOptions().defaultSelectedKeys),
56
+ );
57
+ const [anchorKey, setAnchorKey] = createSignal<Key | null>(null);
58
+
59
+ // Computed selection
60
+ const selectedKeys = createMemo(() => {
61
+ const opts = getOptions();
62
+ if (opts.selectedKeys !== undefined) {
63
+ return normalizeSelection(opts.selectedKeys);
64
+ }
65
+ return internalSelectedKeys();
66
+ });
67
+
68
+ const selectionMode = createMemo(() => getOptions().selectionMode ?? "none");
69
+ const selectionBehavior = createMemo(() => getOptions().selectionBehavior ?? "toggle");
70
+ const disallowEmptySelection = createMemo(() => getOptions().disallowEmptySelection ?? false);
71
+ const disabledBehavior = createMemo(() => getOptions().disabledBehavior ?? "all");
72
+
73
+ // Set focused key
74
+ const setFocusedKey = (key: Key | null, strategy: FocusStrategy = "first") => {
75
+ setFocusedKeyInternal(key);
76
+ setChildFocusStrategy(strategy);
77
+ };
78
+
79
+ // Reset focused key if the item is removed from visible collection
80
+ let cachedCollection: C | null = null;
81
+
82
+ createEffect(
83
+ on(collection, (coll) => {
84
+ const currentFocusedKey = focusedKey();
85
+
86
+ if (currentFocusedKey != null && cachedCollection) {
87
+ // Check if the focused item is still visible
88
+ const visibleKeys = [...coll.getKeys()];
89
+ if (!visibleKeys.includes(currentFocusedKey)) {
90
+ // The focused item was collapsed or removed, find a new one to focus
91
+ // Try to find the parent and focus it
92
+ const node = cachedCollection.getItem(currentFocusedKey);
93
+ if (node?.parentKey != null) {
94
+ const parent = coll.getItem(node.parentKey);
95
+ if (parent && visibleKeys.includes(parent.key)) {
96
+ setFocusedKeyInternal(parent.key);
97
+ cachedCollection = coll;
98
+ return;
99
+ }
100
+ }
101
+
102
+ // Otherwise, find the nearest visible sibling or previous item
103
+ const rows = coll.rows;
104
+ if (rows.length > 0) {
105
+ // Find the closest item to where the focused item was
106
+ const cachedNode = cachedCollection.getItem(currentFocusedKey);
107
+ if (cachedNode?.rowIndex !== undefined) {
108
+ const newIndex = Math.min(cachedNode.rowIndex, rows.length - 1);
109
+ const newNode = rows[newIndex];
110
+ if (newNode && !disabledKeys().has(newNode.key)) {
111
+ setFocusedKeyInternal(newNode.key);
112
+ cachedCollection = coll;
113
+ return;
114
+ }
115
+ }
116
+ // Fall back to first non-disabled item
117
+ for (const row of rows) {
118
+ if (!disabledKeys().has(row.key)) {
119
+ setFocusedKeyInternal(row.key);
120
+ break;
121
+ }
122
+ }
123
+ } else {
124
+ setFocusedKeyInternal(null);
125
+ }
126
+ }
127
+ }
128
+
129
+ cachedCollection = coll;
130
+ }),
131
+ );
132
+
133
+ // Selection methods
134
+ const isSelected = (key: Key): boolean => {
135
+ const keys = selectedKeys();
136
+ if (keys === "all") return true;
137
+ return keys.has(key);
138
+ };
139
+
140
+ const isDisabled = (key: Key): boolean => {
141
+ return disabledKeys().has(key) || collection().getItem(key)?.isDisabled === true;
142
+ };
143
+
144
+ const isExpanded = (key: Key): boolean => {
145
+ return expandedKeys().has(key);
146
+ };
147
+
148
+ const updateSelection = (newSelection: "all" | Set<Key>) => {
149
+ const opts = getOptions();
150
+
151
+ // Controlled mode
152
+ if (opts.selectedKeys !== undefined) {
153
+ opts.onSelectionChange?.(newSelection);
154
+ return;
155
+ }
156
+
157
+ // Uncontrolled mode
158
+ const current = internalSelectedKeys();
159
+ const isDifferent =
160
+ current === "all" ||
161
+ newSelection === "all" ||
162
+ current.size !== (newSelection as Set<Key>).size ||
163
+ ![...current].every((k) => (newSelection as Set<Key>).has(k));
164
+
165
+ if (isDifferent) {
166
+ setInternalSelectedKeys(newSelection);
167
+ opts.onSelectionChange?.(newSelection);
168
+ }
169
+ };
170
+
171
+ const toggleSelection = (key: Key) => {
172
+ if (isDisabled(key) && disabledBehavior() === "all") return;
173
+ if (selectionMode() === "none") return;
174
+
175
+ const current = selectedKeys();
176
+
177
+ if (selectionMode() === "single") {
178
+ if (isSelected(key) && !disallowEmptySelection()) {
179
+ updateSelection(new Set());
180
+ } else {
181
+ updateSelection(new Set([key]));
182
+ }
183
+ return;
184
+ }
185
+
186
+ // Multiple selection
187
+ if (current === "all") {
188
+ // Can't toggle when all selected without knowing all keys
189
+ return;
190
+ }
191
+
192
+ const newSelection = new Set(current);
193
+ if (newSelection.has(key)) {
194
+ if (newSelection.size > 1 || !disallowEmptySelection()) {
195
+ newSelection.delete(key);
196
+ }
197
+ } else {
198
+ newSelection.add(key);
199
+ }
200
+
201
+ updateSelection(newSelection);
202
+ setAnchorKey(key);
203
+ };
204
+
205
+ const replaceSelection = (key: Key) => {
206
+ if (isDisabled(key) && disabledBehavior() === "all") return;
207
+ if (selectionMode() === "none") return;
208
+
209
+ updateSelection(new Set([key]));
210
+ setAnchorKey(key);
211
+ };
212
+
213
+ const extendSelection = (toKey: Key) => {
214
+ if (isDisabled(toKey) && disabledBehavior() === "all") return;
215
+ if (selectionMode() !== "multiple") {
216
+ replaceSelection(toKey);
217
+ return;
218
+ }
219
+
220
+ const anchor = anchorKey();
221
+ if (!anchor) {
222
+ replaceSelection(toKey);
223
+ return;
224
+ }
225
+
226
+ const coll = collection();
227
+ const rows = coll.rows.filter((r) => r.type === "item");
228
+ const keys = rows.map((r) => r.key);
229
+ const anchorIndex = keys.indexOf(anchor);
230
+ const toIndex = keys.indexOf(toKey);
231
+
232
+ if (anchorIndex === -1 || toIndex === -1) {
233
+ replaceSelection(toKey);
234
+ return;
235
+ }
236
+
237
+ const start = Math.min(anchorIndex, toIndex);
238
+ const end = Math.max(anchorIndex, toIndex);
239
+ const rangeKeys = keys.slice(start, end + 1).filter((k) => !isDisabled(k));
240
+
241
+ updateSelection(new Set(rangeKeys));
242
+ };
243
+
244
+ const selectAll = () => {
245
+ if (selectionMode() !== "multiple") return;
246
+ updateSelection("all");
247
+ };
248
+
249
+ const clearSelection = () => {
250
+ if (disallowEmptySelection()) return;
251
+ updateSelection(new Set());
252
+ };
253
+
254
+ const toggleSelectAll = () => {
255
+ if (selectionMode() !== "multiple") return;
256
+
257
+ if (selectedKeys() === "all") {
258
+ clearSelection();
259
+ } else {
260
+ selectAll();
261
+ }
262
+ };
263
+
264
+ // Expansion methods
265
+ const updateExpandedKeys = (newKeys: Set<Key>) => {
266
+ const opts = getOptions();
267
+
268
+ // Controlled mode
269
+ if (opts.expandedKeys !== undefined) {
270
+ opts.onExpandedChange?.(newKeys);
271
+ return;
272
+ }
273
+
274
+ // Uncontrolled mode
275
+ setInternalExpandedKeys(newKeys);
276
+ opts.onExpandedChange?.(newKeys);
277
+ };
278
+
279
+ const toggleKey = (key: Key) => {
280
+ const node = collection().getItem(key);
281
+ if (!node || !node.isExpandable) return;
282
+ if (isDisabled(key) && disabledBehavior() === "all") return;
283
+
284
+ const current = expandedKeys();
285
+ const newKeys = new Set(current);
286
+
287
+ if (newKeys.has(key)) {
288
+ newKeys.delete(key);
289
+ } else {
290
+ newKeys.add(key);
291
+ }
292
+
293
+ updateExpandedKeys(newKeys);
294
+ };
295
+
296
+ const expandKey = (key: Key) => {
297
+ const node = collection().getItem(key);
298
+ if (!node || !node.isExpandable) return;
299
+ if (isDisabled(key) && disabledBehavior() === "all") return;
300
+
301
+ const current = expandedKeys();
302
+ if (current.has(key)) return;
303
+
304
+ const newKeys = new Set(current);
305
+ newKeys.add(key);
306
+ updateExpandedKeys(newKeys);
307
+ };
308
+
309
+ const collapseKey = (key: Key) => {
310
+ const node = collection().getItem(key);
311
+ if (!node || !node.isExpandable) return;
312
+ if (isDisabled(key) && disabledBehavior() === "all") return;
313
+
314
+ const current = expandedKeys();
315
+ if (!current.has(key)) return;
316
+
317
+ const newKeys = new Set(current);
318
+ newKeys.delete(key);
319
+ updateExpandedKeys(newKeys);
320
+ };
321
+
322
+ const setExpandedKeys = (keys: Set<Key>) => {
323
+ updateExpandedKeys(keys);
324
+ };
325
+
326
+ return {
327
+ get collection() {
328
+ return collection();
329
+ },
330
+ get disabledKeys() {
331
+ return disabledKeys();
332
+ },
333
+ get expandedKeys() {
334
+ return expandedKeys();
335
+ },
336
+ get isKeyboardNavigationDisabled() {
337
+ return isKeyboardNavigationDisabled();
338
+ },
339
+ get focusedKey() {
340
+ return focusedKey();
341
+ },
342
+ get childFocusStrategy() {
343
+ return childFocusStrategy();
344
+ },
345
+ get isFocused() {
346
+ return isFocused();
347
+ },
348
+ get selectionMode() {
349
+ return selectionMode();
350
+ },
351
+ get selectionBehavior() {
352
+ return selectionBehavior();
353
+ },
354
+ get selectedKeys() {
355
+ return selectedKeys();
356
+ },
357
+ isSelected,
358
+ isDisabled,
359
+ isExpanded,
360
+ setFocusedKey,
361
+ setFocused: setIsFocused,
362
+ toggleSelection,
363
+ replaceSelection,
364
+ extendSelection,
365
+ selectAll,
366
+ clearSelection,
367
+ toggleSelectAll,
368
+ toggleKey,
369
+ expandKey,
370
+ collapseKey,
371
+ setExpandedKeys,
372
+ setKeyboardNavigationDisabled,
373
+ };
374
+ }
375
+
376
+ // Helper functions
377
+ function getInitialSelection(defaultKeys?: "all" | Iterable<Key>): "all" | Set<Key> {
378
+ if (defaultKeys === undefined) return new Set();
379
+ return normalizeSelection(defaultKeys);
380
+ }
381
+
382
+ function normalizeSelection(keys: "all" | Iterable<Key>): "all" | Set<Key> {
383
+ if (keys === "all") return "all";
384
+ return new Set(keys);
385
+ }
386
+
387
+ function getInitialExpandedKeys(defaultKeys?: Iterable<Key>): Set<Key> {
388
+ if (defaultKeys === undefined) return new Set();
389
+ return new Set(defaultKeys);
390
+ }