@proyecto-viviana/solidaria-components 0.2.5 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/ActionBar.d.ts +71 -0
- package/dist/ActionBar.d.ts.map +1 -0
- package/dist/ActionGroup.d.ts +74 -0
- package/dist/ActionGroup.d.ts.map +1 -0
- package/dist/Alert.d.ts +70 -0
- package/dist/Alert.d.ts.map +1 -0
- package/dist/Breadcrumbs.d.ts +10 -2
- package/dist/Breadcrumbs.d.ts.map +1 -1
- package/dist/Button.d.ts +4 -0
- package/dist/Button.d.ts.map +1 -1
- package/dist/Calendar.d.ts +13 -0
- package/dist/Calendar.d.ts.map +1 -1
- package/dist/Checkbox.d.ts +2 -2
- package/dist/Checkbox.d.ts.map +1 -1
- package/dist/Collection.d.ts +125 -0
- package/dist/Collection.d.ts.map +1 -0
- package/dist/Color.d.ts +114 -2
- package/dist/Color.d.ts.map +1 -1
- package/dist/ColorEditor.d.ts +42 -0
- package/dist/ColorEditor.d.ts.map +1 -0
- package/dist/ComboBox.d.ts +64 -0
- package/dist/ComboBox.d.ts.map +1 -1
- package/dist/ContextualHelpTrigger.d.ts +40 -0
- package/dist/ContextualHelpTrigger.d.ts.map +1 -0
- package/dist/DateField.d.ts +27 -2
- package/dist/DateField.d.ts.map +1 -1
- package/dist/DatePicker.d.ts +67 -2
- package/dist/DatePicker.d.ts.map +1 -1
- package/dist/Dialog.d.ts.map +1 -1
- package/dist/Disclosure.d.ts +2 -0
- package/dist/Disclosure.d.ts.map +1 -1
- package/dist/DragAndDrop.d.ts +80 -0
- package/dist/DragAndDrop.d.ts.map +1 -0
- package/dist/DragPreview.d.ts +14 -0
- package/dist/DragPreview.d.ts.map +1 -0
- package/dist/DropZone.d.ts +27 -0
- package/dist/DropZone.d.ts.map +1 -0
- package/dist/FieldError.d.ts +23 -0
- package/dist/FieldError.d.ts.map +1 -0
- package/dist/FileTrigger.d.ts +26 -0
- package/dist/FileTrigger.d.ts.map +1 -0
- package/dist/Focusable.d.ts +27 -0
- package/dist/Focusable.d.ts.map +1 -0
- package/dist/Form.d.ts +27 -0
- package/dist/Form.d.ts.map +1 -0
- package/dist/GridList.d.ts +40 -1
- package/dist/GridList.d.ts.map +1 -1
- package/dist/Icon.d.ts +57 -0
- package/dist/Icon.d.ts.map +1 -0
- package/dist/Keyboard.d.ts +13 -0
- package/dist/Keyboard.d.ts.map +1 -0
- package/dist/Link.d.ts.map +1 -1
- package/dist/ListBox.d.ts +43 -1
- package/dist/ListBox.d.ts.map +1 -1
- package/dist/ListDropTargetDelegate.d.ts +38 -0
- package/dist/ListDropTargetDelegate.d.ts.map +1 -0
- package/dist/Menu.d.ts +20 -2
- package/dist/Menu.d.ts.map +1 -1
- package/dist/Meter.d.ts +2 -2
- package/dist/Meter.d.ts.map +1 -1
- package/dist/Modal.d.ts +2 -0
- package/dist/Modal.d.ts.map +1 -1
- package/dist/NumberField.d.ts +2 -0
- package/dist/NumberField.d.ts.map +1 -1
- package/dist/Popover.d.ts +4 -2
- package/dist/Popover.d.ts.map +1 -1
- package/dist/Pressable.d.ts +27 -0
- package/dist/Pressable.d.ts.map +1 -0
- package/dist/ProgressBar.d.ts +2 -2
- package/dist/ProgressBar.d.ts.map +1 -1
- package/dist/RadioGroup.d.ts.map +1 -1
- package/dist/RangeCalendar.d.ts +5 -0
- package/dist/RangeCalendar.d.ts.map +1 -1
- package/dist/RouterProvider.d.ts +75 -0
- package/dist/RouterProvider.d.ts.map +1 -0
- package/dist/SearchField.d.ts +2 -3
- package/dist/SearchField.d.ts.map +1 -1
- package/dist/Select.d.ts +11 -0
- package/dist/Select.d.ts.map +1 -1
- package/dist/SelectionIndicator.d.ts +30 -0
- package/dist/SelectionIndicator.d.ts.map +1 -0
- package/dist/SharedElementTransition.d.ts +39 -0
- package/dist/SharedElementTransition.d.ts.map +1 -0
- package/dist/Slider.d.ts +6 -3
- package/dist/Slider.d.ts.map +1 -1
- package/dist/Table.d.ts +39 -0
- package/dist/Table.d.ts.map +1 -1
- package/dist/Tabs.d.ts +4 -3
- package/dist/Tabs.d.ts.map +1 -1
- package/dist/TagGroup.d.ts +12 -2
- package/dist/TagGroup.d.ts.map +1 -1
- package/dist/Text.d.ts +10 -0
- package/dist/Text.d.ts.map +1 -0
- package/dist/TextField.d.ts +4 -0
- package/dist/TextField.d.ts.map +1 -1
- package/dist/TimeField.d.ts +26 -1
- package/dist/TimeField.d.ts.map +1 -1
- package/dist/Toast.d.ts.map +1 -1
- package/dist/ToggleButton.d.ts +30 -0
- package/dist/ToggleButton.d.ts.map +1 -0
- package/dist/ToggleButtonGroup.d.ts +33 -0
- package/dist/ToggleButtonGroup.d.ts.map +1 -0
- package/dist/Toolbar.d.ts.map +1 -1
- package/dist/Tooltip.d.ts +9 -0
- package/dist/Tooltip.d.ts.map +1 -1
- package/dist/Tree.d.ts +44 -2
- package/dist/Tree.d.ts.map +1 -1
- package/dist/Virtualizer.d.ts +61 -0
- package/dist/Virtualizer.d.ts.map +1 -0
- package/dist/VirtualizerLayouts.d.ts +82 -0
- package/dist/VirtualizerLayouts.d.ts.map +1 -0
- package/dist/VisuallyHidden.d.ts +3 -1
- package/dist/VisuallyHidden.d.ts.map +1 -1
- package/dist/contexts.d.ts +1 -0
- package/dist/contexts.d.ts.map +1 -1
- package/dist/index.d.ts +57 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13961 -5946
- package/dist/index.js.map +1 -7
- package/dist/index.ssr.js +9612 -2401
- package/dist/index.ssr.js.map +1 -7
- package/dist/useDragAndDrop.d.ts +93 -0
- package/dist/useDragAndDrop.d.ts.map +1 -0
- package/dist/utils.d.ts +7 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/virtualizer/Layout.d.ts +79 -0
- package/dist/virtualizer/Layout.d.ts.map +1 -0
- package/package.json +8 -6
- package/src/ActionBar.tsx +248 -0
- package/src/ActionGroup.tsx +285 -0
- package/src/Alert.tsx +177 -0
- package/src/Autocomplete.tsx +1 -1
- package/src/Breadcrumbs.tsx +103 -17
- package/src/Button.tsx +65 -21
- package/src/Calendar.tsx +179 -53
- package/src/Checkbox.tsx +1 -2
- package/src/Collection.tsx +341 -0
- package/src/Color.tsx +652 -34
- package/src/ColorEditor.tsx +231 -0
- package/src/ComboBox.tsx +315 -81
- package/src/ContextualHelpTrigger.tsx +183 -0
- package/src/DateField.tsx +93 -19
- package/src/DatePicker.tsx +495 -25
- package/src/Dialog.tsx +40 -9
- package/src/Disclosure.tsx +33 -27
- package/src/DragAndDrop.tsx +334 -0
- package/src/DragPreview.tsx +45 -0
- package/src/DropZone.tsx +213 -0
- package/src/FieldError.tsx +67 -0
- package/src/FileTrigger.tsx +83 -0
- package/src/Focusable.tsx +106 -0
- package/src/Form.tsx +85 -0
- package/src/GridList.tsx +379 -41
- package/src/Icon.tsx +154 -0
- package/src/Keyboard.tsx +26 -0
- package/src/Link.tsx +14 -1
- package/src/ListBox.tsx +484 -33
- package/src/ListDropTargetDelegate.ts +282 -0
- package/src/Menu.tsx +388 -35
- package/src/Meter.tsx +7 -3
- package/src/Modal.tsx +32 -4
- package/src/NumberField.tsx +163 -43
- package/src/Popover.tsx +136 -180
- package/src/Pressable.tsx +108 -0
- package/src/ProgressBar.tsx +7 -3
- package/src/RadioGroup.tsx +35 -25
- package/src/RangeCalendar.tsx +100 -68
- package/src/RouterProvider.tsx +240 -0
- package/src/SearchField.tsx +142 -34
- package/src/Select.tsx +221 -73
- package/src/SelectionIndicator.tsx +105 -0
- package/src/SharedElementTransition.tsx +258 -0
- package/src/Slider.tsx +16 -6
- package/src/Table.tsx +417 -57
- package/src/Tabs.tsx +68 -35
- package/src/TagGroup.tsx +121 -36
- package/src/Text.tsx +18 -0
- package/src/TextField.tsx +25 -8
- package/src/TimeField.tsx +101 -151
- package/src/Toast.tsx +108 -14
- package/src/ToggleButton.tsx +159 -0
- package/src/ToggleButtonGroup.tsx +136 -0
- package/src/Toolbar.tsx +14 -8
- package/src/Tooltip.tsx +108 -19
- package/src/Tree.tsx +1143 -87
- package/src/Virtualizer.tsx +702 -0
- package/src/VirtualizerLayouts.ts +265 -0
- package/src/VisuallyHidden.tsx +15 -21
- package/src/contexts.ts +1 -0
- package/src/index.ts +1057 -620
- package/src/useDragAndDrop.ts +351 -0
- package/src/utils.tsx +37 -3
- package/src/virtualizer/Layout.ts +200 -0
package/src/Tree.tsx
CHANGED
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
12
|
type JSX,
|
|
13
|
+
onCleanup,
|
|
13
14
|
createContext,
|
|
15
|
+
createEffect,
|
|
14
16
|
createMemo,
|
|
15
17
|
createSignal,
|
|
16
18
|
splitProps,
|
|
@@ -24,6 +26,7 @@ import {
|
|
|
24
26
|
createTreeSelectionCheckbox,
|
|
25
27
|
createFocusRing,
|
|
26
28
|
createHover,
|
|
29
|
+
mergeProps,
|
|
27
30
|
type AriaTreeProps,
|
|
28
31
|
} from '@proyecto-viviana/solidaria';
|
|
29
32
|
import {
|
|
@@ -34,6 +37,8 @@ import {
|
|
|
34
37
|
type TreeNode,
|
|
35
38
|
type TreeItemData,
|
|
36
39
|
type Key,
|
|
40
|
+
type DropTarget,
|
|
41
|
+
type ItemDropTarget,
|
|
37
42
|
} from '@proyecto-viviana/solid-stately';
|
|
38
43
|
import {
|
|
39
44
|
type RenderChildren,
|
|
@@ -43,6 +48,27 @@ import {
|
|
|
43
48
|
useRenderProps,
|
|
44
49
|
filterDOMProps,
|
|
45
50
|
} from './utils';
|
|
51
|
+
import { SharedElementTransition } from './SharedElementTransition';
|
|
52
|
+
import { type DragAndDropHooks } from './useDragAndDrop';
|
|
53
|
+
import {
|
|
54
|
+
getNormalizedDropTargetKey,
|
|
55
|
+
mergePersistedKeysIntoVirtualRange,
|
|
56
|
+
useDndPersistedKeys,
|
|
57
|
+
useRenderDropIndicator,
|
|
58
|
+
} from './DragAndDrop';
|
|
59
|
+
import {
|
|
60
|
+
CollectionRendererContext,
|
|
61
|
+
flattenCollectionEntries,
|
|
62
|
+
isCollectionSection,
|
|
63
|
+
Section,
|
|
64
|
+
Header,
|
|
65
|
+
type CollectionEntry,
|
|
66
|
+
type CollectionRendererContextValue,
|
|
67
|
+
type SectionProps,
|
|
68
|
+
type HeaderProps,
|
|
69
|
+
useCollectionRenderer,
|
|
70
|
+
} from './Collection';
|
|
71
|
+
import { useVirtualizerContext } from './Virtualizer';
|
|
46
72
|
|
|
47
73
|
// ============================================
|
|
48
74
|
// TYPES
|
|
@@ -61,9 +87,13 @@ export interface TreeRenderProps {
|
|
|
61
87
|
|
|
62
88
|
export interface TreeProps<T extends object> extends Omit<AriaTreeProps, 'children'>, SlotProps {
|
|
63
89
|
/** The hierarchical items to render in the tree. */
|
|
64
|
-
items: TreeItemData<T
|
|
90
|
+
items: CollectionEntry<TreeItemData<T>>[];
|
|
65
91
|
/** The selection mode. */
|
|
66
92
|
selectionMode?: 'none' | 'single' | 'multiple';
|
|
93
|
+
/** The selection behavior (toggle vs replace). */
|
|
94
|
+
selectionBehavior?: 'toggle' | 'replace';
|
|
95
|
+
/** Whether disabled items can still receive focus. */
|
|
96
|
+
disabledBehavior?: 'selection' | 'all';
|
|
67
97
|
/** Keys of disabled items. */
|
|
68
98
|
disabledKeys?: Iterable<Key>;
|
|
69
99
|
/** Currently selected keys (controlled). */
|
|
@@ -86,6 +116,14 @@ export interface TreeProps<T extends object> extends Omit<AriaTreeProps, 'childr
|
|
|
86
116
|
style?: StyleOrFunction<TreeRenderProps>;
|
|
87
117
|
/** A function to render when the tree is empty. */
|
|
88
118
|
renderEmptyState?: () => JSX.Element;
|
|
119
|
+
/** Whether there are more items to load. */
|
|
120
|
+
hasMore?: boolean;
|
|
121
|
+
/** Whether additional items are currently loading. */
|
|
122
|
+
isLoading?: boolean;
|
|
123
|
+
/** Called when the load more sentinel becomes visible. */
|
|
124
|
+
onLoadMore?: () => void | Promise<void>;
|
|
125
|
+
/** Drag and drop hooks from `useDragAndDrop`. */
|
|
126
|
+
dragAndDropHooks?: DragAndDropHooks<T>;
|
|
89
127
|
}
|
|
90
128
|
|
|
91
129
|
export interface TreeRenderItemState {
|
|
@@ -118,7 +156,7 @@ export interface TreeItemRenderProps {
|
|
|
118
156
|
level: number;
|
|
119
157
|
}
|
|
120
158
|
|
|
121
|
-
export interface TreeItemProps<T extends object> extends SlotProps {
|
|
159
|
+
export interface TreeItemProps<T extends object> extends SlotProps, Omit<JSX.HTMLAttributes<HTMLDivElement>, 'class' | 'style' | 'children' | 'id'> {
|
|
122
160
|
/** The unique key for the item. */
|
|
123
161
|
id: Key;
|
|
124
162
|
/** The item value. */
|
|
@@ -144,6 +182,17 @@ export interface TreeExpandButtonProps {
|
|
|
144
182
|
children?: JSX.Element | ((props: { isExpanded: boolean }) => JSX.Element);
|
|
145
183
|
}
|
|
146
184
|
|
|
185
|
+
export interface TreeLoadMoreItemProps extends SlotProps {
|
|
186
|
+
onLoadMore: () => void | Promise<void>;
|
|
187
|
+
isLoading?: boolean;
|
|
188
|
+
children?: JSX.Element;
|
|
189
|
+
class?: ClassNameOrFunction<{ isLoading: boolean }>;
|
|
190
|
+
style?: StyleOrFunction<{ isLoading: boolean }>;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface TreeSectionProps extends SectionProps {}
|
|
194
|
+
export interface TreeHeaderProps extends HeaderProps {}
|
|
195
|
+
|
|
147
196
|
// ============================================
|
|
148
197
|
// CONTEXT
|
|
149
198
|
// ============================================
|
|
@@ -153,6 +202,500 @@ interface TreeContextValue<T extends object> {
|
|
|
153
202
|
collection: TreeCollection<T>;
|
|
154
203
|
isDisabled: boolean;
|
|
155
204
|
renderItem: (item: TreeItemData<T>, state: TreeRenderItemState) => JSX.Element;
|
|
205
|
+
dragAndDropHooks?: DragAndDropHooks<T>;
|
|
206
|
+
dragState?: unknown;
|
|
207
|
+
dropState?: unknown;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
interface TreeDropTargetDelegate {
|
|
211
|
+
getDropTargetFromPoint: (
|
|
212
|
+
x: number,
|
|
213
|
+
y: number,
|
|
214
|
+
isValidDropTarget: (target: DropTarget) => boolean
|
|
215
|
+
) => DropTarget | null;
|
|
216
|
+
getKeyboardNavigationTarget?: (
|
|
217
|
+
target: DropTarget | null,
|
|
218
|
+
direction: 'next' | 'previous',
|
|
219
|
+
isValidDropTarget: (target: DropTarget) => boolean
|
|
220
|
+
) => DropTarget | null;
|
|
221
|
+
getKeyboardPageNavigationTarget?: (
|
|
222
|
+
target: DropTarget | null,
|
|
223
|
+
direction: 'next' | 'previous',
|
|
224
|
+
isValidDropTarget: (target: DropTarget) => boolean
|
|
225
|
+
) => DropTarget | null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
interface PointerTrackingState {
|
|
229
|
+
lastY: number;
|
|
230
|
+
lastX: number;
|
|
231
|
+
yDirection: 'up' | 'down' | null;
|
|
232
|
+
xDirection: 'left' | 'right' | null;
|
|
233
|
+
boundaryContext: {
|
|
234
|
+
parentKey: Key;
|
|
235
|
+
lastSwitchY: number;
|
|
236
|
+
lastSwitchX: number;
|
|
237
|
+
preferredTargetIndex?: number;
|
|
238
|
+
} | null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const X_SWITCH_THRESHOLD = 10;
|
|
242
|
+
const Y_SWITCH_THRESHOLD = 5;
|
|
243
|
+
const EXPANSION_KEYS = {
|
|
244
|
+
expand: { ltr: 'ArrowRight', rtl: 'ArrowLeft' },
|
|
245
|
+
collapse: { ltr: 'ArrowLeft', rtl: 'ArrowRight' },
|
|
246
|
+
} as const;
|
|
247
|
+
|
|
248
|
+
function resolveTreeDirection(element: HTMLElement | null): 'ltr' | 'rtl' {
|
|
249
|
+
if (element) {
|
|
250
|
+
const dir = element.closest('[dir]')?.getAttribute('dir');
|
|
251
|
+
if (dir === 'rtl') return 'rtl';
|
|
252
|
+
if (dir === 'ltr') return 'ltr';
|
|
253
|
+
if (typeof window !== 'undefined' && typeof window.getComputedStyle === 'function') {
|
|
254
|
+
const computedDirection = window.getComputedStyle(element).direction;
|
|
255
|
+
if (computedDirection === 'rtl') return 'rtl';
|
|
256
|
+
if (computedDirection === 'ltr') return 'ltr';
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (typeof document !== 'undefined') {
|
|
260
|
+
return document.dir === 'rtl' ? 'rtl' : 'ltr';
|
|
261
|
+
}
|
|
262
|
+
return 'ltr';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function createTreeDropTargetDelegate<T extends object>(
|
|
266
|
+
delegate: TreeDropTargetDelegate,
|
|
267
|
+
state: TreeState<T, TreeCollection<T>>,
|
|
268
|
+
direction: 'ltr' | 'rtl',
|
|
269
|
+
baseKeyboardNav?: (
|
|
270
|
+
target: DropTarget | null,
|
|
271
|
+
direction: 'next' | 'previous',
|
|
272
|
+
isValidDropTarget: (target: DropTarget) => boolean
|
|
273
|
+
) => DropTarget | null
|
|
274
|
+
): TreeDropTargetDelegate {
|
|
275
|
+
const pointerTracking: PointerTrackingState = {
|
|
276
|
+
lastY: 0,
|
|
277
|
+
lastX: 0,
|
|
278
|
+
yDirection: null,
|
|
279
|
+
xDirection: null,
|
|
280
|
+
boundaryContext: null,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const getPotentialTargets = (
|
|
284
|
+
originalTarget: ItemDropTarget,
|
|
285
|
+
isValidDropTarget: (target: DropTarget) => boolean
|
|
286
|
+
): ItemDropTarget[] => {
|
|
287
|
+
if (originalTarget.dropPosition === 'on') return [originalTarget];
|
|
288
|
+
|
|
289
|
+
const collection = state.collection;
|
|
290
|
+
const getNodeNextKey = (node: TreeNode<T> | null | undefined): Key | null => {
|
|
291
|
+
if (!node) return null;
|
|
292
|
+
const declaredNextKey = (node as TreeNode<T> & { nextKey?: Key | null }).nextKey;
|
|
293
|
+
return declaredNextKey ?? null;
|
|
294
|
+
};
|
|
295
|
+
const target: ItemDropTarget = { ...originalTarget };
|
|
296
|
+
let currentItem = collection.getItem(target.key);
|
|
297
|
+
while (currentItem && currentItem.type !== 'item') {
|
|
298
|
+
const nextKey = getNodeNextKey(currentItem);
|
|
299
|
+
if (nextKey == null) break;
|
|
300
|
+
target.key = nextKey;
|
|
301
|
+
currentItem = collection.getItem(nextKey);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const potentialTargets: ItemDropTarget[] = [target];
|
|
305
|
+
|
|
306
|
+
if (
|
|
307
|
+
currentItem &&
|
|
308
|
+
currentItem.hasChildNodes &&
|
|
309
|
+
state.expandedKeys.has(currentItem.key) &&
|
|
310
|
+
target.dropPosition === 'after'
|
|
311
|
+
) {
|
|
312
|
+
let firstChildItemNode: TreeNode<T> | null = null;
|
|
313
|
+
for (const child of collection.getChildren(currentItem.key)) {
|
|
314
|
+
if (child.type === 'item') {
|
|
315
|
+
firstChildItemNode = child;
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (firstChildItemNode) {
|
|
321
|
+
const beforeFirstChildTarget: ItemDropTarget = {
|
|
322
|
+
type: 'item',
|
|
323
|
+
key: firstChildItemNode.key,
|
|
324
|
+
dropPosition: 'before',
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
if (isValidDropTarget(beforeFirstChildTarget)) {
|
|
328
|
+
return [beforeFirstChildTarget];
|
|
329
|
+
}
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (getNodeNextKey(currentItem) != null) {
|
|
335
|
+
return [originalTarget];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
let parentKey = currentItem?.parentKey ?? null;
|
|
339
|
+
const ancestorTargets: ItemDropTarget[] = [];
|
|
340
|
+
while (parentKey != null) {
|
|
341
|
+
const parentItem = collection.getItem(parentKey);
|
|
342
|
+
const nextKey = getNodeNextKey(parentItem);
|
|
343
|
+
const nextItem = nextKey != null ? collection.getItem(nextKey) : null;
|
|
344
|
+
const isLastChildAtLevel = !nextItem || nextItem.parentKey !== parentKey;
|
|
345
|
+
|
|
346
|
+
if (isLastChildAtLevel) {
|
|
347
|
+
const afterParentTarget: ItemDropTarget = {
|
|
348
|
+
type: 'item',
|
|
349
|
+
key: parentKey,
|
|
350
|
+
dropPosition: 'after',
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
if (isValidDropTarget(afterParentTarget)) {
|
|
354
|
+
ancestorTargets.push(afterParentTarget);
|
|
355
|
+
}
|
|
356
|
+
if (nextItem) break;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
parentKey = parentItem?.parentKey ?? null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (ancestorTargets.length > 0) {
|
|
363
|
+
potentialTargets.push(...ancestorTargets);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (potentialTargets.length === 1) {
|
|
367
|
+
const nextKey = collection.getKeyAfter(target.key);
|
|
368
|
+
const nextNode = nextKey != null ? collection.getItem(nextKey) : null;
|
|
369
|
+
if (
|
|
370
|
+
nextKey != null &&
|
|
371
|
+
nextNode &&
|
|
372
|
+
currentItem &&
|
|
373
|
+
nextNode.level != null &&
|
|
374
|
+
currentItem.level != null &&
|
|
375
|
+
nextNode.level > currentItem.level
|
|
376
|
+
) {
|
|
377
|
+
const beforeTarget: ItemDropTarget = {
|
|
378
|
+
type: 'item',
|
|
379
|
+
key: nextKey,
|
|
380
|
+
dropPosition: 'before',
|
|
381
|
+
};
|
|
382
|
+
if (isValidDropTarget(beforeTarget)) return [beforeTarget];
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return potentialTargets.filter((candidate) => isValidDropTarget(candidate));
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const selectTarget = (
|
|
390
|
+
potentialTargets: ItemDropTarget[],
|
|
391
|
+
originalTarget: ItemDropTarget,
|
|
392
|
+
x: number,
|
|
393
|
+
y: number,
|
|
394
|
+
currentYMovement: 'up' | 'down' | null,
|
|
395
|
+
currentXMovement: 'left' | 'right' | null
|
|
396
|
+
): ItemDropTarget => {
|
|
397
|
+
if (potentialTargets.length < 2) return potentialTargets[0];
|
|
398
|
+
|
|
399
|
+
const currentItem = state.collection.getItem(originalTarget.key);
|
|
400
|
+
const parentKey = currentItem?.parentKey;
|
|
401
|
+
if (parentKey == null) return potentialTargets[0];
|
|
402
|
+
|
|
403
|
+
if (!pointerTracking.boundaryContext || pointerTracking.boundaryContext.parentKey !== parentKey) {
|
|
404
|
+
const initialTargetIndex = pointerTracking.yDirection === 'up' ? potentialTargets.length - 1 : 0;
|
|
405
|
+
pointerTracking.boundaryContext = {
|
|
406
|
+
parentKey,
|
|
407
|
+
preferredTargetIndex: initialTargetIndex,
|
|
408
|
+
lastSwitchY: y,
|
|
409
|
+
lastSwitchX: x,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const boundaryContext = pointerTracking.boundaryContext;
|
|
414
|
+
const distanceFromLastXSwitch = Math.abs(x - boundaryContext.lastSwitchX);
|
|
415
|
+
const distanceFromLastYSwitch = Math.abs(y - boundaryContext.lastSwitchY);
|
|
416
|
+
|
|
417
|
+
if (distanceFromLastYSwitch > Y_SWITCH_THRESHOLD && currentYMovement) {
|
|
418
|
+
const currentIndex = boundaryContext.preferredTargetIndex ?? 0;
|
|
419
|
+
if (currentYMovement === 'down' && currentIndex === 0) {
|
|
420
|
+
boundaryContext.preferredTargetIndex = potentialTargets.length - 1;
|
|
421
|
+
} else if (currentYMovement === 'up' && currentIndex === potentialTargets.length - 1) {
|
|
422
|
+
boundaryContext.preferredTargetIndex = 0;
|
|
423
|
+
}
|
|
424
|
+
pointerTracking.xDirection = null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (distanceFromLastXSwitch > X_SWITCH_THRESHOLD && currentXMovement) {
|
|
428
|
+
const currentTargetIndex = boundaryContext.preferredTargetIndex ?? 0;
|
|
429
|
+
|
|
430
|
+
if (currentXMovement === 'left') {
|
|
431
|
+
if (direction === 'ltr') {
|
|
432
|
+
if (currentTargetIndex < potentialTargets.length - 1) {
|
|
433
|
+
boundaryContext.preferredTargetIndex = currentTargetIndex + 1;
|
|
434
|
+
boundaryContext.lastSwitchX = x;
|
|
435
|
+
}
|
|
436
|
+
} else if (currentTargetIndex > 0) {
|
|
437
|
+
boundaryContext.preferredTargetIndex = currentTargetIndex - 1;
|
|
438
|
+
boundaryContext.lastSwitchX = x;
|
|
439
|
+
}
|
|
440
|
+
} else if (currentXMovement === 'right') {
|
|
441
|
+
if (direction === 'ltr') {
|
|
442
|
+
if (currentTargetIndex > 0) {
|
|
443
|
+
boundaryContext.preferredTargetIndex = currentTargetIndex - 1;
|
|
444
|
+
boundaryContext.lastSwitchX = x;
|
|
445
|
+
}
|
|
446
|
+
} else if (currentTargetIndex < potentialTargets.length - 1) {
|
|
447
|
+
boundaryContext.preferredTargetIndex = currentTargetIndex + 1;
|
|
448
|
+
boundaryContext.lastSwitchX = x;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
pointerTracking.yDirection = null;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const targetIndex = Math.max(
|
|
456
|
+
0,
|
|
457
|
+
Math.min(boundaryContext.preferredTargetIndex ?? 0, potentialTargets.length - 1)
|
|
458
|
+
);
|
|
459
|
+
return potentialTargets[targetIndex];
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
// --- Tree-aware keyboard DnD navigation (RAC parity) ---
|
|
463
|
+
const getKeyboardNavigationTarget = (
|
|
464
|
+
target: DropTarget | null,
|
|
465
|
+
dir: 'next' | 'previous',
|
|
466
|
+
isValidDropTarget: (target: DropTarget) => boolean
|
|
467
|
+
): DropTarget | null => {
|
|
468
|
+
const collection = state.collection;
|
|
469
|
+
|
|
470
|
+
// If the target key is not a visible row (e.g. collapsed/hidden child node),
|
|
471
|
+
// fall back to the base (non-override) index-based navigation to avoid infinite recursion.
|
|
472
|
+
// The collection keyMap contains ALL nodes (even collapsed), so check visible rows instead.
|
|
473
|
+
if (target && target.type === 'item') {
|
|
474
|
+
const node = collection.getItem(target.key);
|
|
475
|
+
const isVisibleRow = node != null && (node as TreeNode<T> & { rowIndex?: number }).rowIndex != null;
|
|
476
|
+
if (!isVisibleRow) {
|
|
477
|
+
return baseKeyboardNav?.(target, dir, isValidDropTarget) ?? null;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Helpers
|
|
482
|
+
const tryValid = (t: DropTarget): DropTarget | null =>
|
|
483
|
+
isValidDropTarget(t) ? t : null;
|
|
484
|
+
|
|
485
|
+
const getNodeNextKey = (node: TreeNode<T> | null | undefined): Key | null => {
|
|
486
|
+
if (!node) return null;
|
|
487
|
+
return (node as TreeNode<T> & { nextKey?: Key | null }).nextKey ?? null;
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const isExpanded = (key: Key): boolean => {
|
|
491
|
+
const node = collection.getItem(key);
|
|
492
|
+
if (!node || !node.hasChildNodes) return false;
|
|
493
|
+
return state.expandedKeys.has(key);
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const getFirstChildItemKey = (key: Key): Key | null => {
|
|
497
|
+
for (const child of collection.getChildren(key)) {
|
|
498
|
+
if (child.type === 'item') return child.key;
|
|
499
|
+
}
|
|
500
|
+
return null;
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const getLastChildItemKey = (key: Key): Key | null => {
|
|
504
|
+
let lastKey: Key | null = null;
|
|
505
|
+
for (const child of collection.getChildren(key)) {
|
|
506
|
+
if (child.type === 'item') lastKey = child.key;
|
|
507
|
+
}
|
|
508
|
+
return lastKey;
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
// Find the deepest last expanded descendant (for "previous" from 'after')
|
|
512
|
+
const getDeepestLastChild = (key: Key): Key => {
|
|
513
|
+
let current = key;
|
|
514
|
+
while (isExpanded(current)) {
|
|
515
|
+
const lastChild = getLastChildItemKey(current);
|
|
516
|
+
if (lastChild == null) break;
|
|
517
|
+
current = lastChild;
|
|
518
|
+
}
|
|
519
|
+
return current;
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
if (dir === 'next') {
|
|
523
|
+
// From null → root
|
|
524
|
+
if (!target) {
|
|
525
|
+
return tryValid({ type: 'root' });
|
|
526
|
+
}
|
|
527
|
+
// From root → first item 'before'
|
|
528
|
+
if (target.type === 'root') {
|
|
529
|
+
const firstKey = collection.getFirstKey();
|
|
530
|
+
if (firstKey != null) {
|
|
531
|
+
return tryValid({ type: 'item', key: firstKey, dropPosition: 'before' });
|
|
532
|
+
}
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
if (target.type === 'item') {
|
|
536
|
+
switch (target.dropPosition) {
|
|
537
|
+
case 'before':
|
|
538
|
+
return tryValid({ type: 'item', key: target.key, dropPosition: 'on' })
|
|
539
|
+
?? tryValid({ type: 'item', key: target.key, dropPosition: 'after' });
|
|
540
|
+
case 'on': {
|
|
541
|
+
// If item is expanded and has children, go to first child 'before'
|
|
542
|
+
if (isExpanded(target.key)) {
|
|
543
|
+
const firstChild = getFirstChildItemKey(target.key);
|
|
544
|
+
if (firstChild != null) {
|
|
545
|
+
return tryValid({ type: 'item', key: firstChild, dropPosition: 'before' })
|
|
546
|
+
?? tryValid({ type: 'item', key: firstChild, dropPosition: 'on' });
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// Otherwise, next item in collection or 'after'
|
|
550
|
+
const nextKey = collection.getKeyAfter(target.key);
|
|
551
|
+
const targetNode = collection.getItem(target.key);
|
|
552
|
+
const nextNode = nextKey != null ? collection.getItem(nextKey) : null;
|
|
553
|
+
if (targetNode && nextNode && nextNode.level != null && targetNode.level != null && nextNode.level >= targetNode.level) {
|
|
554
|
+
return tryValid({ type: 'item', key: nextNode.key, dropPosition: 'before' })
|
|
555
|
+
?? tryValid({ type: 'item', key: target.key, dropPosition: 'after' });
|
|
556
|
+
}
|
|
557
|
+
return tryValid({ type: 'item', key: target.key, dropPosition: 'after' });
|
|
558
|
+
}
|
|
559
|
+
case 'after': {
|
|
560
|
+
// If item is expanded (and we're at 'after'), first child
|
|
561
|
+
if (isExpanded(target.key)) {
|
|
562
|
+
const firstChild = getFirstChildItemKey(target.key);
|
|
563
|
+
if (firstChild != null) {
|
|
564
|
+
return tryValid({ type: 'item', key: firstChild, dropPosition: 'before' })
|
|
565
|
+
?? tryValid({ type: 'item', key: firstChild, dropPosition: 'on' });
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
// Check if this is the last sibling at its level
|
|
569
|
+
const targetNode = collection.getItem(target.key);
|
|
570
|
+
const nextSiblingKey = getNodeNextKey(targetNode);
|
|
571
|
+
if (nextSiblingKey != null) {
|
|
572
|
+
const nextSibling = collection.getItem(nextSiblingKey);
|
|
573
|
+
if (nextSibling?.type === 'item') {
|
|
574
|
+
return tryValid({ type: 'item', key: nextSibling.key, dropPosition: 'before' })
|
|
575
|
+
?? tryValid({ type: 'item', key: nextSibling.key, dropPosition: 'on' });
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
// Traverse up to parent when at last sibling
|
|
579
|
+
if (targetNode?.parentKey != null) {
|
|
580
|
+
const parentNode = collection.getItem(targetNode.parentKey);
|
|
581
|
+
const parentNextKey = getNodeNextKey(parentNode);
|
|
582
|
+
const parentNextNode = parentNextKey != null ? collection.getItem(parentNextKey) : null;
|
|
583
|
+
if (parentNextNode?.type === 'item') {
|
|
584
|
+
return tryValid({ type: 'item', key: parentNextNode.key, dropPosition: 'before' });
|
|
585
|
+
}
|
|
586
|
+
if (parentNode?.type === 'item') {
|
|
587
|
+
return tryValid({ type: 'item', key: parentNode.key, dropPosition: 'after' });
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
// Reached end — try next item in flat collection
|
|
591
|
+
const nextKey = collection.getKeyAfter(target.key);
|
|
592
|
+
if (nextKey != null) {
|
|
593
|
+
return tryValid({ type: 'item', key: nextKey, dropPosition: 'before' })
|
|
594
|
+
?? tryValid({ type: 'item', key: nextKey, dropPosition: 'on' });
|
|
595
|
+
}
|
|
596
|
+
// Wrap to root
|
|
597
|
+
return tryValid({ type: 'root' });
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// dir === 'previous'
|
|
605
|
+
// From null or root → last root-level item 'after'
|
|
606
|
+
if (!target || target.type === 'root') {
|
|
607
|
+
const lastKey = collection.getLastKey();
|
|
608
|
+
if (lastKey != null) {
|
|
609
|
+
// Find root-level ancestor of last key
|
|
610
|
+
let rootKey = lastKey;
|
|
611
|
+
let node = collection.getItem(lastKey);
|
|
612
|
+
while (node?.parentKey != null) {
|
|
613
|
+
rootKey = node.parentKey;
|
|
614
|
+
node = collection.getItem(rootKey);
|
|
615
|
+
}
|
|
616
|
+
return tryValid({ type: 'item', key: rootKey, dropPosition: 'after' });
|
|
617
|
+
}
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (target.type === 'item') {
|
|
622
|
+
switch (target.dropPosition) {
|
|
623
|
+
case 'after': {
|
|
624
|
+
// If expanded with children, go to deepest last child 'after'
|
|
625
|
+
const deepest = getDeepestLastChild(target.key);
|
|
626
|
+
if (deepest !== target.key) {
|
|
627
|
+
return tryValid({ type: 'item', key: deepest, dropPosition: 'after' })
|
|
628
|
+
?? tryValid({ type: 'item', key: target.key, dropPosition: 'on' });
|
|
629
|
+
}
|
|
630
|
+
return tryValid({ type: 'item', key: target.key, dropPosition: 'on' });
|
|
631
|
+
}
|
|
632
|
+
case 'on':
|
|
633
|
+
return tryValid({ type: 'item', key: target.key, dropPosition: 'before' });
|
|
634
|
+
case 'before': {
|
|
635
|
+
// Move to the previous sibling's deepest last child 'after'
|
|
636
|
+
const prevKey = collection.getKeyBefore(target.key);
|
|
637
|
+
if (prevKey != null) {
|
|
638
|
+
const deepest = getDeepestLastChild(prevKey);
|
|
639
|
+
return tryValid({ type: 'item', key: deepest, dropPosition: 'after' })
|
|
640
|
+
?? tryValid({ type: 'item', key: prevKey, dropPosition: 'on' });
|
|
641
|
+
}
|
|
642
|
+
// No previous — go to root
|
|
643
|
+
return tryValid({ type: 'root' });
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return null;
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
return {
|
|
652
|
+
getDropTargetFromPoint(x, y, isValidDropTarget) {
|
|
653
|
+
const baseTarget = delegate.getDropTargetFromPoint(x, y, isValidDropTarget);
|
|
654
|
+
if (!baseTarget || baseTarget.type === 'root') return baseTarget;
|
|
655
|
+
|
|
656
|
+
const deltaY = y - pointerTracking.lastY;
|
|
657
|
+
const deltaX = x - pointerTracking.lastX;
|
|
658
|
+
let currentYMovement: 'up' | 'down' | null = pointerTracking.yDirection;
|
|
659
|
+
let currentXMovement: 'left' | 'right' | null = pointerTracking.xDirection;
|
|
660
|
+
|
|
661
|
+
if (Math.abs(deltaY) > Y_SWITCH_THRESHOLD) {
|
|
662
|
+
currentYMovement = deltaY > 0 ? 'down' : 'up';
|
|
663
|
+
pointerTracking.yDirection = currentYMovement;
|
|
664
|
+
pointerTracking.lastY = y;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (Math.abs(deltaX) > X_SWITCH_THRESHOLD) {
|
|
668
|
+
currentXMovement = deltaX > 0 ? 'right' : 'left';
|
|
669
|
+
pointerTracking.xDirection = currentXMovement;
|
|
670
|
+
pointerTracking.lastX = x;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
let target: ItemDropTarget = baseTarget;
|
|
674
|
+
if (target.dropPosition === 'before') {
|
|
675
|
+
const keyBefore = state.collection.getKeyBefore(target.key);
|
|
676
|
+
if (keyBefore != null) {
|
|
677
|
+
const normalized: ItemDropTarget = {
|
|
678
|
+
type: 'item',
|
|
679
|
+
key: keyBefore,
|
|
680
|
+
dropPosition: 'after',
|
|
681
|
+
};
|
|
682
|
+
if (isValidDropTarget(normalized)) target = normalized;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const potentialTargets = getPotentialTargets(target, isValidDropTarget);
|
|
687
|
+
if (potentialTargets.length === 0) return { type: 'root' };
|
|
688
|
+
|
|
689
|
+
if (potentialTargets.length > 1) {
|
|
690
|
+
return selectTarget(potentialTargets, target, x, y, currentYMovement, currentXMovement);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
pointerTracking.boundaryContext = null;
|
|
694
|
+
return potentialTargets[0];
|
|
695
|
+
},
|
|
696
|
+
getKeyboardNavigationTarget,
|
|
697
|
+
getKeyboardPageNavigationTarget: delegate.getKeyboardPageNavigationTarget?.bind(delegate),
|
|
698
|
+
};
|
|
156
699
|
}
|
|
157
700
|
|
|
158
701
|
interface TreeItemContextValue<T extends object> {
|
|
@@ -177,11 +720,13 @@ export const TreeItemContext = createContext<TreeItemContextValue<object> | null
|
|
|
177
720
|
export function Tree<T extends object>(props: TreeProps<T>): JSX.Element {
|
|
178
721
|
const [local, stateProps, ariaProps] = splitProps(
|
|
179
722
|
props,
|
|
180
|
-
['class', 'style', 'slot', 'renderEmptyState'],
|
|
723
|
+
['class', 'style', 'slot', 'renderEmptyState', 'hasMore', 'isLoading', 'onLoadMore', 'dragAndDropHooks'],
|
|
181
724
|
[
|
|
182
725
|
'items',
|
|
183
726
|
'disabledKeys',
|
|
727
|
+
'disabledBehavior',
|
|
184
728
|
'selectionMode',
|
|
729
|
+
'selectionBehavior',
|
|
185
730
|
'selectedKeys',
|
|
186
731
|
'defaultSelectedKeys',
|
|
187
732
|
'onSelectionChange',
|
|
@@ -193,13 +738,17 @@ export function Tree<T extends object>(props: TreeProps<T>): JSX.Element {
|
|
|
193
738
|
|
|
194
739
|
// Create ref signal
|
|
195
740
|
const [ref, setRef] = createSignal<HTMLDivElement | null>(null);
|
|
741
|
+
const flatItems = createMemo<TreeItemData<T>[]>(() => flattenCollectionEntries(stateProps.items));
|
|
742
|
+
const hasSections = createMemo(() => stateProps.items.some((entry) => isCollectionSection(entry)));
|
|
196
743
|
|
|
197
744
|
// Create tree state
|
|
198
745
|
const state = createTreeState<T, TreeCollection<T>>(() => ({
|
|
199
746
|
collectionFactory: (expandedKeys) =>
|
|
200
|
-
createTreeCollection(
|
|
747
|
+
createTreeCollection(flatItems(), expandedKeys) as TreeCollection<T>,
|
|
201
748
|
disabledKeys: stateProps.disabledKeys,
|
|
749
|
+
disabledBehavior: stateProps.disabledBehavior,
|
|
202
750
|
selectionMode: stateProps.selectionMode,
|
|
751
|
+
selectionBehavior: stateProps.selectionBehavior,
|
|
203
752
|
selectedKeys: stateProps.selectedKeys,
|
|
204
753
|
defaultSelectedKeys: stateProps.defaultSelectedKeys,
|
|
205
754
|
onSelectionChange: stateProps.onSelectionChange,
|
|
@@ -208,6 +757,22 @@ export function Tree<T extends object>(props: TreeProps<T>): JSX.Element {
|
|
|
208
757
|
onExpandedChange: stateProps.onExpandedChange,
|
|
209
758
|
}));
|
|
210
759
|
|
|
760
|
+
const [lastExpandedKeys, setLastExpandedKeys] = createSignal<Set<Key>>(new Set());
|
|
761
|
+
const [lastItemsLength, setLastItemsLength] = createSignal(flatItems().length);
|
|
762
|
+
const [collectionVersion, setCollectionVersion] = createSignal(0);
|
|
763
|
+
createEffect(() => {
|
|
764
|
+
const expanded = state.expandedKeys;
|
|
765
|
+
const items = flatItems();
|
|
766
|
+
if (!areSetsEqual(lastExpandedKeys(), expanded) || lastItemsLength() !== items.length) {
|
|
767
|
+
setLastExpandedKeys(new Set(expanded));
|
|
768
|
+
setLastItemsLength(items.length);
|
|
769
|
+
setCollectionVersion((v) => v + 1);
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
// Resolve writing direction for keyboard expand/collapse parity
|
|
774
|
+
const treeDirection = createMemo(() => ariaProps.direction ?? resolveTreeDirection(ref()));
|
|
775
|
+
|
|
211
776
|
// Create tree aria props
|
|
212
777
|
const { treeProps } = createTree<T, TreeCollection<T>>(
|
|
213
778
|
() => ({
|
|
@@ -218,6 +783,7 @@ export function Tree<T extends object>(props: TreeProps<T>): JSX.Element {
|
|
|
218
783
|
isVirtualized: ariaProps.isVirtualized,
|
|
219
784
|
onAction: ariaProps.onAction,
|
|
220
785
|
isDisabled: ariaProps.isDisabled,
|
|
786
|
+
direction: treeDirection(),
|
|
221
787
|
}),
|
|
222
788
|
() => state,
|
|
223
789
|
ref
|
|
@@ -231,7 +797,7 @@ export function Tree<T extends object>(props: TreeProps<T>): JSX.Element {
|
|
|
231
797
|
isFocused: state.isFocused || isFocused(),
|
|
232
798
|
isFocusVisible: isFocusVisible(),
|
|
233
799
|
isDisabled: ariaProps.isDisabled ?? false,
|
|
234
|
-
isEmpty:
|
|
800
|
+
isEmpty: flatItems().length === 0,
|
|
235
801
|
}));
|
|
236
802
|
|
|
237
803
|
// Resolve render props
|
|
@@ -260,61 +826,444 @@ export function Tree<T extends object>(props: TreeProps<T>): JSX.Element {
|
|
|
260
826
|
return rest;
|
|
261
827
|
};
|
|
262
828
|
|
|
263
|
-
const isEmpty = () =>
|
|
829
|
+
const isEmpty = () => flatItems().length === 0;
|
|
830
|
+
|
|
831
|
+
// Render visible rows (flat list based on expansion state)
|
|
832
|
+
const visibleRows = createMemo(() => {
|
|
833
|
+
collectionVersion();
|
|
834
|
+
return state.collection.rows;
|
|
835
|
+
});
|
|
836
|
+
const virtualizer = useVirtualizerContext();
|
|
837
|
+
const parentCollectionRenderer = useCollectionRenderer<TreeItemData<T>>();
|
|
838
|
+
const getDropTargetByIndex = (index: number, position: 'before' | 'after' | 'on'): DropTarget | null => {
|
|
839
|
+
const node = visibleRows()[index];
|
|
840
|
+
if (!node) return null;
|
|
841
|
+
return { type: 'item', key: node.key, dropPosition: position };
|
|
842
|
+
};
|
|
843
|
+
const hasDroppableDnd = createMemo(() => {
|
|
844
|
+
const hooks = local.dragAndDropHooks;
|
|
845
|
+
return Boolean(
|
|
846
|
+
hooks?.useDroppableCollectionState &&
|
|
847
|
+
hooks.useDroppableCollection &&
|
|
848
|
+
(hooks.dropTargetDelegate || parentCollectionRenderer?.dropTargetDelegate || hooks.ListDropTargetDelegate)
|
|
849
|
+
);
|
|
850
|
+
});
|
|
851
|
+
const hasDraggableDnd = createMemo(() => {
|
|
852
|
+
const hooks = local.dragAndDropHooks;
|
|
853
|
+
return Boolean(hooks?.useDraggableCollectionState && hooks.useDraggableCollection);
|
|
854
|
+
});
|
|
855
|
+
const dragState = createMemo(() => {
|
|
856
|
+
if (!hasDraggableDnd()) return undefined;
|
|
857
|
+
return local.dragAndDropHooks?.useDraggableCollectionState?.({
|
|
858
|
+
items: visibleRows().map((node) => node.value as T),
|
|
859
|
+
});
|
|
860
|
+
});
|
|
861
|
+
const dropState = createMemo(() => {
|
|
862
|
+
if (!hasDroppableDnd()) return undefined;
|
|
863
|
+
return local.dragAndDropHooks?.useDroppableCollectionState?.({});
|
|
864
|
+
});
|
|
865
|
+
createEffect(() => {
|
|
866
|
+
const activeDropState = dropState();
|
|
867
|
+
if (!activeDropState) return;
|
|
868
|
+
const originalGetDropOperation = activeDropState.getDropOperation.bind(activeDropState);
|
|
869
|
+
|
|
870
|
+
activeDropState.getDropOperation = (target, types, allowedOperations) => {
|
|
871
|
+
const currentDraggingKeys = dragState()?.draggingKeys ?? new Set<string | number>();
|
|
872
|
+
if (target.type === 'item' && currentDraggingKeys.size > 0) {
|
|
873
|
+
if (currentDraggingKeys.has(target.key) && target.dropPosition === 'on') {
|
|
874
|
+
return 'cancel';
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
let currentKey: Key | null = target.key;
|
|
878
|
+
while (currentKey != null) {
|
|
879
|
+
const item = state.collection.getItem(currentKey);
|
|
880
|
+
const parentKey = item?.parentKey;
|
|
881
|
+
if (parentKey != null && currentDraggingKeys.has(parentKey)) {
|
|
882
|
+
return 'cancel';
|
|
883
|
+
}
|
|
884
|
+
currentKey = parentKey ?? null;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
return originalGetDropOperation(target, types, allowedOperations);
|
|
889
|
+
};
|
|
264
890
|
|
|
891
|
+
onCleanup(() => {
|
|
892
|
+
activeDropState.getDropOperation = originalGetDropOperation;
|
|
893
|
+
});
|
|
894
|
+
});
|
|
895
|
+
createEffect(() => {
|
|
896
|
+
if (!hasDraggableDnd()) return;
|
|
897
|
+
const hooks = local.dragAndDropHooks;
|
|
898
|
+
const activeDragState = dragState();
|
|
899
|
+
if (!hooks?.useDraggableCollection || !activeDragState) return;
|
|
900
|
+
hooks.useDraggableCollection({}, activeDragState, () => ref());
|
|
901
|
+
});
|
|
265
902
|
const contextValue = createMemo<TreeContextValue<T>>(() => ({
|
|
266
903
|
state,
|
|
267
904
|
collection: state.collection,
|
|
268
905
|
isDisabled: ariaProps.isDisabled ?? false,
|
|
269
906
|
renderItem: props.children,
|
|
907
|
+
dragAndDropHooks: local.dragAndDropHooks,
|
|
908
|
+
dragState: dragState(),
|
|
909
|
+
dropState: dropState(),
|
|
270
910
|
}));
|
|
911
|
+
const droppableCollection = createMemo(() => {
|
|
912
|
+
if (!hasDroppableDnd()) return undefined;
|
|
913
|
+
const hooks = local.dragAndDropHooks;
|
|
914
|
+
const activeDropState = dropState();
|
|
915
|
+
if (!hooks?.useDroppableCollection || !activeDropState) return undefined;
|
|
916
|
+
const direction = resolveTreeDirection(ref());
|
|
917
|
+
const baseDropTargetDelegate = hooks.dropTargetDelegate
|
|
918
|
+
?? parentCollectionRenderer?.dropTargetDelegate
|
|
919
|
+
?? (hooks.ListDropTargetDelegate
|
|
920
|
+
? new hooks.ListDropTargetDelegate(
|
|
921
|
+
() => state.collection,
|
|
922
|
+
() => ref(),
|
|
923
|
+
{ layout: 'stack', orientation: 'vertical', direction }
|
|
924
|
+
)
|
|
925
|
+
: undefined);
|
|
926
|
+
if (!baseDropTargetDelegate) return undefined;
|
|
927
|
+
const dropTargetDelegate = createTreeDropTargetDelegate(
|
|
928
|
+
baseDropTargetDelegate as TreeDropTargetDelegate,
|
|
929
|
+
state,
|
|
930
|
+
direction,
|
|
931
|
+
virtualizer?.getBaseKeyboardNavigationTarget
|
|
932
|
+
);
|
|
933
|
+
return hooks.useDroppableCollection(
|
|
934
|
+
{
|
|
935
|
+
dropTargetDelegate,
|
|
936
|
+
keyboardDelegate: {
|
|
937
|
+
getFirstKey: () => state.collection.getFirstKey(),
|
|
938
|
+
getLastKey: () => state.collection.getLastKey(),
|
|
939
|
+
getKeyBelow: (key) => state.collection.getKeyAfter(key),
|
|
940
|
+
getKeyAbove: (key) => state.collection.getKeyBefore(key),
|
|
941
|
+
getKeyPageBelow: (key) => state.collection.getKeyAfter(key),
|
|
942
|
+
getKeyPageAbove: (key) => state.collection.getKeyBefore(key),
|
|
943
|
+
},
|
|
944
|
+
onDropActivate: (event) => {
|
|
945
|
+
if (event.target.type !== 'item') return;
|
|
946
|
+
const key = event.target.key;
|
|
947
|
+
const item = state.collection.getItem(key);
|
|
948
|
+
const isExpanded = state.isExpanded(key);
|
|
949
|
+
if (item?.hasChildNodes && (!isExpanded || hooks.isVirtualDragging?.())) {
|
|
950
|
+
state.toggleKey(key);
|
|
951
|
+
}
|
|
952
|
+
},
|
|
953
|
+
onKeyDown: (event) => {
|
|
954
|
+
const target = activeDropState.target;
|
|
955
|
+
if (!target || target.type !== 'item' || target.dropPosition !== 'on') return;
|
|
956
|
+
const item = state.collection.getItem(target.key);
|
|
957
|
+
if (!item?.hasChildNodes) return;
|
|
958
|
+
const expandKey = EXPANSION_KEYS.expand[direction];
|
|
959
|
+
const collapseKey = EXPANSION_KEYS.collapse[direction];
|
|
960
|
+
if (event.key === expandKey && !state.isExpanded(target.key)) {
|
|
961
|
+
state.toggleKey(target.key);
|
|
962
|
+
} else if (event.key === collapseKey && state.isExpanded(target.key)) {
|
|
963
|
+
state.toggleKey(target.key);
|
|
964
|
+
}
|
|
965
|
+
},
|
|
966
|
+
},
|
|
967
|
+
activeDropState,
|
|
968
|
+
() => ref()
|
|
969
|
+
);
|
|
970
|
+
});
|
|
971
|
+
const isRootDropTarget = createMemo(() => {
|
|
972
|
+
return Boolean(dropState()?.target?.type === 'root');
|
|
973
|
+
});
|
|
974
|
+
const dndRenderDropIndicator = createMemo(() => useRenderDropIndicator(local.dragAndDropHooks, dropState()));
|
|
975
|
+
const dndDropIndicator = (index: number, position: 'before' | 'after' | 'on') => {
|
|
976
|
+
const target = getDropTargetByIndex(index, position);
|
|
977
|
+
if (!target || target.type !== 'item') return undefined;
|
|
978
|
+
return dndRenderDropIndicator()?.(target);
|
|
979
|
+
};
|
|
980
|
+
const persistedKeys = useDndPersistedKeys(
|
|
981
|
+
{ focusedKey: () => state.focusedKey },
|
|
982
|
+
local.dragAndDropHooks,
|
|
983
|
+
dropState(),
|
|
984
|
+
state.collection
|
|
985
|
+
);
|
|
986
|
+
const virtualRange = createMemo(() => {
|
|
987
|
+
if (!virtualizer || !parentCollectionRenderer?.isVirtualized) return null;
|
|
988
|
+
const rows = visibleRows();
|
|
989
|
+
const baseRange = virtualizer.getVisibleRange(rows.length);
|
|
990
|
+
const persistedIndexes = Array.from(persistedKeys())
|
|
991
|
+
.map((key) => rows.findIndex((node) => node.key === key))
|
|
992
|
+
.filter((index) => index >= 0);
|
|
993
|
+
const dropTarget = dropState()?.target;
|
|
994
|
+
const normalizedDropKey = getNormalizedDropTargetKey(dropTarget, state.collection);
|
|
995
|
+
const focusedKey = state.focusedKey;
|
|
996
|
+
const focusedIndex = focusedKey != null ? rows.findIndex((node) => node.key === focusedKey) : -1;
|
|
997
|
+
const forceIncludeIndexes = [
|
|
998
|
+
dropTarget?.type === 'item' ? rows.findIndex((node) => node.key === dropTarget.key) : -1,
|
|
999
|
+
normalizedDropKey != null ? rows.findIndex((node) => node.key === normalizedDropKey) : -1,
|
|
1000
|
+
dropTarget?.type === 'item' ? -1 : focusedIndex,
|
|
1001
|
+
].filter((index) => index >= 0);
|
|
1002
|
+
return mergePersistedKeysIntoVirtualRange(baseRange, persistedIndexes, rows.length, virtualizer, 80, {
|
|
1003
|
+
forceIncludeIndexes,
|
|
1004
|
+
forceIncludeMaxSpan: 320,
|
|
1005
|
+
});
|
|
1006
|
+
});
|
|
1007
|
+
const virtualizedVisibleRows = createMemo(() => {
|
|
1008
|
+
const range = virtualRange();
|
|
1009
|
+
if (!range) return visibleRows();
|
|
1010
|
+
return visibleRows().slice(range.start, range.end);
|
|
1011
|
+
});
|
|
1012
|
+
createEffect(() => {
|
|
1013
|
+
if (!virtualizer || !parentCollectionRenderer?.isVirtualized) return;
|
|
1014
|
+
virtualizer.setDropTargetItemCountResolver(() => visibleRows().length);
|
|
1015
|
+
virtualizer.setDropTargetIndexResolver((key) => {
|
|
1016
|
+
const rows = visibleRows();
|
|
1017
|
+
const index = rows.findIndex((node) => node.key === key);
|
|
1018
|
+
return index >= 0 ? index : null;
|
|
1019
|
+
});
|
|
1020
|
+
virtualizer.setDropTargetResolver((target) => {
|
|
1021
|
+
const node = visibleRows()[target.index];
|
|
1022
|
+
if (!node) return target;
|
|
1023
|
+
return {
|
|
1024
|
+
...target,
|
|
1025
|
+
key: typeof node.key === 'string' || typeof node.key === 'number' ? node.key : undefined,
|
|
1026
|
+
parentKey:
|
|
1027
|
+
typeof node.parentKey === 'string' || typeof node.parentKey === 'number'
|
|
1028
|
+
? node.parentKey
|
|
1029
|
+
: node.parentKey == null
|
|
1030
|
+
? null
|
|
1031
|
+
: undefined,
|
|
1032
|
+
level: typeof node.level === 'number' ? node.level : undefined,
|
|
1033
|
+
};
|
|
1034
|
+
});
|
|
1035
|
+
onCleanup(() => {
|
|
1036
|
+
virtualizer.setDropTargetIndexResolver(undefined);
|
|
1037
|
+
virtualizer.setDropTargetItemCountResolver(undefined);
|
|
1038
|
+
virtualizer.setDropTargetResolver(undefined);
|
|
1039
|
+
});
|
|
1040
|
+
});
|
|
1041
|
+
const rowIndexByKey = createMemo(() => {
|
|
1042
|
+
const map = new Map<Key, number>();
|
|
1043
|
+
const rows = visibleRows();
|
|
1044
|
+
for (let i = 0; i < rows.length; i += 1) {
|
|
1045
|
+
map.set(rows[i].key, i);
|
|
1046
|
+
}
|
|
1047
|
+
return map;
|
|
1048
|
+
});
|
|
1049
|
+
const getAfterIndicatorIndexes = (
|
|
1050
|
+
absoluteIndex: number,
|
|
1051
|
+
renderRange?: { start: number; end: number } | null
|
|
1052
|
+
): number[] => {
|
|
1053
|
+
const rows = visibleRows();
|
|
1054
|
+
const current = rows[absoluteIndex];
|
|
1055
|
+
if (!current) return [];
|
|
1056
|
+
const next = rows[absoluteIndex + 1];
|
|
1057
|
+
// "after" is equivalent to next sibling's "before" when next row is at same or deeper level.
|
|
1058
|
+
if (next && next.level >= current.level) {
|
|
1059
|
+
return [];
|
|
1060
|
+
}
|
|
271
1061
|
|
|
272
|
-
|
|
273
|
-
|
|
1062
|
+
const result: number[] = [];
|
|
1063
|
+
let cursorIndex: number | null = absoluteIndex;
|
|
1064
|
+
|
|
1065
|
+
// Emit after indicators for current and ancestor boundary levels, matching RAC branch exit semantics.
|
|
1066
|
+
while (cursorIndex != null) {
|
|
1067
|
+
const cursor: TreeNode<T> | undefined = rows[cursorIndex];
|
|
1068
|
+
if (!cursor) break;
|
|
1069
|
+
const shouldRender =
|
|
1070
|
+
!next || (cursor.parentKey !== next.parentKey && next.level < cursor.level);
|
|
1071
|
+
if (!shouldRender) break;
|
|
1072
|
+
result.push(cursorIndex);
|
|
1073
|
+
if (cursor.parentKey == null) break;
|
|
1074
|
+
cursorIndex = rowIndexByKey().get(cursor.parentKey) ?? null;
|
|
1075
|
+
}
|
|
1076
|
+
if (!renderRange) return result;
|
|
1077
|
+
return result.filter((index) => index >= renderRange.start && index < renderRange.end);
|
|
1078
|
+
};
|
|
1079
|
+
// Install tree-aware keyboard navigation override into the Virtualizer (if present).
|
|
1080
|
+
// This replaces the generic index-based navigation with collection-level semantics
|
|
1081
|
+
// (tree branch traversal, level-aware wrapping — RAC parity item #36).
|
|
1082
|
+
createEffect(() => {
|
|
1083
|
+
if (!virtualizer) return;
|
|
1084
|
+
const direction = resolveTreeDirection(ref());
|
|
1085
|
+
const parentDelegate: TreeDropTargetDelegate = {
|
|
1086
|
+
getDropTargetFromPoint: parentCollectionRenderer?.dropTargetDelegate?.getDropTargetFromPoint
|
|
1087
|
+
?? ((_x, _y, _v) => null),
|
|
1088
|
+
getKeyboardNavigationTarget: parentCollectionRenderer?.dropTargetDelegate?.getKeyboardNavigationTarget,
|
|
1089
|
+
getKeyboardPageNavigationTarget: parentCollectionRenderer?.dropTargetDelegate?.getKeyboardPageNavigationTarget,
|
|
1090
|
+
};
|
|
1091
|
+
const treeDelegate = createTreeDropTargetDelegate(
|
|
1092
|
+
parentDelegate, state, direction,
|
|
1093
|
+
virtualizer.getBaseKeyboardNavigationTarget
|
|
1094
|
+
);
|
|
1095
|
+
virtualizer.setKeyboardNavigationOverride(
|
|
1096
|
+
treeDelegate.getKeyboardNavigationTarget
|
|
1097
|
+
? (target, dir, isValid) => treeDelegate.getKeyboardNavigationTarget!(target, dir, isValid)
|
|
1098
|
+
: undefined
|
|
1099
|
+
);
|
|
1100
|
+
onCleanup(() => {
|
|
1101
|
+
virtualizer.setKeyboardNavigationOverride(undefined);
|
|
1102
|
+
});
|
|
1103
|
+
});
|
|
1104
|
+
const collectionRenderer = createMemo<CollectionRendererContextValue<unknown>>(() => ({
|
|
1105
|
+
...parentCollectionRenderer,
|
|
1106
|
+
renderItem: (item) => item as JSX.Element,
|
|
1107
|
+
renderDropIndicator: (index: number, position: 'before' | 'after' | 'on') =>
|
|
1108
|
+
dndDropIndicator(index, position) ?? parentCollectionRenderer?.renderDropIndicator?.(index, position),
|
|
1109
|
+
}));
|
|
1110
|
+
const rootKeyByNodeKey = createMemo(() => {
|
|
1111
|
+
const rootMap = new Map<Key, Key>();
|
|
1112
|
+
for (const row of visibleRows()) {
|
|
1113
|
+
let rootKey: Key = row.key;
|
|
1114
|
+
let parentKey = row.parentKey;
|
|
1115
|
+
while (parentKey != null) {
|
|
1116
|
+
rootKey = parentKey;
|
|
1117
|
+
parentKey = state.collection.getParentKey(parentKey);
|
|
1118
|
+
}
|
|
1119
|
+
rootMap.set(row.key, rootKey);
|
|
1120
|
+
}
|
|
1121
|
+
return rootMap;
|
|
1122
|
+
});
|
|
1123
|
+
const renderRange = createMemo(() => {
|
|
1124
|
+
const range = virtualRange();
|
|
1125
|
+
if (!range) return null;
|
|
1126
|
+
return { start: range.start, end: range.end };
|
|
1127
|
+
});
|
|
1128
|
+
const renderableRows = createMemo(() => {
|
|
1129
|
+
const offset = renderRange()?.start ?? 0;
|
|
1130
|
+
return virtualizedVisibleRows().map((node, index) => ({
|
|
1131
|
+
node,
|
|
1132
|
+
globalIndex: offset + index,
|
|
1133
|
+
}));
|
|
1134
|
+
});
|
|
1135
|
+
const sectionedRenderableRows = createMemo(() => {
|
|
1136
|
+
if (!hasSections()) return null;
|
|
1137
|
+
const rootMap = rootKeyByNodeKey();
|
|
1138
|
+
const rows = renderableRows();
|
|
1139
|
+
return stateProps.items.map((entry) => {
|
|
1140
|
+
if (!isCollectionSection(entry)) {
|
|
1141
|
+
const matching = rows.filter((row) => rootMap.get(row.node.key) === entry.key);
|
|
1142
|
+
return {
|
|
1143
|
+
type: 'single' as const,
|
|
1144
|
+
item: entry,
|
|
1145
|
+
rows: matching,
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
const sectionRootKeys = new Set(entry.items.map((item) => item.key));
|
|
1149
|
+
const sectionRows = rows.filter((row) => {
|
|
1150
|
+
const rootKey = rootMap.get(row.node.key);
|
|
1151
|
+
return rootKey != null && sectionRootKeys.has(rootKey);
|
|
1152
|
+
});
|
|
1153
|
+
return {
|
|
1154
|
+
type: 'section' as const,
|
|
1155
|
+
section: entry,
|
|
1156
|
+
rows: sectionRows,
|
|
1157
|
+
};
|
|
1158
|
+
});
|
|
1159
|
+
});
|
|
1160
|
+
const renderTreeRow = (node: TreeNode<T>, itemIndex: number) => {
|
|
1161
|
+
const beforeIndicator = () => collectionRenderer().renderDropIndicator?.(itemIndex, 'before');
|
|
1162
|
+
const onIndicator = () => collectionRenderer().renderDropIndicator?.(itemIndex, 'on');
|
|
1163
|
+
const afterIndicatorIndexes = () => getAfterIndicatorIndexes(itemIndex, renderRange());
|
|
1164
|
+
// Find the original item data to pass to render function
|
|
1165
|
+
const itemData: TreeItemData<T> = {
|
|
1166
|
+
key: node.key,
|
|
1167
|
+
value: node.value as T,
|
|
1168
|
+
textValue: node.textValue,
|
|
1169
|
+
children: node.hasChildNodes
|
|
1170
|
+
? node.childNodes.map((child) => ({
|
|
1171
|
+
key: child.key,
|
|
1172
|
+
value: child.value as T,
|
|
1173
|
+
textValue: child.textValue,
|
|
1174
|
+
}))
|
|
1175
|
+
: undefined,
|
|
1176
|
+
};
|
|
1177
|
+
const itemState: TreeRenderItemState = {
|
|
1178
|
+
isExpanded: node.isExpanded ?? false,
|
|
1179
|
+
isExpandable: node.isExpandable ?? false,
|
|
1180
|
+
level: node.level,
|
|
1181
|
+
};
|
|
1182
|
+
return (
|
|
1183
|
+
<>
|
|
1184
|
+
{beforeIndicator()}
|
|
1185
|
+
{onIndicator()}
|
|
1186
|
+
{props.children(itemData, itemState)}
|
|
1187
|
+
<For each={afterIndicatorIndexes()}>
|
|
1188
|
+
{(afterIndex) => collectionRenderer().renderDropIndicator?.(afterIndex, 'after')}
|
|
1189
|
+
</For>
|
|
1190
|
+
</>
|
|
1191
|
+
);
|
|
1192
|
+
};
|
|
274
1193
|
|
|
275
1194
|
return (
|
|
276
1195
|
<TreeContext.Provider value={contextValue() as unknown as TreeContextValue<object>}>
|
|
277
1196
|
<TreeStateContext.Provider value={state as unknown as TreeState<object, TreeCollection<object>>}>
|
|
278
|
-
<
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
1197
|
+
<CollectionRendererContext.Provider value={collectionRenderer()}>
|
|
1198
|
+
<div
|
|
1199
|
+
ref={setRef}
|
|
1200
|
+
{...mergeProps(
|
|
1201
|
+
domProps(),
|
|
1202
|
+
cleanTreeProps(),
|
|
1203
|
+
cleanFocusProps(),
|
|
1204
|
+
(droppableCollection()?.collectionProps as Record<string, unknown> | undefined) ?? {}
|
|
1205
|
+
)}
|
|
1206
|
+
class={renderProps.class()}
|
|
1207
|
+
style={renderProps.style()}
|
|
1208
|
+
data-focused={state.isFocused || undefined}
|
|
1209
|
+
data-focus-visible={isFocusVisible() || undefined}
|
|
1210
|
+
data-disabled={ariaProps.isDisabled || undefined}
|
|
1211
|
+
data-empty={isEmpty() || undefined}
|
|
1212
|
+
data-drop-target={isRootDropTarget() || undefined}
|
|
1213
|
+
data-selection-mode={stateProps.selectionMode !== 'none' ? stateProps.selectionMode : undefined}
|
|
1214
|
+
data-allows-dragging={hasDraggableDnd() || undefined}
|
|
1215
|
+
>
|
|
1216
|
+
<SharedElementTransition>
|
|
1217
|
+
{isEmpty() && local.renderEmptyState ? (
|
|
1218
|
+
local.renderEmptyState()
|
|
1219
|
+
) : (
|
|
1220
|
+
<>
|
|
1221
|
+
{virtualRange()?.offsetTop
|
|
1222
|
+
? <div role="presentation" aria-hidden="true" style={{ height: `${virtualRange()!.offsetTop}px` }} data-virtualizer-spacer="top" />
|
|
1223
|
+
: null}
|
|
1224
|
+
<Show
|
|
1225
|
+
when={hasSections()}
|
|
1226
|
+
fallback={(
|
|
1227
|
+
<For each={renderableRows()}>
|
|
1228
|
+
{(row) => renderTreeRow(row.node, row.globalIndex)}
|
|
1229
|
+
</For>
|
|
1230
|
+
)}
|
|
1231
|
+
>
|
|
1232
|
+
<For each={sectionedRenderableRows() ?? []}>
|
|
1233
|
+
{(entry) => (
|
|
1234
|
+
<Show when={entry.rows.length > 0}>
|
|
1235
|
+
<Show
|
|
1236
|
+
when={entry.type === 'section'}
|
|
1237
|
+
fallback={(
|
|
1238
|
+
<For each={entry.rows}>
|
|
1239
|
+
{(row) => renderTreeRow(row.node, row.globalIndex)}
|
|
1240
|
+
</For>
|
|
1241
|
+
)}
|
|
1242
|
+
>
|
|
1243
|
+
<TreeSection>
|
|
1244
|
+
{entry.type === 'section' && entry.section.title
|
|
1245
|
+
? <TreeHeader>{entry.section.title}</TreeHeader>
|
|
1246
|
+
: null}
|
|
1247
|
+
<For each={entry.rows}>
|
|
1248
|
+
{(row) => renderTreeRow(row.node, row.globalIndex)}
|
|
1249
|
+
</For>
|
|
1250
|
+
</TreeSection>
|
|
1251
|
+
</Show>
|
|
1252
|
+
</Show>
|
|
1253
|
+
)}
|
|
1254
|
+
</For>
|
|
1255
|
+
</Show>
|
|
1256
|
+
{virtualRange()?.offsetBottom
|
|
1257
|
+
? <div role="presentation" aria-hidden="true" style={{ height: `${virtualRange()!.offsetBottom}px` }} data-virtualizer-spacer="bottom" />
|
|
1258
|
+
: null}
|
|
1259
|
+
</>
|
|
1260
|
+
)}
|
|
1261
|
+
</SharedElementTransition>
|
|
1262
|
+
{local.hasMore && local.onLoadMore && (
|
|
1263
|
+
<TreeLoadMoreItem onLoadMore={local.onLoadMore} isLoading={local.isLoading} />
|
|
1264
|
+
)}
|
|
1265
|
+
</div>
|
|
1266
|
+
</CollectionRendererContext.Provider>
|
|
318
1267
|
</TreeStateContext.Provider>
|
|
319
1268
|
</TreeContext.Provider>
|
|
320
1269
|
);
|
|
@@ -324,7 +1273,7 @@ export function Tree<T extends object>(props: TreeProps<T>): JSX.Element {
|
|
|
324
1273
|
* An item in a tree.
|
|
325
1274
|
*/
|
|
326
1275
|
export function TreeItem<T extends object>(props: TreeItemProps<T>): JSX.Element {
|
|
327
|
-
const [local] = splitProps(props, [
|
|
1276
|
+
const [local, domProps] = splitProps(props, [
|
|
328
1277
|
'class',
|
|
329
1278
|
'style',
|
|
330
1279
|
'slot',
|
|
@@ -332,6 +1281,7 @@ export function TreeItem<T extends object>(props: TreeItemProps<T>): JSX.Element
|
|
|
332
1281
|
'item',
|
|
333
1282
|
'textValue',
|
|
334
1283
|
'onAction',
|
|
1284
|
+
'children',
|
|
335
1285
|
]);
|
|
336
1286
|
|
|
337
1287
|
// Get state from context
|
|
@@ -340,6 +1290,7 @@ export function TreeItem<T extends object>(props: TreeItemProps<T>): JSX.Element
|
|
|
340
1290
|
throw new Error('TreeItem must be used within a Tree');
|
|
341
1291
|
}
|
|
342
1292
|
const state = context as TreeState<T, TreeCollection<T>>;
|
|
1293
|
+
const treeContext = useContext(TreeContext) as TreeContextValue<T> | null;
|
|
343
1294
|
|
|
344
1295
|
// Create ref signal
|
|
345
1296
|
const [ref, setRef] = createSignal<HTMLDivElement | null>(null);
|
|
@@ -366,29 +1317,26 @@ export function TreeItem<T extends object>(props: TreeItemProps<T>): JSX.Element
|
|
|
366
1317
|
});
|
|
367
1318
|
|
|
368
1319
|
// Create item aria props
|
|
369
|
-
const
|
|
370
|
-
rowProps,
|
|
371
|
-
gridCellProps,
|
|
372
|
-
expandButtonProps: _expandButtonProps,
|
|
373
|
-
isSelected,
|
|
374
|
-
isDisabled,
|
|
375
|
-
isPressed,
|
|
376
|
-
isExpanded,
|
|
377
|
-
isExpandable,
|
|
378
|
-
level,
|
|
379
|
-
} = createTreeItem<T, TreeCollection<T>>(
|
|
1320
|
+
const treeItemAria = createTreeItem<T, TreeCollection<T>>(
|
|
380
1321
|
() => ({
|
|
381
1322
|
node: itemNode(),
|
|
382
1323
|
onAction: local.onAction,
|
|
1324
|
+
textValue: local.textValue,
|
|
383
1325
|
}),
|
|
384
1326
|
() => state,
|
|
385
1327
|
ref
|
|
386
1328
|
);
|
|
1329
|
+
const isSelected = () => treeItemAria.isSelected;
|
|
1330
|
+
const isDisabled = () => treeItemAria.isDisabled;
|
|
1331
|
+
const isPressed = () => treeItemAria.isPressed;
|
|
1332
|
+
const isExpanded = () => treeItemAria.isExpanded;
|
|
1333
|
+
const isExpandable = () => treeItemAria.isExpandable;
|
|
1334
|
+
const level = () => treeItemAria.level;
|
|
387
1335
|
|
|
388
1336
|
// Create hover
|
|
389
1337
|
const { isHovered, hoverProps } = createHover({
|
|
390
1338
|
get isDisabled() {
|
|
391
|
-
return isDisabled;
|
|
1339
|
+
return isDisabled();
|
|
392
1340
|
},
|
|
393
1341
|
});
|
|
394
1342
|
|
|
@@ -397,18 +1345,37 @@ export function TreeItem<T extends object>(props: TreeItemProps<T>): JSX.Element
|
|
|
397
1345
|
|
|
398
1346
|
// Check if focused
|
|
399
1347
|
const isFocused = createMemo(() => state.focusedKey === local.id);
|
|
1348
|
+
const draggableItem = createMemo(() => {
|
|
1349
|
+
if (!treeContext?.dragAndDropHooks?.useDraggableItem || !treeContext.dragState) return undefined;
|
|
1350
|
+
return treeContext.dragAndDropHooks.useDraggableItem(
|
|
1351
|
+
{
|
|
1352
|
+
key: local.id as string | number,
|
|
1353
|
+
},
|
|
1354
|
+
treeContext.dragState as Parameters<NonNullable<DragAndDropHooks<T>['useDraggableItem']>>[1]
|
|
1355
|
+
);
|
|
1356
|
+
});
|
|
1357
|
+
const droppableItem = createMemo(() => {
|
|
1358
|
+
if (!treeContext?.dragAndDropHooks?.useDroppableItem || !treeContext.dropState) return undefined;
|
|
1359
|
+
return treeContext.dragAndDropHooks.useDroppableItem(
|
|
1360
|
+
{
|
|
1361
|
+
key: local.id as string | number,
|
|
1362
|
+
},
|
|
1363
|
+
treeContext.dropState as Parameters<NonNullable<DragAndDropHooks<T>['useDroppableItem']>>[1],
|
|
1364
|
+
() => ref()
|
|
1365
|
+
);
|
|
1366
|
+
});
|
|
400
1367
|
|
|
401
1368
|
// Render props values
|
|
402
1369
|
const renderValues = createMemo<TreeItemRenderProps>(() => ({
|
|
403
|
-
isSelected,
|
|
1370
|
+
isSelected: isSelected(),
|
|
404
1371
|
isFocused: isFocused(),
|
|
405
1372
|
isFocusVisible: isFocusVisible() && isFocused(),
|
|
406
|
-
isPressed,
|
|
1373
|
+
isPressed: isPressed(),
|
|
407
1374
|
isHovered: isHovered(),
|
|
408
|
-
isDisabled,
|
|
409
|
-
isExpanded,
|
|
410
|
-
isExpandable,
|
|
411
|
-
level,
|
|
1375
|
+
isDisabled: isDisabled(),
|
|
1376
|
+
isExpanded: isExpanded(),
|
|
1377
|
+
isExpandable: isExpandable(),
|
|
1378
|
+
level: level(),
|
|
412
1379
|
}));
|
|
413
1380
|
|
|
414
1381
|
// Resolve render props
|
|
@@ -424,7 +1391,7 @@ export function TreeItem<T extends object>(props: TreeItemProps<T>): JSX.Element
|
|
|
424
1391
|
|
|
425
1392
|
// Remove ref from spread props
|
|
426
1393
|
const cleanRowProps = () => {
|
|
427
|
-
const { ref: _ref1, ...rest } = rowProps as Record<string, unknown>;
|
|
1394
|
+
const { ref: _ref1, ...rest } = treeItemAria.rowProps as Record<string, unknown>;
|
|
428
1395
|
return rest;
|
|
429
1396
|
};
|
|
430
1397
|
const cleanHoverProps = () => {
|
|
@@ -439,31 +1406,40 @@ export function TreeItem<T extends object>(props: TreeItemProps<T>): JSX.Element
|
|
|
439
1406
|
// Item context for nested components
|
|
440
1407
|
const itemContextValue = createMemo<TreeItemContextValue<T>>(() => ({
|
|
441
1408
|
node: itemNode(),
|
|
442
|
-
isExpanded,
|
|
443
|
-
isExpandable,
|
|
444
|
-
level,
|
|
1409
|
+
isExpanded: isExpanded(),
|
|
1410
|
+
isExpandable: isExpandable(),
|
|
1411
|
+
level: level(),
|
|
445
1412
|
}));
|
|
446
1413
|
|
|
447
1414
|
return (
|
|
448
|
-
<TreeItemContext.Provider value={itemContextValue() as TreeItemContextValue<object>}>
|
|
1415
|
+
<TreeItemContext.Provider value={itemContextValue() as unknown as TreeItemContextValue<object>}>
|
|
449
1416
|
<div
|
|
450
1417
|
ref={setRef}
|
|
451
|
-
{...
|
|
452
|
-
{...
|
|
453
|
-
|
|
1418
|
+
{...domProps}
|
|
1419
|
+
{...mergeProps(
|
|
1420
|
+
cleanRowProps(),
|
|
1421
|
+
cleanHoverProps(),
|
|
1422
|
+
cleanFocusProps(),
|
|
1423
|
+
(draggableItem()?.dragProps as Record<string, unknown> | undefined) ?? {},
|
|
1424
|
+
(droppableItem()?.dropProps as Record<string, unknown> | undefined) ?? {}
|
|
1425
|
+
)}
|
|
454
1426
|
class={renderProps.class()}
|
|
455
|
-
style={renderProps.style()}
|
|
456
|
-
data-selected={isSelected || undefined}
|
|
1427
|
+
style={{ '--tree-item-level': String(level()), ...((typeof renderProps.style() === 'object' ? renderProps.style() : {}) as Record<string, string>) }}
|
|
1428
|
+
data-selected={isSelected() || undefined}
|
|
457
1429
|
data-focused={isFocused() || undefined}
|
|
458
1430
|
data-focus-visible={(isFocusVisible() && isFocused()) || undefined}
|
|
459
|
-
data-pressed={isPressed || undefined}
|
|
1431
|
+
data-pressed={isPressed() || undefined}
|
|
460
1432
|
data-hovered={isHovered() || undefined}
|
|
461
|
-
data-disabled={isDisabled || undefined}
|
|
462
|
-
data-expanded={isExpanded || undefined}
|
|
463
|
-
data-expandable={isExpandable || undefined}
|
|
464
|
-
data-
|
|
1433
|
+
data-disabled={isDisabled() || undefined}
|
|
1434
|
+
data-expanded={isExpanded() || undefined}
|
|
1435
|
+
data-expandable={isExpandable() || undefined}
|
|
1436
|
+
data-has-child-items={isExpandable() || undefined}
|
|
1437
|
+
data-level={level()}
|
|
1438
|
+
data-selection-mode={treeContext?.state.selectionMode !== 'none' ? treeContext?.state.selectionMode : undefined}
|
|
1439
|
+
data-dragging={draggableItem()?.isDragging || undefined}
|
|
1440
|
+
data-drop-target={droppableItem()?.isDropTarget || undefined}
|
|
465
1441
|
>
|
|
466
|
-
<div {...gridCellProps} class="solidaria-Tree-item-content">
|
|
1442
|
+
<div {...treeItemAria.gridCellProps} class="solidaria-Tree-item-content">
|
|
467
1443
|
{renderProps.renderChildren()}
|
|
468
1444
|
</div>
|
|
469
1445
|
</div>
|
|
@@ -490,7 +1466,7 @@ export function TreeExpandButton(props: TreeExpandButtonProps): JSX.Element {
|
|
|
490
1466
|
const state = stateContext as TreeState<object, TreeCollection<object>>;
|
|
491
1467
|
|
|
492
1468
|
// Create expand button props
|
|
493
|
-
const
|
|
1469
|
+
const treeItemAria = createTreeItem(
|
|
494
1470
|
() => ({ node: itemContext.node }),
|
|
495
1471
|
() => state,
|
|
496
1472
|
() => null
|
|
@@ -498,7 +1474,7 @@ export function TreeExpandButton(props: TreeExpandButtonProps): JSX.Element {
|
|
|
498
1474
|
|
|
499
1475
|
// Remove ref and add custom handling
|
|
500
1476
|
const cleanExpandProps = () => {
|
|
501
|
-
const { ref: _ref, ...rest } = expandButtonProps as Record<string, unknown>;
|
|
1477
|
+
const { ref: _ref, ...rest } = treeItemAria.expandButtonProps as Record<string, unknown>;
|
|
502
1478
|
return rest;
|
|
503
1479
|
};
|
|
504
1480
|
|
|
@@ -537,15 +1513,95 @@ export function TreeSelectionCheckbox(props: { itemKey: Key }): JSX.Element {
|
|
|
537
1513
|
|
|
538
1514
|
const state = context as TreeState<object, TreeCollection<object>>;
|
|
539
1515
|
|
|
540
|
-
const
|
|
1516
|
+
const treeSelectionCheckboxAria = createTreeSelectionCheckbox<object, TreeCollection<object>>(
|
|
541
1517
|
() => ({ key: props.itemKey }),
|
|
542
1518
|
() => state
|
|
543
1519
|
);
|
|
544
1520
|
|
|
545
|
-
return <input {...checkboxProps} class="solidaria-Tree-checkbox" />;
|
|
1521
|
+
return <input {...treeSelectionCheckboxAria.checkboxProps} class="solidaria-Tree-checkbox" />;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
export function TreeLoadMoreItem(props: TreeLoadMoreItemProps): JSX.Element {
|
|
1525
|
+
let ref: HTMLDivElement | undefined;
|
|
1526
|
+
const [isPending, setIsPending] = createSignal(false);
|
|
1527
|
+
const isLoading = () => !!props.isLoading || isPending();
|
|
1528
|
+
|
|
1529
|
+
const triggerLoadMore = async () => {
|
|
1530
|
+
if (isLoading()) return;
|
|
1531
|
+
setIsPending(true);
|
|
1532
|
+
try {
|
|
1533
|
+
await props.onLoadMore();
|
|
1534
|
+
} finally {
|
|
1535
|
+
setIsPending(false);
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1538
|
+
|
|
1539
|
+
createEffect(() => {
|
|
1540
|
+
if (!ref || typeof IntersectionObserver !== 'function') return;
|
|
1541
|
+
const observer = new IntersectionObserver((entries) => {
|
|
1542
|
+
if (entries[0]?.isIntersecting) {
|
|
1543
|
+
void triggerLoadMore();
|
|
1544
|
+
}
|
|
1545
|
+
});
|
|
1546
|
+
observer.observe(ref);
|
|
1547
|
+
return () => observer.disconnect();
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1550
|
+
const renderProps = useRenderProps(
|
|
1551
|
+
{
|
|
1552
|
+
children: props.children ?? (() => (isLoading() ? 'Loading more...' : 'Load more')),
|
|
1553
|
+
class: props.class,
|
|
1554
|
+
style: props.style,
|
|
1555
|
+
defaultClassName: 'solidaria-Tree-loadMore',
|
|
1556
|
+
},
|
|
1557
|
+
() => ({ isLoading: isLoading() })
|
|
1558
|
+
);
|
|
1559
|
+
|
|
1560
|
+
return (
|
|
1561
|
+
<div
|
|
1562
|
+
ref={ref}
|
|
1563
|
+
role="treeitem"
|
|
1564
|
+
tabIndex={0}
|
|
1565
|
+
aria-disabled={true}
|
|
1566
|
+
onFocus={() => {
|
|
1567
|
+
void triggerLoadMore();
|
|
1568
|
+
}}
|
|
1569
|
+
class={renderProps.class()}
|
|
1570
|
+
style={renderProps.style()}
|
|
1571
|
+
data-loading={isLoading() || undefined}
|
|
1572
|
+
>
|
|
1573
|
+
{renderProps.renderChildren()}
|
|
1574
|
+
</div>
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
export interface TreeItemContentProps<T extends object> extends TreeItemProps<T> {}
|
|
1579
|
+
export type TreeItemContentRenderProps = TreeItemRenderProps;
|
|
1580
|
+
|
|
1581
|
+
export function TreeItemContent<T extends object>(props: TreeItemContentProps<T>): JSX.Element {
|
|
1582
|
+
return <TreeItem {...props} />;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
export function TreeSection(props: TreeSectionProps): JSX.Element {
|
|
1586
|
+
return <Section {...props} />;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
export function TreeHeader(props: TreeHeaderProps): JSX.Element {
|
|
1590
|
+
return <Header {...props} />;
|
|
546
1591
|
}
|
|
547
1592
|
|
|
548
1593
|
// Attach static properties
|
|
549
1594
|
Tree.Item = TreeItem;
|
|
550
1595
|
Tree.ExpandButton = TreeExpandButton;
|
|
551
1596
|
Tree.SelectionCheckbox = TreeSelectionCheckbox;
|
|
1597
|
+
Tree.LoadMoreItem = TreeLoadMoreItem;
|
|
1598
|
+
Tree.Section = TreeSection;
|
|
1599
|
+
Tree.Header = TreeHeader;
|
|
1600
|
+
|
|
1601
|
+
function areSetsEqual<T>(a: Set<T>, b: Set<T>): boolean {
|
|
1602
|
+
if (a.size !== b.size) return false;
|
|
1603
|
+
for (const entry of a) {
|
|
1604
|
+
if (!b.has(entry)) return false;
|
|
1605
|
+
}
|
|
1606
|
+
return true;
|
|
1607
|
+
}
|