@proyecto-viviana/solidaria-components 0.2.9 → 0.3.1

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