@proyecto-viviana/solidaria-components 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +39 -272
- package/dist/ActionBar.d.ts +79 -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/Autocomplete.d.ts +5 -5
- package/dist/Autocomplete.d.ts.map +1 -1
- package/dist/Breadcrumbs.d.ts +27 -8
- package/dist/Breadcrumbs.d.ts.map +1 -1
- package/dist/Button.d.ts +28 -5
- package/dist/Button.d.ts.map +1 -1
- package/dist/Calendar.d.ts +51 -7
- package/dist/Calendar.d.ts.map +1 -1
- package/dist/Checkbox.d.ts +33 -8
- package/dist/Checkbox.d.ts.map +1 -1
- package/dist/Collection.d.ts +130 -0
- package/dist/Collection.d.ts.map +1 -0
- package/dist/Color.d.ts +210 -9
- 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 +146 -16
- 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 +35 -8
- package/dist/DateField.d.ts.map +1 -1
- package/dist/DatePicker.d.ts +101 -5
- package/dist/DatePicker.d.ts.map +1 -1
- package/dist/DateRangePickerContext.d.ts +30 -0
- package/dist/DateRangePickerContext.d.ts.map +1 -0
- package/dist/Dialog.d.ts +5 -5
- package/dist/Dialog.d.ts.map +1 -1
- package/dist/Disclosure.d.ts +25 -5
- 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 +27 -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 +41 -0
- package/dist/Form.d.ts.map +1 -0
- package/dist/GridList.d.ts +69 -10
- package/dist/GridList.d.ts.map +1 -1
- package/dist/HiddenDateInput.d.ts +26 -0
- package/dist/HiddenDateInput.d.ts.map +1 -0
- package/dist/HiddenTimeInput.d.ts +25 -0
- package/dist/HiddenTimeInput.d.ts.map +1 -0
- 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/Landmark.d.ts +3 -3
- package/dist/Landmark.d.ts.map +1 -1
- package/dist/Link.d.ts +10 -4
- package/dist/Link.d.ts.map +1 -1
- package/dist/ListBox.d.ts +73 -11
- 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 +79 -10
- package/dist/Menu.d.ts.map +1 -1
- package/dist/Meter.d.ts +4 -4
- package/dist/Meter.d.ts.map +1 -1
- package/dist/Modal.d.ts +6 -4
- package/dist/Modal.d.ts.map +1 -1
- package/dist/NumberField.d.ts +10 -12
- package/dist/NumberField.d.ts.map +1 -1
- package/dist/Popover.d.ts +32 -7
- 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 +6 -4
- package/dist/ProgressBar.d.ts.map +1 -1
- package/dist/RadioGroup.d.ts +43 -9
- package/dist/RadioGroup.d.ts.map +1 -1
- package/dist/RangeCalendar.d.ts +39 -7
- 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 +23 -21
- package/dist/SearchField.d.ts.map +1 -1
- package/dist/Select.d.ts +48 -7
- 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/Separator.d.ts +9 -3
- package/dist/Separator.d.ts.map +1 -1
- package/dist/SharedElementTransition.d.ts +41 -0
- package/dist/SharedElementTransition.d.ts.map +1 -0
- package/dist/Slider.d.ts +15 -8
- package/dist/Slider.d.ts.map +1 -1
- package/dist/StepList.d.ts +90 -0
- package/dist/StepList.d.ts.map +1 -0
- package/dist/Switch.d.ts +11 -5
- package/dist/Switch.d.ts.map +1 -1
- package/dist/Table.d.ts +222 -19
- package/dist/Table.d.ts.map +1 -1
- package/dist/Tabs.d.ts +47 -10
- package/dist/Tabs.d.ts.map +1 -1
- package/dist/TagGroup.d.ts +22 -10
- 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 +19 -11
- package/dist/TextField.d.ts.map +1 -1
- package/dist/TimeField.d.ts +32 -7
- package/dist/TimeField.d.ts.map +1 -1
- package/dist/Toast.d.ts +29 -14
- package/dist/Toast.d.ts.map +1 -1
- package/dist/ToggleButton.d.ts +36 -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 +7 -3
- package/dist/Toolbar.d.ts.map +1 -1
- package/dist/Tooltip.d.ts +58 -7
- package/dist/Tooltip.d.ts.map +1 -1
- package/dist/Tree.d.ts +102 -11
- 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 +4 -2
- package/dist/VisuallyHidden.d.ts.map +1 -1
- package/dist/contexts.d.ts +6 -1
- package/dist/contexts.d.ts.map +1 -1
- package/dist/index.d.ts +73 -39
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23342 -10644
- package/dist/index.js.map +1 -7
- package/dist/index.jsx +18110 -0
- package/dist/index.jsx.map +1 -0
- package/dist/useDragAndDrop.d.ts +93 -0
- package/dist/useDragAndDrop.d.ts.map +1 -0
- package/dist/utils.d.ts +8 -2
- 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 +33 -32
- package/src/ActionBar.tsx +251 -0
- package/src/ActionGroup.tsx +277 -0
- package/src/Alert.tsx +152 -0
- package/src/Autocomplete.tsx +39 -44
- package/src/Breadcrumbs.tsx +227 -72
- package/src/Button.tsx +315 -74
- package/src/Calendar.tsx +347 -141
- package/src/Checkbox.tsx +414 -123
- package/src/Collection.tsx +350 -0
- package/src/Color.tsx +1325 -284
- package/src/ColorEditor.tsx +213 -0
- package/src/ComboBox.tsx +644 -245
- package/src/ContextualHelpTrigger.tsx +195 -0
- package/src/DateField.tsx +274 -106
- package/src/DatePicker.tsx +892 -111
- package/src/DateRangePickerContext.tsx +44 -0
- package/src/Dialog.tsx +173 -104
- package/src/Disclosure.tsx +158 -105
- package/src/DragAndDrop.tsx +340 -0
- package/src/DragPreview.tsx +47 -0
- package/src/DropZone.tsx +233 -0
- package/src/FieldError.tsx +89 -0
- package/src/FileTrigger.tsx +83 -0
- package/src/Focusable.tsx +103 -0
- package/src/Form.tsx +140 -0
- package/src/GridList.tsx +542 -128
- package/src/HiddenDateInput.tsx +153 -0
- package/src/HiddenTimeInput.tsx +133 -0
- package/src/Icon.tsx +133 -0
- package/src/Keyboard.tsx +26 -0
- package/src/Landmark.tsx +37 -63
- package/src/Link.tsx +132 -69
- package/src/ListBox.tsx +656 -106
- package/src/ListDropTargetDelegate.ts +283 -0
- package/src/Menu.tsx +1234 -132
- package/src/Meter.tsx +44 -58
- package/src/Modal.tsx +262 -166
- package/src/NumberField.tsx +267 -151
- package/src/Popover.tsx +452 -343
- package/src/Pressable.tsx +108 -0
- package/src/ProgressBar.tsx +54 -59
- package/src/RadioGroup.tsx +533 -121
- package/src/RangeCalendar.tsx +249 -150
- package/src/RouterProvider.tsx +223 -0
- package/src/SearchField.tsx +460 -133
- package/src/Select.tsx +804 -233
- package/src/SelectionIndicator.tsx +108 -0
- package/src/Separator.tsx +47 -49
- package/src/SharedElementTransition.tsx +264 -0
- package/src/Slider.tsx +148 -98
- package/src/StepList.tsx +272 -0
- package/src/Switch.tsx +93 -46
- package/src/Table.tsx +1551 -225
- package/src/Tabs.tsx +377 -123
- package/src/TagGroup.tsx +233 -135
- package/src/Text.tsx +18 -0
- package/src/TextField.tsx +413 -86
- package/src/TimeField.tsx +232 -222
- package/src/Toast.tsx +306 -160
- package/src/ToggleButton.tsx +169 -0
- package/src/ToggleButtonGroup.tsx +141 -0
- package/src/Toolbar.tsx +61 -70
- package/src/Tooltip.tsx +473 -116
- package/src/Tree.tsx +1514 -175
- package/src/Virtualizer.tsx +730 -0
- package/src/VirtualizerLayouts.ts +280 -0
- package/src/VisuallyHidden.tsx +32 -38
- package/src/contexts.ts +29 -36
- package/src/index.ts +972 -620
- package/src/useDragAndDrop.ts +367 -0
- package/src/utils.tsx +69 -50
- package/src/virtualizer/Layout.ts +192 -0
- package/dist/index.ssr.js +0 -9785
- package/dist/index.ssr.js.map +0 -7
package/src/Tree.tsx
CHANGED
|
@@ -10,22 +10,25 @@
|
|
|
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,
|
|
17
19
|
useContext,
|
|
18
20
|
For,
|
|
19
21
|
Show,
|
|
20
|
-
} from
|
|
22
|
+
} from "solid-js";
|
|
21
23
|
import {
|
|
22
24
|
createTree,
|
|
23
25
|
createTreeItem,
|
|
24
26
|
createTreeSelectionCheckbox,
|
|
25
27
|
createFocusRing,
|
|
26
28
|
createHover,
|
|
29
|
+
mergeProps,
|
|
27
30
|
type AriaTreeProps,
|
|
28
|
-
} from
|
|
31
|
+
} from "@proyecto-viviana/solidaria";
|
|
29
32
|
import {
|
|
30
33
|
createTreeState,
|
|
31
34
|
createTreeCollection,
|
|
@@ -34,7 +37,9 @@ import {
|
|
|
34
37
|
type TreeNode,
|
|
35
38
|
type TreeItemData,
|
|
36
39
|
type Key,
|
|
37
|
-
|
|
40
|
+
type DropTarget,
|
|
41
|
+
type ItemDropTarget,
|
|
42
|
+
} from "@proyecto-viviana/solid-stately";
|
|
38
43
|
import {
|
|
39
44
|
type RenderChildren,
|
|
40
45
|
type ClassNameOrFunction,
|
|
@@ -42,11 +47,46 @@ import {
|
|
|
42
47
|
type SlotProps,
|
|
43
48
|
useRenderProps,
|
|
44
49
|
filterDOMProps,
|
|
45
|
-
} from
|
|
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";
|
|
72
|
+
import {
|
|
73
|
+
handleLinkClick,
|
|
74
|
+
type LinkDOMProps,
|
|
75
|
+
type RouterOptions,
|
|
76
|
+
useLinkProps,
|
|
77
|
+
useRouter,
|
|
78
|
+
} from "./RouterProvider";
|
|
46
79
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
80
|
+
type RefLike<T> = ((el: T) => void) | { current?: T | null } | undefined;
|
|
81
|
+
|
|
82
|
+
function assignRef<T>(ref: RefLike<T>, el: T): void {
|
|
83
|
+
if (!ref) return;
|
|
84
|
+
if (typeof ref === "function") {
|
|
85
|
+
ref(el);
|
|
86
|
+
} else {
|
|
87
|
+
ref.current = el;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
50
90
|
|
|
51
91
|
export interface TreeRenderProps {
|
|
52
92
|
/** Whether the tree has focus. */
|
|
@@ -59,19 +99,23 @@ export interface TreeRenderProps {
|
|
|
59
99
|
isEmpty: boolean;
|
|
60
100
|
}
|
|
61
101
|
|
|
62
|
-
export interface TreeProps<T extends object> extends Omit<AriaTreeProps,
|
|
102
|
+
export interface TreeProps<T extends object> extends Omit<AriaTreeProps, "children">, SlotProps {
|
|
63
103
|
/** The hierarchical items to render in the tree. */
|
|
64
|
-
items
|
|
104
|
+
items?: CollectionEntry<TreeItemData<T>>[];
|
|
65
105
|
/** The selection mode. */
|
|
66
|
-
selectionMode?:
|
|
106
|
+
selectionMode?: "none" | "single" | "multiple";
|
|
107
|
+
/** The selection behavior (toggle vs replace). */
|
|
108
|
+
selectionBehavior?: "toggle" | "replace";
|
|
109
|
+
/** Whether disabled items can still receive focus. */
|
|
110
|
+
disabledBehavior?: "selection" | "all";
|
|
67
111
|
/** Keys of disabled items. */
|
|
68
112
|
disabledKeys?: Iterable<Key>;
|
|
69
113
|
/** Currently selected keys (controlled). */
|
|
70
|
-
selectedKeys?:
|
|
114
|
+
selectedKeys?: "all" | Iterable<Key>;
|
|
71
115
|
/** Default selected keys (uncontrolled). */
|
|
72
|
-
defaultSelectedKeys?:
|
|
116
|
+
defaultSelectedKeys?: "all" | Iterable<Key>;
|
|
73
117
|
/** Handler called when selection changes. */
|
|
74
|
-
onSelectionChange?: (keys:
|
|
118
|
+
onSelectionChange?: (keys: "all" | Set<Key>) => void;
|
|
75
119
|
/** Currently expanded keys (controlled). */
|
|
76
120
|
expandedKeys?: Iterable<Key>;
|
|
77
121
|
/** Default expanded keys (uncontrolled). */
|
|
@@ -86,6 +130,24 @@ export interface TreeProps<T extends object> extends Omit<AriaTreeProps, 'childr
|
|
|
86
130
|
style?: StyleOrFunction<TreeRenderProps>;
|
|
87
131
|
/** A function to render when the tree is empty. */
|
|
88
132
|
renderEmptyState?: () => JSX.Element;
|
|
133
|
+
/** Whether there are more items to load. */
|
|
134
|
+
hasMore?: boolean;
|
|
135
|
+
/** Whether additional items are currently loading. */
|
|
136
|
+
isLoading?: boolean;
|
|
137
|
+
/** Loading state for async collection parity. */
|
|
138
|
+
loadingState?: "idle" | "loading" | "loadingMore" | "sorting" | "filtering" | "error";
|
|
139
|
+
/** Called when the load more sentinel becomes visible. */
|
|
140
|
+
onLoadMore?: () => void | Promise<void>;
|
|
141
|
+
/** Renders the load-more sentinel content. */
|
|
142
|
+
renderLoadMoreItem?: (state: { isLoading: boolean }) => JSX.Element;
|
|
143
|
+
/** CSS className for the load-more sentinel. */
|
|
144
|
+
loadMoreClass?: ClassNameOrFunction<{ isLoading: boolean }>;
|
|
145
|
+
/** Inline style for the load-more sentinel. */
|
|
146
|
+
loadMoreStyle?: StyleOrFunction<{ isLoading: boolean }>;
|
|
147
|
+
/** Drag and drop hooks from `useDragAndDrop`. */
|
|
148
|
+
dragAndDropHooks?: DragAndDropHooks<T>;
|
|
149
|
+
/** Ref for the rendered tree element. */
|
|
150
|
+
ref?: RefLike<HTMLDivElement>;
|
|
89
151
|
}
|
|
90
152
|
|
|
91
153
|
export interface TreeRenderItemState {
|
|
@@ -116,9 +178,16 @@ export interface TreeItemRenderProps {
|
|
|
116
178
|
isExpandable: boolean;
|
|
117
179
|
/** The nesting level (0 = root). */
|
|
118
180
|
level: number;
|
|
181
|
+
/** The selection mode active on the tree. */
|
|
182
|
+
selectionMode: "none" | "single" | "multiple";
|
|
183
|
+
/** The selection behavior active on the tree. */
|
|
184
|
+
selectionBehavior: "toggle" | "replace";
|
|
119
185
|
}
|
|
120
186
|
|
|
121
|
-
export interface TreeItemProps<T extends object>
|
|
187
|
+
export interface TreeItemProps<T extends object>
|
|
188
|
+
extends
|
|
189
|
+
SlotProps,
|
|
190
|
+
Omit<JSX.HTMLAttributes<HTMLElement>, "class" | "style" | "children" | "id" | "ref"> {
|
|
122
191
|
/** The unique key for the item. */
|
|
123
192
|
id: Key;
|
|
124
193
|
/** The item value. */
|
|
@@ -133,6 +202,21 @@ export interface TreeItemProps<T extends object> extends SlotProps {
|
|
|
133
202
|
textValue?: string;
|
|
134
203
|
/** Handler called when the item is activated. */
|
|
135
204
|
onAction?: () => void;
|
|
205
|
+
/** Whether this item has children that may not be loaded yet. */
|
|
206
|
+
hasChildItems?: boolean;
|
|
207
|
+
/** Whether this item is disabled. */
|
|
208
|
+
isDisabled?: boolean;
|
|
209
|
+
/** Link target metadata. */
|
|
210
|
+
href?: string;
|
|
211
|
+
target?: LinkDOMProps["target"];
|
|
212
|
+
download?: LinkDOMProps["download"];
|
|
213
|
+
rel?: LinkDOMProps["rel"];
|
|
214
|
+
hrefLang?: string;
|
|
215
|
+
ping?: LinkDOMProps["ping"];
|
|
216
|
+
referrerPolicy?: LinkDOMProps["referrerPolicy"];
|
|
217
|
+
routerOptions?: RouterOptions;
|
|
218
|
+
/** Ref for the rendered row element. */
|
|
219
|
+
ref?: RefLike<HTMLElement>;
|
|
136
220
|
}
|
|
137
221
|
|
|
138
222
|
export interface TreeExpandButtonProps {
|
|
@@ -142,17 +226,548 @@ export interface TreeExpandButtonProps {
|
|
|
142
226
|
style?: JSX.CSSProperties;
|
|
143
227
|
/** Children to render inside the button. */
|
|
144
228
|
children?: JSX.Element | ((props: { isExpanded: boolean }) => JSX.Element);
|
|
229
|
+
[key: `data-${string}`]: string | undefined;
|
|
145
230
|
}
|
|
146
231
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
232
|
+
export interface TreeLoadMoreItemProps extends SlotProps {
|
|
233
|
+
onLoadMore: () => void | Promise<void>;
|
|
234
|
+
isLoading?: boolean;
|
|
235
|
+
loadingState?: "idle" | "loading" | "loadingMore" | "sorting" | "filtering" | "error";
|
|
236
|
+
level?: number;
|
|
237
|
+
/** Scroll offset multiplier for early loading trigger (default: 1 = 100% of viewport height). */
|
|
238
|
+
scrollOffset?: number;
|
|
239
|
+
children?: JSX.Element;
|
|
240
|
+
class?: ClassNameOrFunction<{ isLoading: boolean }>;
|
|
241
|
+
style?: StyleOrFunction<{ isLoading: boolean }>;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export interface TreeSectionProps extends SectionProps {}
|
|
245
|
+
export interface TreeHeaderProps extends HeaderProps {}
|
|
150
246
|
|
|
151
247
|
interface TreeContextValue<T extends object> {
|
|
152
248
|
state: TreeState<T, TreeCollection<T>>;
|
|
153
249
|
collection: TreeCollection<T>;
|
|
154
250
|
isDisabled: boolean;
|
|
155
251
|
renderItem: (item: TreeItemData<T>, state: TreeRenderItemState) => JSX.Element;
|
|
252
|
+
dragAndDropHooks?: DragAndDropHooks<T>;
|
|
253
|
+
dragState?: unknown;
|
|
254
|
+
dropState?: unknown;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
interface TreeDropTargetDelegate {
|
|
258
|
+
getDropTargetFromPoint: (
|
|
259
|
+
x: number,
|
|
260
|
+
y: number,
|
|
261
|
+
isValidDropTarget: (target: DropTarget) => boolean,
|
|
262
|
+
) => DropTarget | null;
|
|
263
|
+
getKeyboardNavigationTarget?: (
|
|
264
|
+
target: DropTarget | null,
|
|
265
|
+
direction: "next" | "previous",
|
|
266
|
+
isValidDropTarget: (target: DropTarget) => boolean,
|
|
267
|
+
) => DropTarget | null;
|
|
268
|
+
getKeyboardPageNavigationTarget?: (
|
|
269
|
+
target: DropTarget | null,
|
|
270
|
+
direction: "next" | "previous",
|
|
271
|
+
isValidDropTarget: (target: DropTarget) => boolean,
|
|
272
|
+
) => DropTarget | null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
interface PointerTrackingState {
|
|
276
|
+
lastY: number;
|
|
277
|
+
lastX: number;
|
|
278
|
+
yDirection: "up" | "down" | null;
|
|
279
|
+
xDirection: "left" | "right" | null;
|
|
280
|
+
boundaryContext: {
|
|
281
|
+
parentKey: Key;
|
|
282
|
+
lastSwitchY: number;
|
|
283
|
+
lastSwitchX: number;
|
|
284
|
+
preferredTargetIndex?: number;
|
|
285
|
+
} | null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const X_SWITCH_THRESHOLD = 10;
|
|
289
|
+
const Y_SWITCH_THRESHOLD = 5;
|
|
290
|
+
const EXPANSION_KEYS = {
|
|
291
|
+
expand: { ltr: "ArrowRight", rtl: "ArrowLeft" },
|
|
292
|
+
collapse: { ltr: "ArrowLeft", rtl: "ArrowRight" },
|
|
293
|
+
} as const;
|
|
294
|
+
|
|
295
|
+
function resolveTreeDirection(element: HTMLElement | null): "ltr" | "rtl" {
|
|
296
|
+
if (element) {
|
|
297
|
+
const dir = element.closest("[dir]")?.getAttribute("dir");
|
|
298
|
+
if (dir === "rtl") return "rtl";
|
|
299
|
+
if (dir === "ltr") return "ltr";
|
|
300
|
+
if (typeof window !== "undefined" && typeof window.getComputedStyle === "function") {
|
|
301
|
+
const computedDirection = window.getComputedStyle(element).direction;
|
|
302
|
+
if (computedDirection === "rtl") return "rtl";
|
|
303
|
+
if (computedDirection === "ltr") return "ltr";
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (typeof document !== "undefined") {
|
|
307
|
+
return document.dir === "rtl" ? "rtl" : "ltr";
|
|
308
|
+
}
|
|
309
|
+
return "ltr";
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function createTreeDropTargetDelegate<T extends object>(
|
|
313
|
+
delegate: TreeDropTargetDelegate,
|
|
314
|
+
state: TreeState<T, TreeCollection<T>>,
|
|
315
|
+
direction: "ltr" | "rtl",
|
|
316
|
+
baseKeyboardNav?: (
|
|
317
|
+
target: DropTarget | null,
|
|
318
|
+
direction: "next" | "previous",
|
|
319
|
+
isValidDropTarget: (target: DropTarget) => boolean,
|
|
320
|
+
) => DropTarget | null,
|
|
321
|
+
): TreeDropTargetDelegate {
|
|
322
|
+
const pointerTracking: PointerTrackingState = {
|
|
323
|
+
lastY: 0,
|
|
324
|
+
lastX: 0,
|
|
325
|
+
yDirection: null,
|
|
326
|
+
xDirection: null,
|
|
327
|
+
boundaryContext: null,
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const getPotentialTargets = (
|
|
331
|
+
originalTarget: ItemDropTarget,
|
|
332
|
+
isValidDropTarget: (target: DropTarget) => boolean,
|
|
333
|
+
): ItemDropTarget[] => {
|
|
334
|
+
if (originalTarget.dropPosition === "on") return [originalTarget];
|
|
335
|
+
|
|
336
|
+
const collection = state.collection;
|
|
337
|
+
const getNodeNextKey = (node: TreeNode<T> | null | undefined): Key | null => {
|
|
338
|
+
if (!node) return null;
|
|
339
|
+
const declaredNextKey = (node as TreeNode<T> & { nextKey?: Key | null }).nextKey;
|
|
340
|
+
return declaredNextKey ?? null;
|
|
341
|
+
};
|
|
342
|
+
const target: ItemDropTarget = { ...originalTarget };
|
|
343
|
+
let currentItem = collection.getItem(target.key);
|
|
344
|
+
while (currentItem && currentItem.type !== "item") {
|
|
345
|
+
const nextKey = getNodeNextKey(currentItem);
|
|
346
|
+
if (nextKey == null) break;
|
|
347
|
+
target.key = nextKey;
|
|
348
|
+
currentItem = collection.getItem(nextKey);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const potentialTargets: ItemDropTarget[] = [target];
|
|
352
|
+
|
|
353
|
+
if (
|
|
354
|
+
currentItem &&
|
|
355
|
+
currentItem.hasChildNodes &&
|
|
356
|
+
state.expandedKeys.has(currentItem.key) &&
|
|
357
|
+
target.dropPosition === "after"
|
|
358
|
+
) {
|
|
359
|
+
let firstChildItemNode: TreeNode<T> | null = null;
|
|
360
|
+
for (const child of collection.getChildren(currentItem.key)) {
|
|
361
|
+
if (child.type === "item") {
|
|
362
|
+
firstChildItemNode = child;
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (firstChildItemNode) {
|
|
368
|
+
const beforeFirstChildTarget: ItemDropTarget = {
|
|
369
|
+
type: "item",
|
|
370
|
+
key: firstChildItemNode.key,
|
|
371
|
+
dropPosition: "before",
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
if (isValidDropTarget(beforeFirstChildTarget)) {
|
|
375
|
+
return [beforeFirstChildTarget];
|
|
376
|
+
}
|
|
377
|
+
return [];
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (getNodeNextKey(currentItem) != null) {
|
|
382
|
+
return [originalTarget];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
let parentKey = currentItem?.parentKey ?? null;
|
|
386
|
+
const ancestorTargets: ItemDropTarget[] = [];
|
|
387
|
+
while (parentKey != null) {
|
|
388
|
+
const parentItem = collection.getItem(parentKey);
|
|
389
|
+
const nextKey = getNodeNextKey(parentItem);
|
|
390
|
+
const nextItem = nextKey != null ? collection.getItem(nextKey) : null;
|
|
391
|
+
const isLastChildAtLevel = !nextItem || nextItem.parentKey !== parentKey;
|
|
392
|
+
|
|
393
|
+
if (isLastChildAtLevel) {
|
|
394
|
+
const afterParentTarget: ItemDropTarget = {
|
|
395
|
+
type: "item",
|
|
396
|
+
key: parentKey,
|
|
397
|
+
dropPosition: "after",
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
if (isValidDropTarget(afterParentTarget)) {
|
|
401
|
+
ancestorTargets.push(afterParentTarget);
|
|
402
|
+
}
|
|
403
|
+
if (nextItem) break;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
parentKey = parentItem?.parentKey ?? null;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (ancestorTargets.length > 0) {
|
|
410
|
+
potentialTargets.push(...ancestorTargets);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (potentialTargets.length === 1) {
|
|
414
|
+
const nextKey = collection.getKeyAfter(target.key);
|
|
415
|
+
const nextNode = nextKey != null ? collection.getItem(nextKey) : null;
|
|
416
|
+
if (
|
|
417
|
+
nextKey != null &&
|
|
418
|
+
nextNode &&
|
|
419
|
+
currentItem &&
|
|
420
|
+
nextNode.level != null &&
|
|
421
|
+
currentItem.level != null &&
|
|
422
|
+
nextNode.level > currentItem.level
|
|
423
|
+
) {
|
|
424
|
+
const beforeTarget: ItemDropTarget = {
|
|
425
|
+
type: "item",
|
|
426
|
+
key: nextKey,
|
|
427
|
+
dropPosition: "before",
|
|
428
|
+
};
|
|
429
|
+
if (isValidDropTarget(beforeTarget)) return [beforeTarget];
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return potentialTargets.filter((candidate) => isValidDropTarget(candidate));
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const selectTarget = (
|
|
437
|
+
potentialTargets: ItemDropTarget[],
|
|
438
|
+
originalTarget: ItemDropTarget,
|
|
439
|
+
x: number,
|
|
440
|
+
y: number,
|
|
441
|
+
currentYMovement: "up" | "down" | null,
|
|
442
|
+
currentXMovement: "left" | "right" | null,
|
|
443
|
+
): ItemDropTarget => {
|
|
444
|
+
if (potentialTargets.length < 2) return potentialTargets[0];
|
|
445
|
+
|
|
446
|
+
const currentItem = state.collection.getItem(originalTarget.key);
|
|
447
|
+
const parentKey = currentItem?.parentKey;
|
|
448
|
+
if (parentKey == null) return potentialTargets[0];
|
|
449
|
+
|
|
450
|
+
if (
|
|
451
|
+
!pointerTracking.boundaryContext ||
|
|
452
|
+
pointerTracking.boundaryContext.parentKey !== parentKey
|
|
453
|
+
) {
|
|
454
|
+
const initialTargetIndex =
|
|
455
|
+
pointerTracking.yDirection === "up" ? potentialTargets.length - 1 : 0;
|
|
456
|
+
pointerTracking.boundaryContext = {
|
|
457
|
+
parentKey,
|
|
458
|
+
preferredTargetIndex: initialTargetIndex,
|
|
459
|
+
lastSwitchY: y,
|
|
460
|
+
lastSwitchX: x,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const boundaryContext = pointerTracking.boundaryContext;
|
|
465
|
+
const distanceFromLastXSwitch = Math.abs(x - boundaryContext.lastSwitchX);
|
|
466
|
+
const distanceFromLastYSwitch = Math.abs(y - boundaryContext.lastSwitchY);
|
|
467
|
+
|
|
468
|
+
if (distanceFromLastYSwitch > Y_SWITCH_THRESHOLD && currentYMovement) {
|
|
469
|
+
const currentIndex = boundaryContext.preferredTargetIndex ?? 0;
|
|
470
|
+
if (currentYMovement === "down" && currentIndex === 0) {
|
|
471
|
+
boundaryContext.preferredTargetIndex = potentialTargets.length - 1;
|
|
472
|
+
} else if (currentYMovement === "up" && currentIndex === potentialTargets.length - 1) {
|
|
473
|
+
boundaryContext.preferredTargetIndex = 0;
|
|
474
|
+
}
|
|
475
|
+
pointerTracking.xDirection = null;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (distanceFromLastXSwitch > X_SWITCH_THRESHOLD && currentXMovement) {
|
|
479
|
+
const currentTargetIndex = boundaryContext.preferredTargetIndex ?? 0;
|
|
480
|
+
|
|
481
|
+
if (currentXMovement === "left") {
|
|
482
|
+
if (direction === "ltr") {
|
|
483
|
+
if (currentTargetIndex < potentialTargets.length - 1) {
|
|
484
|
+
boundaryContext.preferredTargetIndex = currentTargetIndex + 1;
|
|
485
|
+
boundaryContext.lastSwitchX = x;
|
|
486
|
+
}
|
|
487
|
+
} else if (currentTargetIndex > 0) {
|
|
488
|
+
boundaryContext.preferredTargetIndex = currentTargetIndex - 1;
|
|
489
|
+
boundaryContext.lastSwitchX = x;
|
|
490
|
+
}
|
|
491
|
+
} else if (currentXMovement === "right") {
|
|
492
|
+
if (direction === "ltr") {
|
|
493
|
+
if (currentTargetIndex > 0) {
|
|
494
|
+
boundaryContext.preferredTargetIndex = currentTargetIndex - 1;
|
|
495
|
+
boundaryContext.lastSwitchX = x;
|
|
496
|
+
}
|
|
497
|
+
} else if (currentTargetIndex < potentialTargets.length - 1) {
|
|
498
|
+
boundaryContext.preferredTargetIndex = currentTargetIndex + 1;
|
|
499
|
+
boundaryContext.lastSwitchX = x;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
pointerTracking.yDirection = null;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const targetIndex = Math.max(
|
|
507
|
+
0,
|
|
508
|
+
Math.min(boundaryContext.preferredTargetIndex ?? 0, potentialTargets.length - 1),
|
|
509
|
+
);
|
|
510
|
+
return potentialTargets[targetIndex];
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// --- Tree-aware keyboard DnD navigation (RAC parity) ---
|
|
514
|
+
const getKeyboardNavigationTarget = (
|
|
515
|
+
target: DropTarget | null,
|
|
516
|
+
dir: "next" | "previous",
|
|
517
|
+
isValidDropTarget: (target: DropTarget) => boolean,
|
|
518
|
+
): DropTarget | null => {
|
|
519
|
+
const collection = state.collection;
|
|
520
|
+
|
|
521
|
+
// If the target key is not a visible row (e.g. collapsed/hidden child node),
|
|
522
|
+
// fall back to the base (non-override) index-based navigation to avoid infinite recursion.
|
|
523
|
+
// The collection keyMap contains ALL nodes (even collapsed), so check visible rows instead.
|
|
524
|
+
if (target && target.type === "item") {
|
|
525
|
+
const node = collection.getItem(target.key);
|
|
526
|
+
const isVisibleRow =
|
|
527
|
+
node != null && (node as TreeNode<T> & { rowIndex?: number }).rowIndex != null;
|
|
528
|
+
if (!isVisibleRow) {
|
|
529
|
+
return baseKeyboardNav?.(target, dir, isValidDropTarget) ?? null;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Helpers
|
|
534
|
+
const tryValid = (t: DropTarget): DropTarget | null => (isValidDropTarget(t) ? t : null);
|
|
535
|
+
|
|
536
|
+
const getNodeNextKey = (node: TreeNode<T> | null | undefined): Key | null => {
|
|
537
|
+
if (!node) return null;
|
|
538
|
+
return (node as TreeNode<T> & { nextKey?: Key | null }).nextKey ?? null;
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const isExpanded = (key: Key): boolean => {
|
|
542
|
+
const node = collection.getItem(key);
|
|
543
|
+
if (!node || !node.hasChildNodes) return false;
|
|
544
|
+
return state.expandedKeys.has(key);
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
const getFirstChildItemKey = (key: Key): Key | null => {
|
|
548
|
+
for (const child of collection.getChildren(key)) {
|
|
549
|
+
if (child.type === "item") return child.key;
|
|
550
|
+
}
|
|
551
|
+
return null;
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const getLastChildItemKey = (key: Key): Key | null => {
|
|
555
|
+
let lastKey: Key | null = null;
|
|
556
|
+
for (const child of collection.getChildren(key)) {
|
|
557
|
+
if (child.type === "item") lastKey = child.key;
|
|
558
|
+
}
|
|
559
|
+
return lastKey;
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
// Find the deepest last expanded descendant (for "previous" from 'after')
|
|
563
|
+
const getDeepestLastChild = (key: Key): Key => {
|
|
564
|
+
let current = key;
|
|
565
|
+
while (isExpanded(current)) {
|
|
566
|
+
const lastChild = getLastChildItemKey(current);
|
|
567
|
+
if (lastChild == null) break;
|
|
568
|
+
current = lastChild;
|
|
569
|
+
}
|
|
570
|
+
return current;
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
if (dir === "next") {
|
|
574
|
+
// From null → root
|
|
575
|
+
if (!target) {
|
|
576
|
+
return tryValid({ type: "root" });
|
|
577
|
+
}
|
|
578
|
+
// From root → first item 'before'
|
|
579
|
+
if (target.type === "root") {
|
|
580
|
+
const firstKey = collection.getFirstKey();
|
|
581
|
+
if (firstKey != null) {
|
|
582
|
+
return tryValid({ type: "item", key: firstKey, dropPosition: "before" });
|
|
583
|
+
}
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
if (target.type === "item") {
|
|
587
|
+
switch (target.dropPosition) {
|
|
588
|
+
case "before":
|
|
589
|
+
return (
|
|
590
|
+
tryValid({ type: "item", key: target.key, dropPosition: "on" }) ??
|
|
591
|
+
tryValid({ type: "item", key: target.key, dropPosition: "after" })
|
|
592
|
+
);
|
|
593
|
+
case "on": {
|
|
594
|
+
// If item is expanded and has children, go to first child 'before'
|
|
595
|
+
if (isExpanded(target.key)) {
|
|
596
|
+
const firstChild = getFirstChildItemKey(target.key);
|
|
597
|
+
if (firstChild != null) {
|
|
598
|
+
return (
|
|
599
|
+
tryValid({ type: "item", key: firstChild, dropPosition: "before" }) ??
|
|
600
|
+
tryValid({ type: "item", key: firstChild, dropPosition: "on" })
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
// Otherwise, next item in collection or 'after'
|
|
605
|
+
const nextKey = collection.getKeyAfter(target.key);
|
|
606
|
+
const targetNode = collection.getItem(target.key);
|
|
607
|
+
const nextNode = nextKey != null ? collection.getItem(nextKey) : null;
|
|
608
|
+
if (
|
|
609
|
+
targetNode &&
|
|
610
|
+
nextNode &&
|
|
611
|
+
nextNode.level != null &&
|
|
612
|
+
targetNode.level != null &&
|
|
613
|
+
nextNode.level >= targetNode.level
|
|
614
|
+
) {
|
|
615
|
+
return (
|
|
616
|
+
tryValid({ type: "item", key: nextNode.key, dropPosition: "before" }) ??
|
|
617
|
+
tryValid({ type: "item", key: target.key, dropPosition: "after" })
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
return tryValid({ type: "item", key: target.key, dropPosition: "after" });
|
|
621
|
+
}
|
|
622
|
+
case "after": {
|
|
623
|
+
// If item is expanded (and we're at 'after'), first child
|
|
624
|
+
if (isExpanded(target.key)) {
|
|
625
|
+
const firstChild = getFirstChildItemKey(target.key);
|
|
626
|
+
if (firstChild != null) {
|
|
627
|
+
return (
|
|
628
|
+
tryValid({ type: "item", key: firstChild, dropPosition: "before" }) ??
|
|
629
|
+
tryValid({ type: "item", key: firstChild, dropPosition: "on" })
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
// Check if this is the last sibling at its level
|
|
634
|
+
const targetNode = collection.getItem(target.key);
|
|
635
|
+
const nextSiblingKey = getNodeNextKey(targetNode);
|
|
636
|
+
if (nextSiblingKey != null) {
|
|
637
|
+
const nextSibling = collection.getItem(nextSiblingKey);
|
|
638
|
+
if (nextSibling?.type === "item") {
|
|
639
|
+
return (
|
|
640
|
+
tryValid({ type: "item", key: nextSibling.key, dropPosition: "before" }) ??
|
|
641
|
+
tryValid({ type: "item", key: nextSibling.key, dropPosition: "on" })
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
// Traverse up to parent when at last sibling
|
|
646
|
+
if (targetNode?.parentKey != null) {
|
|
647
|
+
const parentNode = collection.getItem(targetNode.parentKey);
|
|
648
|
+
const parentNextKey = getNodeNextKey(parentNode);
|
|
649
|
+
const parentNextNode =
|
|
650
|
+
parentNextKey != null ? collection.getItem(parentNextKey) : null;
|
|
651
|
+
if (parentNextNode?.type === "item") {
|
|
652
|
+
return tryValid({ type: "item", key: parentNextNode.key, dropPosition: "before" });
|
|
653
|
+
}
|
|
654
|
+
if (parentNode?.type === "item") {
|
|
655
|
+
return tryValid({ type: "item", key: parentNode.key, dropPosition: "after" });
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// Reached end — try next item in flat collection
|
|
659
|
+
const nextKey = collection.getKeyAfter(target.key);
|
|
660
|
+
if (nextKey != null) {
|
|
661
|
+
return (
|
|
662
|
+
tryValid({ type: "item", key: nextKey, dropPosition: "before" }) ??
|
|
663
|
+
tryValid({ type: "item", key: nextKey, dropPosition: "on" })
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
return tryValid({ type: "root" });
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// From null or root → last root-level item 'after'
|
|
674
|
+
if (!target || target.type === "root") {
|
|
675
|
+
const lastKey = collection.getLastKey();
|
|
676
|
+
if (lastKey != null) {
|
|
677
|
+
// Find root-level ancestor of last key
|
|
678
|
+
let rootKey = lastKey;
|
|
679
|
+
let node = collection.getItem(lastKey);
|
|
680
|
+
while (node?.parentKey != null) {
|
|
681
|
+
rootKey = node.parentKey;
|
|
682
|
+
node = collection.getItem(rootKey);
|
|
683
|
+
}
|
|
684
|
+
return tryValid({ type: "item", key: rootKey, dropPosition: "after" });
|
|
685
|
+
}
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (target.type === "item") {
|
|
690
|
+
switch (target.dropPosition) {
|
|
691
|
+
case "after": {
|
|
692
|
+
// If expanded with children, go to deepest last child 'after'
|
|
693
|
+
const deepest = getDeepestLastChild(target.key);
|
|
694
|
+
if (deepest !== target.key) {
|
|
695
|
+
return (
|
|
696
|
+
tryValid({ type: "item", key: deepest, dropPosition: "after" }) ??
|
|
697
|
+
tryValid({ type: "item", key: target.key, dropPosition: "on" })
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
return tryValid({ type: "item", key: target.key, dropPosition: "on" });
|
|
701
|
+
}
|
|
702
|
+
case "on":
|
|
703
|
+
return tryValid({ type: "item", key: target.key, dropPosition: "before" });
|
|
704
|
+
case "before": {
|
|
705
|
+
// Move to the previous sibling's deepest last child 'after'
|
|
706
|
+
const prevKey = collection.getKeyBefore(target.key);
|
|
707
|
+
if (prevKey != null) {
|
|
708
|
+
const deepest = getDeepestLastChild(prevKey);
|
|
709
|
+
return (
|
|
710
|
+
tryValid({ type: "item", key: deepest, dropPosition: "after" }) ??
|
|
711
|
+
tryValid({ type: "item", key: prevKey, dropPosition: "on" })
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
// No previous — go to root
|
|
715
|
+
return tryValid({ type: "root" });
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return null;
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
return {
|
|
724
|
+
getDropTargetFromPoint(x, y, isValidDropTarget) {
|
|
725
|
+
const baseTarget = delegate.getDropTargetFromPoint(x, y, isValidDropTarget);
|
|
726
|
+
if (!baseTarget || baseTarget.type === "root") return baseTarget;
|
|
727
|
+
|
|
728
|
+
const deltaY = y - pointerTracking.lastY;
|
|
729
|
+
const deltaX = x - pointerTracking.lastX;
|
|
730
|
+
let currentYMovement: "up" | "down" | null = pointerTracking.yDirection;
|
|
731
|
+
let currentXMovement: "left" | "right" | null = pointerTracking.xDirection;
|
|
732
|
+
|
|
733
|
+
if (Math.abs(deltaY) > Y_SWITCH_THRESHOLD) {
|
|
734
|
+
currentYMovement = deltaY > 0 ? "down" : "up";
|
|
735
|
+
pointerTracking.yDirection = currentYMovement;
|
|
736
|
+
pointerTracking.lastY = y;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (Math.abs(deltaX) > X_SWITCH_THRESHOLD) {
|
|
740
|
+
currentXMovement = deltaX > 0 ? "right" : "left";
|
|
741
|
+
pointerTracking.xDirection = currentXMovement;
|
|
742
|
+
pointerTracking.lastX = x;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
let target: ItemDropTarget = baseTarget;
|
|
746
|
+
if (target.dropPosition === "before") {
|
|
747
|
+
const keyBefore = state.collection.getKeyBefore(target.key);
|
|
748
|
+
if (keyBefore != null) {
|
|
749
|
+
const normalized: ItemDropTarget = {
|
|
750
|
+
type: "item",
|
|
751
|
+
key: keyBefore,
|
|
752
|
+
dropPosition: "after",
|
|
753
|
+
};
|
|
754
|
+
if (isValidDropTarget(normalized)) target = normalized;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const potentialTargets = getPotentialTargets(target, isValidDropTarget);
|
|
759
|
+
if (potentialTargets.length === 0) return { type: "root" };
|
|
760
|
+
|
|
761
|
+
if (potentialTargets.length > 1) {
|
|
762
|
+
return selectTarget(potentialTargets, target, x, y, currentYMovement, currentXMovement);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
pointerTracking.boundaryContext = null;
|
|
766
|
+
return potentialTargets[0];
|
|
767
|
+
},
|
|
768
|
+
getKeyboardNavigationTarget,
|
|
769
|
+
getKeyboardPageNavigationTarget: delegate.getKeyboardPageNavigationTarget?.bind(delegate),
|
|
770
|
+
};
|
|
156
771
|
}
|
|
157
772
|
|
|
158
773
|
interface TreeItemContextValue<T extends object> {
|
|
@@ -163,12 +778,33 @@ interface TreeItemContextValue<T extends object> {
|
|
|
163
778
|
}
|
|
164
779
|
|
|
165
780
|
export const TreeContext = createContext<TreeContextValue<object> | null>(null);
|
|
166
|
-
export const TreeStateContext = createContext<TreeState<object, TreeCollection<object>> | null>(
|
|
781
|
+
export const TreeStateContext = createContext<TreeState<object, TreeCollection<object>> | null>(
|
|
782
|
+
null,
|
|
783
|
+
);
|
|
167
784
|
export const TreeItemContext = createContext<TreeItemContextValue<object> | null>(null);
|
|
785
|
+
const TreeItemContentContext = createContext<TreeItemRenderProps | null>(null);
|
|
168
786
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
787
|
+
function isTreeItemRecord(value: unknown): value is Record<PropertyKey, unknown> {
|
|
788
|
+
return typeof value === "object" && value !== null;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function treeItemDataFromNode<T extends object>(node: TreeNode<T>): TreeItemData<T> {
|
|
792
|
+
const value = node.value;
|
|
793
|
+
const item: Record<PropertyKey, unknown> = isTreeItemRecord(value) ? value : {};
|
|
794
|
+
return {
|
|
795
|
+
...item,
|
|
796
|
+
key: node.key,
|
|
797
|
+
id: (item.id as Key | undefined) ?? node.key,
|
|
798
|
+
value: (value ?? undefined) as T | undefined,
|
|
799
|
+
textValue: node.textValue,
|
|
800
|
+
isDisabled: node.isDisabled,
|
|
801
|
+
hasChildItems: node.hasChildNodes,
|
|
802
|
+
children:
|
|
803
|
+
node.childNodes.length > 0
|
|
804
|
+
? node.childNodes.map((child) => treeItemDataFromNode(child))
|
|
805
|
+
: undefined,
|
|
806
|
+
};
|
|
807
|
+
}
|
|
172
808
|
|
|
173
809
|
/**
|
|
174
810
|
* A tree displays hierarchical data with expandable/collapsible nodes,
|
|
@@ -177,29 +813,51 @@ export const TreeItemContext = createContext<TreeItemContextValue<object> | null
|
|
|
177
813
|
export function Tree<T extends object>(props: TreeProps<T>): JSX.Element {
|
|
178
814
|
const [local, stateProps, ariaProps] = splitProps(
|
|
179
815
|
props,
|
|
180
|
-
['class', 'style', 'slot', 'renderEmptyState'],
|
|
181
816
|
[
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
817
|
+
"class",
|
|
818
|
+
"style",
|
|
819
|
+
"slot",
|
|
820
|
+
"renderEmptyState",
|
|
821
|
+
"hasMore",
|
|
822
|
+
"isLoading",
|
|
823
|
+
"loadingState",
|
|
824
|
+
"onLoadMore",
|
|
825
|
+
"renderLoadMoreItem",
|
|
826
|
+
"loadMoreClass",
|
|
827
|
+
"loadMoreStyle",
|
|
828
|
+
"dragAndDropHooks",
|
|
829
|
+
"ref",
|
|
830
|
+
],
|
|
831
|
+
[
|
|
832
|
+
"items",
|
|
833
|
+
"disabledKeys",
|
|
834
|
+
"disabledBehavior",
|
|
835
|
+
"selectionMode",
|
|
836
|
+
"selectionBehavior",
|
|
837
|
+
"selectedKeys",
|
|
838
|
+
"defaultSelectedKeys",
|
|
839
|
+
"onSelectionChange",
|
|
840
|
+
"expandedKeys",
|
|
841
|
+
"defaultExpandedKeys",
|
|
842
|
+
"onExpandedChange",
|
|
843
|
+
],
|
|
192
844
|
);
|
|
193
845
|
|
|
194
|
-
// Create ref signal
|
|
195
846
|
const [ref, setRef] = createSignal<HTMLDivElement | null>(null);
|
|
847
|
+
const flatItems = createMemo<TreeItemData<T>[]>(() =>
|
|
848
|
+
flattenCollectionEntries(stateProps.items ?? []),
|
|
849
|
+
);
|
|
850
|
+
const hasSections = createMemo(() =>
|
|
851
|
+
(stateProps.items ?? []).some((entry) => isCollectionSection(entry)),
|
|
852
|
+
);
|
|
196
853
|
|
|
197
|
-
// Create tree state
|
|
198
854
|
const state = createTreeState<T, TreeCollection<T>>(() => ({
|
|
199
855
|
collectionFactory: (expandedKeys) =>
|
|
200
|
-
createTreeCollection(
|
|
856
|
+
createTreeCollection(flatItems(), expandedKeys) as TreeCollection<T>,
|
|
201
857
|
disabledKeys: stateProps.disabledKeys,
|
|
858
|
+
disabledBehavior: stateProps.disabledBehavior,
|
|
202
859
|
selectionMode: stateProps.selectionMode,
|
|
860
|
+
selectionBehavior: stateProps.selectionBehavior,
|
|
203
861
|
selectedKeys: stateProps.selectedKeys,
|
|
204
862
|
defaultSelectedKeys: stateProps.defaultSelectedKeys,
|
|
205
863
|
onSelectionChange: stateProps.onSelectionChange,
|
|
@@ -208,49 +866,60 @@ export function Tree<T extends object>(props: TreeProps<T>): JSX.Element {
|
|
|
208
866
|
onExpandedChange: stateProps.onExpandedChange,
|
|
209
867
|
}));
|
|
210
868
|
|
|
211
|
-
|
|
869
|
+
const [lastExpandedKeys, setLastExpandedKeys] = createSignal<Set<Key>>(new Set());
|
|
870
|
+
const [lastItemsLength, setLastItemsLength] = createSignal(flatItems().length);
|
|
871
|
+
const [collectionVersion, setCollectionVersion] = createSignal(0);
|
|
872
|
+
createEffect(() => {
|
|
873
|
+
const expanded = state.expandedKeys;
|
|
874
|
+
const items = flatItems();
|
|
875
|
+
if (!areSetsEqual(lastExpandedKeys(), expanded) || lastItemsLength() !== items.length) {
|
|
876
|
+
setLastExpandedKeys(new Set(expanded));
|
|
877
|
+
setLastItemsLength(items.length);
|
|
878
|
+
setCollectionVersion((v) => v + 1);
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
// Resolve writing direction for keyboard expand/collapse parity
|
|
883
|
+
const treeDirection = createMemo(() => ariaProps.direction ?? resolveTreeDirection(ref()));
|
|
884
|
+
|
|
212
885
|
const { treeProps } = createTree<T, TreeCollection<T>>(
|
|
213
886
|
() => ({
|
|
214
887
|
id: ariaProps.id,
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
888
|
+
"aria-label": ariaProps["aria-label"],
|
|
889
|
+
"aria-labelledby": ariaProps["aria-labelledby"],
|
|
890
|
+
"aria-describedby": ariaProps["aria-describedby"],
|
|
218
891
|
isVirtualized: ariaProps.isVirtualized,
|
|
219
892
|
onAction: ariaProps.onAction,
|
|
220
893
|
isDisabled: ariaProps.isDisabled,
|
|
894
|
+
direction: treeDirection(),
|
|
221
895
|
}),
|
|
222
896
|
() => state,
|
|
223
|
-
ref
|
|
897
|
+
ref,
|
|
224
898
|
);
|
|
225
899
|
|
|
226
|
-
// Create focus ring
|
|
227
900
|
const { isFocused, isFocusVisible, focusProps } = createFocusRing();
|
|
228
901
|
|
|
229
|
-
// Render props values
|
|
230
902
|
const renderValues = createMemo<TreeRenderProps>(() => ({
|
|
231
903
|
isFocused: state.isFocused || isFocused(),
|
|
232
904
|
isFocusVisible: isFocusVisible(),
|
|
233
905
|
isDisabled: ariaProps.isDisabled ?? false,
|
|
234
|
-
isEmpty:
|
|
906
|
+
isEmpty: flatItems().length === 0,
|
|
235
907
|
}));
|
|
236
908
|
|
|
237
|
-
// Resolve render props
|
|
238
909
|
const renderProps = useRenderProps(
|
|
239
910
|
{
|
|
240
911
|
class: local.class,
|
|
241
912
|
style: local.style,
|
|
242
|
-
defaultClassName:
|
|
913
|
+
defaultClassName: "solidaria-Tree",
|
|
243
914
|
},
|
|
244
|
-
renderValues
|
|
915
|
+
renderValues,
|
|
245
916
|
);
|
|
246
917
|
|
|
247
|
-
// Filter DOM props
|
|
248
918
|
const domProps = createMemo(() => {
|
|
249
919
|
const filtered = filterDOMProps(ariaProps as Record<string, unknown>, { global: true });
|
|
250
920
|
return filtered;
|
|
251
921
|
});
|
|
252
922
|
|
|
253
|
-
// Remove ref from spread props
|
|
254
923
|
const cleanTreeProps = () => {
|
|
255
924
|
const { ref: _ref1, ...rest } = treeProps as Record<string, unknown>;
|
|
256
925
|
return rest;
|
|
@@ -260,61 +929,502 @@ export function Tree<T extends object>(props: TreeProps<T>): JSX.Element {
|
|
|
260
929
|
return rest;
|
|
261
930
|
};
|
|
262
931
|
|
|
263
|
-
const isEmpty = () =>
|
|
932
|
+
const isEmpty = () => flatItems().length === 0;
|
|
933
|
+
|
|
934
|
+
const visibleRows = createMemo(() => {
|
|
935
|
+
collectionVersion();
|
|
936
|
+
return state.collection.rows;
|
|
937
|
+
});
|
|
938
|
+
const virtualizer = useVirtualizerContext();
|
|
939
|
+
const parentCollectionRenderer = useCollectionRenderer<TreeItemData<T>>();
|
|
940
|
+
const getDropTargetByIndex = (
|
|
941
|
+
index: number,
|
|
942
|
+
position: "before" | "after" | "on",
|
|
943
|
+
): DropTarget | null => {
|
|
944
|
+
const node = visibleRows()[index];
|
|
945
|
+
if (!node) return null;
|
|
946
|
+
return { type: "item", key: node.key, dropPosition: position };
|
|
947
|
+
};
|
|
948
|
+
const hasDroppableDnd = createMemo(() => {
|
|
949
|
+
const hooks = local.dragAndDropHooks;
|
|
950
|
+
return Boolean(
|
|
951
|
+
hooks?.useDroppableCollectionState &&
|
|
952
|
+
hooks.useDroppableCollection &&
|
|
953
|
+
(hooks.dropTargetDelegate ||
|
|
954
|
+
parentCollectionRenderer?.dropTargetDelegate ||
|
|
955
|
+
hooks.ListDropTargetDelegate),
|
|
956
|
+
);
|
|
957
|
+
});
|
|
958
|
+
const hasDraggableDnd = createMemo(() => {
|
|
959
|
+
const hooks = local.dragAndDropHooks;
|
|
960
|
+
return Boolean(hooks?.useDraggableCollectionState && hooks.useDraggableCollection);
|
|
961
|
+
});
|
|
962
|
+
const dragState = createMemo(() => {
|
|
963
|
+
if (!hasDraggableDnd()) return undefined;
|
|
964
|
+
return local.dragAndDropHooks?.useDraggableCollectionState?.({
|
|
965
|
+
items: visibleRows().map((node) => node.value as T),
|
|
966
|
+
});
|
|
967
|
+
});
|
|
968
|
+
const dropState = createMemo(() => {
|
|
969
|
+
if (!hasDroppableDnd()) return undefined;
|
|
970
|
+
return local.dragAndDropHooks?.useDroppableCollectionState?.({});
|
|
971
|
+
});
|
|
972
|
+
createEffect(() => {
|
|
973
|
+
const activeDropState = dropState();
|
|
974
|
+
if (!activeDropState) return;
|
|
975
|
+
const originalGetDropOperation = activeDropState.getDropOperation.bind(activeDropState);
|
|
976
|
+
|
|
977
|
+
activeDropState.getDropOperation = (target, types, allowedOperations) => {
|
|
978
|
+
const currentDraggingKeys = dragState()?.draggingKeys ?? new Set<string | number>();
|
|
979
|
+
if (target.type === "item" && currentDraggingKeys.size > 0) {
|
|
980
|
+
if (currentDraggingKeys.has(target.key) && target.dropPosition === "on") {
|
|
981
|
+
return "cancel";
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
let currentKey: Key | null = target.key;
|
|
985
|
+
while (currentKey != null) {
|
|
986
|
+
const item = state.collection.getItem(currentKey);
|
|
987
|
+
const parentKey = item?.parentKey;
|
|
988
|
+
if (parentKey != null && currentDraggingKeys.has(parentKey)) {
|
|
989
|
+
return "cancel";
|
|
990
|
+
}
|
|
991
|
+
currentKey = parentKey ?? null;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
return originalGetDropOperation(target, types, allowedOperations);
|
|
996
|
+
};
|
|
264
997
|
|
|
998
|
+
onCleanup(() => {
|
|
999
|
+
activeDropState.getDropOperation = originalGetDropOperation;
|
|
1000
|
+
});
|
|
1001
|
+
});
|
|
1002
|
+
createEffect(() => {
|
|
1003
|
+
if (!hasDraggableDnd()) return;
|
|
1004
|
+
const hooks = local.dragAndDropHooks;
|
|
1005
|
+
const activeDragState = dragState();
|
|
1006
|
+
if (!hooks?.useDraggableCollection || !activeDragState) return;
|
|
1007
|
+
hooks.useDraggableCollection({}, activeDragState, () => ref());
|
|
1008
|
+
});
|
|
265
1009
|
const contextValue = createMemo<TreeContextValue<T>>(() => ({
|
|
266
1010
|
state,
|
|
267
1011
|
collection: state.collection,
|
|
268
1012
|
isDisabled: ariaProps.isDisabled ?? false,
|
|
269
1013
|
renderItem: props.children,
|
|
1014
|
+
dragAndDropHooks: local.dragAndDropHooks,
|
|
1015
|
+
dragState: dragState(),
|
|
1016
|
+
dropState: dropState(),
|
|
270
1017
|
}));
|
|
1018
|
+
const droppableCollection = createMemo(() => {
|
|
1019
|
+
if (!hasDroppableDnd()) return undefined;
|
|
1020
|
+
const hooks = local.dragAndDropHooks;
|
|
1021
|
+
const activeDropState = dropState();
|
|
1022
|
+
if (!hooks?.useDroppableCollection || !activeDropState) return undefined;
|
|
1023
|
+
const direction = resolveTreeDirection(ref());
|
|
1024
|
+
const baseDropTargetDelegate =
|
|
1025
|
+
hooks.dropTargetDelegate ??
|
|
1026
|
+
parentCollectionRenderer?.dropTargetDelegate ??
|
|
1027
|
+
(hooks.ListDropTargetDelegate
|
|
1028
|
+
? new hooks.ListDropTargetDelegate(
|
|
1029
|
+
() => state.collection,
|
|
1030
|
+
() => ref(),
|
|
1031
|
+
{ layout: "stack", orientation: "vertical", direction },
|
|
1032
|
+
)
|
|
1033
|
+
: undefined);
|
|
1034
|
+
if (!baseDropTargetDelegate) return undefined;
|
|
1035
|
+
const dropTargetDelegate = createTreeDropTargetDelegate(
|
|
1036
|
+
baseDropTargetDelegate as TreeDropTargetDelegate,
|
|
1037
|
+
state,
|
|
1038
|
+
direction,
|
|
1039
|
+
virtualizer?.getBaseKeyboardNavigationTarget,
|
|
1040
|
+
);
|
|
1041
|
+
return hooks.useDroppableCollection(
|
|
1042
|
+
{
|
|
1043
|
+
dropTargetDelegate,
|
|
1044
|
+
keyboardDelegate: {
|
|
1045
|
+
getFirstKey: () => state.collection.getFirstKey(),
|
|
1046
|
+
getLastKey: () => state.collection.getLastKey(),
|
|
1047
|
+
getKeyBelow: (key) => state.collection.getKeyAfter(key),
|
|
1048
|
+
getKeyAbove: (key) => state.collection.getKeyBefore(key),
|
|
1049
|
+
getKeyPageBelow: (key) => state.collection.getKeyAfter(key),
|
|
1050
|
+
getKeyPageAbove: (key) => state.collection.getKeyBefore(key),
|
|
1051
|
+
},
|
|
1052
|
+
get collection() {
|
|
1053
|
+
return state.collection;
|
|
1054
|
+
},
|
|
1055
|
+
get selectedKeys() {
|
|
1056
|
+
return state.selectedKeys;
|
|
1057
|
+
},
|
|
1058
|
+
setSelectedKeys: (keys: Set<Key>) => {
|
|
1059
|
+
if (state.selectionMode === "none") return;
|
|
1060
|
+
state.clearSelection();
|
|
1061
|
+
for (const key of keys) {
|
|
1062
|
+
state.toggleSelection(key);
|
|
1063
|
+
}
|
|
1064
|
+
},
|
|
1065
|
+
setFocusedKey: (key) => state.setFocusedKey(key),
|
|
1066
|
+
onDropActivate: (event) => {
|
|
1067
|
+
if (event.target.type !== "item") return;
|
|
1068
|
+
const key = event.target.key;
|
|
1069
|
+
const item = state.collection.getItem(key);
|
|
1070
|
+
const isExpanded = state.isExpanded(key);
|
|
1071
|
+
if (item?.hasChildNodes && (!isExpanded || hooks.isVirtualDragging?.())) {
|
|
1072
|
+
state.toggleKey(key);
|
|
1073
|
+
}
|
|
1074
|
+
},
|
|
1075
|
+
onKeyDown: (event) => {
|
|
1076
|
+
const target = activeDropState.target;
|
|
1077
|
+
if (!target || target.type !== "item" || target.dropPosition !== "on") return;
|
|
1078
|
+
const item = state.collection.getItem(target.key);
|
|
1079
|
+
if (!item?.hasChildNodes) return;
|
|
1080
|
+
const currentDirection = ariaProps.direction ?? resolveTreeDirection(ref());
|
|
1081
|
+
const expandKey = EXPANSION_KEYS.expand[currentDirection];
|
|
1082
|
+
const collapseKey = EXPANSION_KEYS.collapse[currentDirection];
|
|
1083
|
+
if (event.key === expandKey && !state.isExpanded(target.key)) {
|
|
1084
|
+
state.toggleKey(target.key);
|
|
1085
|
+
} else if (event.key === collapseKey && state.isExpanded(target.key)) {
|
|
1086
|
+
state.toggleKey(target.key);
|
|
1087
|
+
}
|
|
1088
|
+
},
|
|
1089
|
+
},
|
|
1090
|
+
activeDropState,
|
|
1091
|
+
() => ref(),
|
|
1092
|
+
);
|
|
1093
|
+
});
|
|
1094
|
+
const isRootDropTarget = createMemo(() => {
|
|
1095
|
+
return Boolean(dropState()?.target?.type === "root");
|
|
1096
|
+
});
|
|
1097
|
+
const dndRenderDropIndicator = createMemo(() =>
|
|
1098
|
+
useRenderDropIndicator(local.dragAndDropHooks, dropState()),
|
|
1099
|
+
);
|
|
1100
|
+
const dndDropIndicator = (index: number, position: "before" | "after" | "on") => {
|
|
1101
|
+
const target = getDropTargetByIndex(index, position);
|
|
1102
|
+
if (!target || target.type !== "item") return undefined;
|
|
1103
|
+
return dndRenderDropIndicator()?.(target);
|
|
1104
|
+
};
|
|
1105
|
+
const persistedKeys = useDndPersistedKeys(
|
|
1106
|
+
{ focusedKey: () => state.focusedKey },
|
|
1107
|
+
local.dragAndDropHooks,
|
|
1108
|
+
dropState(),
|
|
1109
|
+
state.collection,
|
|
1110
|
+
);
|
|
1111
|
+
const virtualRange = createMemo(() => {
|
|
1112
|
+
if (!virtualizer || !parentCollectionRenderer?.isVirtualized) return null;
|
|
1113
|
+
const rows = visibleRows();
|
|
1114
|
+
const baseRange = virtualizer.getVisibleRange(rows.length);
|
|
1115
|
+
const persistedIndexes = Array.from(persistedKeys())
|
|
1116
|
+
.map((key) => rows.findIndex((node) => node.key === key))
|
|
1117
|
+
.filter((index) => index >= 0);
|
|
1118
|
+
const dropTarget = dropState()?.target;
|
|
1119
|
+
const normalizedDropKey = getNormalizedDropTargetKey(dropTarget, state.collection);
|
|
1120
|
+
const focusedKey = state.focusedKey;
|
|
1121
|
+
const focusedIndex =
|
|
1122
|
+
focusedKey != null ? rows.findIndex((node) => node.key === focusedKey) : -1;
|
|
1123
|
+
const forceIncludeIndexes = [
|
|
1124
|
+
dropTarget?.type === "item" ? rows.findIndex((node) => node.key === dropTarget.key) : -1,
|
|
1125
|
+
normalizedDropKey != null ? rows.findIndex((node) => node.key === normalizedDropKey) : -1,
|
|
1126
|
+
dropTarget?.type === "item" ? -1 : focusedIndex,
|
|
1127
|
+
].filter((index) => index >= 0);
|
|
1128
|
+
return mergePersistedKeysIntoVirtualRange(
|
|
1129
|
+
baseRange,
|
|
1130
|
+
persistedIndexes,
|
|
1131
|
+
rows.length,
|
|
1132
|
+
virtualizer,
|
|
1133
|
+
80,
|
|
1134
|
+
{
|
|
1135
|
+
forceIncludeIndexes,
|
|
1136
|
+
forceIncludeMaxSpan: 320,
|
|
1137
|
+
},
|
|
1138
|
+
);
|
|
1139
|
+
});
|
|
1140
|
+
const virtualizedVisibleRows = createMemo(() => {
|
|
1141
|
+
const range = virtualRange();
|
|
1142
|
+
if (!range) return visibleRows();
|
|
1143
|
+
return visibleRows().slice(range.start, range.end);
|
|
1144
|
+
});
|
|
1145
|
+
createEffect(() => {
|
|
1146
|
+
if (!virtualizer || !parentCollectionRenderer?.isVirtualized) return;
|
|
1147
|
+
virtualizer.setDropTargetItemCountResolver(() => visibleRows().length);
|
|
1148
|
+
virtualizer.setDropTargetIndexResolver((key) => {
|
|
1149
|
+
const rows = visibleRows();
|
|
1150
|
+
const index = rows.findIndex((node) => node.key === key);
|
|
1151
|
+
return index >= 0 ? index : null;
|
|
1152
|
+
});
|
|
1153
|
+
virtualizer.setDropTargetResolver((target) => {
|
|
1154
|
+
const node = visibleRows()[target.index];
|
|
1155
|
+
if (!node) return target;
|
|
1156
|
+
return {
|
|
1157
|
+
...target,
|
|
1158
|
+
key: typeof node.key === "string" || typeof node.key === "number" ? node.key : undefined,
|
|
1159
|
+
parentKey:
|
|
1160
|
+
typeof node.parentKey === "string" || typeof node.parentKey === "number"
|
|
1161
|
+
? node.parentKey
|
|
1162
|
+
: node.parentKey == null
|
|
1163
|
+
? null
|
|
1164
|
+
: undefined,
|
|
1165
|
+
level: typeof node.level === "number" ? node.level : undefined,
|
|
1166
|
+
};
|
|
1167
|
+
});
|
|
1168
|
+
onCleanup(() => {
|
|
1169
|
+
virtualizer.setDropTargetIndexResolver(undefined);
|
|
1170
|
+
virtualizer.setDropTargetItemCountResolver(undefined);
|
|
1171
|
+
virtualizer.setDropTargetResolver(undefined);
|
|
1172
|
+
});
|
|
1173
|
+
});
|
|
1174
|
+
const rowIndexByKey = createMemo(() => {
|
|
1175
|
+
const map = new Map<Key, number>();
|
|
1176
|
+
const rows = visibleRows();
|
|
1177
|
+
for (let i = 0; i < rows.length; i += 1) {
|
|
1178
|
+
map.set(rows[i].key, i);
|
|
1179
|
+
}
|
|
1180
|
+
return map;
|
|
1181
|
+
});
|
|
1182
|
+
const getAfterIndicatorIndexes = (
|
|
1183
|
+
absoluteIndex: number,
|
|
1184
|
+
renderRange?: { start: number; end: number } | null,
|
|
1185
|
+
): number[] => {
|
|
1186
|
+
const rows = visibleRows();
|
|
1187
|
+
const current = rows[absoluteIndex];
|
|
1188
|
+
if (!current) return [];
|
|
1189
|
+
const next = rows[absoluteIndex + 1];
|
|
1190
|
+
// "after" is equivalent to next sibling's "before" when next row is at same or deeper level.
|
|
1191
|
+
if (next && next.level >= current.level) {
|
|
1192
|
+
return [];
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
const result: number[] = [];
|
|
1196
|
+
let cursorIndex: number | null = absoluteIndex;
|
|
271
1197
|
|
|
272
|
-
|
|
273
|
-
|
|
1198
|
+
// Emit after indicators for current and ancestor boundary levels, matching RAC branch exit semantics.
|
|
1199
|
+
while (cursorIndex != null) {
|
|
1200
|
+
const cursor: TreeNode<T> | undefined = rows[cursorIndex];
|
|
1201
|
+
if (!cursor) break;
|
|
1202
|
+
const shouldRender =
|
|
1203
|
+
!next || (cursor.parentKey !== next.parentKey && next.level < cursor.level);
|
|
1204
|
+
if (!shouldRender) break;
|
|
1205
|
+
result.push(cursorIndex);
|
|
1206
|
+
if (cursor.parentKey == null) break;
|
|
1207
|
+
cursorIndex = rowIndexByKey().get(cursor.parentKey) ?? null;
|
|
1208
|
+
}
|
|
1209
|
+
if (!renderRange) return result;
|
|
1210
|
+
return result.filter((index) => index >= renderRange.start && index < renderRange.end);
|
|
1211
|
+
};
|
|
1212
|
+
// Install tree-aware keyboard navigation override into the Virtualizer (if present).
|
|
1213
|
+
// This replaces the generic index-based navigation with collection-level semantics
|
|
1214
|
+
// (tree branch traversal, level-aware wrapping — RAC parity item #36).
|
|
1215
|
+
createEffect(() => {
|
|
1216
|
+
if (!virtualizer) return;
|
|
1217
|
+
const direction = resolveTreeDirection(ref());
|
|
1218
|
+
const parentDelegate: TreeDropTargetDelegate = {
|
|
1219
|
+
getDropTargetFromPoint:
|
|
1220
|
+
parentCollectionRenderer?.dropTargetDelegate?.getDropTargetFromPoint ??
|
|
1221
|
+
((_x, _y, _v) => null),
|
|
1222
|
+
getKeyboardNavigationTarget:
|
|
1223
|
+
parentCollectionRenderer?.dropTargetDelegate?.getKeyboardNavigationTarget,
|
|
1224
|
+
getKeyboardPageNavigationTarget:
|
|
1225
|
+
parentCollectionRenderer?.dropTargetDelegate?.getKeyboardPageNavigationTarget,
|
|
1226
|
+
};
|
|
1227
|
+
const treeDelegate = createTreeDropTargetDelegate(
|
|
1228
|
+
parentDelegate,
|
|
1229
|
+
state,
|
|
1230
|
+
direction,
|
|
1231
|
+
virtualizer.getBaseKeyboardNavigationTarget,
|
|
1232
|
+
);
|
|
1233
|
+
virtualizer.setKeyboardNavigationOverride(
|
|
1234
|
+
treeDelegate.getKeyboardNavigationTarget
|
|
1235
|
+
? (target, dir, isValid) => treeDelegate.getKeyboardNavigationTarget!(target, dir, isValid)
|
|
1236
|
+
: undefined,
|
|
1237
|
+
);
|
|
1238
|
+
onCleanup(() => {
|
|
1239
|
+
virtualizer.setKeyboardNavigationOverride(undefined);
|
|
1240
|
+
});
|
|
1241
|
+
});
|
|
1242
|
+
const collectionRenderer = createMemo<CollectionRendererContextValue<unknown>>(() => ({
|
|
1243
|
+
...parentCollectionRenderer,
|
|
1244
|
+
renderItem: (item) => item as JSX.Element,
|
|
1245
|
+
renderDropIndicator: (index: number, position: "before" | "after" | "on") =>
|
|
1246
|
+
dndDropIndicator(index, position) ??
|
|
1247
|
+
parentCollectionRenderer?.renderDropIndicator?.(index, position),
|
|
1248
|
+
}));
|
|
1249
|
+
const rootKeyByNodeKey = createMemo(() => {
|
|
1250
|
+
const rootMap = new Map<Key, Key>();
|
|
1251
|
+
for (const row of visibleRows()) {
|
|
1252
|
+
let rootKey: Key = row.key;
|
|
1253
|
+
let parentKey = row.parentKey;
|
|
1254
|
+
while (parentKey != null) {
|
|
1255
|
+
rootKey = parentKey;
|
|
1256
|
+
parentKey = state.collection.getParentKey(parentKey);
|
|
1257
|
+
}
|
|
1258
|
+
rootMap.set(row.key, rootKey);
|
|
1259
|
+
}
|
|
1260
|
+
return rootMap;
|
|
1261
|
+
});
|
|
1262
|
+
const renderRange = createMemo(() => {
|
|
1263
|
+
const range = virtualRange();
|
|
1264
|
+
if (!range) return null;
|
|
1265
|
+
return { start: range.start, end: range.end };
|
|
1266
|
+
});
|
|
1267
|
+
const renderableRows = createMemo(() => {
|
|
1268
|
+
const offset = renderRange()?.start ?? 0;
|
|
1269
|
+
return virtualizedVisibleRows().map((node, index) => ({
|
|
1270
|
+
node,
|
|
1271
|
+
globalIndex: offset + index,
|
|
1272
|
+
}));
|
|
1273
|
+
});
|
|
1274
|
+
const sectionedRenderableRows = createMemo(() => {
|
|
1275
|
+
if (!hasSections()) return null;
|
|
1276
|
+
const rootMap = rootKeyByNodeKey();
|
|
1277
|
+
const rows = renderableRows();
|
|
1278
|
+
return (stateProps.items ?? []).map((entry) => {
|
|
1279
|
+
if (!isCollectionSection(entry)) {
|
|
1280
|
+
const matching = rows.filter((row) => rootMap.get(row.node.key) === entry.key);
|
|
1281
|
+
return {
|
|
1282
|
+
type: "single" as const,
|
|
1283
|
+
item: entry,
|
|
1284
|
+
rows: matching,
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
const sectionRootKeys = new Set(entry.items.map((item) => item.key));
|
|
1288
|
+
const sectionRows = rows.filter((row) => {
|
|
1289
|
+
const rootKey = rootMap.get(row.node.key);
|
|
1290
|
+
return rootKey != null && sectionRootKeys.has(rootKey);
|
|
1291
|
+
});
|
|
1292
|
+
return {
|
|
1293
|
+
type: "section" as const,
|
|
1294
|
+
section: entry,
|
|
1295
|
+
rows: sectionRows,
|
|
1296
|
+
};
|
|
1297
|
+
});
|
|
1298
|
+
});
|
|
1299
|
+
const renderTreeRow = (node: TreeNode<T>, itemIndex: number) => {
|
|
1300
|
+
const beforeIndicator = () => collectionRenderer().renderDropIndicator?.(itemIndex, "before");
|
|
1301
|
+
const onIndicator = () => collectionRenderer().renderDropIndicator?.(itemIndex, "on");
|
|
1302
|
+
const afterIndicatorIndexes = () => getAfterIndicatorIndexes(itemIndex, renderRange());
|
|
1303
|
+
const itemData = treeItemDataFromNode(node);
|
|
1304
|
+
const itemState: TreeRenderItemState = {
|
|
1305
|
+
isExpanded: node.isExpanded ?? false,
|
|
1306
|
+
isExpandable: node.isExpandable ?? false,
|
|
1307
|
+
level: node.level,
|
|
1308
|
+
};
|
|
1309
|
+
return (
|
|
1310
|
+
<>
|
|
1311
|
+
{beforeIndicator()}
|
|
1312
|
+
{onIndicator()}
|
|
1313
|
+
{props.children(itemData, itemState)}
|
|
1314
|
+
<For each={afterIndicatorIndexes()}>
|
|
1315
|
+
{(afterIndex) => collectionRenderer().renderDropIndicator?.(afterIndex, "after")}
|
|
1316
|
+
</For>
|
|
1317
|
+
</>
|
|
1318
|
+
);
|
|
1319
|
+
};
|
|
274
1320
|
|
|
275
1321
|
return (
|
|
276
1322
|
<TreeContext.Provider value={contextValue() as unknown as TreeContextValue<object>}>
|
|
277
|
-
<TreeStateContext.Provider
|
|
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
|
-
|
|
1323
|
+
<TreeStateContext.Provider
|
|
1324
|
+
value={state as unknown as TreeState<object, TreeCollection<object>>}
|
|
1325
|
+
>
|
|
1326
|
+
<CollectionRendererContext.Provider value={collectionRenderer()}>
|
|
1327
|
+
<div
|
|
1328
|
+
ref={(element) => {
|
|
1329
|
+
setRef(element);
|
|
1330
|
+
assignRef(local.ref, element);
|
|
1331
|
+
}}
|
|
1332
|
+
{...mergeProps(
|
|
1333
|
+
domProps(),
|
|
1334
|
+
cleanTreeProps(),
|
|
1335
|
+
cleanFocusProps(),
|
|
1336
|
+
(droppableCollection()?.collectionProps as Record<string, unknown> | undefined) ?? {},
|
|
1337
|
+
)}
|
|
1338
|
+
class={renderProps.class()}
|
|
1339
|
+
style={renderProps.style()}
|
|
1340
|
+
data-focused={state.isFocused || undefined}
|
|
1341
|
+
data-focus-visible={isFocusVisible() || undefined}
|
|
1342
|
+
data-disabled={ariaProps.isDisabled || undefined}
|
|
1343
|
+
data-empty={isEmpty() || undefined}
|
|
1344
|
+
data-drop-target={isRootDropTarget() || undefined}
|
|
1345
|
+
data-selection-mode={
|
|
1346
|
+
stateProps.selectionMode !== "none" ? stateProps.selectionMode : undefined
|
|
1347
|
+
}
|
|
1348
|
+
data-allows-dragging={hasDraggableDnd() || undefined}
|
|
1349
|
+
>
|
|
1350
|
+
<SharedElementTransition>
|
|
1351
|
+
{isEmpty() && local.renderEmptyState ? (
|
|
1352
|
+
<div role="row" aria-level={1} style={{ display: "contents" }}>
|
|
1353
|
+
<div role="gridcell" style={{ display: "contents" }}>
|
|
1354
|
+
{local.renderEmptyState()}
|
|
1355
|
+
</div>
|
|
1356
|
+
</div>
|
|
1357
|
+
) : (
|
|
1358
|
+
<>
|
|
1359
|
+
{virtualRange()?.offsetTop ? (
|
|
1360
|
+
<div
|
|
1361
|
+
role="presentation"
|
|
1362
|
+
aria-hidden="true"
|
|
1363
|
+
style={{ height: `${virtualRange()!.offsetTop}px` }}
|
|
1364
|
+
data-virtualizer-spacer="top"
|
|
1365
|
+
/>
|
|
1366
|
+
) : null}
|
|
1367
|
+
<Show
|
|
1368
|
+
when={hasSections()}
|
|
1369
|
+
fallback={
|
|
1370
|
+
<For each={renderableRows()}>
|
|
1371
|
+
{(row) => renderTreeRow(row.node, row.globalIndex)}
|
|
1372
|
+
</For>
|
|
1373
|
+
}
|
|
1374
|
+
>
|
|
1375
|
+
<For each={sectionedRenderableRows() ?? []}>
|
|
1376
|
+
{(entry) => (
|
|
1377
|
+
<Show when={entry.rows.length > 0}>
|
|
1378
|
+
<Show
|
|
1379
|
+
when={entry.type === "section"}
|
|
1380
|
+
fallback={
|
|
1381
|
+
<For each={entry.rows}>
|
|
1382
|
+
{(row) => renderTreeRow(row.node, row.globalIndex)}
|
|
1383
|
+
</For>
|
|
1384
|
+
}
|
|
1385
|
+
>
|
|
1386
|
+
<TreeSection>
|
|
1387
|
+
{entry.type === "section" && entry.section.title ? (
|
|
1388
|
+
<TreeHeader>{entry.section.title}</TreeHeader>
|
|
1389
|
+
) : null}
|
|
1390
|
+
<For each={entry.rows}>
|
|
1391
|
+
{(row) => renderTreeRow(row.node, row.globalIndex)}
|
|
1392
|
+
</For>
|
|
1393
|
+
</TreeSection>
|
|
1394
|
+
</Show>
|
|
1395
|
+
</Show>
|
|
1396
|
+
)}
|
|
1397
|
+
</For>
|
|
1398
|
+
</Show>
|
|
1399
|
+
{virtualRange()?.offsetBottom ? (
|
|
1400
|
+
<div
|
|
1401
|
+
role="presentation"
|
|
1402
|
+
aria-hidden="true"
|
|
1403
|
+
style={{ height: `${virtualRange()!.offsetBottom}px` }}
|
|
1404
|
+
data-virtualizer-spacer="bottom"
|
|
1405
|
+
/>
|
|
1406
|
+
) : null}
|
|
1407
|
+
</>
|
|
1408
|
+
)}
|
|
1409
|
+
</SharedElementTransition>
|
|
1410
|
+
{local.hasMore && local.onLoadMore && (
|
|
1411
|
+
<TreeLoadMoreItem
|
|
1412
|
+
onLoadMore={local.onLoadMore}
|
|
1413
|
+
isLoading={local.isLoading}
|
|
1414
|
+
loadingState={local.loadingState}
|
|
1415
|
+
class={local.loadMoreClass}
|
|
1416
|
+
style={local.loadMoreStyle}
|
|
1417
|
+
>
|
|
1418
|
+
{local.renderLoadMoreItem?.({
|
|
1419
|
+
isLoading:
|
|
1420
|
+
!!local.isLoading ||
|
|
1421
|
+
local.loadingState === "loading" ||
|
|
1422
|
+
local.loadingState === "loadingMore",
|
|
1423
|
+
})}
|
|
1424
|
+
</TreeLoadMoreItem>
|
|
1425
|
+
)}
|
|
1426
|
+
</div>
|
|
1427
|
+
</CollectionRendererContext.Provider>
|
|
318
1428
|
</TreeStateContext.Provider>
|
|
319
1429
|
</TreeContext.Provider>
|
|
320
1430
|
);
|
|
@@ -324,107 +1434,147 @@ export function Tree<T extends object>(props: TreeProps<T>): JSX.Element {
|
|
|
324
1434
|
* An item in a tree.
|
|
325
1435
|
*/
|
|
326
1436
|
export function TreeItem<T extends object>(props: TreeItemProps<T>): JSX.Element {
|
|
327
|
-
const [local] = splitProps(props, [
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
1437
|
+
const [local, domProps] = splitProps(props, [
|
|
1438
|
+
"class",
|
|
1439
|
+
"style",
|
|
1440
|
+
"slot",
|
|
1441
|
+
"id",
|
|
1442
|
+
"item",
|
|
1443
|
+
"textValue",
|
|
1444
|
+
"onAction",
|
|
1445
|
+
"hasChildItems",
|
|
1446
|
+
"isDisabled",
|
|
1447
|
+
"href",
|
|
1448
|
+
"target",
|
|
1449
|
+
"download",
|
|
1450
|
+
"rel",
|
|
1451
|
+
"hrefLang",
|
|
1452
|
+
"ping",
|
|
1453
|
+
"referrerPolicy",
|
|
1454
|
+
"routerOptions",
|
|
1455
|
+
"ref",
|
|
1456
|
+
"children",
|
|
335
1457
|
]);
|
|
336
1458
|
|
|
337
|
-
// Get state from context
|
|
338
1459
|
const context = useContext(TreeStateContext);
|
|
339
1460
|
if (!context) {
|
|
340
|
-
throw new Error(
|
|
1461
|
+
throw new Error("TreeItem must be used within a Tree");
|
|
341
1462
|
}
|
|
342
1463
|
const state = context as TreeState<T, TreeCollection<T>>;
|
|
1464
|
+
const treeContext = useContext(TreeContext) as TreeContextValue<T> | null;
|
|
1465
|
+
const router = useRouter();
|
|
1466
|
+
const linkProps = createMemo(() =>
|
|
1467
|
+
useLinkProps({
|
|
1468
|
+
href: local.href,
|
|
1469
|
+
target: local.target,
|
|
1470
|
+
rel: local.rel,
|
|
1471
|
+
download: local.download,
|
|
1472
|
+
ping: local.ping,
|
|
1473
|
+
referrerPolicy: local.referrerPolicy,
|
|
1474
|
+
}),
|
|
1475
|
+
);
|
|
343
1476
|
|
|
344
|
-
|
|
345
|
-
const
|
|
1477
|
+
const [ref, setRef] = createSignal<HTMLElement | null>(null);
|
|
1478
|
+
const setItemRef = (element: HTMLElement) => {
|
|
1479
|
+
setRef(element);
|
|
1480
|
+
assignRef(local.ref, element);
|
|
1481
|
+
};
|
|
346
1482
|
|
|
347
|
-
// Find the item node
|
|
348
1483
|
const itemNode = createMemo(() => {
|
|
349
1484
|
const node = state.collection.getItem(local.id);
|
|
350
1485
|
if (!node) {
|
|
351
|
-
// Create a simple node for the item
|
|
352
1486
|
return {
|
|
353
|
-
type:
|
|
1487
|
+
type: "item" as const,
|
|
354
1488
|
key: local.id,
|
|
355
1489
|
value: local.item?.value ?? null,
|
|
356
1490
|
textValue: local.textValue ?? String(local.id),
|
|
357
1491
|
level: 0,
|
|
358
1492
|
index: 0,
|
|
359
|
-
hasChildNodes:
|
|
1493
|
+
hasChildNodes: !!local.hasChildItems,
|
|
360
1494
|
childNodes: [],
|
|
361
|
-
|
|
1495
|
+
isDisabled: local.isDisabled,
|
|
1496
|
+
isExpandable: !!local.hasChildItems,
|
|
362
1497
|
isExpanded: false,
|
|
363
1498
|
} as TreeNode<T>;
|
|
364
1499
|
}
|
|
365
1500
|
return node;
|
|
366
1501
|
});
|
|
367
1502
|
|
|
368
|
-
|
|
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>>(
|
|
1503
|
+
const treeItemAria = createTreeItem<T, TreeCollection<T>>(
|
|
380
1504
|
() => ({
|
|
381
1505
|
node: itemNode(),
|
|
1506
|
+
selectionBehavior: state.selectionBehavior,
|
|
382
1507
|
onAction: local.onAction,
|
|
1508
|
+
isDisabled: local.isDisabled,
|
|
1509
|
+
textValue: local.textValue,
|
|
383
1510
|
}),
|
|
384
1511
|
() => state,
|
|
385
|
-
ref
|
|
1512
|
+
ref,
|
|
386
1513
|
);
|
|
1514
|
+
const isSelected = () => treeItemAria.isSelected;
|
|
1515
|
+
const isDisabled = () => treeItemAria.isDisabled;
|
|
1516
|
+
const isPressed = () => treeItemAria.isPressed;
|
|
1517
|
+
const isExpanded = () => treeItemAria.isExpanded;
|
|
1518
|
+
const isExpandable = () => treeItemAria.isExpandable;
|
|
1519
|
+
const level = () => treeItemAria.level;
|
|
387
1520
|
|
|
388
|
-
// Create hover
|
|
389
1521
|
const { isHovered, hoverProps } = createHover({
|
|
390
1522
|
get isDisabled() {
|
|
391
|
-
return isDisabled;
|
|
1523
|
+
return isDisabled();
|
|
392
1524
|
},
|
|
393
1525
|
});
|
|
394
1526
|
|
|
395
|
-
// Create focus ring
|
|
396
1527
|
const { isFocusVisible, focusProps } = createFocusRing();
|
|
397
1528
|
|
|
398
|
-
// Check if focused
|
|
399
1529
|
const isFocused = createMemo(() => state.focusedKey === local.id);
|
|
1530
|
+
const draggableItem = createMemo(() => {
|
|
1531
|
+
if (!treeContext?.dragAndDropHooks?.useDraggableItem || !treeContext.dragState)
|
|
1532
|
+
return undefined;
|
|
1533
|
+
return treeContext.dragAndDropHooks.useDraggableItem(
|
|
1534
|
+
{
|
|
1535
|
+
key: local.id as string | number,
|
|
1536
|
+
},
|
|
1537
|
+
treeContext.dragState as Parameters<NonNullable<DragAndDropHooks<T>["useDraggableItem"]>>[1],
|
|
1538
|
+
);
|
|
1539
|
+
});
|
|
1540
|
+
const droppableItem = createMemo(() => {
|
|
1541
|
+
if (!treeContext?.dragAndDropHooks?.useDroppableItem || !treeContext.dropState)
|
|
1542
|
+
return undefined;
|
|
1543
|
+
return treeContext.dragAndDropHooks.useDroppableItem(
|
|
1544
|
+
{
|
|
1545
|
+
key: local.id as string | number,
|
|
1546
|
+
},
|
|
1547
|
+
treeContext.dropState as Parameters<NonNullable<DragAndDropHooks<T>["useDroppableItem"]>>[1],
|
|
1548
|
+
() => ref(),
|
|
1549
|
+
);
|
|
1550
|
+
});
|
|
400
1551
|
|
|
401
|
-
// Render props values
|
|
402
1552
|
const renderValues = createMemo<TreeItemRenderProps>(() => ({
|
|
403
|
-
isSelected,
|
|
1553
|
+
isSelected: isSelected(),
|
|
404
1554
|
isFocused: isFocused(),
|
|
405
1555
|
isFocusVisible: isFocusVisible() && isFocused(),
|
|
406
|
-
isPressed,
|
|
1556
|
+
isPressed: isPressed(),
|
|
407
1557
|
isHovered: isHovered(),
|
|
408
|
-
isDisabled,
|
|
409
|
-
isExpanded,
|
|
410
|
-
isExpandable,
|
|
411
|
-
level,
|
|
1558
|
+
isDisabled: isDisabled(),
|
|
1559
|
+
isExpanded: isExpanded(),
|
|
1560
|
+
isExpandable: isExpandable(),
|
|
1561
|
+
level: level(),
|
|
1562
|
+
selectionMode: state.selectionMode,
|
|
1563
|
+
selectionBehavior: state.selectionBehavior,
|
|
412
1564
|
}));
|
|
413
1565
|
|
|
414
|
-
// Resolve render props
|
|
415
1566
|
const renderProps = useRenderProps(
|
|
416
1567
|
{
|
|
417
1568
|
children: props.children,
|
|
418
1569
|
class: local.class,
|
|
419
1570
|
style: local.style,
|
|
420
|
-
defaultClassName:
|
|
1571
|
+
defaultClassName: "solidaria-Tree-item",
|
|
421
1572
|
},
|
|
422
|
-
renderValues
|
|
1573
|
+
renderValues,
|
|
423
1574
|
);
|
|
424
1575
|
|
|
425
|
-
// Remove ref from spread props
|
|
426
1576
|
const cleanRowProps = () => {
|
|
427
|
-
const { ref: _ref1, ...rest } = rowProps as Record<string, unknown>;
|
|
1577
|
+
const { ref: _ref1, ...rest } = treeItemAria.rowProps as Record<string, unknown>;
|
|
428
1578
|
return rest;
|
|
429
1579
|
};
|
|
430
1580
|
const cleanHoverProps = () => {
|
|
@@ -436,36 +1586,90 @@ export function TreeItem<T extends object>(props: TreeItemProps<T>): JSX.Element
|
|
|
436
1586
|
return rest;
|
|
437
1587
|
};
|
|
438
1588
|
|
|
439
|
-
// Item context for nested components
|
|
440
1589
|
const itemContextValue = createMemo<TreeItemContextValue<T>>(() => ({
|
|
441
1590
|
node: itemNode(),
|
|
442
|
-
isExpanded,
|
|
443
|
-
isExpandable,
|
|
444
|
-
level,
|
|
1591
|
+
isExpanded: isExpanded(),
|
|
1592
|
+
isExpandable: isExpandable(),
|
|
1593
|
+
level: level(),
|
|
445
1594
|
}));
|
|
446
1595
|
|
|
1596
|
+
const rowStyle = () => ({
|
|
1597
|
+
"--tree-item-level": String(level()),
|
|
1598
|
+
...((typeof renderProps.style() === "object" ? renderProps.style() : {}) as Record<
|
|
1599
|
+
string,
|
|
1600
|
+
string
|
|
1601
|
+
>),
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
const rowContent = () => (
|
|
1605
|
+
<TreeItemContentContext.Provider value={renderValues()}>
|
|
1606
|
+
<div {...treeItemAria.gridCellProps} class="solidaria-Tree-item-content">
|
|
1607
|
+
{renderProps.renderChildren()}
|
|
1608
|
+
</div>
|
|
1609
|
+
</TreeItemContentContext.Provider>
|
|
1610
|
+
);
|
|
1611
|
+
|
|
1612
|
+
const mergedRowProps = () =>
|
|
1613
|
+
mergeProps(
|
|
1614
|
+
cleanRowProps(),
|
|
1615
|
+
cleanHoverProps(),
|
|
1616
|
+
cleanFocusProps(),
|
|
1617
|
+
(draggableItem()?.dragProps as Record<string, unknown> | undefined) ?? {},
|
|
1618
|
+
(droppableItem()?.dropProps as Record<string, unknown> | undefined) ?? {},
|
|
1619
|
+
);
|
|
1620
|
+
|
|
1621
|
+
const onLinkedRowClick = (event: MouseEvent) => {
|
|
1622
|
+
const onClick = (mergedRowProps() as { onClick?: (event: MouseEvent) => void }).onClick;
|
|
1623
|
+
onClick?.(event);
|
|
1624
|
+
handleLinkClick(event, router, local.href, local.routerOptions);
|
|
1625
|
+
};
|
|
1626
|
+
|
|
1627
|
+
const downloadAttr = () => {
|
|
1628
|
+
const download = linkProps().download;
|
|
1629
|
+
return typeof download === "boolean" ? (download ? "" : undefined) : download;
|
|
1630
|
+
};
|
|
1631
|
+
|
|
1632
|
+
const referrerPolicyAttr = () => linkProps().referrerPolicy || undefined;
|
|
1633
|
+
const linkedRowDomProps = () =>
|
|
1634
|
+
local.href
|
|
1635
|
+
? {
|
|
1636
|
+
onClick: onLinkedRowClick,
|
|
1637
|
+
"data-href": linkProps().href,
|
|
1638
|
+
"data-target": linkProps().target,
|
|
1639
|
+
"data-download": downloadAttr(),
|
|
1640
|
+
"data-rel": linkProps().rel,
|
|
1641
|
+
"data-hreflang": local.hrefLang,
|
|
1642
|
+
"data-ping": linkProps().ping,
|
|
1643
|
+
"data-referrer-policy": referrerPolicyAttr(),
|
|
1644
|
+
}
|
|
1645
|
+
: {};
|
|
1646
|
+
|
|
447
1647
|
return (
|
|
448
|
-
<TreeItemContext.Provider value={itemContextValue() as TreeItemContextValue<object>}>
|
|
1648
|
+
<TreeItemContext.Provider value={itemContextValue() as unknown as TreeItemContextValue<object>}>
|
|
449
1649
|
<div
|
|
450
|
-
ref={
|
|
451
|
-
{...
|
|
452
|
-
{...
|
|
453
|
-
{...
|
|
1650
|
+
ref={setItemRef}
|
|
1651
|
+
{...domProps}
|
|
1652
|
+
{...mergedRowProps()}
|
|
1653
|
+
{...linkedRowDomProps()}
|
|
454
1654
|
class={renderProps.class()}
|
|
455
|
-
style={
|
|
456
|
-
data-selected={isSelected || undefined}
|
|
1655
|
+
style={rowStyle()}
|
|
1656
|
+
data-selected={isSelected() || undefined}
|
|
457
1657
|
data-focused={isFocused() || undefined}
|
|
458
1658
|
data-focus-visible={(isFocusVisible() && isFocused()) || undefined}
|
|
459
|
-
data-pressed={isPressed || undefined}
|
|
1659
|
+
data-pressed={isPressed() || undefined}
|
|
460
1660
|
data-hovered={isHovered() || undefined}
|
|
461
|
-
data-disabled={isDisabled || undefined}
|
|
462
|
-
data-expanded={isExpanded || undefined}
|
|
463
|
-
data-expandable={isExpandable || undefined}
|
|
464
|
-
data-
|
|
1661
|
+
data-disabled={isDisabled() || undefined}
|
|
1662
|
+
data-expanded={isExpanded() || undefined}
|
|
1663
|
+
data-expandable={isExpandable() || undefined}
|
|
1664
|
+
data-has-child-items={isExpandable() || undefined}
|
|
1665
|
+
data-level={level()}
|
|
1666
|
+
data-selection-mode={
|
|
1667
|
+
treeContext?.state.selectionMode !== "none" ? treeContext?.state.selectionMode : undefined
|
|
1668
|
+
}
|
|
1669
|
+
data-dragging={draggableItem()?.isDragging || undefined}
|
|
1670
|
+
data-drop-target={droppableItem()?.isDropTarget || undefined}
|
|
465
1671
|
>
|
|
466
|
-
|
|
467
|
-
{renderProps.renderChildren()}
|
|
468
|
-
</div>
|
|
1672
|
+
{rowContent()}
|
|
469
1673
|
</div>
|
|
470
1674
|
</TreeItemContext.Provider>
|
|
471
1675
|
);
|
|
@@ -475,38 +1679,42 @@ export function TreeItem<T extends object>(props: TreeItemProps<T>): JSX.Element
|
|
|
475
1679
|
* A button to expand/collapse a tree item.
|
|
476
1680
|
*/
|
|
477
1681
|
export function TreeExpandButton(props: TreeExpandButtonProps): JSX.Element {
|
|
478
|
-
// Get item context
|
|
479
1682
|
const itemContext = useContext(TreeItemContext);
|
|
480
1683
|
if (!itemContext) {
|
|
481
|
-
throw new Error(
|
|
1684
|
+
throw new Error("TreeExpandButton must be used within a Tree");
|
|
482
1685
|
}
|
|
483
1686
|
|
|
484
|
-
// Get state context
|
|
485
1687
|
const stateContext = useContext(TreeStateContext);
|
|
486
1688
|
if (!stateContext) {
|
|
487
|
-
throw new Error(
|
|
1689
|
+
throw new Error("TreeExpandButton must be used within a Tree");
|
|
488
1690
|
}
|
|
489
1691
|
|
|
490
1692
|
const state = stateContext as TreeState<object, TreeCollection<object>>;
|
|
491
1693
|
|
|
492
|
-
|
|
493
|
-
const { expandButtonProps } = createTreeItem(
|
|
1694
|
+
const treeItemAria = createTreeItem(
|
|
494
1695
|
() => ({ node: itemContext.node }),
|
|
495
1696
|
() => state,
|
|
496
|
-
() => null
|
|
1697
|
+
() => null,
|
|
497
1698
|
);
|
|
498
1699
|
|
|
499
|
-
// Remove ref and add custom handling
|
|
500
1700
|
const cleanExpandProps = () => {
|
|
501
|
-
const { ref: _ref, ...rest } = expandButtonProps as Record<string, unknown>;
|
|
1701
|
+
const { ref: _ref, ...rest } = treeItemAria.expandButtonProps as Record<string, unknown>;
|
|
502
1702
|
return rest;
|
|
503
1703
|
};
|
|
1704
|
+
const dataProps = () => {
|
|
1705
|
+
const result: Record<string, string | undefined> = {};
|
|
1706
|
+
for (const key in props) {
|
|
1707
|
+
if (key.startsWith("data-")) {
|
|
1708
|
+
result[key] = props[key as `data-${string}`];
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
return result;
|
|
1712
|
+
};
|
|
504
1713
|
|
|
505
1714
|
const isExpanded = createMemo(() => state.isExpanded(itemContext.node.key));
|
|
506
1715
|
|
|
507
|
-
// Render children
|
|
508
1716
|
const renderChildren = () => {
|
|
509
|
-
if (typeof props.children ===
|
|
1717
|
+
if (typeof props.children === "function") {
|
|
510
1718
|
return props.children({ isExpanded: isExpanded() });
|
|
511
1719
|
}
|
|
512
1720
|
return props.children;
|
|
@@ -516,7 +1724,8 @@ export function TreeExpandButton(props: TreeExpandButtonProps): JSX.Element {
|
|
|
516
1724
|
<Show when={itemContext.isExpandable}>
|
|
517
1725
|
<button
|
|
518
1726
|
{...cleanExpandProps()}
|
|
519
|
-
|
|
1727
|
+
{...dataProps()}
|
|
1728
|
+
class={props.class ?? "solidaria-Tree-expand-button"}
|
|
520
1729
|
style={props.style}
|
|
521
1730
|
data-expanded={isExpanded() || undefined}
|
|
522
1731
|
>
|
|
@@ -529,23 +1738,153 @@ export function TreeExpandButton(props: TreeExpandButtonProps): JSX.Element {
|
|
|
529
1738
|
/**
|
|
530
1739
|
* A checkbox for item selection in a tree.
|
|
531
1740
|
*/
|
|
532
|
-
export function TreeSelectionCheckbox(props: {
|
|
1741
|
+
export function TreeSelectionCheckbox(props: {
|
|
1742
|
+
itemKey: Key;
|
|
1743
|
+
class?: string;
|
|
1744
|
+
style?: JSX.CSSProperties;
|
|
1745
|
+
excludeFromTabOrder?: boolean;
|
|
1746
|
+
"aria-label"?: string;
|
|
1747
|
+
}): JSX.Element {
|
|
533
1748
|
const context = useContext(TreeStateContext);
|
|
534
1749
|
if (!context) {
|
|
535
|
-
throw new Error(
|
|
1750
|
+
throw new Error("TreeSelectionCheckbox must be used within a Tree");
|
|
536
1751
|
}
|
|
537
1752
|
|
|
538
1753
|
const state = context as TreeState<object, TreeCollection<object>>;
|
|
539
1754
|
|
|
540
|
-
const
|
|
1755
|
+
const treeSelectionCheckboxAria = createTreeSelectionCheckbox<object, TreeCollection<object>>(
|
|
541
1756
|
() => ({ key: props.itemKey }),
|
|
542
|
-
() => state
|
|
1757
|
+
() => state,
|
|
543
1758
|
);
|
|
544
1759
|
|
|
545
|
-
return
|
|
1760
|
+
return (
|
|
1761
|
+
<input
|
|
1762
|
+
{...treeSelectionCheckboxAria.checkboxProps}
|
|
1763
|
+
class={props.class ?? "solidaria-Tree-checkbox"}
|
|
1764
|
+
style={props.style}
|
|
1765
|
+
tabIndex={props.excludeFromTabOrder ? -1 : undefined}
|
|
1766
|
+
aria-label={props["aria-label"] ?? treeSelectionCheckboxAria.checkboxProps["aria-label"]}
|
|
1767
|
+
/>
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
export function TreeLoadMoreItem(props: TreeLoadMoreItemProps): JSX.Element {
|
|
1772
|
+
let sentinelRef: HTMLDivElement | undefined;
|
|
1773
|
+
const [isPending, setIsPending] = createSignal(false);
|
|
1774
|
+
const isLoading = () =>
|
|
1775
|
+
!!props.isLoading ||
|
|
1776
|
+
props.loadingState === "loading" ||
|
|
1777
|
+
props.loadingState === "loadingMore" ||
|
|
1778
|
+
isPending();
|
|
1779
|
+
|
|
1780
|
+
const triggerLoadMore = async () => {
|
|
1781
|
+
if (isLoading()) return;
|
|
1782
|
+
setIsPending(true);
|
|
1783
|
+
try {
|
|
1784
|
+
await props.onLoadMore();
|
|
1785
|
+
} finally {
|
|
1786
|
+
setIsPending(false);
|
|
1787
|
+
}
|
|
1788
|
+
};
|
|
1789
|
+
|
|
1790
|
+
createEffect(() => {
|
|
1791
|
+
if (!sentinelRef || typeof IntersectionObserver !== "function") return;
|
|
1792
|
+
const offset = props.scrollOffset ?? 1;
|
|
1793
|
+
const margin = `0px 0px ${100 * offset}% 0px`;
|
|
1794
|
+
const observer = new IntersectionObserver(
|
|
1795
|
+
(entries) => {
|
|
1796
|
+
if (entries[0]?.isIntersecting) {
|
|
1797
|
+
void triggerLoadMore();
|
|
1798
|
+
}
|
|
1799
|
+
},
|
|
1800
|
+
{ rootMargin: margin },
|
|
1801
|
+
);
|
|
1802
|
+
observer.observe(sentinelRef);
|
|
1803
|
+
return () => observer.disconnect();
|
|
1804
|
+
});
|
|
1805
|
+
|
|
1806
|
+
const renderProps = useRenderProps(
|
|
1807
|
+
{
|
|
1808
|
+
children: props.children ?? (() => (isLoading() ? "Loading more..." : "Load more")),
|
|
1809
|
+
class: props.class,
|
|
1810
|
+
style: props.style,
|
|
1811
|
+
defaultClassName: "solidaria-Tree-loadMore",
|
|
1812
|
+
},
|
|
1813
|
+
() => ({ isLoading: isLoading() }),
|
|
1814
|
+
);
|
|
1815
|
+
|
|
1816
|
+
return (
|
|
1817
|
+
<>
|
|
1818
|
+
<div style={{ position: "relative", width: 0, height: 0, overflow: "hidden" }} inert>
|
|
1819
|
+
<div ref={sentinelRef} style={{ position: "absolute", height: "1px", width: "1px" }} />
|
|
1820
|
+
</div>
|
|
1821
|
+
<div
|
|
1822
|
+
role="row"
|
|
1823
|
+
aria-level={props.level ?? 1}
|
|
1824
|
+
onFocus={() => {
|
|
1825
|
+
void triggerLoadMore();
|
|
1826
|
+
}}
|
|
1827
|
+
onFocusIn={() => {
|
|
1828
|
+
void triggerLoadMore();
|
|
1829
|
+
}}
|
|
1830
|
+
class={renderProps.class()}
|
|
1831
|
+
style={renderProps.style()}
|
|
1832
|
+
data-loading={isLoading() || undefined}
|
|
1833
|
+
data-level={props.level ?? 1}
|
|
1834
|
+
>
|
|
1835
|
+
<div
|
|
1836
|
+
role="gridcell"
|
|
1837
|
+
onFocus={() => {
|
|
1838
|
+
void triggerLoadMore();
|
|
1839
|
+
}}
|
|
1840
|
+
>
|
|
1841
|
+
{renderProps.renderChildren()}
|
|
1842
|
+
</div>
|
|
1843
|
+
</div>
|
|
1844
|
+
</>
|
|
1845
|
+
);
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
export interface TreeItemContentProps {
|
|
1849
|
+
children?: RenderChildren<TreeItemContentRenderProps>;
|
|
1850
|
+
}
|
|
1851
|
+
export type TreeItemContentRenderProps = TreeItemRenderProps;
|
|
1852
|
+
|
|
1853
|
+
export function TreeItemContent(props: TreeItemContentProps): JSX.Element {
|
|
1854
|
+
const context = useContext(TreeItemContentContext);
|
|
1855
|
+
if (!context) {
|
|
1856
|
+
throw new Error("TreeItemContent must be used within a TreeItem");
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
const renderProps = useRenderProps(
|
|
1860
|
+
{
|
|
1861
|
+
children: props.children,
|
|
1862
|
+
},
|
|
1863
|
+
() => context,
|
|
1864
|
+
);
|
|
1865
|
+
|
|
1866
|
+
return <>{renderProps.renderChildren()}</>;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
export function TreeSection(props: TreeSectionProps): JSX.Element {
|
|
1870
|
+
return <Section {...props} />;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
export function TreeHeader(props: TreeHeaderProps): JSX.Element {
|
|
1874
|
+
return <Header {...props} />;
|
|
546
1875
|
}
|
|
547
1876
|
|
|
548
|
-
// Attach static properties
|
|
549
1877
|
Tree.Item = TreeItem;
|
|
550
1878
|
Tree.ExpandButton = TreeExpandButton;
|
|
551
1879
|
Tree.SelectionCheckbox = TreeSelectionCheckbox;
|
|
1880
|
+
Tree.LoadMoreItem = TreeLoadMoreItem;
|
|
1881
|
+
Tree.Section = TreeSection;
|
|
1882
|
+
Tree.Header = TreeHeader;
|
|
1883
|
+
|
|
1884
|
+
function areSetsEqual<T>(a: Set<T>, b: Set<T>): boolean {
|
|
1885
|
+
if (a.size !== b.size) return false;
|
|
1886
|
+
for (const entry of a) {
|
|
1887
|
+
if (!b.has(entry)) return false;
|
|
1888
|
+
}
|
|
1889
|
+
return true;
|
|
1890
|
+
}
|