@octanejs/aria 0.0.5 → 0.0.6

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 (120) hide show
  1. package/package.json +3 -3
  2. package/src/components/Table.ts +2618 -0
  3. package/src/components/Tree.ts +1450 -0
  4. package/src/components/TreeDropTargetDelegate.ts +363 -0
  5. package/src/components/index.ts +57 -0
  6. package/src/grid/GridKeyboardDelegate.ts +514 -0
  7. package/src/grid/useGrid.ts +263 -0
  8. package/src/grid/useGridCell.ts +346 -0
  9. package/src/grid/useGridRow.ts +108 -0
  10. package/src/grid/useGridRowGroup.ts +29 -0
  11. package/src/grid/utils.ts +24 -0
  12. package/src/index.ts +38 -0
  13. package/src/intl/autocomplete/index.ts +9 -2
  14. package/src/intl/breadcrumbs/index.ts +9 -2
  15. package/src/intl/combobox/index.ts +9 -2
  16. package/src/intl/compileMessages.ts +356 -0
  17. package/src/intl/grid/index.ts +9 -2
  18. package/src/intl/gridlist/index.ts +9 -2
  19. package/src/intl/menu/index.ts +9 -2
  20. package/src/intl/numberfield/index.ts +9 -2
  21. package/src/intl/overlays/index.ts +9 -2
  22. package/src/intl/searchfield/index.ts +9 -2
  23. package/src/intl/spinbutton/index.ts +9 -2
  24. package/src/intl/table/ar-AE.json +13 -0
  25. package/src/intl/table/bg-BG.json +13 -0
  26. package/src/intl/table/cs-CZ.json +13 -0
  27. package/src/intl/table/da-DK.json +13 -0
  28. package/src/intl/table/de-DE.json +13 -0
  29. package/src/intl/table/el-GR.json +13 -0
  30. package/src/intl/table/en-US.json +13 -0
  31. package/src/intl/table/es-ES.json +13 -0
  32. package/src/intl/table/et-EE.json +13 -0
  33. package/src/intl/table/fi-FI.json +13 -0
  34. package/src/intl/table/fr-FR.json +13 -0
  35. package/src/intl/table/he-IL.json +13 -0
  36. package/src/intl/table/hr-HR.json +13 -0
  37. package/src/intl/table/hu-HU.json +13 -0
  38. package/src/intl/table/index.ts +82 -0
  39. package/src/intl/table/it-IT.json +13 -0
  40. package/src/intl/table/ja-JP.json +13 -0
  41. package/src/intl/table/ko-KR.json +13 -0
  42. package/src/intl/table/lt-LT.json +13 -0
  43. package/src/intl/table/lv-LV.json +13 -0
  44. package/src/intl/table/nb-NO.json +13 -0
  45. package/src/intl/table/nl-NL.json +13 -0
  46. package/src/intl/table/pl-PL.json +13 -0
  47. package/src/intl/table/pt-BR.json +13 -0
  48. package/src/intl/table/pt-PT.json +13 -0
  49. package/src/intl/table/ro-RO.json +13 -0
  50. package/src/intl/table/ru-RU.json +13 -0
  51. package/src/intl/table/sk-SK.json +13 -0
  52. package/src/intl/table/sl-SI.json +13 -0
  53. package/src/intl/table/sr-SP.json +13 -0
  54. package/src/intl/table/sv-SE.json +13 -0
  55. package/src/intl/table/tr-TR.json +13 -0
  56. package/src/intl/table/uk-UA.json +13 -0
  57. package/src/intl/table/zh-CN.json +13 -0
  58. package/src/intl/table/zh-TW.json +13 -0
  59. package/src/intl/tag/index.ts +9 -2
  60. package/src/intl/tree/ar-AE.json +4 -0
  61. package/src/intl/tree/bg-BG.json +4 -0
  62. package/src/intl/tree/cs-CZ.json +4 -0
  63. package/src/intl/tree/da-DK.json +4 -0
  64. package/src/intl/tree/de-DE.json +4 -0
  65. package/src/intl/tree/el-GR.json +4 -0
  66. package/src/intl/tree/en-US.json +4 -0
  67. package/src/intl/tree/es-ES.json +4 -0
  68. package/src/intl/tree/et-EE.json +4 -0
  69. package/src/intl/tree/fi-FI.json +4 -0
  70. package/src/intl/tree/fr-FR.json +4 -0
  71. package/src/intl/tree/he-IL.json +4 -0
  72. package/src/intl/tree/hr-HR.json +4 -0
  73. package/src/intl/tree/hu-HU.json +4 -0
  74. package/src/intl/tree/index.ts +82 -0
  75. package/src/intl/tree/it-IT.json +4 -0
  76. package/src/intl/tree/ja-JP.json +4 -0
  77. package/src/intl/tree/ko-KR.json +4 -0
  78. package/src/intl/tree/lt-LT.json +4 -0
  79. package/src/intl/tree/lv-LV.json +4 -0
  80. package/src/intl/tree/nb-NO.json +4 -0
  81. package/src/intl/tree/nl-NL.json +4 -0
  82. package/src/intl/tree/pl-PL.json +4 -0
  83. package/src/intl/tree/pt-BR.json +4 -0
  84. package/src/intl/tree/pt-PT.json +4 -0
  85. package/src/intl/tree/ro-RO.json +4 -0
  86. package/src/intl/tree/ru-RU.json +4 -0
  87. package/src/intl/tree/sk-SK.json +4 -0
  88. package/src/intl/tree/sl-SI.json +4 -0
  89. package/src/intl/tree/sr-SP.json +4 -0
  90. package/src/intl/tree/sv-SE.json +4 -0
  91. package/src/intl/tree/tr-TR.json +4 -0
  92. package/src/intl/tree/uk-UA.json +4 -0
  93. package/src/intl/tree/zh-CN.json +4 -0
  94. package/src/intl/tree/zh-TW.json +4 -0
  95. package/src/stately/grid/GridCollection.ts +200 -0
  96. package/src/stately/grid/useGridState.ts +160 -0
  97. package/src/stately/index.ts +43 -0
  98. package/src/stately/table/Cell.ts +49 -0
  99. package/src/stately/table/Column.ts +121 -0
  100. package/src/stately/table/Row.ts +145 -0
  101. package/src/stately/table/TableBody.ts +66 -0
  102. package/src/stately/table/TableCollection.ts +390 -0
  103. package/src/stately/table/TableColumnLayout.ts +148 -0
  104. package/src/stately/table/TableHeader.ts +65 -0
  105. package/src/stately/table/TableUtils.ts +269 -0
  106. package/src/stately/table/useTableColumnResizeState.ts +202 -0
  107. package/src/stately/table/useTableState.ts +235 -0
  108. package/src/stately/table/useTreeGridState.ts +248 -0
  109. package/src/table/TableKeyboardDelegate.ts +205 -0
  110. package/src/table/useTable.ts +175 -0
  111. package/src/table/useTableCell.ts +81 -0
  112. package/src/table/useTableColumnHeader.ts +155 -0
  113. package/src/table/useTableColumnResize.ts +374 -0
  114. package/src/table/useTableHeaderRow.ts +57 -0
  115. package/src/table/useTableRow.ts +198 -0
  116. package/src/table/useTableRowGroup.ts +13 -0
  117. package/src/table/useTableSelectionCheckbox.ts +105 -0
  118. package/src/table/utils.ts +42 -0
  119. package/src/tree/useTree.ts +76 -0
  120. package/src/tree/useTreeItem.ts +109 -0
@@ -0,0 +1,1450 @@
1
+ // Ported from react-aria-components (source: .react-spectrum/packages/react-aria-components/src/Tree.tsx).
2
+ // octane adaptations: `.tsx` → `.ts`, JSX → `createElement`; NO forwardRef — the forwarded
3
+ // ref is `props.ref` (Tree passes it into `useContextProps` explicitly; TreeItem,
4
+ // TreeSection, TreeLoadMoreItem and the drop-indicator wrapper adapt theirs with
5
+ // `useObjectRef` exactly like upstream's forwarded refs); the plain-`.ts` components use
6
+ // the S()/subSlot component-slot convention. The collection composes the Phase-4 engine:
7
+ // `CollectionBuilder`/`createLeafComponent`/`createBranchComponent` from
8
+ // `../collections/CollectionBuilder` and the renderer's `CollectionRoot`/`CollectionBranch`
9
+ // via `CollectionRendererContext`. Upstream's RAC-local `CollectionProps` import is our
10
+ // `ItemCollectionProps` (see ./Collection.ts). Upstream's `inertValue` (React <19 string
11
+ // compat) collapses to the plain boolean — octane follows React 19 `inert` semantics.
12
+ // `React.useMemo` in TreeItem is octane's `useMemo` with the identical explicit deps. The
13
+ // dnd branches use the PHASE-7 structural aliases from ./useDragAndDrop and stay inert
14
+ // until a consumer can construct `dragAndDropHooks` (that includes the
15
+ // `TreeDropTargetDelegate` wiring, which is only reachable through `hasDropHooks`).
16
+ // react-aria's private `useLoadMoreSentinel` comes from ../utils/useLoadMoreSentinel and
17
+ // `useCachedChildren` from ../collections/useCachedChildren. Explicit dep arrays are
18
+ // preserved verbatim. The flattened-collection derivation keeps upstream's render-phase
19
+ // setState idiom (octane matches React's render-phase-update semantics).
20
+ import type {
21
+ DisabledBehavior,
22
+ Expandable,
23
+ HoverEvents,
24
+ Key,
25
+ LinkDOMProps,
26
+ MultipleSelection,
27
+ Node,
28
+ PressEvents,
29
+ SelectionBehavior,
30
+ SelectionMode,
31
+ } from '@react-types/shared';
32
+ import {
33
+ Fragment,
34
+ createContext,
35
+ createElement,
36
+ useContext,
37
+ useEffect,
38
+ useMemo,
39
+ useRef,
40
+ useState,
41
+ } from 'octane';
42
+
43
+ import {
44
+ BaseCollection,
45
+ CollectionNode,
46
+ LoaderNode,
47
+ SectionNode,
48
+ } from '../collections/BaseCollection';
49
+ import {
50
+ CollectionBuilder,
51
+ createBranchComponent,
52
+ createLeafComponent,
53
+ } from '../collections/CollectionBuilder';
54
+ import { useCachedChildren } from '../collections/useCachedChildren';
55
+ import { FocusScope } from '../focus/FocusScope';
56
+ import { useFocusRing } from '../focus/useFocusRing';
57
+ import { useGridListSection } from '../gridlist/useGridListSection';
58
+ import { useGridListSelectionCheckbox } from '../gridlist/useGridListSelectionCheckbox';
59
+ import { useCollator } from '../i18n/useCollator';
60
+ import { useLocale } from '../i18n/I18nProvider';
61
+ import { useHover } from '../interactions/useHover';
62
+ import { S, subSlot } from '../internal';
63
+ import { ListKeyboardDelegate } from '../selection/ListKeyboardDelegate';
64
+ import { type TreeState, useTreeState } from '../stately/tree/useTreeState';
65
+ import { useControlledState } from '../stately/utils/useControlledState';
66
+ import { type AriaTreeProps, useTree } from '../tree/useTree';
67
+ import { type AriaTreeItemOptions, useTreeItem } from '../tree/useTreeItem';
68
+ import { filterDOMProps } from '../utils/filterDOMProps';
69
+ import { mergeProps } from '../utils/mergeProps';
70
+ import { useId } from '../utils/useId';
71
+ import { type LoadMoreSentinelProps, useLoadMoreSentinel } from '../utils/useLoadMoreSentinel';
72
+ import { useObjectRef } from '../utils/useObjectRef';
73
+ import { useVisuallyHidden } from '../visually-hidden/VisuallyHidden';
74
+ import { ButtonContext } from './Button';
75
+ import { CheckboxContext, CheckboxFieldContext } from './Checkbox';
76
+ import {
77
+ CollectionRendererContext,
78
+ DefaultCollectionRenderer,
79
+ type ItemCollectionProps,
80
+ type ItemRenderProps,
81
+ type SectionProps,
82
+ } from './Collection';
83
+ import { Collection } from './Collection';
84
+ import {
85
+ DragAndDropContext,
86
+ DropIndicatorContext,
87
+ type DropIndicatorProps,
88
+ useDndPersistedKeys,
89
+ useRenderDropIndicator,
90
+ } from './DragAndDrop';
91
+ import type {
92
+ DragAndDropHooks,
93
+ DraggableCollectionState,
94
+ DraggableItemResult,
95
+ DropIndicatorAria,
96
+ DroppableCollectionResult,
97
+ DroppableCollectionState,
98
+ } from './useDragAndDrop';
99
+ import {
100
+ GridListHeader,
101
+ GridListHeaderContext,
102
+ GridListHeaderInnerContext,
103
+ type GridListHeaderProps,
104
+ } from './GridList';
105
+ import { SelectionIndicatorContext } from './SelectionIndicator';
106
+ import { SharedElementTransition } from './SharedElementTransition';
107
+ import { TreeDropTargetDelegate } from './TreeDropTargetDelegate';
108
+ import {
109
+ type ChildrenOrFunction,
110
+ type ClassNameOrFunction,
111
+ type ContextValue,
112
+ DEFAULT_SLOT,
113
+ dom,
114
+ type DOMRenderProps,
115
+ Provider,
116
+ type RenderProps,
117
+ type SlotProps,
118
+ type StyleRenderProps,
119
+ useContextProps,
120
+ useRenderProps,
121
+ } from './utils';
122
+
123
+ // octane adaptation: structural bag (upstream's `GlobalDOMAttributes` drags React handler types).
124
+ type GlobalDOMAttributes = Record<string, any>;
125
+ type RefObject<T> = { current: T };
126
+ type ReactNode = any;
127
+
128
+ class TreeCollection<T> extends BaseCollection<T> {
129
+ private expandedKeys: Set<Key> = new Set();
130
+
131
+ withExpandedKeys(lastExpandedKeys: Set<Key>, expandedKeys: Set<Key>): TreeCollection<T> {
132
+ let collection = this.clone();
133
+ collection.expandedKeys = expandedKeys;
134
+
135
+ // Clone ancestor section nodes so the renderer knows to re-render since the same item won't cause a new render but a clone creating a new object with the same value will
136
+ // Without this change, the items won't expand and collapse when virtualized inside a section
137
+ TreeCollection.cloneAncestorSections(expandedKeys, lastExpandedKeys, collection);
138
+ TreeCollection.cloneAncestorSections(lastExpandedKeys, expandedKeys, collection);
139
+
140
+ collection.frozen = this.frozen;
141
+ return collection;
142
+ }
143
+
144
+ // diff lastExpandedKeys and expandedKeys so we only clone what has changed
145
+ private static cloneAncestorSections<T>(
146
+ keys: Iterable<Key>,
147
+ excludeSet: Set<Key>,
148
+ collection: TreeCollection<T>,
149
+ ) {
150
+ for (let key of keys) {
151
+ if (!excludeSet.has(key)) {
152
+ let currentKey: Key | null = key;
153
+ while (currentKey != null) {
154
+ let item = collection.getItem(currentKey) as CollectionNode<T>;
155
+ if (item?.type === 'section') {
156
+ collection.keyMap.set(currentKey, item.clone());
157
+ break;
158
+ } else {
159
+ currentKey = item?.parentKey ?? null;
160
+ }
161
+ }
162
+ }
163
+ }
164
+ }
165
+
166
+ *[Symbol.iterator](): IterableIterator<Node<T>> {
167
+ let firstKey = this.getFirstKey();
168
+ let node: Node<T> | null = firstKey != null ? this.getItem(firstKey) : null;
169
+
170
+ while (node) {
171
+ yield node as Node<T>;
172
+ if (node.type === 'section') {
173
+ node = node.nextKey != null ? this.getItem(node.nextKey) : null;
174
+ } else {
175
+ // This will include both item and content nodes
176
+ // We handle the content nodes in useCollectionRenderer and ListLayout
177
+ let key = this.getKeyAfter(node.key);
178
+ node = key != null ? this.getItem(key) : null;
179
+ }
180
+ }
181
+ }
182
+
183
+ getLastKey(): Key | null {
184
+ // Find the deepest expanded child. We don't use collection.getLastKey() here
185
+ // because that will return the deepest child regardless of expandedKeys.
186
+ // Instead, start from the last top-level key and walk down.
187
+ let key = this.lastKey;
188
+ if (key == null) {
189
+ return null;
190
+ }
191
+
192
+ let node = this.getItem(key) as CollectionNode<T>;
193
+
194
+ while (
195
+ node?.lastChildKey != null &&
196
+ (node.type !== 'item' || this.expandedKeys.has(node.key))
197
+ ) {
198
+ node = this.getItem(node.lastChildKey) as CollectionNode<T>;
199
+ }
200
+
201
+ return node?.key ?? null;
202
+ }
203
+
204
+ getKeyAfter(key: Key): Key | null {
205
+ let node = this.getItem(key) as CollectionNode<T>;
206
+ if (!node) {
207
+ return null;
208
+ }
209
+
210
+ if ((this.expandedKeys.has(node.key) || node.type !== 'item') && node.firstChildKey != null) {
211
+ return node.firstChildKey;
212
+ }
213
+
214
+ while (node) {
215
+ if (node.nextKey != null) {
216
+ return node.nextKey;
217
+ }
218
+
219
+ if (node.parentKey != null) {
220
+ node = this.getItem(node.parentKey) as CollectionNode<T>;
221
+ } else {
222
+ return null;
223
+ }
224
+ }
225
+
226
+ return null;
227
+ }
228
+
229
+ getKeyBefore(key: Key): Key | null {
230
+ let node = this.getItem(key) as CollectionNode<T>;
231
+ if (!node) {
232
+ return null;
233
+ }
234
+
235
+ if (node.prevKey != null) {
236
+ node = this.getItem(node.prevKey) as CollectionNode<T>;
237
+
238
+ // If the lastChildKey is expanded, check its lastChildKey
239
+ while (
240
+ node &&
241
+ (node.type !== 'item' || this.expandedKeys.has(node.key)) &&
242
+ node.lastChildKey != null
243
+ ) {
244
+ node = this.getItem(node.lastChildKey) as CollectionNode<T>;
245
+ }
246
+
247
+ return node?.key ?? null;
248
+ }
249
+
250
+ return node.parentKey;
251
+ }
252
+
253
+ getChildren(key: Key): Iterable<Node<T>> {
254
+ let self = this;
255
+ return {
256
+ *[Symbol.iterator]() {
257
+ let parent = self.getItem(key) as CollectionNode<T> | null;
258
+ let node =
259
+ parent?.firstChildKey != null
260
+ ? (self.getItem(parent.firstChildKey) as CollectionNode<T>)
261
+ : null;
262
+ if (parent && parent.type === 'section' && node) {
263
+ // Stop once either the node is null or the node is the parent's sibling
264
+ while (node && node.key !== parent.nextKey) {
265
+ yield self.getItem(node.key)!;
266
+ // This will include content nodes which we skip in ListLayout
267
+ let key = self.getKeyAfter(node.key);
268
+ node = key != null ? (self.getItem(key)! as CollectionNode<T>) : null;
269
+ }
270
+ } else {
271
+ while (node) {
272
+ yield node as Node<T>;
273
+ node = node.nextKey != null ? (self.getItem(node.nextKey)! as CollectionNode<T>) : null;
274
+ }
275
+ }
276
+ },
277
+ };
278
+ }
279
+
280
+ getTextValue(key: Key): string {
281
+ let item = this.getItem(key);
282
+ return item ? item.textValue : '';
283
+ }
284
+ }
285
+
286
+ export interface TreeRenderProps {
287
+ /**
288
+ * Whether the tree has no items and should display its empty state.
289
+ *
290
+ * @selector [data-empty]
291
+ */
292
+ isEmpty: boolean;
293
+ /**
294
+ * Whether the tree is currently focused.
295
+ *
296
+ * @selector [data-focused]
297
+ */
298
+ isFocused: boolean;
299
+ /**
300
+ * Whether the tree is currently keyboard focused.
301
+ *
302
+ * @selector [data-focus-visible]
303
+ */
304
+ isFocusVisible: boolean;
305
+ /**
306
+ * The type of selection that is allowed in the collection.
307
+ *
308
+ * @selector [data-selection-mode="single | multiple"]
309
+ */
310
+ selectionMode: SelectionMode;
311
+ /**
312
+ * Whether the tree allows dragging.
313
+ *
314
+ * @selector [data-allows-dragging]
315
+ */
316
+ allowsDragging: boolean;
317
+ /**
318
+ * Whether the tree is currently the active drop target.
319
+ *
320
+ * @selector [data-drop-target]
321
+ */
322
+ isDropTarget: boolean;
323
+ /**
324
+ * State of the tree.
325
+ */
326
+ state: TreeState<unknown>;
327
+ }
328
+
329
+ export interface TreeEmptyStateRenderProps extends Omit<TreeRenderProps, 'isEmpty'> {}
330
+
331
+ export interface TreeProps<T>
332
+ extends
333
+ Omit<AriaTreeProps<T>, 'children'>,
334
+ MultipleSelection,
335
+ ItemCollectionProps<T>,
336
+ StyleRenderProps<TreeRenderProps>,
337
+ SlotProps,
338
+ Expandable,
339
+ GlobalDOMAttributes {
340
+ /**
341
+ * The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
342
+ * element. A function may be provided to compute the class based on component state.
343
+ *
344
+ * @default 'react-aria-Tree'
345
+ */
346
+ className?: ClassNameOrFunction<TreeRenderProps>;
347
+ /**
348
+ * How multiple selection should behave in the tree.
349
+ *
350
+ * @default 'toggle'
351
+ */
352
+ selectionBehavior?: SelectionBehavior;
353
+ /** Provides content to display when there are no items in the list. */
354
+ renderEmptyState?: (props: TreeEmptyStateRenderProps) => ReactNode;
355
+ /**
356
+ * Whether `disabledKeys` applies to all interactions, or only selection.
357
+ *
358
+ * @default 'all'
359
+ */
360
+ disabledBehavior?: DisabledBehavior;
361
+ /**
362
+ * The drag and drop hooks returned by `useDragAndDrop` used to enable drag and drop behavior for
363
+ * the Tree.
364
+ */
365
+ dragAndDropHooks?: DragAndDropHooks<NoInfer<T>>;
366
+ }
367
+
368
+ export const TreeContext = createContext<ContextValue<TreeProps<any>, HTMLDivElement>>(null);
369
+ export const TreeStateContext = createContext<TreeState<any> | null>(null);
370
+
371
+ /**
372
+ * A tree provides users with a way to navigate nested hierarchical information, with support for
373
+ * keyboard navigation and selection.
374
+ */
375
+ export function Tree<T extends object>(props: TreeProps<T>): any {
376
+ const slot = S('Tree');
377
+ // Render the portal first so that we have the collection by the time we render the DOM in SSR.
378
+ let ref: any;
379
+ [props, ref] = useContextProps(props, (props as any).ref, TreeContext, subSlot(slot, 'ctx'));
380
+
381
+ return createElement(CollectionBuilder, {
382
+ content: createElement(Collection, props as any),
383
+ createCollection: () => new TreeCollection<T>(),
384
+ children: (collection: TreeCollection<T>) =>
385
+ createElement(TreeInner, { props, collection, treeRef: ref }),
386
+ } as any);
387
+ }
388
+
389
+ const EXPANSION_KEYS = {
390
+ expand: {
391
+ ltr: 'ArrowRight',
392
+ rtl: 'ArrowLeft',
393
+ },
394
+ collapse: {
395
+ ltr: 'ArrowLeft',
396
+ rtl: 'ArrowRight',
397
+ },
398
+ };
399
+
400
+ interface TreeInnerProps<T> {
401
+ props: TreeProps<T>;
402
+ collection: TreeCollection<T>;
403
+ treeRef: RefObject<HTMLDivElement | null>;
404
+ }
405
+
406
+ function TreeInner<T>({ props, collection, treeRef: ref }: TreeInnerProps<T>): any {
407
+ const slot = S('TreeInner');
408
+ const { dragAndDropHooks } = props;
409
+ let { direction } = useLocale(subSlot(slot, 'locale'));
410
+ let collator = useCollator({ usage: 'search', sensitivity: 'base' }, subSlot(slot, 'collator'));
411
+ let hasDragHooks = !!dragAndDropHooks?.useDraggableCollectionState;
412
+ let hasDropHooks = !!dragAndDropHooks?.useDroppableCollectionState;
413
+ let dragHooksProvided = useRef(hasDragHooks, subSlot(slot, 'dragProvided'));
414
+ let dropHooksProvided = useRef(hasDropHooks, subSlot(slot, 'dropProvided'));
415
+
416
+ useEffect(
417
+ () => {
418
+ if (dragHooksProvided.current !== hasDragHooks) {
419
+ console.warn(
420
+ 'Drag hooks were provided during one render, but not another. This should be avoided as it may produce unexpected behavior.',
421
+ );
422
+ }
423
+ if (dropHooksProvided.current !== hasDropHooks) {
424
+ console.warn(
425
+ 'Drop hooks were provided during one render, but not another. This should be avoided as it may produce unexpected behavior.',
426
+ );
427
+ }
428
+ },
429
+ [hasDragHooks, hasDropHooks],
430
+ subSlot(slot, 'dndWarn'),
431
+ );
432
+ let {
433
+ selectionMode = 'none',
434
+ expandedKeys: propExpandedKeys,
435
+ defaultExpandedKeys: propDefaultExpandedKeys,
436
+ onExpandedChange,
437
+ disabledBehavior = 'all',
438
+ } = props;
439
+ let {
440
+ CollectionRoot,
441
+ isVirtualized,
442
+ layoutDelegate,
443
+ dropTargetDelegate: ctxDropTargetDelegate,
444
+ } = useContext(CollectionRendererContext);
445
+
446
+ // Kinda annoying that we have to replicate this code here as well as in useTreeState, but don't want to add
447
+ // flattenCollection stuff to useTreeState. Think about this later
448
+ let [expandedKeys, setExpandedKeys] = useControlledState(
449
+ propExpandedKeys ? new Set(propExpandedKeys) : undefined,
450
+ propDefaultExpandedKeys ? new Set(propDefaultExpandedKeys) : new Set(),
451
+ onExpandedChange as any,
452
+ subSlot(slot, 'expandedKeys'),
453
+ );
454
+
455
+ let [lastCollection, setLastCollection] = useState(collection, subSlot(slot, 'lastCollection'));
456
+ let [lastExpandedKeys, setLastExpandedKeys] = useState(
457
+ expandedKeys,
458
+ subSlot(slot, 'lastExpandedKeys'),
459
+ );
460
+ let [flattenedCollection, setFlattenedCollection] = useState(
461
+ () => collection.withExpandedKeys(lastExpandedKeys, expandedKeys),
462
+ subSlot(slot, 'flattenedCollection'),
463
+ );
464
+
465
+ // if the lastExpandedKeys is not the same as the currentExpandedKeys or the collection has changed, then run this
466
+ if (!areSetsEqual(lastExpandedKeys, expandedKeys) || collection !== lastCollection) {
467
+ setFlattenedCollection(collection.withExpandedKeys(lastExpandedKeys, expandedKeys));
468
+ setLastCollection(collection);
469
+ setLastExpandedKeys(expandedKeys);
470
+ }
471
+
472
+ let state = useTreeState(
473
+ {
474
+ ...props,
475
+ selectionMode,
476
+ expandedKeys,
477
+ onExpandedChange: setExpandedKeys,
478
+ collection: flattenedCollection,
479
+ children: undefined,
480
+ disabledBehavior,
481
+ } as any,
482
+ subSlot(slot, 'state'),
483
+ );
484
+
485
+ let { gridProps } = useTree(
486
+ {
487
+ ...props,
488
+ isVirtualized,
489
+ layoutDelegate,
490
+ } as any,
491
+ state,
492
+ ref,
493
+ subSlot(slot, 'tree'),
494
+ );
495
+
496
+ let dragState: DraggableCollectionState | undefined = undefined;
497
+ let dropState: DroppableCollectionState | undefined = undefined;
498
+ let droppableCollection: DroppableCollectionResult | undefined = undefined;
499
+ let isRootDropTarget = false;
500
+ let dragPreview: any = null;
501
+ let preview = useRef<any>(null, subSlot(slot, 'preview'));
502
+
503
+ // PHASE-7: these branches call consumer-provided dnd hooks (upstream contract); they are
504
+ // unreachable until `useDragAndDrop` is ported and a consumer can construct hooks.
505
+ if (hasDragHooks && dragAndDropHooks) {
506
+ dragState = dragAndDropHooks.useDraggableCollectionState!({
507
+ collection: state.collection,
508
+ selectionManager: state.selectionManager,
509
+ preview: dragAndDropHooks.renderDragPreview ? preview : undefined,
510
+ });
511
+ dragAndDropHooks.useDraggableCollection!({}, dragState, ref);
512
+
513
+ let DragPreview = dragAndDropHooks.DragPreview!;
514
+ dragPreview = dragAndDropHooks.renderDragPreview
515
+ ? createElement(DragPreview, { ref: preview, children: dragAndDropHooks.renderDragPreview })
516
+ : null;
517
+ }
518
+
519
+ let [treeDropTargetDelegate] = useState(
520
+ () => new TreeDropTargetDelegate(),
521
+ subSlot(slot, 'dropDelegate'),
522
+ );
523
+ if (hasDropHooks && dragAndDropHooks) {
524
+ dropState = dragAndDropHooks.useDroppableCollectionState!({
525
+ collection: state.collection,
526
+ selectionManager: state.selectionManager,
527
+ });
528
+ let dropTargetDelegate =
529
+ dragAndDropHooks.dropTargetDelegate ||
530
+ ctxDropTargetDelegate ||
531
+ new dragAndDropHooks.ListDropTargetDelegate(state.collection, ref, { direction });
532
+ treeDropTargetDelegate.setup(dropTargetDelegate, state as any, direction);
533
+
534
+ let keyboardDelegate = new ListKeyboardDelegate({
535
+ collection: state.collection,
536
+ collator,
537
+ ref,
538
+ disabledKeys: state.selectionManager.disabledKeys,
539
+ disabledBehavior: state.selectionManager.disabledBehavior,
540
+ direction,
541
+ layoutDelegate,
542
+ });
543
+
544
+ droppableCollection = dragAndDropHooks.useDroppableCollection!(
545
+ {
546
+ keyboardDelegate,
547
+ dropTargetDelegate: treeDropTargetDelegate,
548
+ onDropActivate: (e: any) => {
549
+ // Expand collapsed item when dragging over. For keyboard, allow collapsing.
550
+ if (e.target.type === 'item') {
551
+ let key = e.target.key;
552
+ let item = state.collection.getItem(key);
553
+ let isExpanded = expandedKeys.has(key);
554
+ if (
555
+ item &&
556
+ item.hasChildNodes &&
557
+ (!isExpanded || dragAndDropHooks?.isVirtualDragging?.())
558
+ ) {
559
+ state.toggleKey(key);
560
+ }
561
+ }
562
+ },
563
+ onKeyDown: (e: KeyboardEvent) => {
564
+ let target = dropState?.target;
565
+ if (target && target.type === 'item' && target.dropPosition === 'on') {
566
+ let item = state.collection.getItem(target.key);
567
+ if (
568
+ e.key === EXPANSION_KEYS['expand'][direction] &&
569
+ item?.hasChildNodes &&
570
+ !state.expandedKeys.has(target.key)
571
+ ) {
572
+ state.toggleKey(target.key);
573
+ } else if (
574
+ e.key === EXPANSION_KEYS['collapse'][direction] &&
575
+ item?.hasChildNodes &&
576
+ state.expandedKeys.has(target.key)
577
+ ) {
578
+ state.toggleKey(target.key);
579
+ }
580
+ }
581
+ },
582
+ },
583
+ dropState,
584
+ ref,
585
+ );
586
+
587
+ isRootDropTarget = dropState.isDropTarget({ type: 'root' });
588
+ }
589
+
590
+ let isTreeDraggable = !!(hasDragHooks && !dragState?.isDisabled);
591
+
592
+ let { focusProps, isFocused, isFocusVisible } = useFocusRing(
593
+ undefined,
594
+ subSlot(slot, 'focusRing'),
595
+ );
596
+ let renderValues = {
597
+ isEmpty: state.collection.size === 0,
598
+ isFocused,
599
+ isFocusVisible,
600
+ isDropTarget: isRootDropTarget,
601
+ selectionMode: state.selectionManager.selectionMode,
602
+ allowsDragging: !!isTreeDraggable,
603
+ state,
604
+ };
605
+
606
+ let renderProps = useRenderProps(
607
+ {
608
+ ...props,
609
+ children: undefined,
610
+ defaultClassName: 'react-aria-Tree',
611
+ values: renderValues,
612
+ } as any,
613
+ subSlot(slot, 'render'),
614
+ );
615
+
616
+ let emptyState: any = null;
617
+ if (state.collection.size === 0 && props.renderEmptyState) {
618
+ let { isEmpty: _isEmpty, ...values } = renderValues;
619
+ let content = props.renderEmptyState({ ...values } as any);
620
+ let treeGridRowProps = {
621
+ 'aria-level': 1,
622
+ };
623
+
624
+ emptyState = createElement('div', {
625
+ role: 'row',
626
+ style: { display: 'contents' },
627
+ ...treeGridRowProps,
628
+ children: createElement('div', {
629
+ role: 'gridcell',
630
+ style: { display: 'contents' },
631
+ children: content,
632
+ }),
633
+ });
634
+ }
635
+
636
+ let DOMProps = filterDOMProps(props, { global: true });
637
+
638
+ // octane adaptation: hooks hoisted out of the createElement argument list (upstream calls
639
+ // them inline in JSX attribute position).
640
+ let persistedKeys = useDndPersistedKeys(
641
+ state.selectionManager,
642
+ dragAndDropHooks,
643
+ dropState,
644
+ subSlot(slot, 'persisted'),
645
+ );
646
+ let renderDropIndicator = useRenderDropIndicator(
647
+ dragAndDropHooks,
648
+ dropState,
649
+ subSlot(slot, 'dropIndicator'),
650
+ );
651
+
652
+ return createElement(
653
+ Fragment,
654
+ null,
655
+ createElement(FocusScope, {
656
+ children: createElement(dom.div, {
657
+ ...mergeProps(
658
+ DOMProps,
659
+ renderProps,
660
+ gridProps,
661
+ focusProps,
662
+ droppableCollection?.collectionProps,
663
+ ),
664
+ ref,
665
+ slot: props.slot || undefined,
666
+ 'data-empty': state.collection.size === 0 || undefined,
667
+ 'data-focused': isFocused || undefined,
668
+ 'data-drop-target': isRootDropTarget || undefined,
669
+ 'data-focus-visible': isFocusVisible || undefined,
670
+ 'data-selection-mode':
671
+ state.selectionManager.selectionMode === 'none'
672
+ ? undefined
673
+ : state.selectionManager.selectionMode,
674
+ 'data-allows-dragging': !!isTreeDraggable || undefined,
675
+ // Positional (non-array) children: the trailing empty-state slot is an unkeyed
676
+ // sibling of the collection Provider, exactly like upstream's JSX.
677
+ children: createElement(
678
+ Fragment,
679
+ null,
680
+ createElement(
681
+ Provider,
682
+ {
683
+ values: [
684
+ [TreeStateContext, state],
685
+ [DragAndDropContext, { dragAndDropHooks, dragState, dropState }],
686
+ [DropIndicatorContext, { render: TreeDropIndicatorWrapper }],
687
+ ] as any,
688
+ } as any,
689
+ hasDropHooks ? createElement(RootDropIndicator, {}) : null,
690
+ createElement(SharedElementTransition, {
691
+ children: createElement(CollectionRoot, {
692
+ collection: state.collection,
693
+ persistedKeys,
694
+ scrollRef: ref,
695
+ renderDropIndicator,
696
+ }),
697
+ }),
698
+ ),
699
+ emptyState,
700
+ ),
701
+ }),
702
+ }),
703
+ dragPreview,
704
+ );
705
+ }
706
+
707
+ // TODO: readd the rest of the render props when tree supports them
708
+ export interface TreeItemRenderProps extends ItemRenderProps {
709
+ /**
710
+ * Whether the tree item is expanded.
711
+ *
712
+ * @selector [data-expanded]
713
+ */
714
+ isExpanded: boolean;
715
+ /**
716
+ * Whether the tree item has child tree items.
717
+ *
718
+ * @selector [data-has-child-items]
719
+ */
720
+ hasChildItems: boolean;
721
+ /**
722
+ * What level the tree item has within the tree.
723
+ *
724
+ * @selector [data-level="number"]
725
+ */
726
+ level: number;
727
+ /**
728
+ * Whether the tree item's children have keyboard focus.
729
+ *
730
+ * @selector [data-focus-visible-within]
731
+ */
732
+ isFocusVisibleWithin: boolean;
733
+ /** The state of the tree. */
734
+ state: TreeState<unknown>;
735
+ /** The unique id of the tree row. */
736
+ id: Key;
737
+ }
738
+
739
+ export interface TreeItemContentRenderProps extends TreeItemRenderProps {}
740
+
741
+ // The TreeItemContent is the one that accepts RenderProps because we would get much more complicated logic in TreeItem otherwise since we'd
742
+ // need to do a bunch of check to figure out what is the Content and what are the actual collection elements (aka child rows) of the TreeItem
743
+ export interface TreeItemContentProps {
744
+ /**
745
+ * The children of the component. A function may be provided to alter the children based on
746
+ * component state.
747
+ */
748
+ children: ChildrenOrFunction<TreeItemContentRenderProps>;
749
+ }
750
+
751
+ class TreeContentNode extends CollectionNode<any> {
752
+ static readonly type = 'content';
753
+ }
754
+
755
+ export const TreeItemContent: (props: TreeItemContentProps) => any =
756
+ /*#__PURE__*/ createLeafComponent(
757
+ TreeContentNode,
758
+ function TreeItemContent(props: TreeItemContentProps): any {
759
+ const slot = S('TreeItemContent');
760
+ let values = useContext(TreeItemContentContext)!;
761
+ let renderProps = useRenderProps(
762
+ {
763
+ children: props.children,
764
+ values,
765
+ } as any,
766
+ subSlot(slot, 'render'),
767
+ );
768
+ return createElement(CollectionRendererContext.Provider, {
769
+ value: DefaultCollectionRenderer,
770
+ children: renderProps.children,
771
+ });
772
+ },
773
+ );
774
+
775
+ export const TreeItemContentContext = createContext<TreeItemContentRenderProps | null>(null);
776
+
777
+ export interface TreeItemProps<T = object>
778
+ extends
779
+ StyleRenderProps<TreeItemRenderProps>,
780
+ LinkDOMProps,
781
+ HoverEvents,
782
+ PressEvents,
783
+ Pick<AriaTreeItemOptions, 'hasChildItems'>,
784
+ Omit<GlobalDOMAttributes, 'onClick'> {
785
+ /**
786
+ * The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
787
+ * element. A function may be provided to compute the class based on component state.
788
+ *
789
+ * @default 'react-aria-TreeItem'
790
+ */
791
+ className?: ClassNameOrFunction<TreeItemRenderProps>;
792
+ /** The unique id of the tree row. */
793
+ id?: Key;
794
+ /**
795
+ * The object value that this tree item represents. When using dynamic collections, this is set
796
+ * automatically.
797
+ */
798
+ value?: T;
799
+ /** A string representation of the tree item's contents, used for features like typeahead. */
800
+ textValue: string;
801
+ /** An accessibility label for this tree item. */
802
+ 'aria-label'?: string;
803
+ /**
804
+ * The content of the tree item along with any nested children. Supports static nested tree items
805
+ * or use of a Collection to dynamically render nested tree items.
806
+ */
807
+ children: ReactNode;
808
+ /** Whether the item is disabled. */
809
+ isDisabled?: boolean;
810
+ /**
811
+ * Handler that is called when a user performs an action on this tree item. The exact user event
812
+ * depends on the collection's `selectionBehavior` prop and the interaction modality.
813
+ */
814
+ onAction?: () => void;
815
+ }
816
+
817
+ class TreeItemNode extends CollectionNode<any> {
818
+ static readonly type = 'item';
819
+ }
820
+
821
+ /**
822
+ * A TreeItem represents an individual item in a Tree.
823
+ */
824
+ export const TreeItem: <T extends object = object>(props: TreeItemProps<T> & { ref?: any }) => any =
825
+ /*#__PURE__*/ createBranchComponent(TreeItemNode, function TreeItem<
826
+ T,
827
+ >(props: TreeItemProps<T>, forwardedRef: any, item?: Node<T>): any {
828
+ const slot = S('TreeItem');
829
+ let state = useContext(TreeStateContext)!;
830
+ let ref = useObjectRef<HTMLDivElement>(forwardedRef, subSlot(slot, 'objectRef'));
831
+ let { dragAndDropHooks, dragState, dropState } = useContext(DragAndDropContext);
832
+ let isDraggable =
833
+ dragState && !(dragState.isDisabled || dragState.selectionManager.isDisabled(item!.key));
834
+
835
+ // TODO: remove this when we support description in tree row
836
+ let { rowProps, gridCellProps, expandButtonProps, descriptionProps, ...states } = useTreeItem(
837
+ {
838
+ node: item!,
839
+ shouldSelectOnPressUp: !!dragState,
840
+ },
841
+ state,
842
+ ref,
843
+ subSlot(slot, 'treeItem'),
844
+ );
845
+ let isExpanded = rowProps['aria-expanded'] === true;
846
+ let hasChildItems =
847
+ props.hasChildItems || [...state.collection.getChildren!(item!.key)]?.length > 1;
848
+ let level = rowProps['aria-level'] || 1;
849
+
850
+ let { hoverProps, isHovered } = useHover(
851
+ {
852
+ // because of https://bugs.webkit.org/show_bug.cgi?id=214609, supporting hover styles when a item is ONLY isDraggable
853
+ // results in hover styles sticking around after a reorder/drop operation...
854
+ isDisabled: !states.allowsSelection && !states.hasAction && !isDraggable,
855
+ onHoverStart: props.onHoverStart,
856
+ onHoverChange: props.onHoverChange,
857
+ onHoverEnd: props.onHoverEnd,
858
+ },
859
+ subSlot(slot, 'hover'),
860
+ );
861
+
862
+ let { isFocusVisible, focusProps } = useFocusRing(undefined, subSlot(slot, 'focusRing'));
863
+ let { isFocusVisible: isFocusVisibleWithin, focusProps: focusWithinProps } = useFocusRing(
864
+ {
865
+ within: true,
866
+ },
867
+ subSlot(slot, 'focusRingWithin'),
868
+ );
869
+ let { checkboxProps } = useGridListSelectionCheckbox(
870
+ { key: item!.key },
871
+ state as any,
872
+ subSlot(slot, 'checkbox'),
873
+ );
874
+
875
+ // PHASE-7: consumer-provided dnd item hooks (unreachable until useDragAndDrop is ported).
876
+ let draggableItem: DraggableItemResult | null = null;
877
+ if (dragState && dragAndDropHooks) {
878
+ draggableItem = dragAndDropHooks.useDraggableItem!(
879
+ { key: item!.key, hasDragButton: true },
880
+ dragState,
881
+ );
882
+ }
883
+
884
+ let dropIndicator: DropIndicatorAria | null = null;
885
+ let expandButtonRef = useRef<HTMLButtonElement | null>(null, subSlot(slot, 'expandButton'));
886
+ let dropIndicatorRef = useRef<HTMLDivElement | null>(null, subSlot(slot, 'dropIndicatorRef'));
887
+ let activateButtonRef = useRef<HTMLDivElement | null>(null, subSlot(slot, 'activateButton'));
888
+ let { visuallyHiddenProps } = useVisuallyHidden(undefined, subSlot(slot, 'visuallyHidden'));
889
+ if (dropState && dragAndDropHooks) {
890
+ dropIndicator = dragAndDropHooks.useDropIndicator!(
891
+ {
892
+ target: { type: 'item', key: item!.key, dropPosition: 'on' },
893
+ activateButtonRef,
894
+ },
895
+ dropState,
896
+ dropIndicatorRef,
897
+ );
898
+ }
899
+
900
+ let isDragging = dragState && dragState.isDragging(item!.key);
901
+ let isDropTarget = dropIndicator?.isDropTarget;
902
+
903
+ let selectionMode = state.selectionManager.selectionMode;
904
+ let selectionBehavior = state.selectionManager.selectionBehavior;
905
+ let renderPropValues = useMemo<TreeItemContentRenderProps>(
906
+ () =>
907
+ ({
908
+ ...states,
909
+ isHovered,
910
+ isFocusVisible,
911
+ isExpanded,
912
+ hasChildItems,
913
+ level,
914
+ selectionMode,
915
+ selectionBehavior,
916
+ isFocusVisibleWithin,
917
+ state,
918
+ id: item!.key,
919
+ allowsDragging: !!dragState,
920
+ isDragging,
921
+ isDropTarget,
922
+ }) as any,
923
+ [
924
+ states,
925
+ isHovered,
926
+ isFocusVisible,
927
+ isExpanded,
928
+ hasChildItems,
929
+ level,
930
+ isFocusVisibleWithin,
931
+ state,
932
+ item!.key,
933
+ dragState,
934
+ isDragging,
935
+ isDropTarget,
936
+ selectionBehavior,
937
+ selectionMode,
938
+ ],
939
+ subSlot(slot, 'renderPropValues'),
940
+ );
941
+
942
+ let renderProps = useRenderProps(
943
+ {
944
+ ...props,
945
+ id: undefined,
946
+ children: item!.rendered,
947
+ defaultClassName: 'react-aria-TreeItem',
948
+ defaultStyle: {
949
+ '--tree-item-level': level,
950
+ },
951
+ values: renderPropValues,
952
+ } as any,
953
+ subSlot(slot, 'render'),
954
+ );
955
+
956
+ useEffect(
957
+ () => {
958
+ if (!item!.textValue && process.env.NODE_ENV !== 'production') {
959
+ console.warn(
960
+ 'A `textValue` prop is required for <TreeItem> elements in order to support accessibility features such as type to select.',
961
+ );
962
+ }
963
+ },
964
+ [item!.textValue],
965
+ subSlot(slot, 'textValueWarn'),
966
+ );
967
+
968
+ useEffect(
969
+ () => {
970
+ if (hasChildItems && !expandButtonRef.current && process.env.NODE_ENV !== 'production') {
971
+ console.warn(
972
+ 'Expandable tree items must contain a expand button so screen reader users can expand/collapse the item.',
973
+ );
974
+ }
975
+ },
976
+ [],
977
+ subSlot(slot, 'expandButtonWarn'),
978
+ );
979
+
980
+ let dragButtonRef = useRef<HTMLButtonElement | null>(null, subSlot(slot, 'dragButton'));
981
+ useEffect(
982
+ () => {
983
+ if (dragState && !dragButtonRef.current && process.env.NODE_ENV !== 'production') {
984
+ console.warn(
985
+ 'Draggable items in a Tree must contain a <Button slot="drag"> element so that keyboard and screen reader users can drag them.',
986
+ );
987
+ }
988
+ },
989
+ [],
990
+ subSlot(slot, 'dragButtonWarn'),
991
+ );
992
+
993
+ let children = useCachedChildren(
994
+ {
995
+ items: state.collection.getChildren!(item!.key),
996
+ children: (item: Node<any>) => {
997
+ switch (item.type) {
998
+ case 'content': {
999
+ return item.render!(item);
1000
+ }
1001
+ // Skip item since we don't render the nested rows as children of the parent row, the flattened collection
1002
+ // will render them each as siblings instead
1003
+ case 'loader':
1004
+ case 'item':
1005
+ return createElement(Fragment, null);
1006
+ default:
1007
+ throw new Error('Unsupported element type in TreeRow: ' + item.type);
1008
+ }
1009
+ },
1010
+ },
1011
+ subSlot(slot, 'cachedChildren'),
1012
+ );
1013
+
1014
+ let activateButtonId = useId(undefined, subSlot(slot, 'activateButtonId'));
1015
+ let DOMProps = filterDOMProps(props as any, { global: true });
1016
+ delete DOMProps.id;
1017
+ delete DOMProps.onClick;
1018
+
1019
+ return createElement(
1020
+ Fragment,
1021
+ null,
1022
+ dropIndicator && !dropIndicator.isHidden
1023
+ ? createElement('div', {
1024
+ role: 'row',
1025
+ 'aria-level': rowProps['aria-level'],
1026
+ 'aria-expanded': rowProps['aria-expanded'],
1027
+ 'aria-label': dropIndicator.dropIndicatorProps['aria-label'],
1028
+ children: createElement('div', {
1029
+ role: 'gridcell',
1030
+ 'aria-colindex': 1,
1031
+ style: { display: 'contents' },
1032
+ // Positional (non-array) children, exactly like upstream's JSX.
1033
+ children: createElement(
1034
+ Fragment,
1035
+ null,
1036
+ createElement('div', {
1037
+ role: 'button',
1038
+ ...visuallyHiddenProps,
1039
+ ...dropIndicator.dropIndicatorProps,
1040
+ ref: dropIndicatorRef,
1041
+ }),
1042
+ rowProps['aria-expanded'] != null
1043
+ ? // Button to allow touch screen reader users to expand the item while dragging.
1044
+ createElement('div', {
1045
+ role: 'button',
1046
+ ...visuallyHiddenProps,
1047
+ id: activateButtonId,
1048
+ 'aria-label': expandButtonProps['aria-label'],
1049
+ 'aria-labelledby': `${activateButtonId} ${rowProps.id}`,
1050
+ tabIndex: -1,
1051
+ ref: activateButtonRef,
1052
+ })
1053
+ : null,
1054
+ ),
1055
+ }),
1056
+ })
1057
+ : null,
1058
+ createElement(dom.div, {
1059
+ ...mergeProps(
1060
+ DOMProps,
1061
+ rowProps,
1062
+ focusProps,
1063
+ hoverProps,
1064
+ focusWithinProps,
1065
+ draggableItem?.dragProps,
1066
+ ),
1067
+ ...renderProps,
1068
+ ref,
1069
+ // TODO: missing selectionBehavior, hasAction and allowsSelection data attribute equivalents (available in renderProps). Do we want those?
1070
+ 'data-expanded': (hasChildItems && isExpanded) || undefined,
1071
+ 'data-has-child-items': hasChildItems || undefined,
1072
+ 'data-level': level,
1073
+ 'data-selected': states.isSelected || undefined,
1074
+ 'data-disabled': states.isDisabled || undefined,
1075
+ 'data-hovered': isHovered || undefined,
1076
+ 'data-focused': states.isFocused || undefined,
1077
+ 'data-focus-visible': isFocusVisible || undefined,
1078
+ 'data-pressed': states.isPressed || undefined,
1079
+ 'data-selection-mode':
1080
+ state.selectionManager.selectionMode === 'none'
1081
+ ? undefined
1082
+ : state.selectionManager.selectionMode,
1083
+ 'data-allows-dragging': !!dragState || undefined,
1084
+ 'data-dragging': isDragging || undefined,
1085
+ 'data-drop-target': isDropTarget || undefined,
1086
+ children: createElement('div', {
1087
+ ...gridCellProps,
1088
+ style: { display: 'contents' },
1089
+ children: createElement(Provider, {
1090
+ values: [
1091
+ [
1092
+ CheckboxContext,
1093
+ {
1094
+ slots: {
1095
+ selection: checkboxProps,
1096
+ },
1097
+ },
1098
+ ],
1099
+ [
1100
+ CheckboxFieldContext,
1101
+ {
1102
+ slots: {
1103
+ selection: checkboxProps,
1104
+ },
1105
+ },
1106
+ ],
1107
+ // TODO: support description in the tree row
1108
+ // TODO: don't think I need to pass isExpanded to the button here since it can be sourced from the renderProps? Might be worthwhile passing it down?
1109
+ [
1110
+ ButtonContext,
1111
+ {
1112
+ slots: {
1113
+ [DEFAULT_SLOT]: {},
1114
+ chevron: {
1115
+ ...expandButtonProps,
1116
+ ref: expandButtonRef,
1117
+ },
1118
+ drag: {
1119
+ ...draggableItem?.dragButtonProps,
1120
+ ref: dragButtonRef,
1121
+ style: {
1122
+ pointerEvents: 'none',
1123
+ },
1124
+ },
1125
+ },
1126
+ },
1127
+ ],
1128
+ [
1129
+ TreeItemContentContext,
1130
+ {
1131
+ ...renderPropValues,
1132
+ },
1133
+ ],
1134
+ [SelectionIndicatorContext, { isSelected: states.isSelected }],
1135
+ ] as any,
1136
+ children,
1137
+ }),
1138
+ }),
1139
+ }),
1140
+ );
1141
+ });
1142
+
1143
+ export interface TreeLoadMoreItemRenderProps {
1144
+ /**
1145
+ * What level the tree item has within the tree.
1146
+ *
1147
+ * @selector [data-level]
1148
+ */
1149
+ level: number;
1150
+ }
1151
+
1152
+ export interface TreeLoadMoreItemProps
1153
+ extends Omit<LoadMoreSentinelProps, 'collection'>, RenderProps<TreeLoadMoreItemRenderProps> {
1154
+ /**
1155
+ * The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
1156
+ * element. A function may be provided to compute the class based on component state.
1157
+ *
1158
+ * @default 'react-aria-TreeLoadMoreItem'
1159
+ */
1160
+ className?: ClassNameOrFunction<TreeLoadMoreItemRenderProps>;
1161
+ /**
1162
+ * The load more spinner to render when loading additional items.
1163
+ */
1164
+ children?: ChildrenOrFunction<TreeLoadMoreItemRenderProps>;
1165
+ /**
1166
+ * Whether or not the loading spinner should be rendered or not.
1167
+ */
1168
+ isLoading?: boolean;
1169
+ }
1170
+
1171
+ export const TreeLoadMoreItem: (props: TreeLoadMoreItemProps & { ref?: any }) => any =
1172
+ createLeafComponent(LoaderNode, function TreeLoadingSentinel<
1173
+ T,
1174
+ >(props: TreeLoadMoreItemProps, forwardedRef: any, item?: Node<T>): any {
1175
+ const slot = S('TreeLoadMoreItem');
1176
+ let { isVirtualized } = useContext(CollectionRendererContext);
1177
+ let state = useContext(TreeStateContext)!;
1178
+ let { isLoading, onLoadMore, scrollOffset, ...otherProps } = props;
1179
+ let sentinelRef = useRef<HTMLDivElement | null>(null, subSlot(slot, 'sentinel'));
1180
+ let memoedLoadMoreProps = useMemo(
1181
+ () => ({
1182
+ onLoadMore,
1183
+ // this collection will update anytime a row is expanded/collapsed becaused the flattenedRows will change.
1184
+ // This means onLoadMore will trigger but that might be ok cause the user should have logic to handle multiple loadMore calls
1185
+ collection: state?.collection,
1186
+ sentinelRef,
1187
+ scrollOffset,
1188
+ }),
1189
+ [onLoadMore, scrollOffset, state?.collection],
1190
+ subSlot(slot, 'loadMoreProps'),
1191
+ );
1192
+ useLoadMoreSentinel(memoedLoadMoreProps as any, sentinelRef as any, subSlot(slot, 'loadMore'));
1193
+
1194
+ let ref = useObjectRef<HTMLDivElement>(forwardedRef, subSlot(slot, 'objectRef'));
1195
+ let { rowProps, gridCellProps } = useTreeItem(
1196
+ { node: item! },
1197
+ state,
1198
+ ref,
1199
+ subSlot(slot, 'treeItem'),
1200
+ );
1201
+ let level = rowProps['aria-level'] || 1;
1202
+
1203
+ // For now don't include aria-posinset and aria-setsize on loader since they aren't keyboard focusable
1204
+ // Arguably shouldn't include them ever since it might be confusing to the user to include the loaders as part of the
1205
+ // item count
1206
+ let ariaProps = {
1207
+ role: 'row',
1208
+ 'aria-level': rowProps['aria-level'],
1209
+ };
1210
+
1211
+ let renderProps = useRenderProps(
1212
+ {
1213
+ ...otherProps,
1214
+ id: undefined,
1215
+ children: item!.rendered,
1216
+ defaultClassName: 'react-aria-TreeLoader',
1217
+ values: {
1218
+ level,
1219
+ },
1220
+ } as any,
1221
+ subSlot(slot, 'render'),
1222
+ );
1223
+ let style = {};
1224
+
1225
+ if (isVirtualized) {
1226
+ style = { display: 'contents' };
1227
+ }
1228
+
1229
+ return createElement(
1230
+ Fragment,
1231
+ null,
1232
+ // Alway render the sentinel. For now onus is on the user for styling when using flex + gap
1233
+ // (this would introduce a gap even though it doesn't take room). octane adaptation:
1234
+ // upstream `inertValue` is React <19 string compat; octane follows React 19 boolean
1235
+ // `inert` semantics.
1236
+ createElement('div', {
1237
+ style: { position: 'relative', width: 0, height: 0 },
1238
+ inert: true,
1239
+ children: createElement('div', {
1240
+ 'data-testid': 'loadMoreSentinel',
1241
+ ref: sentinelRef,
1242
+ style: { position: 'absolute', height: 1, width: 1 },
1243
+ }),
1244
+ }),
1245
+ isLoading && renderProps.children
1246
+ ? createElement(dom.div, {
1247
+ ref,
1248
+ ...mergeProps(filterDOMProps(props as any), ariaProps),
1249
+ ...renderProps,
1250
+ 'data-level': level,
1251
+ children: createElement('div', {
1252
+ ...gridCellProps,
1253
+ style,
1254
+ children: renderProps.children,
1255
+ }),
1256
+ })
1257
+ : null,
1258
+ );
1259
+ });
1260
+
1261
+ function TreeDropIndicatorWrapper(props: DropIndicatorProps, forwardedRef: any): any {
1262
+ const slot = S('TreeDropIndicatorWrapper');
1263
+ let ref = useObjectRef<HTMLElement>(forwardedRef, subSlot(slot, 'objectRef'));
1264
+ let { dragAndDropHooks, dropState } = useContext(DragAndDropContext);
1265
+ let buttonRef = useRef<HTMLDivElement | null>(null, subSlot(slot, 'buttonRef'));
1266
+ let { dropIndicatorProps, isHidden, isDropTarget } = dragAndDropHooks!.useDropIndicator!(
1267
+ props,
1268
+ dropState!,
1269
+ buttonRef,
1270
+ );
1271
+
1272
+ if (isHidden) {
1273
+ return null;
1274
+ }
1275
+
1276
+ let level =
1277
+ dropState && props.target.type === 'item'
1278
+ ? (dropState.collection.getItem(props.target.key)?.level || 0) + 1
1279
+ : 1;
1280
+ return createElement(TreeDropIndicator, {
1281
+ ...props,
1282
+ dropIndicatorProps,
1283
+ isDropTarget,
1284
+ ref,
1285
+ buttonRef,
1286
+ level,
1287
+ });
1288
+ }
1289
+
1290
+ interface TreeDropIndicatorProps extends DropIndicatorProps {
1291
+ dropIndicatorProps: Record<string, any>;
1292
+ isDropTarget: boolean;
1293
+ buttonRef: RefObject<HTMLDivElement | null>;
1294
+ level: number;
1295
+ ref?: any;
1296
+ }
1297
+
1298
+ // octane adaptation: no forwardRef — the forwarded ref arrives as `props.ref` (upstream wraps
1299
+ // this in `forwardRef` as TreeDropIndicatorForwardRef).
1300
+ function TreeDropIndicator(props: TreeDropIndicatorProps): any {
1301
+ const slot = S('TreeDropIndicator');
1302
+ let { dropIndicatorProps, isDropTarget, buttonRef, level, ref, ...otherProps } = props;
1303
+ let { visuallyHiddenProps } = useVisuallyHidden(undefined, subSlot(slot, 'visuallyHidden'));
1304
+ let renderProps = useRenderProps(
1305
+ {
1306
+ ...otherProps,
1307
+ defaultClassName: 'react-aria-DropIndicator',
1308
+ defaultStyle: {
1309
+ position: 'relative',
1310
+ '--tree-item-level': level,
1311
+ },
1312
+ values: {
1313
+ isDropTarget,
1314
+ },
1315
+ } as any,
1316
+ subSlot(slot, 'render'),
1317
+ );
1318
+
1319
+ return createElement(dom.div, {
1320
+ ...renderProps,
1321
+ role: 'row',
1322
+ 'aria-level': level,
1323
+ ref,
1324
+ 'data-drop-target': isDropTarget || undefined,
1325
+ // Positional (non-array) children inside the gridcell, exactly like upstream's JSX.
1326
+ children: createElement('div', {
1327
+ role: 'gridcell',
1328
+ children: createElement(
1329
+ Fragment,
1330
+ null,
1331
+ createElement('div', {
1332
+ ...visuallyHiddenProps,
1333
+ role: 'button',
1334
+ ...dropIndicatorProps,
1335
+ ref: buttonRef,
1336
+ }),
1337
+ renderProps.children,
1338
+ ),
1339
+ }),
1340
+ });
1341
+ }
1342
+
1343
+ function RootDropIndicator(): any {
1344
+ const slot = S('TreeRootDropIndicator');
1345
+ let { dragAndDropHooks, dropState } = useContext(DragAndDropContext);
1346
+ let ref = useRef<HTMLDivElement | null>(null, subSlot(slot, 'ref'));
1347
+ let { dropIndicatorProps } = dragAndDropHooks!.useDropIndicator!(
1348
+ {
1349
+ target: { type: 'root' },
1350
+ },
1351
+ dropState!,
1352
+ ref,
1353
+ );
1354
+ let isDropTarget = dropState!.isDropTarget({ type: 'root' });
1355
+ let { visuallyHiddenProps } = useVisuallyHidden(undefined, subSlot(slot, 'visuallyHidden'));
1356
+
1357
+ if (!isDropTarget && dropIndicatorProps['aria-hidden']) {
1358
+ return null;
1359
+ }
1360
+
1361
+ return createElement('div', {
1362
+ role: 'row',
1363
+ 'aria-hidden': dropIndicatorProps['aria-hidden'],
1364
+ style: { position: 'absolute' },
1365
+ children: createElement('div', {
1366
+ role: 'gridcell',
1367
+ children: createElement('div', {
1368
+ role: 'button',
1369
+ ...visuallyHiddenProps,
1370
+ ...dropIndicatorProps,
1371
+ ref,
1372
+ }),
1373
+ }),
1374
+ });
1375
+ }
1376
+
1377
+ export interface GridListSectionProps<T>
1378
+ extends SectionProps<T>, DOMRenderProps<'div', undefined> {}
1379
+
1380
+ /**
1381
+ * A TreeSection represents a section within a Tree.
1382
+ */
1383
+ export const TreeSection: <T extends object = object>(
1384
+ props: GridListSectionProps<T> & { ref?: any },
1385
+ ) => any = /*#__PURE__*/ createBranchComponent(SectionNode, function TreeSection<
1386
+ T,
1387
+ >(props: GridListSectionProps<T>, forwardedRef: any, item?: Node<T>): any {
1388
+ const slot = S('TreeSection');
1389
+ let state = useContext(TreeStateContext)!;
1390
+ let { CollectionBranch } = useContext(CollectionRendererContext);
1391
+ let headingRef = useRef<HTMLElement | null>(null, subSlot(slot, 'heading'));
1392
+ let ref = useObjectRef<HTMLDivElement>(forwardedRef, subSlot(slot, 'objectRef'));
1393
+ let { rowHeaderProps, rowProps, rowGroupProps } = useGridListSection(
1394
+ {
1395
+ 'aria-label': props['aria-label'] ?? undefined,
1396
+ },
1397
+ state as any,
1398
+ ref,
1399
+ subSlot(slot, 'section'),
1400
+ );
1401
+ let renderProps = useRenderProps(
1402
+ {
1403
+ ...props,
1404
+ id: undefined,
1405
+ children: undefined,
1406
+ defaultClassName: 'react-aria-TreeSection',
1407
+ values: undefined,
1408
+ } as any,
1409
+ subSlot(slot, 'render'),
1410
+ );
1411
+
1412
+ let DOMProps = filterDOMProps(props as any, { global: true });
1413
+ delete DOMProps.id;
1414
+
1415
+ return createElement(dom.div, {
1416
+ ...mergeProps(DOMProps, renderProps, rowGroupProps),
1417
+ ref,
1418
+ children: createElement(Provider, {
1419
+ values: [
1420
+ [GridListHeaderContext, { ...rowProps, ref: headingRef }],
1421
+ [GridListHeaderInnerContext, { ...rowHeaderProps }],
1422
+ ] as any,
1423
+ children: createElement(CollectionBranch, {
1424
+ collection: state.collection,
1425
+ parent: item!,
1426
+ }),
1427
+ }),
1428
+ });
1429
+ });
1430
+
1431
+ export const TreeHeader = (props: GridListHeaderProps): ReactNode => {
1432
+ return createElement(GridListHeader, {
1433
+ className: 'react-aria-TreeHeader',
1434
+ ...props,
1435
+ children: props.children,
1436
+ });
1437
+ };
1438
+
1439
+ function areSetsEqual<T>(a: Set<T>, b: Set<T>): boolean {
1440
+ if (a.size !== b.size) {
1441
+ return false;
1442
+ }
1443
+
1444
+ for (let item of a) {
1445
+ if (!b.has(item)) {
1446
+ return false;
1447
+ }
1448
+ }
1449
+ return true;
1450
+ }