@react-aria/collections 3.0.0-rc.3 → 3.0.0-rc.5

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.
@@ -17,8 +17,11 @@ export type Mutable<T> = {
17
17
  -readonly[P in keyof T]: T[P]
18
18
  }
19
19
 
20
+ type FilterFn<T> = (textValue: string, node: Node<T>) => boolean;
21
+
20
22
  /** An immutable object representing a Node in a Collection. */
21
23
  export class CollectionNode<T> implements Node<T> {
24
+ static readonly type: string;
22
25
  readonly type: string;
23
26
  readonly key: Key;
24
27
  readonly value: T | null = null;
@@ -38,8 +41,8 @@ export class CollectionNode<T> implements Node<T> {
38
41
  readonly colSpan: number | null = null;
39
42
  readonly colIndex: number | null = null;
40
43
 
41
- constructor(type: string, key: Key) {
42
- this.type = type;
44
+ constructor(key: Key) {
45
+ this.type = (this.constructor as typeof CollectionNode).type;
43
46
  this.key = key;
44
47
  }
45
48
 
@@ -47,8 +50,8 @@ export class CollectionNode<T> implements Node<T> {
47
50
  throw new Error('childNodes is not supported');
48
51
  }
49
52
 
50
- clone(): CollectionNode<T> {
51
- let node: Mutable<CollectionNode<T>> = new CollectionNode(this.type, this.key);
53
+ clone(): this {
54
+ let node: Mutable<this> = new (this.constructor as any)(this.key);
52
55
  node.value = this.value;
53
56
  node.level = this.level;
54
57
  node.hasChildNodes = this.hasChildNodes;
@@ -67,6 +70,63 @@ export class CollectionNode<T> implements Node<T> {
67
70
  node.colIndex = this.colIndex;
68
71
  return node;
69
72
  }
73
+
74
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
75
+ filter(collection: BaseCollection<T>, newCollection: BaseCollection<T>, filterFn: FilterFn<T>): CollectionNode<T> | null {
76
+ let clone = this.clone();
77
+ newCollection.addDescendants(clone, collection);
78
+ return clone;
79
+ }
80
+ }
81
+
82
+ export class FilterableNode<T> extends CollectionNode<T> {
83
+ filter(collection: BaseCollection<T>, newCollection: BaseCollection<T>, filterFn: FilterFn<T>): CollectionNode<T> | null {
84
+ let [firstKey, lastKey] = filterChildren(collection, newCollection, this.firstChildKey, filterFn);
85
+ let newNode: Mutable<CollectionNode<T>> = this.clone();
86
+ newNode.firstChildKey = firstKey;
87
+ newNode.lastChildKey = lastKey;
88
+ return newNode;
89
+ }
90
+ }
91
+
92
+ export class HeaderNode extends CollectionNode<unknown> {
93
+ static readonly type = 'header';
94
+ }
95
+
96
+ export class LoaderNode extends CollectionNode<unknown> {
97
+ static readonly type = 'loader';
98
+ }
99
+
100
+ export class ItemNode<T> extends FilterableNode<T> {
101
+ static readonly type = 'item';
102
+
103
+ filter(collection: BaseCollection<T>, newCollection: BaseCollection<T>, filterFn: FilterFn<T>): ItemNode<T> | null {
104
+ if (filterFn(this.textValue, this)) {
105
+ let clone = this.clone();
106
+ newCollection.addDescendants(clone, collection);
107
+ return clone;
108
+ }
109
+
110
+ return null;
111
+ }
112
+ }
113
+
114
+ export class SectionNode<T> extends FilterableNode<T> {
115
+ static readonly type = 'section';
116
+
117
+ filter(collection: BaseCollection<T>, newCollection: BaseCollection<T>, filterFn: FilterFn<T>): SectionNode<T> | null {
118
+ let filteredSection = super.filter(collection, newCollection, filterFn);
119
+ if (filteredSection) {
120
+ if (filteredSection.lastChildKey !== null) {
121
+ let lastChild = collection.getItem(filteredSection.lastChildKey);
122
+ if (lastChild && lastChild.type !== 'header') {
123
+ return filteredSection;
124
+ }
125
+ }
126
+ }
127
+
128
+ return null;
129
+ }
70
130
  }
71
131
 
72
132
  /**
@@ -79,9 +139,11 @@ export class BaseCollection<T> implements ICollection<Node<T>> {
79
139
  private firstKey: Key | null = null;
80
140
  private lastKey: Key | null = null;
81
141
  private frozen = false;
142
+ private itemCount: number = 0;
143
+ isComplete = true;
82
144
 
83
145
  get size(): number {
84
- return this.keyMap.size;
146
+ return this.itemCount;
85
147
  }
86
148
 
87
149
  getKeys(): IterableIterator<Key> {
@@ -184,6 +246,7 @@ export class BaseCollection<T> implements ICollection<Node<T>> {
184
246
  collection.keyMap = new Map(this.keyMap);
185
247
  collection.firstKey = this.firstKey;
186
248
  collection.lastKey = this.lastKey;
249
+ collection.itemCount = this.itemCount;
187
250
  return collection;
188
251
  }
189
252
 
@@ -192,14 +255,32 @@ export class BaseCollection<T> implements ICollection<Node<T>> {
192
255
  throw new Error('Cannot add a node to a frozen collection');
193
256
  }
194
257
 
258
+ if (node.type === 'item' && this.keyMap.get(node.key) == null) {
259
+ this.itemCount++;
260
+ }
261
+
195
262
  this.keyMap.set(node.key, node);
196
263
  }
197
264
 
265
+ // Deeply add a node and its children to the collection from another collection, primarily used when filtering a collection
266
+ addDescendants(node: CollectionNode<T>, oldCollection: BaseCollection<T>): void {
267
+ this.addNode(node);
268
+ let children = oldCollection.getChildren(node.key);
269
+ for (let child of children) {
270
+ this.addDescendants(child as CollectionNode<T>, oldCollection);
271
+ }
272
+ }
273
+
198
274
  removeNode(key: Key): void {
199
275
  if (this.frozen) {
200
276
  throw new Error('Cannot remove a node to a frozen collection');
201
277
  }
202
278
 
279
+ let node = this.keyMap.get(key);
280
+ if (node != null && node.type === 'item') {
281
+ this.itemCount--;
282
+ }
283
+
203
284
  this.keyMap.delete(key);
204
285
  }
205
286
 
@@ -213,134 +294,61 @@ export class BaseCollection<T> implements ICollection<Node<T>> {
213
294
  this.frozen = !isSSR;
214
295
  }
215
296
 
216
- // TODO: this is pretty specific to menu, will need to check if it is generic enough
217
- // Will need to handle varying levels I assume but will revisit after I get searchable menu working for base menu
218
- // TODO: an alternative is to simply walk the collection and add all item nodes that match the filter and any sections/separators we encounter
219
- // to an array, then walk that new array and fix all the next/Prev keys while adding them to the new collection
220
- UNSTABLE_filter(filterFn: (nodeValue: string) => boolean): BaseCollection<T> {
221
- let newCollection = new BaseCollection<T>();
222
- // This tracks the absolute last node we've visited in the collection when filtering, used for setting up the filteredCollection's lastKey and
223
- // for updating the next/prevKey for every non-filtered node.
224
- let lastNode: Mutable<CollectionNode<T>> | null = null;
225
-
226
- for (let node of this) {
227
- if (node.type === 'section' && node.hasChildNodes) {
228
- let clonedSection: Mutable<CollectionNode<T>> = (node as CollectionNode<T>).clone();
229
- let lastChildInSection: Mutable<CollectionNode<T>> | null = null;
230
- for (let child of this.getChildren(node.key)) {
231
- if (shouldKeepNode(child, filterFn, this, newCollection)) {
232
- let clonedChild: Mutable<CollectionNode<T>> = (child as CollectionNode<T>).clone();
233
- // eslint-disable-next-line max-depth
234
- if (lastChildInSection == null) {
235
- clonedSection.firstChildKey = clonedChild.key;
236
- }
237
-
238
- // eslint-disable-next-line max-depth
239
- if (newCollection.firstKey == null) {
240
- newCollection.firstKey = clonedSection.key;
241
- }
242
-
243
- // eslint-disable-next-line max-depth
244
- if (lastChildInSection && lastChildInSection.parentKey === clonedChild.parentKey) {
245
- lastChildInSection.nextKey = clonedChild.key;
246
- clonedChild.prevKey = lastChildInSection.key;
247
- } else {
248
- clonedChild.prevKey = null;
249
- }
250
-
251
- clonedChild.nextKey = null;
252
- newCollection.addNode(clonedChild);
253
- lastChildInSection = clonedChild;
254
- }
255
- }
297
+ filter(filterFn: FilterFn<T>): this {
298
+ let newCollection = new (this.constructor as any)();
299
+ let [firstKey, lastKey] = filterChildren(this, newCollection, this.firstKey, filterFn);
300
+ newCollection?.commit(firstKey, lastKey);
301
+ return newCollection;
302
+ }
303
+ }
256
304
 
257
- // Add newly filtered section to collection if it has any valid child nodes, otherwise remove it and its header if any
258
- if (lastChildInSection) {
259
- if (lastChildInSection.type !== 'header') {
260
- clonedSection.lastChildKey = lastChildInSection.key;
261
-
262
- // If the old prev section was filtered out, will need to attach to whatever came before
263
- // eslint-disable-next-line max-depth
264
- if (lastNode == null) {
265
- clonedSection.prevKey = null;
266
- } else if (lastNode.type === 'section' || lastNode.type === 'separator') {
267
- lastNode.nextKey = clonedSection.key;
268
- clonedSection.prevKey = lastNode.key;
269
- }
270
- clonedSection.nextKey = null;
271
- lastNode = clonedSection;
272
- newCollection.addNode(clonedSection);
273
- } else {
274
- if (newCollection.firstKey === clonedSection.key) {
275
- newCollection.firstKey = null;
276
- }
277
- newCollection.removeNode(lastChildInSection.key);
278
- }
279
- }
280
- } else if (node.type === 'separator') {
281
- // will need to check if previous section key exists, if it does then we add the separator to the collection.
282
- // After the full collection is created we'll need to remove it it is the last node in the section (aka no following section after the separator)
283
- let clonedSeparator: Mutable<CollectionNode<T>> = (node as CollectionNode<T>).clone();
284
- clonedSeparator.nextKey = null;
285
- if (lastNode?.type === 'section') {
286
- lastNode.nextKey = clonedSeparator.key;
287
- clonedSeparator.prevKey = lastNode.key;
288
- lastNode = clonedSeparator;
289
- newCollection.addNode(clonedSeparator);
290
- }
291
- } else {
292
- // At this point, the node is either a subdialogtrigger node or a standard row/item
293
- let clonedNode: Mutable<CollectionNode<T>> = (node as CollectionNode<T>).clone();
294
- if (shouldKeepNode(clonedNode, filterFn, this, newCollection)) {
295
- if (newCollection.firstKey == null) {
296
- newCollection.firstKey = clonedNode.key;
297
- }
298
-
299
- if (lastNode != null && (lastNode.type !== 'section' && lastNode.type !== 'separator') && lastNode.parentKey === clonedNode.parentKey) {
300
- lastNode.nextKey = clonedNode.key;
301
- clonedNode.prevKey = lastNode.key;
302
- } else {
303
- clonedNode.prevKey = null;
304
- }
305
-
306
- clonedNode.nextKey = null;
307
- newCollection.addNode(clonedNode);
308
- lastNode = clonedNode;
309
- }
305
+ function filterChildren<T>(collection: BaseCollection<T>, newCollection: BaseCollection<T>, firstChildKey: Key | null, filterFn: FilterFn<T>): [Key | null, Key | null] {
306
+ // loop over the siblings for firstChildKey
307
+ // create new nodes based on calling node.filter for each child
308
+ // if it returns null then don't include it, otherwise update its prev/next keys
309
+ // add them to the newCollection
310
+ if (firstChildKey == null) {
311
+ return [null, null];
312
+ }
313
+
314
+ let firstNode: Node<T> | null = null;
315
+ let lastNode: Node<T> | null = null;
316
+ let currentNode = collection.getItem(firstChildKey);
317
+
318
+ while (currentNode != null) {
319
+ let newNode: Mutable<CollectionNode<T>> | null = (currentNode as CollectionNode<T>).filter(collection, newCollection, filterFn);
320
+ if (newNode != null) {
321
+ newNode.nextKey = null;
322
+ if (lastNode) {
323
+ newNode.prevKey = lastNode.key;
324
+ lastNode.nextKey = newNode.key;
310
325
  }
311
- }
312
326
 
313
- if (lastNode?.type === 'separator' && lastNode.nextKey === null) {
314
- let lastSection;
315
- if (lastNode.prevKey != null) {
316
- lastSection = newCollection.getItem(lastNode.prevKey) as Mutable<CollectionNode<T>>;
317
- lastSection.nextKey = null;
327
+ if (firstNode == null) {
328
+ firstNode = newNode;
318
329
  }
319
- newCollection.removeNode(lastNode.key);
320
- lastNode = lastSection;
321
- }
322
330
 
323
- newCollection.lastKey = lastNode?.key || null;
331
+ newCollection.addNode(newNode);
332
+ lastNode = newNode;
333
+ }
324
334
 
325
- return newCollection;
335
+ currentNode = currentNode.nextKey ? collection.getItem(currentNode.nextKey) : null;
326
336
  }
327
- }
328
337
 
329
- function shouldKeepNode<T>(node: Node<T>, filterFn: (nodeValue: string) => boolean, oldCollection: BaseCollection<T>, newCollection: BaseCollection<T>): boolean {
330
- if (node.type === 'subdialogtrigger' || node.type === 'submenutrigger') {
331
- // Subdialog wrapper should only have one child, if it passes the filter add it to the new collection since we don't need to
332
- // do any extra handling for its first/next key
333
- let triggerChild = [...oldCollection.getChildren(node.key)][0];
334
- if (triggerChild && filterFn(triggerChild.textValue)) {
335
- let clonedChild: Mutable<CollectionNode<T>> = (triggerChild as CollectionNode<T>).clone();
336
- newCollection.addNode(clonedChild);
337
- return true;
338
+ // TODO: this is pretty specific to dividers but doesn't feel like there is a good way to get around it since we only can know
339
+ // to filter the last separator in a collection only after performing a filter for the rest of the contents after it
340
+ // Its gross that it needs to live here, might be nice if somehow we could have this live in the separator code
341
+ if (lastNode && lastNode.type === 'separator') {
342
+ let prevKey = lastNode.prevKey;
343
+ newCollection.removeNode(lastNode.key);
344
+
345
+ if (prevKey) {
346
+ lastNode = newCollection.getItem(prevKey) as Mutable<CollectionNode<T>>;
347
+ lastNode.nextKey = null;
338
348
  } else {
339
- return false;
349
+ lastNode = null;
340
350
  }
341
- } else if (node.type === 'header') {
342
- return true;
343
- } else {
344
- return filterFn(node.textValue);
345
351
  }
352
+
353
+ return [firstNode?.key ?? null, lastNode?.key ?? null];
346
354
  }
@@ -10,12 +10,12 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {BaseCollection} from './BaseCollection';
13
+ import {BaseCollection, CollectionNode} from './BaseCollection';
14
14
  import {BaseNode, Document, ElementNode} from './Document';
15
15
  import {CachedChildrenOptions, useCachedChildren} from './useCachedChildren';
16
16
  import {createPortal} from 'react-dom';
17
17
  import {FocusableContext} from '@react-aria/interactions';
18
- import {forwardRefType, Node} from '@react-types/shared';
18
+ import {forwardRefType, Key, Node} from '@react-types/shared';
19
19
  import {Hidden} from './Hidden';
20
20
  import React, {createContext, ForwardedRef, forwardRef, JSX, ReactElement, ReactNode, useCallback, useContext, useMemo, useRef, useState} from 'react';
21
21
  import {useIsSSR} from '@react-aria/ssr';
@@ -116,6 +116,7 @@ function useCollectionDocument<T extends object, C extends BaseCollection<T>>(cr
116
116
  let collection = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
117
117
  useLayoutEffect(() => {
118
118
  document.isMounted = true;
119
+ document.isInitialRender = false;
119
120
  return () => {
120
121
  // Mark unmounted so we can skip all of the collection updates caused by
121
122
  // React calling removeChild on every item in the collection.
@@ -127,22 +128,39 @@ function useCollectionDocument<T extends object, C extends BaseCollection<T>>(cr
127
128
 
128
129
  const SSRContext = createContext<BaseNode<any> | null>(null);
129
130
 
130
- function useSSRCollectionNode<T extends Element>(Type: string, props: object, ref: ForwardedRef<T>, rendered?: any, children?: ReactNode, render?: (node: Node<T>) => ReactElement) {
131
+ export type CollectionNodeClass<T> = {
132
+ new (key: Key): CollectionNode<T>,
133
+ readonly type: string
134
+ };
135
+
136
+ function createCollectionNodeClass(type: string): CollectionNodeClass<any> {
137
+ let NodeClass = class extends CollectionNode<any> {
138
+ static readonly type = type;
139
+ };
140
+ return NodeClass;
141
+ }
142
+
143
+ function useSSRCollectionNode<T extends Element>(CollectionNodeClass: CollectionNodeClass<T> | string, props: object, ref: ForwardedRef<T>, rendered?: any, children?: ReactNode, render?: (node: Node<any>) => ReactElement) {
144
+ // To prevent breaking change, if CollectionNodeClass is a string, create a CollectionNodeClass using the string as the type
145
+ if (typeof CollectionNodeClass === 'string') {
146
+ CollectionNodeClass = createCollectionNodeClass(CollectionNodeClass);
147
+ }
148
+
131
149
  // During SSR, portals are not supported, so the collection children will be wrapped in an SSRContext.
132
150
  // Since SSR occurs only once, we assume that the elements are rendered in order and never re-render.
133
151
  // Therefore we can create elements in our collection document during render so that they are in the
134
152
  // collection by the time we need to use the collection to render to the real DOM.
135
153
  // After hydration, we switch to client rendering using the portal.
136
154
  let itemRef = useCallback((element: ElementNode<any> | null) => {
137
- element?.setProps(props, ref, rendered, render);
138
- }, [props, ref, rendered, render]);
155
+ element?.setProps(props, ref, CollectionNodeClass, rendered, render);
156
+ }, [props, ref, rendered, render, CollectionNodeClass]);
139
157
  let parentNode = useContext(SSRContext);
140
158
  if (parentNode) {
141
159
  // Guard against double rendering in strict mode.
142
160
  let element = parentNode.ownerDocument.nodesByProps.get(props);
143
161
  if (!element) {
144
- element = parentNode.ownerDocument.createElement(Type);
145
- element.setProps(props, ref, rendered, render);
162
+ element = parentNode.ownerDocument.createElement(CollectionNodeClass.type);
163
+ element.setProps(props, ref, CollectionNodeClass, rendered, render);
146
164
  parentNode.appendChild(element);
147
165
  parentNode.ownerDocument.updateCollection();
148
166
  parentNode.ownerDocument.nodesByProps.set(props, element);
@@ -154,12 +172,12 @@ function useSSRCollectionNode<T extends Element>(Type: string, props: object, re
154
172
  }
155
173
 
156
174
  // @ts-ignore
157
- return <Type ref={itemRef}>{children}</Type>;
175
+ return <CollectionNodeClass.type ref={itemRef}>{children}</CollectionNodeClass.type>;
158
176
  }
159
177
 
160
- export function createLeafComponent<T extends object, P extends object, E extends Element>(type: string, render: (props: P, ref: ForwardedRef<E>) => ReactElement | null): (props: P & React.RefAttributes<T>) => ReactElement | null;
161
- export function createLeafComponent<T extends object, P extends object, E extends Element>(type: string, render: (props: P, ref: ForwardedRef<E>, node: Node<T>) => ReactElement | null): (props: P & React.RefAttributes<T>) => ReactElement | null;
162
- export function createLeafComponent<P extends object, E extends Element>(type: string, render: (props: P, ref: ForwardedRef<E>, node?: any) => ReactElement | null): (props: P & React.RefAttributes<any>) => ReactElement | null {
178
+ export function createLeafComponent<T extends object, P extends object, E extends Element>(CollectionNodeClass: CollectionNodeClass<any> | string, render: (props: P, ref: ForwardedRef<E>) => ReactElement | null): (props: P & React.RefAttributes<T>) => ReactElement | null;
179
+ export function createLeafComponent<T extends object, P extends object, E extends Element>(CollectionNodeClass: CollectionNodeClass<any> | string, render: (props: P, ref: ForwardedRef<E>, node: Node<T>) => ReactElement | null): (props: P & React.RefAttributes<T>) => ReactElement | null;
180
+ export function createLeafComponent<P extends object, E extends Element>(CollectionNodeClass: CollectionNodeClass<any> | string, render: (props: P, ref: ForwardedRef<E>, node?: any) => ReactElement | null): (props: P & React.RefAttributes<any>) => ReactElement | null {
163
181
  let Component = ({node}) => render(node.props, node.props.ref, node);
164
182
  let Result = (forwardRef as forwardRefType)((props: P, ref: ForwardedRef<E>) => {
165
183
  let focusableProps = useContext(FocusableContext);
@@ -172,7 +190,7 @@ export function createLeafComponent<P extends object, E extends Element>(type: s
172
190
  }
173
191
 
174
192
  return useSSRCollectionNode(
175
- type,
193
+ CollectionNodeClass,
176
194
  props,
177
195
  ref,
178
196
  'children' in props ? props.children : null,
@@ -190,11 +208,11 @@ export function createLeafComponent<P extends object, E extends Element>(type: s
190
208
  return Result;
191
209
  }
192
210
 
193
- export function createBranchComponent<T extends object, P extends {children?: any}, E extends Element>(type: string, render: (props: P, ref: ForwardedRef<E>, node: Node<T>) => ReactElement | null, useChildren: (props: P) => ReactNode = useCollectionChildren): (props: P & React.RefAttributes<E>) => ReactElement | null {
211
+ export function createBranchComponent<T extends object, P extends {children?: any}, E extends Element>(CollectionNodeClass: CollectionNodeClass<any> | string, render: (props: P, ref: ForwardedRef<E>, node: Node<T>) => ReactElement | null, useChildren: (props: P) => ReactNode = useCollectionChildren): (props: P & React.RefAttributes<E>) => ReactElement | null {
194
212
  let Component = ({node}) => render(node.props, node.props.ref, node);
195
213
  let Result = (forwardRef as forwardRefType)((props: P, ref: ForwardedRef<E>) => {
196
214
  let children = useChildren(props);
197
- return useSSRCollectionNode(type, props, ref, null, children, node => <Component node={node} />) ?? <></>;
215
+ return useSSRCollectionNode(CollectionNodeClass, props, ref, null, children, node => <Component node={node} />) ?? <></>;
198
216
  });
199
217
  // @ts-ignore
200
218
  Result.displayName = render.name;
package/src/Document.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import {BaseCollection, CollectionNode, Mutable} from './BaseCollection';
14
+ import {CollectionNodeClass} from './CollectionBuilder';
14
15
  import {CSSProperties, ForwardedRef, ReactElement, ReactNode} from 'react';
15
16
  import {Node} from '@react-types/shared';
16
17
 
@@ -256,15 +257,14 @@ export class BaseNode<T> {
256
257
  */
257
258
  export class ElementNode<T> extends BaseNode<T> {
258
259
  nodeType = 8; // COMMENT_NODE (we'd use ELEMENT_NODE but React DevTools will fail to get its dimensions)
259
- node: CollectionNode<T>;
260
+ private _node: CollectionNode<T> | null;
260
261
  isMutated = true;
261
262
  private _index: number = 0;
262
- hasSetProps = false;
263
263
  isHidden = false;
264
264
 
265
265
  constructor(type: string, ownerDocument: Document<T, any>) {
266
266
  super(ownerDocument);
267
- this.node = new CollectionNode(type, `react-aria-${++ownerDocument.nodeId}`);
267
+ this._node = null;
268
268
  }
269
269
 
270
270
  get index(): number {
@@ -278,12 +278,24 @@ export class ElementNode<T> extends BaseNode<T> {
278
278
 
279
279
  get level(): number {
280
280
  if (this.parentNode instanceof ElementNode) {
281
- return this.parentNode.level + (this.node.type === 'item' ? 1 : 0);
281
+ return this.parentNode.level + (this.node?.type === 'item' ? 1 : 0);
282
282
  }
283
283
 
284
284
  return 0;
285
285
  }
286
286
 
287
+ get node(): CollectionNode<T> {
288
+ if (this._node == null) {
289
+ throw Error('Attempted to access node before it was defined. Check if setProps wasn\'t called before attempting to access the node.');
290
+ }
291
+
292
+ return this._node;
293
+ }
294
+
295
+ set node(node: CollectionNode<T>) {
296
+ this._node = node;
297
+ }
298
+
287
299
  /**
288
300
  * Lazily gets a mutable instance of a Node. If the node has already
289
301
  * been cloned during this update cycle, it just returns the existing one.
@@ -321,9 +333,16 @@ export class ElementNode<T> extends BaseNode<T> {
321
333
  }
322
334
  }
323
335
 
324
- setProps<E extends Element>(obj: {[key: string]: any}, ref: ForwardedRef<E>, rendered?: ReactNode, render?: (node: Node<T>) => ReactElement): void {
325
- let node = this.getMutableNode();
336
+ setProps<E extends Element>(obj: {[key: string]: any}, ref: ForwardedRef<E>, CollectionNodeClass: CollectionNodeClass<any>, rendered?: ReactNode, render?: (node: Node<T>) => ReactElement): void {
337
+ let node;
326
338
  let {value, textValue, id, ...props} = obj;
339
+ if (this._node == null) {
340
+ node = new CollectionNodeClass(id ?? `react-aria-${++this.ownerDocument.nodeId}`);
341
+ this.node = node;
342
+ } else {
343
+ node = this.getMutableNode();
344
+ }
345
+
327
346
  props.ref = ref;
328
347
  node.props = props;
329
348
  node.rendered = rendered;
@@ -331,17 +350,13 @@ export class ElementNode<T> extends BaseNode<T> {
331
350
  node.value = value;
332
351
  node.textValue = textValue || (typeof props.children === 'string' ? props.children : '') || obj['aria-label'] || '';
333
352
  if (id != null && id !== node.key) {
334
- if (this.hasSetProps) {
335
- throw new Error('Cannot change the id of an item');
336
- }
337
- node.key = id;
353
+ throw new Error('Cannot change the id of an item');
338
354
  }
339
355
 
340
356
  if (props.colSpan != null) {
341
357
  node.colSpan = props.colSpan;
342
358
  }
343
359
 
344
- this.hasSetProps = true;
345
360
  if (this.isConnected) {
346
361
  this.ownerDocument.queueUpdate();
347
362
  }
@@ -395,12 +410,13 @@ export class ElementNode<T> extends BaseNode<T> {
395
410
  */
396
411
  export class Document<T, C extends BaseCollection<T> = BaseCollection<T>> extends BaseNode<T> {
397
412
  nodeType = 11; // DOCUMENT_FRAGMENT_NODE
398
- ownerDocument = this;
413
+ ownerDocument: Document<T, C> = this;
399
414
  dirtyNodes: Set<BaseNode<T>> = new Set();
400
415
  isSSR = false;
401
416
  nodeId = 0;
402
- nodesByProps = new WeakMap<object, ElementNode<T>>();
417
+ nodesByProps: WeakMap<object, ElementNode<T>> = new WeakMap<object, ElementNode<T>>();
403
418
  isMounted = true;
419
+ isInitialRender = true;
404
420
  private collection: C;
405
421
  private nextCollection: C | null = null;
406
422
  private subscriptions: Set<() => void> = new Set();
@@ -446,7 +462,7 @@ export class Document<T, C extends BaseCollection<T> = BaseCollection<T>> extend
446
462
  }
447
463
  }
448
464
 
449
- collection.addNode(element.node);
465
+ collection.addNode(element.node!);
450
466
  }
451
467
 
452
468
  private removeNode(node: ElementNode<T>): void {
@@ -507,6 +523,10 @@ export class Document<T, C extends BaseCollection<T> = BaseCollection<T>> extend
507
523
  this.nextCollection = null;
508
524
  }
509
525
  }
526
+
527
+ if (this.isInitialRender) {
528
+ this.collection.isComplete = false;
529
+ }
510
530
  }
511
531
 
512
532
  queueUpdate(): void {
package/src/Hidden.tsx CHANGED
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import {forwardRefType} from '@react-types/shared';
14
- import React, {createContext, forwardRef, JSX, ReactElement, ReactNode, useContext} from 'react';
14
+ import React, {Context, createContext, forwardRef, JSX, ReactElement, ReactNode, useContext} from 'react';
15
15
 
16
16
  // React doesn't understand the <template> element, which doesn't have children like a normal element.
17
17
  // It will throw an error during hydration when it expects the firstChild to contain content rendered
@@ -33,7 +33,7 @@ if (typeof HTMLTemplateElement !== 'undefined') {
33
33
  });
34
34
  }
35
35
 
36
- export const HiddenContext = createContext<boolean>(false);
36
+ export const HiddenContext: Context<boolean> = createContext<boolean>(false);
37
37
 
38
38
  export function Hidden(props: {children: ReactNode}): JSX.Element {
39
39
  let isHidden = useContext(HiddenContext);
package/src/index.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  export {CollectionBuilder, Collection, createLeafComponent, createBranchComponent} from './CollectionBuilder';
14
14
  export {createHideableComponent, useIsHidden} from './Hidden';
15
15
  export {useCachedChildren} from './useCachedChildren';
16
- export {BaseCollection, CollectionNode} from './BaseCollection';
16
+ export {BaseCollection, CollectionNode, ItemNode, SectionNode, FilterableNode, LoaderNode, HeaderNode} from './BaseCollection';
17
17
 
18
18
  export type {CollectionBuilderProps, CollectionProps} from './CollectionBuilder';
19
19
  export type {CachedChildrenOptions} from './useCachedChildren';