@proyecto-viviana/solid-stately 0.2.4 → 0.2.7

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 (82) hide show
  1. package/LICENSE +21 -0
  2. package/dist/autocomplete/createAutocompleteState.d.ts +2 -1
  3. package/dist/checkbox/createCheckboxGroupState.d.ts +10 -1
  4. package/dist/collections/types.d.ts +11 -0
  5. package/dist/color/getColorChannels.d.ts +20 -0
  6. package/dist/data/createAsyncList.d.ts +111 -0
  7. package/dist/data/createListData.d.ts +65 -0
  8. package/dist/data/createTreeData.d.ts +61 -0
  9. package/dist/data/index.d.ts +3 -0
  10. package/dist/datepicker/index.d.ts +10 -0
  11. package/dist/grid/types.d.ts +5 -1
  12. package/dist/index.d.ts +6 -1
  13. package/dist/index.js +3737 -2697
  14. package/dist/index.js.map +1 -7
  15. package/dist/menu/index.d.ts +8 -0
  16. package/dist/radio/createRadioGroupState.d.ts +10 -1
  17. package/dist/select/createSelectState.d.ts +17 -0
  18. package/dist/selection/index.d.ts +11 -0
  19. package/dist/toast/createToastState.d.ts +7 -1
  20. package/dist/toggle/createToggleGroupState.d.ts +45 -0
  21. package/dist/toggle/index.d.ts +1 -0
  22. package/dist/tree/TreeCollection.d.ts +3 -2
  23. package/package.json +6 -5
  24. package/src/autocomplete/createAutocompleteState.ts +10 -11
  25. package/src/calendar/createDateFieldState.ts +24 -1
  26. package/src/checkbox/createCheckboxGroupState.ts +42 -6
  27. package/src/collections/ListCollection.ts +152 -146
  28. package/src/collections/createListState.ts +266 -264
  29. package/src/collections/createMenuState.ts +106 -106
  30. package/src/collections/createSelectionState.ts +336 -336
  31. package/src/collections/index.ts +46 -46
  32. package/src/collections/types.ts +181 -169
  33. package/src/color/Color.ts +951 -951
  34. package/src/color/createColorAreaState.ts +293 -293
  35. package/src/color/createColorFieldState.ts +292 -292
  36. package/src/color/createColorSliderState.ts +241 -241
  37. package/src/color/createColorWheelState.ts +211 -211
  38. package/src/color/getColorChannels.ts +34 -0
  39. package/src/color/index.ts +47 -47
  40. package/src/color/types.ts +127 -127
  41. package/src/combobox/createComboBoxState.ts +703 -703
  42. package/src/combobox/index.ts +13 -13
  43. package/src/data/createAsyncList.ts +377 -0
  44. package/src/data/createListData.ts +298 -0
  45. package/src/data/createTreeData.ts +433 -0
  46. package/src/data/index.ts +25 -0
  47. package/src/datepicker/index.ts +36 -0
  48. package/src/disclosure/createDisclosureState.ts +4 -4
  49. package/src/dnd/createDragState.ts +153 -153
  50. package/src/dnd/createDraggableCollectionState.ts +165 -165
  51. package/src/dnd/createDropState.ts +212 -212
  52. package/src/dnd/createDroppableCollectionState.ts +357 -357
  53. package/src/dnd/index.ts +76 -76
  54. package/src/dnd/types.ts +317 -317
  55. package/src/form/createFormValidationState.ts +389 -389
  56. package/src/form/index.ts +15 -15
  57. package/src/grid/types.ts +5 -0
  58. package/src/index.ts +49 -0
  59. package/src/menu/index.ts +19 -0
  60. package/src/numberfield/createNumberFieldState.ts +427 -383
  61. package/src/numberfield/index.ts +5 -5
  62. package/src/overlays/createOverlayTriggerState.ts +67 -67
  63. package/src/overlays/index.ts +5 -5
  64. package/src/radio/createRadioGroupState.ts +44 -6
  65. package/src/searchfield/createSearchFieldState.ts +62 -62
  66. package/src/searchfield/index.ts +5 -5
  67. package/src/select/createSelectState.ts +290 -181
  68. package/src/select/index.ts +5 -5
  69. package/src/selection/index.ts +28 -0
  70. package/src/slider/createSliderState.ts +211 -211
  71. package/src/slider/index.ts +6 -6
  72. package/src/tabs/createTabListState.ts +37 -11
  73. package/src/toast/createToastState.d.ts +6 -1
  74. package/src/toast/createToastState.ts +8 -1
  75. package/src/toggle/createToggleGroupState.ts +127 -0
  76. package/src/toggle/index.ts +6 -0
  77. package/src/tooltip/createTooltipTriggerState.ts +183 -183
  78. package/src/tooltip/index.ts +6 -6
  79. package/src/tree/TreeCollection.ts +208 -175
  80. package/src/tree/createTreeState.ts +392 -392
  81. package/src/tree/index.ts +13 -13
  82. package/src/tree/types.ts +174 -174
@@ -1,392 +1,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 {
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 {
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
+ }