react-aria-components 1.15.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/Button.main.js +3 -2
  2. package/dist/Button.main.js.map +1 -1
  3. package/dist/Button.mjs +3 -2
  4. package/dist/Button.module.js +3 -2
  5. package/dist/Button.module.js.map +1 -1
  6. package/dist/Collection.main.js +3 -0
  7. package/dist/Collection.main.js.map +1 -1
  8. package/dist/Collection.mjs +3 -0
  9. package/dist/Collection.module.js +3 -0
  10. package/dist/Collection.module.js.map +1 -1
  11. package/dist/ComboBox.main.js +59 -8
  12. package/dist/ComboBox.main.js.map +1 -1
  13. package/dist/ComboBox.mjs +61 -12
  14. package/dist/ComboBox.module.js +61 -12
  15. package/dist/ComboBox.module.js.map +1 -1
  16. package/dist/DateField.main.js +8 -4
  17. package/dist/DateField.main.js.map +1 -1
  18. package/dist/DateField.mjs +8 -4
  19. package/dist/DateField.module.js +8 -4
  20. package/dist/DateField.module.js.map +1 -1
  21. package/dist/DatePicker.main.js +6 -2
  22. package/dist/DatePicker.main.js.map +1 -1
  23. package/dist/DatePicker.mjs +6 -2
  24. package/dist/DatePicker.module.js +6 -2
  25. package/dist/DatePicker.module.js.map +1 -1
  26. package/dist/DropZone.main.js +1 -1
  27. package/dist/DropZone.main.js.map +1 -1
  28. package/dist/DropZone.mjs +2 -2
  29. package/dist/DropZone.module.js +2 -2
  30. package/dist/DropZone.module.js.map +1 -1
  31. package/dist/FileTrigger.main.js +1 -1
  32. package/dist/FileTrigger.main.js.map +1 -1
  33. package/dist/FileTrigger.mjs +2 -2
  34. package/dist/FileTrigger.module.js +2 -2
  35. package/dist/FileTrigger.module.js.map +1 -1
  36. package/dist/GridList.main.js +7 -4
  37. package/dist/GridList.main.js.map +1 -1
  38. package/dist/GridList.mjs +7 -5
  39. package/dist/GridList.module.js +7 -5
  40. package/dist/GridList.module.js.map +1 -1
  41. package/dist/HiddenDateInput.main.js +3 -1
  42. package/dist/HiddenDateInput.main.js.map +1 -1
  43. package/dist/HiddenDateInput.mjs +3 -1
  44. package/dist/HiddenDateInput.module.js +3 -1
  45. package/dist/HiddenDateInput.module.js.map +1 -1
  46. package/dist/Keyboard.main.js.map +1 -1
  47. package/dist/Keyboard.module.js.map +1 -1
  48. package/dist/ListBox.main.js +2 -1
  49. package/dist/ListBox.main.js.map +1 -1
  50. package/dist/ListBox.mjs +3 -2
  51. package/dist/ListBox.module.js +3 -2
  52. package/dist/ListBox.module.js.map +1 -1
  53. package/dist/Menu.main.js +3 -1
  54. package/dist/Menu.main.js.map +1 -1
  55. package/dist/Menu.mjs +3 -1
  56. package/dist/Menu.module.js +3 -1
  57. package/dist/Menu.module.js.map +1 -1
  58. package/dist/Popover.main.js +1 -1
  59. package/dist/Popover.main.js.map +1 -1
  60. package/dist/Popover.mjs +3 -3
  61. package/dist/Popover.module.js +3 -3
  62. package/dist/Popover.module.js.map +1 -1
  63. package/dist/Tree.main.js +151 -102
  64. package/dist/Tree.main.js.map +1 -1
  65. package/dist/Tree.mjs +153 -106
  66. package/dist/Tree.module.js +153 -106
  67. package/dist/Tree.module.js.map +1 -1
  68. package/dist/import.mjs +3 -3
  69. package/dist/main.js +4 -0
  70. package/dist/main.js.map +1 -1
  71. package/dist/module.js +3 -3
  72. package/dist/module.js.map +1 -1
  73. package/dist/types.d.ts +77 -23
  74. package/dist/types.d.ts.map +1 -1
  75. package/dist/utils.main.js.map +1 -1
  76. package/dist/utils.module.js.map +1 -1
  77. package/package.json +23 -23
  78. package/src/Button.tsx +4 -2
  79. package/src/Collection.tsx +6 -0
  80. package/src/ComboBox.tsx +89 -12
  81. package/src/DateField.tsx +14 -5
  82. package/src/DatePicker.tsx +11 -2
  83. package/src/DropZone.tsx +2 -2
  84. package/src/FileTrigger.tsx +2 -2
  85. package/src/GridList.tsx +12 -3
  86. package/src/HiddenDateInput.tsx +2 -1
  87. package/src/Keyboard.tsx +2 -2
  88. package/src/ListBox.tsx +5 -4
  89. package/src/Menu.tsx +3 -2
  90. package/src/Popover.tsx +3 -3
  91. package/src/Tree.tsx +209 -146
  92. package/src/index.ts +4 -3
  93. package/src/utils.tsx +7 -6
package/src/Menu.tsx CHANGED
@@ -431,12 +431,13 @@ export const MenuItem = /*#__PURE__*/ createLeafComponent(ItemNode, function Men
431
431
  let state = useContext(MenuStateContext)!;
432
432
  let ref = useObjectRef<any>(forwardedRef);
433
433
  let selectionManager = useContext(SelectionManagerContext)!;
434
-
434
+ let {isVirtualized} = useContext(CollectionRendererContext);
435
435
  let {menuItemProps, labelProps, descriptionProps, keyboardShortcutProps, ...states} = useMenuItem({
436
436
  ...props,
437
437
  id,
438
438
  key: item.key,
439
- selectionManager
439
+ selectionManager,
440
+ isVirtualized: isVirtualized
440
441
  }, state, ref);
441
442
 
442
443
  let {hoverProps, isHovered} = useHover({
package/src/Popover.tsx CHANGED
@@ -21,8 +21,8 @@ import {
21
21
  useContextProps,
22
22
  useRenderProps
23
23
  } from './utils';
24
- import {filterDOMProps, mergeProps, nodeContains, useEnterAnimation, useExitAnimation, useLayoutEffect} from '@react-aria/utils';
25
- import {focusSafely} from '@react-aria/interactions';
24
+ import {filterDOMProps, isFocusWithin, mergeProps, useEnterAnimation, useExitAnimation, useLayoutEffect} from '@react-aria/utils';
25
+ import {focusSafely, getInteractionModality} from '@react-aria/interactions';
26
26
  import {OverlayArrowContext} from './OverlayArrow';
27
27
  import {OverlayTriggerProps, OverlayTriggerState, useOverlayTriggerState} from 'react-stately';
28
28
  import {OverlayTriggerStateContext} from './Dialog';
@@ -199,7 +199,7 @@ function PopoverInner({state, isExiting, UNSTABLE_portalContainer, clearContexts
199
199
  // Focus the popover itself on mount, unless a child element is already focused.
200
200
  // Skip this for submenus since hovering a submenutrigger should keep focus on the trigger
201
201
  useEffect(() => {
202
- if (isDialog && props.trigger !== 'SubmenuTrigger' && ref.current && !nodeContains(ref.current, document.activeElement)) {
202
+ if (isDialog && (props.trigger !== 'SubmenuTrigger' || getInteractionModality() !== 'pointer') && ref.current && !isFocusWithin(ref.current)) {
203
203
  focusSafely(ref.current);
204
204
  }
205
205
  }, [isDialog, ref, props.trigger]);
package/src/Tree.tsx CHANGED
@@ -10,7 +10,8 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {AriaTreeItemOptions, AriaTreeProps, DraggableItemResult, DropIndicatorAria, DropIndicatorProps, DroppableCollectionResult, FocusScope, ListKeyboardDelegate, mergeProps, useCollator, useFocusRing, useGridListSelectionCheckbox, useHover, useId, useLocale, useTree, useTreeItem, useVisuallyHidden} from 'react-aria';
13
+ import {AriaTreeItemOptions, AriaTreeProps, DraggableItemResult, DropIndicatorAria, DropIndicatorProps, DroppableCollectionResult, FocusScope, ListKeyboardDelegate, mergeProps, useCollator, useFocusRing, useGridListSection, useGridListSelectionCheckbox, useHover, useId, useLocale, useTree, useTreeItem, useVisuallyHidden} from 'react-aria';
14
+ import {BaseCollection, Collection, CollectionBuilder, CollectionNode, createBranchComponent, createLeafComponent, LoaderNode, SectionNode, useCachedChildren} from '@react-aria/collections';
14
15
  import {ButtonContext} from './Button';
15
16
  import {CheckboxContext} from './RSPContexts';
16
17
  import {
@@ -19,6 +20,7 @@ import {
19
20
  ContextValue,
20
21
  DEFAULT_SLOT,
21
22
  dom,
23
+ DOMRenderProps,
22
24
  Provider,
23
25
  RenderProps,
24
26
  SlotProps,
@@ -26,83 +28,156 @@ import {
26
28
  useContextProps,
27
29
  useRenderProps
28
30
  } from './utils';
29
- import {Collection, CollectionBuilder, CollectionNode, createBranchComponent, createLeafComponent, LoaderNode, useCachedChildren} from '@react-aria/collections';
30
- import {CollectionProps, CollectionRendererContext, DefaultCollectionRenderer, ItemRenderProps} from './Collection';
31
+ import {CollectionProps, CollectionRendererContext, DefaultCollectionRenderer, ItemRenderProps, SectionProps} from './Collection';
31
32
  import {DisabledBehavior, DragPreviewRenderer, Expandable, forwardRefType, GlobalDOMAttributes, HoverEvents, Key, LinkDOMProps, MultipleSelection, PressEvents, RefObject, SelectionMode} from '@react-types/shared';
32
33
  import {DragAndDropContext, DropIndicatorContext, useDndPersistedKeys, useRenderDropIndicator} from './DragAndDrop';
33
34
  import {DragAndDropHooks} from './useDragAndDrop';
34
- import {DraggableCollectionState, DroppableCollectionState, Collection as ICollection, Node, SelectionBehavior, TreeState, useTreeState} from 'react-stately';
35
+ import {DraggableCollectionState, DroppableCollectionState, Node, SelectionBehavior, TreeState, useTreeState} from 'react-stately';
35
36
  import {filterDOMProps, inertValue, LoadMoreSentinelProps, useLoadMoreSentinel, useObjectRef} from '@react-aria/utils';
37
+ import {GridListHeader, GridListHeaderContext, GridListHeaderInnerContext, GridListHeaderProps} from './GridList';
36
38
  import React, {createContext, ForwardedRef, forwardRef, JSX, ReactNode, useContext, useEffect, useMemo, useRef, useState} from 'react';
37
39
  import {SelectionIndicatorContext} from './SelectionIndicator';
38
40
  import {SharedElementTransition} from './SharedElementTransition';
39
41
  import {TreeDropTargetDelegate} from './TreeDropTargetDelegate';
40
42
  import {useControlledState} from '@react-stately/utils';
41
43
 
42
- class TreeCollection<T> implements ICollection<Node<T>> {
43
- private flattenedRows: Node<T>[];
44
- private keyMap: Map<Key, CollectionNode<T>> = new Map();
45
- private itemCount: number = 0;
46
-
47
- constructor(opts) {
48
- let {collection, expandedKeys} = opts;
49
- let {flattenedRows, keyMap, itemCount} = flattenTree<T>(collection, {expandedKeys});
50
- this.flattenedRows = flattenedRows;
51
- // Use generated keyMap because it contains the modified collection nodes (aka it adjusts the indexes so that they ignore the existence of the Content items)
52
- this.keyMap = keyMap;
53
- this.itemCount = itemCount;
54
- }
44
+ class TreeCollection<T> extends BaseCollection<T> {
45
+ private expandedKeys: Set<Key> = new Set();
55
46
 
56
- // TODO: should this collection's getters reflect the flattened structure or the original structure
57
- // If we respresent the flattened structure, it is easier for the keyboard nav but harder to find all the nodes
58
- *[Symbol.iterator]() {
59
- yield* this.flattenedRows;
60
- }
47
+ withExpandedKeys(lastExpandedKeys: Set<Key>, expandedKeys: Set<Key>) {
48
+ let collection = this.clone();
49
+ collection.expandedKeys = expandedKeys;
61
50
 
62
- get size() {
63
- return this.itemCount;
64
- }
51
+ // Clone ancestor section nodes so React 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
52
+ // Without this change, the items won't expand and collapse when virtualized inside a section
53
+ TreeCollection.cloneAncestorSections(expandedKeys, lastExpandedKeys, collection);
54
+ TreeCollection.cloneAncestorSections(lastExpandedKeys, expandedKeys, collection);
65
55
 
66
- getKeys() {
67
- return this.keyMap.keys();
56
+ collection.frozen = this.frozen;
57
+ return collection;
68
58
  }
69
59
 
70
- getItem(key: Key): Node<T> | null {
71
- return this.keyMap.get(key) || null;
60
+ // diff lastExpandedKeys and expandedKeys so we only clone what has changed
61
+ private static cloneAncestorSections<T>(
62
+ keys: Iterable<Key>,
63
+ excludeSet: Set<Key>,
64
+ collection: TreeCollection<T>
65
+ ) {
66
+ for (let key of keys) {
67
+ if (!excludeSet.has(key)) {
68
+ let currentKey: Key | null = key;
69
+ while (currentKey != null) {
70
+ let item = collection.getItem(currentKey) as CollectionNode<T>;
71
+ if (item?.type === 'section') {
72
+ collection.keyMap.set(currentKey, item.clone());
73
+ break;
74
+ } else {
75
+ currentKey = item?.parentKey ?? null;
76
+ }
77
+ }
78
+ }
79
+ }
72
80
  }
73
81
 
74
- at(idx: number) {
75
- return this.flattenedRows[idx];
76
- }
82
+ *[Symbol.iterator]() {
83
+ let firstKey = this.getFirstKey();
84
+ let node: Node<T> | null = firstKey != null ? this.getItem(firstKey) : null;
77
85
 
78
- getFirstKey() {
79
- return this.flattenedRows[0]?.key;
86
+ while (node) {
87
+ yield node as Node<T>;
88
+ if (node.type === 'section') {
89
+ node = node.nextKey ? this.getItem(node.nextKey) : null;
90
+ } else {
91
+ // This will include both item and content nodes
92
+ // We handle the content nodes in useCollectionRenderer and ListLayout
93
+ let key = this.getKeyAfter(node.key);
94
+ node = key ? this.getItem(key) : null;
95
+ }
96
+ }
80
97
  }
81
98
 
82
99
  getLastKey() {
83
- return this.flattenedRows[this.flattenedRows.length - 1]?.key;
100
+ // Find the deepest expanded child. We don't use collection.getLastKey() here
101
+ // because that will return the deepest child regardless of expandedKeys.
102
+ // Instead, start from the last top-level key and walk down.
103
+ let key = this.lastKey;
104
+ if (key == null) {
105
+ return null;
106
+ }
107
+
108
+ let node = this.getItem(key) as CollectionNode<T>;
109
+
110
+ while (node?.lastChildKey != null && (node.type !== 'item' || this.expandedKeys.has(node.key))) {
111
+ node = this.getItem(node.lastChildKey) as CollectionNode<T>;
112
+ }
113
+
114
+ return node?.key;
84
115
  }
85
116
 
86
117
  getKeyAfter(key: Key) {
87
- let index = this.flattenedRows.findIndex(row => row.key === key);
88
- return this.flattenedRows[index + 1]?.key;
118
+ let node = this.getItem(key) as CollectionNode<T>;
119
+ if (!node) {
120
+ return null;
121
+ }
122
+
123
+ if ((this.expandedKeys.has(node.key) || node.type !== 'item') && node.firstChildKey != null) {
124
+ return node.firstChildKey;
125
+ }
126
+
127
+ while (node) {
128
+ if (node.nextKey != null) {
129
+ return node.nextKey;
130
+ }
131
+
132
+ if (node.parentKey != null) {
133
+ node = this.getItem(node.parentKey) as CollectionNode<T>;
134
+ } else {
135
+ return null;
136
+ }
137
+ }
138
+
139
+ return null;
89
140
  }
90
141
 
91
142
  getKeyBefore(key: Key) {
92
- let index = this.flattenedRows.findIndex(row => row.key === key);
93
- return this.flattenedRows[index - 1]?.key;
143
+ let node = this.getItem(key) as CollectionNode<T>;
144
+ if (!node) {
145
+ return null;
146
+ }
147
+
148
+ if (node.prevKey != null) {
149
+ node = this.getItem(node.prevKey) as CollectionNode<T>;
150
+
151
+ // If the lastChildKey is expanded, check its lastChildKey
152
+ while (node && (node.type !== 'item' || this.expandedKeys.has(node.key)) && node.lastChildKey != null) {
153
+ node = this.getItem(node.lastChildKey) as CollectionNode<T>;
154
+ }
155
+
156
+ return node?.key ?? null;
157
+ }
158
+
159
+ return node.parentKey;
94
160
  }
95
161
 
96
- // Note that this will return Content nodes in addition to nested TreeItems
97
162
  getChildren(key: Key): Iterable<Node<T>> {
98
- let keyMap = this.keyMap;
163
+ let self = this;
99
164
  return {
100
165
  *[Symbol.iterator]() {
101
- let parent = keyMap.get(key);
102
- let node = parent?.firstChildKey != null ? keyMap.get(parent.firstChildKey) : null;
103
- while (node) {
104
- yield node as Node<T>;
105
- node = node.nextKey != null ? keyMap.get(node.nextKey) : undefined;
166
+ let parent = self.getItem(key) as CollectionNode<T> | null;
167
+ let node = parent?.firstChildKey != null ? self.getItem(parent.firstChildKey) as CollectionNode<T> : null;
168
+ if (parent && parent.type === 'section' && node) {
169
+ // Stop once either the node is null or the node is the parent's sibling
170
+ while (node && node.key !== parent.nextKey) {
171
+ yield self.getItem(node.key)!;
172
+ // This will include content nodes which we skip in ListLayout
173
+ let key = self.getKeyAfter(node.key);
174
+ node = key != null ? self.getItem(key)! as CollectionNode<T> : null;
175
+ }
176
+ } else {
177
+ while (node) {
178
+ yield node as Node<T>;
179
+ node = node.nextKey != null ? self.getItem(node.nextKey)! as CollectionNode<T> : null;
180
+ }
106
181
  }
107
182
  }
108
183
  };
@@ -183,7 +258,7 @@ export const Tree = /*#__PURE__*/ (forwardRef as forwardRefType)(function Tree<T
183
258
  [props, ref] = useContextProps(props, ref, TreeContext);
184
259
 
185
260
  return (
186
- <CollectionBuilder content={<Collection {...props} />}>
261
+ <CollectionBuilder content={<Collection {...props} />} createCollection={() => new TreeCollection<T>()}>
187
262
  {collection => <TreeInner props={props} collection={collection} treeRef={ref} />}
188
263
  </CollectionBuilder>
189
264
  );
@@ -202,7 +277,7 @@ const EXPANSION_KEYS = {
202
277
 
203
278
  interface TreeInnerProps<T extends object> {
204
279
  props: TreeProps<T>,
205
- collection: ICollection<unknown>,
280
+ collection: TreeCollection<T>,
206
281
  treeRef: RefObject<HTMLDivElement | null>
207
282
  }
208
283
 
@@ -235,14 +310,22 @@ function TreeInner<T extends object>({props, collection, treeRef: ref}: TreeInne
235
310
  // Kinda annoying that we have to replicate this code here as well as in useTreeState, but don't want to add
236
311
  // flattenCollection stuff to useTreeState. Think about this later
237
312
  let [expandedKeys, setExpandedKeys] = useControlledState(
238
- propExpandedKeys ? convertExpanded(propExpandedKeys) : undefined,
239
- propDefaultExpandedKeys ? convertExpanded(propDefaultExpandedKeys) : new Set(),
313
+ propExpandedKeys ? new Set(propExpandedKeys) : undefined,
314
+ propDefaultExpandedKeys ? new Set(propDefaultExpandedKeys) : new Set(),
240
315
  onExpandedChange
241
316
  );
242
317
 
243
- let flattenedCollection = useMemo(() => {
244
- return new TreeCollection<object>({collection, expandedKeys});
245
- }, [collection, expandedKeys]);
318
+ let [lastCollection, setLastCollection] = useState(collection);
319
+ let [lastExpandedKeys, setLastExpandedKeys] = useState(expandedKeys);
320
+ let [flattenedCollection, setFlattenedCollection] = useState(() => collection.withExpandedKeys(lastExpandedKeys, expandedKeys));
321
+
322
+
323
+ // if the lastExpandedKeys is not the same as the currentExpandedKeys or the collection has changed, then run this
324
+ if (!areSetsEqual(lastExpandedKeys, expandedKeys) || collection !== lastCollection) {
325
+ setFlattenedCollection(collection.withExpandedKeys(lastExpandedKeys, expandedKeys));
326
+ setLastCollection(collection);
327
+ setLastExpandedKeys(expandedKeys);
328
+ }
246
329
 
247
330
  let state = useTreeState({
248
331
  ...props,
@@ -290,16 +373,16 @@ function TreeInner<T extends object>({props, collection, treeRef: ref}: TreeInne
290
373
  let dropTargetDelegate = dragAndDropHooks.dropTargetDelegate || ctxDropTargetDelegate || new dragAndDropHooks.ListDropTargetDelegate(state.collection, ref, {direction});
291
374
  treeDropTargetDelegate.setup(dropTargetDelegate, state, direction);
292
375
 
293
- let keyboardDelegate =
294
- new ListKeyboardDelegate({
295
- collection: state.collection,
296
- collator,
297
- ref,
298
- disabledKeys: state.selectionManager.disabledKeys,
299
- disabledBehavior: state.selectionManager.disabledBehavior,
300
- direction,
301
- layoutDelegate
302
- });
376
+ let keyboardDelegate = new ListKeyboardDelegate({
377
+ collection: state.collection,
378
+ collator,
379
+ ref,
380
+ disabledKeys: state.selectionManager.disabledKeys,
381
+ disabledBehavior: state.selectionManager.disabledBehavior,
382
+ direction,
383
+ layoutDelegate
384
+ });
385
+
303
386
  droppableCollection = dragAndDropHooks.useDroppableCollection!(
304
387
  {
305
388
  keyboardDelegate,
@@ -309,7 +392,7 @@ function TreeInner<T extends object>({props, collection, treeRef: ref}: TreeInne
309
392
  if (e.target.type === 'item') {
310
393
  let key = e.target.key;
311
394
  let item = state.collection.getItem(key);
312
- let isExpanded = expandedKeys !== 'all' && expandedKeys.has(key);
395
+ let isExpanded = expandedKeys.has(key);
313
396
  if (item && item.hasChildNodes && (!isExpanded || dragAndDropHooks?.isVirtualDragging?.())) {
314
397
  state.toggleKey(key);
315
398
  }
@@ -826,88 +909,6 @@ export const TreeLoadMoreItem = createLeafComponent(LoaderNode, function TreeLoa
826
909
  );
827
910
  });
828
911
 
829
- function convertExpanded(expanded: 'all' | Iterable<Key>): 'all' | Set<Key> {
830
- if (!expanded) {
831
- return new Set<Key>();
832
- }
833
-
834
- return expanded === 'all'
835
- ? 'all'
836
- : new Set(expanded);
837
- }
838
- interface TreeGridCollectionOptions {
839
- expandedKeys: Set<Key>
840
- }
841
-
842
- interface FlattenedTree<T> {
843
- flattenedRows: Node<T>[],
844
- keyMap: Map<Key, CollectionNode<T>>,
845
- itemCount: number
846
- }
847
-
848
- function flattenTree<T>(collection: TreeCollection<T>, opts: TreeGridCollectionOptions): FlattenedTree<T> {
849
- let {
850
- expandedKeys = new Set()
851
- } = opts;
852
- let keyMap: Map<Key, CollectionNode<T>> = new Map();
853
- let flattenedRows: Node<T>[] = [];
854
- // Need to count the items here because BaseCollection will return the full item count regardless if items are hidden via collapsed rows
855
- let itemCount = 0;
856
- let parentLookup: Map<Key, boolean> = new Map();
857
-
858
- let visitNode = (node: Node<T>) => {
859
- if (node.type === 'item' || node.type === 'loader') {
860
- let parentKey = node?.parentKey;
861
- let clone = {...node};
862
- if (parentKey != null) {
863
- // TODO: assumes that non item content node (aka TreeItemContent always placed before Collection) will be always placed before the child rows. If we can't make this assumption then we can filter out
864
- // every non-item per level and assign indicies based off the node's position in said filtered array
865
- let hasContentNode = [...collection.getChildren(parentKey)][0].type !== 'item';
866
- if (hasContentNode) {
867
- clone.index = node?.index != null ? node?.index - 1 : 0;
868
- }
869
-
870
- // For loader nodes that have a parent (aka non-root level loaders), these need their levels incremented by 1 for parity with their sibiling rows
871
- // (Collection only increments the level if it is a "item" type node).
872
- if (node.type === 'loader') {
873
- clone.level = node.level + 1;
874
- }
875
-
876
- keyMap.set(clone.key, clone as CollectionNode<T>);
877
- } else {
878
- keyMap.set(node.key, node as CollectionNode<T>);
879
- }
880
-
881
- // Grab the modified node from the key map so our flattened list and modified key map point to the same nodes
882
- let modifiedNode = keyMap.get(node.key) || node;
883
- if (modifiedNode.level === 0 || (modifiedNode.parentKey != null && expandedKeys.has(modifiedNode.parentKey) && parentLookup.get(modifiedNode.parentKey))) {
884
- if (modifiedNode.type === 'item') {
885
- itemCount++;
886
- }
887
-
888
- flattenedRows.push(modifiedNode);
889
- parentLookup.set(modifiedNode.key, true);
890
- }
891
- } else if (node.type !== null) {
892
- keyMap.set(node.key, node as CollectionNode<T>);
893
- }
894
-
895
- for (let child of collection.getChildren(node.key)) {
896
- visitNode(child);
897
- }
898
- };
899
-
900
- for (let node of collection) {
901
- visitNode(node);
902
- }
903
-
904
- return {
905
- flattenedRows,
906
- keyMap,
907
- itemCount
908
- };
909
- }
910
-
911
912
  function TreeDropIndicatorWrapper(props: DropIndicatorProps, ref: ForwardedRef<HTMLElement>): JSX.Element | null {
912
913
  ref = useObjectRef(ref);
913
914
  let {dragAndDropHooks, dropState} = useContext(DragAndDropContext)!;
@@ -1001,3 +1002,65 @@ function RootDropIndicator() {
1001
1002
  </div>
1002
1003
  );
1003
1004
  }
1005
+
1006
+ export interface GridListSectionProps<T> extends SectionProps<T>, DOMRenderProps<'section', undefined> {}
1007
+
1008
+ /**
1009
+ * A TreeSection represents a section within a Tree.
1010
+ */
1011
+ export const TreeSection = /*#__PURE__*/ createBranchComponent(SectionNode, <T extends object>(props: GridListSectionProps<T>, ref: ForwardedRef<HTMLElement>, item: Node<T>) => {
1012
+ let state = useContext(TreeStateContext)!;
1013
+ let {CollectionBranch} = useContext(CollectionRendererContext);
1014
+ let headingRef = useRef(null);
1015
+ ref = useObjectRef<HTMLElement>(ref);
1016
+ let {rowHeaderProps, rowProps, rowGroupProps} = useGridListSection({
1017
+ 'aria-label': props['aria-label'] ?? undefined
1018
+ }, state, ref);
1019
+ let renderProps = useRenderProps({
1020
+ ...props,
1021
+ id: undefined,
1022
+ children: undefined,
1023
+ defaultClassName: 'react-aria-TreeSection',
1024
+ values: undefined
1025
+ });
1026
+
1027
+ let DOMProps = filterDOMProps(props as any, {global: true});
1028
+ delete DOMProps.id;
1029
+
1030
+ return (
1031
+ <dom.section
1032
+ {...mergeProps(DOMProps, renderProps, rowGroupProps)}
1033
+ ref={ref}>
1034
+ <Provider
1035
+ values={[
1036
+ [GridListHeaderContext, {...rowProps, ref: headingRef}],
1037
+ [GridListHeaderInnerContext, {...rowHeaderProps}]
1038
+ ]}>
1039
+ <CollectionBranch
1040
+ collection={state.collection}
1041
+ parent={item} />
1042
+ </Provider>
1043
+ </dom.section>
1044
+ );
1045
+ });
1046
+
1047
+ export const TreeHeader = (props: GridListHeaderProps): ReactNode => {
1048
+ return (
1049
+ <GridListHeader className="react-aria-TreeHeader" {...props}>
1050
+ {props.children}
1051
+ </GridListHeader>
1052
+ );
1053
+ };
1054
+
1055
+ function areSetsEqual<T>(a: Set<T>, b: Set<T>) {
1056
+ if (a.size !== b.size) {
1057
+ return false;
1058
+ }
1059
+
1060
+ for (let item of a) {
1061
+ if (!b.has(item)) {
1062
+ return false;
1063
+ }
1064
+ }
1065
+ return true;
1066
+ }
package/src/index.ts CHANGED
@@ -29,7 +29,7 @@ export {ColorSwatch, ColorSwatchContext} from './ColorSwatch';
29
29
  export {ColorSwatchPicker, ColorSwatchPickerItem, ColorSwatchPickerContext} from './ColorSwatchPicker';
30
30
  export {ColorThumb} from './ColorThumb';
31
31
  export {ColorWheel, ColorWheelTrack, ColorWheelTrackContext, ColorWheelStateContext} from './ColorWheel';
32
- export {ComboBox, ComboBoxContext, ComboBoxStateContext} from './ComboBox';
32
+ export {ComboBox, ComboBoxValue, ComboBoxContext, ComboBoxStateContext, ComboBoxValueContext} from './ComboBox';
33
33
  export {composeRenderProps, DEFAULT_SLOT, Provider, useContextProps, useRenderProps, useSlottedContext} from './utils';
34
34
  export {DateField, DateInput, DateSegment, TimeField, DateFieldContext, TimeFieldContext, DateFieldStateContext, TimeFieldStateContext} from './DateField';
35
35
  export {DatePicker, DateRangePicker, DatePickerContext, DateRangePickerContext, DatePickerStateContext, DateRangePickerStateContext} from './DatePicker';
@@ -77,7 +77,7 @@ export {ToggleButton, ToggleButtonContext} from './ToggleButton';
77
77
  export {ToggleButtonGroup, ToggleButtonGroupContext, ToggleGroupStateContext} from './ToggleButtonGroup';
78
78
  export {Toolbar, ToolbarContext} from './Toolbar';
79
79
  export {TooltipTrigger, Tooltip, TooltipTriggerStateContext, TooltipContext} from './Tooltip';
80
- export {TreeLoadMoreItem, Tree, TreeItem, TreeContext, TreeItemContent, TreeStateContext} from './Tree';
80
+ export {TreeLoadMoreItem, Tree, TreeItem, TreeContext, TreeItemContent, TreeHeader, TreeSection, TreeStateContext} from './Tree';
81
81
  export {useDrag, useDrop} from '@react-aria/dnd';
82
82
  export {useDragAndDrop} from './useDragAndDrop';
83
83
  export {DropIndicator, DropIndicatorContext, DragAndDropContext} from './DragAndDrop';
@@ -100,7 +100,7 @@ export type {ColorSwatchPickerProps, ColorSwatchPickerRenderProps, ColorSwatchPi
100
100
  export type {ColorThumbProps, ColorThumbRenderProps} from './ColorThumb';
101
101
  export type {ColorPickerProps, ColorPickerRenderProps} from './ColorPicker';
102
102
  export type {ColorWheelProps, ColorWheelRenderProps, ColorWheelTrackProps, ColorWheelTrackRenderProps} from './ColorWheel';
103
- export type {ComboBoxProps, ComboBoxRenderProps} from './ComboBox';
103
+ export type {ComboBoxProps, ComboBoxRenderProps, ComboBoxValueProps, ComboBoxValueRenderProps} from './ComboBox';
104
104
  export type {DateFieldProps, DateFieldRenderProps, DateInputProps, DateInputRenderProps, DateSegmentProps, DateSegmentRenderProps, TimeFieldProps} from './DateField';
105
105
  export type {DatePickerProps, DatePickerRenderProps, DateRangePickerProps, DateRangePickerRenderProps} from './DatePicker';
106
106
  export type {DialogProps, DialogTriggerProps, DialogRenderProps} from './Dialog';
@@ -111,6 +111,7 @@ export type {FileTriggerProps} from './FileTrigger';
111
111
  export type {FormProps} from './Form';
112
112
  export type {GridListProps, GridListRenderProps, GridListItemProps, GridListItemRenderProps, GridListLoadMoreItemProps, GridListSectionProps} from './GridList';
113
113
  export type {GroupProps, GroupRenderProps} from './Group';
114
+ export type {HeaderProps} from './Header';
114
115
  export type {HeadingProps} from './Heading';
115
116
  export type {InputProps, InputRenderProps} from './Input';
116
117
  export type {SectionProps, CollectionRenderer} from './Collection';
package/src/utils.tsx CHANGED
@@ -20,11 +20,11 @@ interface SlottedValue<T> {
20
20
  slots?: Record<string | symbol, T>
21
21
  }
22
22
 
23
- export type SlottedContextValue<T> = (SlottedValue<T> & T) | null | undefined;
23
+ export type SlottedContextValue<T> = SlottedValue<T> | T | null | undefined;
24
24
  export type ContextValue<T, E> = SlottedContextValue<WithRef<T, E>>;
25
25
 
26
26
  type ProviderValue<T> = [Context<T>, T];
27
- type ProviderValues<A, B, C, D, E, F, G, H, I, J, K> =
27
+ type ProviderValues<A, B, C, D, E, F, G, H, I, J, K, L> =
28
28
  | [ProviderValue<A>]
29
29
  | [ProviderValue<A>, ProviderValue<B>]
30
30
  | [ProviderValue<A>, ProviderValue<B>, ProviderValue<C>]
@@ -35,14 +35,15 @@ type ProviderValues<A, B, C, D, E, F, G, H, I, J, K> =
35
35
  | [ProviderValue<A>, ProviderValue<B>, ProviderValue<C>, ProviderValue<D>, ProviderValue<E>, ProviderValue<F>, ProviderValue<G>, ProviderValue<H>]
36
36
  | [ProviderValue<A>, ProviderValue<B>, ProviderValue<C>, ProviderValue<D>, ProviderValue<E>, ProviderValue<F>, ProviderValue<G>, ProviderValue<H>, ProviderValue<I>]
37
37
  | [ProviderValue<A>, ProviderValue<B>, ProviderValue<C>, ProviderValue<D>, ProviderValue<E>, ProviderValue<F>, ProviderValue<G>, ProviderValue<H>, ProviderValue<I>, ProviderValue<J>]
38
- | [ProviderValue<A>, ProviderValue<B>, ProviderValue<C>, ProviderValue<D>, ProviderValue<E>, ProviderValue<F>, ProviderValue<G>, ProviderValue<H>, ProviderValue<I>, ProviderValue<J>, ProviderValue<K>];
38
+ | [ProviderValue<A>, ProviderValue<B>, ProviderValue<C>, ProviderValue<D>, ProviderValue<E>, ProviderValue<F>, ProviderValue<G>, ProviderValue<H>, ProviderValue<I>, ProviderValue<J>, ProviderValue<K>]
39
+ | [ProviderValue<A>, ProviderValue<B>, ProviderValue<C>, ProviderValue<D>, ProviderValue<E>, ProviderValue<F>, ProviderValue<G>, ProviderValue<H>, ProviderValue<I>, ProviderValue<J>, ProviderValue<K>, ProviderValue<L>];
39
40
 
40
- interface ProviderProps<A, B, C, D, E, F, G, H, I, J, K> {
41
- values: ProviderValues<A, B, C, D, E, F, G, H, I, J, K>,
41
+ interface ProviderProps<A, B, C, D, E, F, G, H, I, J, K, L> {
42
+ values: ProviderValues<A, B, C, D, E, F, G, H, I, J, K, L>,
42
43
  children: ReactNode
43
44
  }
44
45
 
45
- export function Provider<A, B, C, D, E, F, G, H, I, J, K>({values, children}: ProviderProps<A, B, C, D, E, F, G, H, I, J, K>): JSX.Element {
46
+ export function Provider<A, B, C, D, E, F, G, H, I, J, K, L>({values, children}: ProviderProps<A, B, C, D, E, F, G, H, I, J, K, L>): JSX.Element {
46
47
  for (let [Context, value] of values) {
47
48
  // @ts-ignore
48
49
  children = <Context.Provider value={value}>{children}</Context.Provider>;