@@ -118,7 +118,6 @@ export class BaseNode<T> {
118
118
}
119
119
120
120
appendChild ( child : ElementNode < T > ) : void {
121
- this . ownerDocument . startTransaction ( ) ;
122
121
if ( child . parentNode ) {
123
122
child . parentNode . removeChild ( child ) ;
124
123
}
@@ -141,13 +140,6 @@ export class BaseNode<T> {
141
140
this . lastChild = child ;
142
141
143
142
this . ownerDocument . markDirty ( this ) ;
144
- if ( child . hasSetProps ) {
145
- // Only add the node to the collection if we already received props for it.
146
- // Otherwise wait until then so we have the correct id for the node.
147
- this . ownerDocument . addNode ( child ) ;
148
- }
149
-
150
- this . ownerDocument . endTransaction ( ) ;
151
143
this . ownerDocument . queueUpdate ( ) ;
152
144
}
153
145
@@ -156,7 +148,6 @@ export class BaseNode<T> {
156
148
return this . appendChild ( newNode ) ;
157
149
}
158
150
159
- this . ownerDocument . startTransaction ( ) ;
160
151
if ( newNode . parentNode ) {
161
152
newNode . parentNode . removeChild ( newNode ) ;
162
153
}
@@ -175,21 +166,13 @@ export class BaseNode<T> {
175
166
newNode . parentNode = referenceNode . parentNode ;
176
167
177
168
this . invalidateChildIndices ( referenceNode ) ;
178
-
179
- if ( newNode . hasSetProps ) {
180
- this . ownerDocument . addNode ( newNode ) ;
181
- }
182
-
183
- this . ownerDocument . endTransaction ( ) ;
184
169
this . ownerDocument . queueUpdate ( ) ;
185
170
}
186
171
187
172
removeChild ( child : ElementNode < T > ) : void {
188
173
if ( child . parentNode !== this || ! this . ownerDocument . isMounted ) {
189
174
return ;
190
175
}
191
-
192
- this . ownerDocument . startTransaction ( ) ;
193
176
194
177
if ( child . nextSibling ) {
195
178
this . invalidateChildIndices ( child . nextSibling ) ;
@@ -213,13 +196,44 @@ export class BaseNode<T> {
213
196
child . previousSibling = null ;
214
197
child . index = 0 ;
215
198
216
- this . ownerDocument . removeNode ( child ) ;
217
- this . ownerDocument . endTransaction ( ) ;
199
+ this . ownerDocument . markDirty ( child ) ;
218
200
this . ownerDocument . queueUpdate ( ) ;
219
201
}
220
202
221
203
addEventListener ( ) : void { }
222
204
removeEventListener ( ) : void { }
205
+
206
+ get previousVisibleSibling ( ) : ElementNode < T > | null {
207
+ let node = this . previousSibling ;
208
+ while ( node && node . isHidden ) {
209
+ node = node . previousSibling ;
210
+ }
211
+ return node ;
212
+ }
213
+
214
+ get nextVisibleSibling ( ) : ElementNode < T > | null {
215
+ let node = this . nextSibling ;
216
+ while ( node && node . isHidden ) {
217
+ node = node . nextSibling ;
218
+ }
219
+ return node ;
220
+ }
221
+
222
+ get firstVisibleChild ( ) : ElementNode < T > | null {
223
+ let node = this . firstChild ;
224
+ while ( node && node . isHidden ) {
225
+ node = node . nextSibling ;
226
+ }
227
+ return node ;
228
+ }
229
+
230
+ get lastVisibleChild ( ) : ElementNode < T > | null {
231
+ let node = this . lastChild ;
232
+ while ( node && node . isHidden ) {
233
+ node = node . previousSibling ;
234
+ }
235
+ return node ;
236
+ }
223
237
}
224
238
225
239
/**
@@ -229,16 +243,14 @@ export class BaseNode<T> {
229
243
export class ElementNode < T > extends BaseNode < T > {
230
244
nodeType = 8 ; // COMMENT_NODE (we'd use ELEMENT_NODE but React DevTools will fail to get its dimensions)
231
245
node : CollectionNode < T > ;
246
+ isMutated = true ;
232
247
private _index : number = 0 ;
233
248
hasSetProps = false ;
249
+ isHidden = false ;
234
250
235
251
constructor ( type : string , ownerDocument : Document < T , any > ) {
236
252
super ( ownerDocument ) ;
237
253
this . node = new CollectionNode ( type , `react-aria-${ ++ ownerDocument . nodeId } ` ) ;
238
- // Start a transaction so that no updates are emitted from the collection
239
- // until the props for this node are set. We don't know the real id for the
240
- // node until then, so we need to avoid emitting collections in an inconsistent state.
241
- this . ownerDocument . startTransaction ( ) ;
242
254
}
243
255
244
256
get index ( ) : number {
@@ -258,30 +270,45 @@ export class ElementNode<T> extends BaseNode<T> {
258
270
return 0 ;
259
271
}
260
272
273
+ /**
274
+ * Lazily gets a mutable instance of a Node. If the node has already
275
+ * been cloned during this update cycle, it just returns the existing one.
276
+ */
277
+ private getMutableNode ( ) : Mutable < CollectionNode < T > > {
278
+ if ( ! this . isMutated ) {
279
+ this . node = this . node . clone ( ) ;
280
+ this . isMutated = true ;
281
+ }
282
+
283
+ this . ownerDocument . markDirty ( this ) ;
284
+ return this . node ;
285
+ }
286
+
261
287
updateNode ( ) : void {
262
- let node = this . ownerDocument . getMutableNode ( this ) ;
288
+ let nextSibling = this . nextVisibleSibling ;
289
+ let node = this . getMutableNode ( ) ;
263
290
node . index = this . index ;
264
291
node . level = this . level ;
265
292
node . parentKey = this . parentNode instanceof ElementNode ? this . parentNode . node . key : null ;
266
- node . prevKey = this . previousSibling ?. node . key ?? null ;
267
- node . nextKey = this . nextSibling ?. node . key ?? null ;
293
+ node . prevKey = this . previousVisibleSibling ?. node . key ?? null ;
294
+ node . nextKey = nextSibling ?. node . key ?? null ;
268
295
node . hasChildNodes = ! ! this . firstChild ;
269
- node . firstChildKey = this . firstChild ?. node . key ?? null ;
270
- node . lastChildKey = this . lastChild ?. node . key ?? null ;
296
+ node . firstChildKey = this . firstVisibleChild ?. node . key ?? null ;
297
+ node . lastChildKey = this . lastVisibleChild ?. node . key ?? null ;
271
298
272
299
// Update the colIndex of sibling nodes if this node has a colSpan.
273
- if ( ( node . colSpan != null || node . colIndex != null ) && this . nextSibling ) {
300
+ if ( ( node . colSpan != null || node . colIndex != null ) && nextSibling ) {
274
301
// This queues the next sibling for update, which means this happens recursively.
275
302
let nextColIndex = ( node . colIndex ?? node . index ) + ( node . colSpan ?? 1 ) ;
276
- if ( nextColIndex !== this . nextSibling . node . colIndex ) {
277
- let siblingNode = this . ownerDocument . getMutableNode ( this . nextSibling ) ;
303
+ if ( nextColIndex !== nextSibling . node . colIndex ) {
304
+ let siblingNode = nextSibling . getMutableNode ( ) ;
278
305
siblingNode . colIndex = nextColIndex ;
279
306
}
280
307
}
281
308
}
282
309
283
310
setProps < E extends Element > ( obj : { [ key : string ] : any } , ref : ForwardedRef < E > , rendered ?: ReactNode , render ?: ( node : Node < T > ) => ReactElement ) : void {
284
- let node = this . ownerDocument . getMutableNode ( this ) ;
311
+ let node = this . getMutableNode ( ) ;
285
312
let { value, textValue, id, ...props } = obj ;
286
313
props . ref = ref ;
287
314
node . props = props ;
@@ -300,19 +327,44 @@ export class ElementNode<T> extends BaseNode<T> {
300
327
node . colSpan = props . colSpan ;
301
328
}
302
329
303
- // If this is the first time props have been set, end the transaction started in the constructor
304
- // so this node can be emitted.
305
- if ( ! this . hasSetProps ) {
306
- this . ownerDocument . addNode ( this ) ;
307
- this . ownerDocument . endTransaction ( ) ;
308
- this . hasSetProps = true ;
309
- }
310
-
330
+ this . hasSetProps = true ;
311
331
this . ownerDocument . queueUpdate ( ) ;
312
332
}
313
333
314
334
get style ( ) : CSSProperties {
315
- return { } ;
335
+ // React sets display: none to hide elements during Suspense.
336
+ // We'll handle this by setting the element to hidden and invalidating
337
+ // its siblings/parent. Hidden elements remain in the Document, but
338
+ // are removed from the Collection.
339
+ let element = this ;
340
+ return {
341
+ get display ( ) {
342
+ return element . isHidden ? 'none' : '' ;
343
+ } ,
344
+ set display ( value ) {
345
+ let isHidden = value === 'none' ;
346
+ if ( element . isHidden !== isHidden ) {
347
+ // Mark parent node dirty if this element is currently the first or last visible child.
348
+ if ( element . parentNode ?. firstVisibleChild === element || element . parentNode ?. lastVisibleChild === element ) {
349
+ element . ownerDocument . markDirty ( element . parentNode ) ;
350
+ }
351
+
352
+ // Mark sibling visible elements dirty.
353
+ let prev = element . previousVisibleSibling ;
354
+ let next = element . nextVisibleSibling ;
355
+ if ( prev ) {
356
+ element . ownerDocument . markDirty ( prev ) ;
357
+ }
358
+ if ( next ) {
359
+ element . ownerDocument . markDirty ( next ) ;
360
+ }
361
+
362
+ // Mark self dirty.
363
+ element . isHidden = isHidden ;
364
+ element . ownerDocument . markDirty ( element ) ;
365
+ }
366
+ }
367
+ } ;
316
368
}
317
369
318
370
hasAttribute ( ) : void { }
@@ -334,18 +386,16 @@ export class Document<T, C extends BaseCollection<T> = BaseCollection<T>> extend
334
386
nodesByProps = new WeakMap < object , ElementNode < T > > ( ) ;
335
387
isMounted = true ;
336
388
private collection : C ;
337
- private collectionMutated : boolean ;
338
- private mutatedNodes : Set < ElementNode < T > > = new Set ( ) ;
389
+ private nextCollection : C | null = null ;
339
390
private subscriptions : Set < ( ) => void > = new Set ( ) ;
340
- private transactionCount = 0 ;
341
391
private queuedRender = false ;
342
392
private inSubscription = false ;
343
393
344
394
constructor ( collection : C ) {
345
395
// @ts -ignore
346
396
super ( null ) ;
347
397
this . collection = collection ;
348
- this . collectionMutated = true ;
398
+ this . nextCollection = collection ;
349
399
}
350
400
351
401
get isConnected ( ) : boolean {
@@ -356,78 +406,56 @@ export class Document<T, C extends BaseCollection<T> = BaseCollection<T>> extend
356
406
return new ElementNode ( type , this ) ;
357
407
}
358
408
359
- /**
360
- * Lazily gets a mutable instance of a Node. If the node has already
361
- * been cloned during this update cycle, it just returns the existing one.
362
- */
363
- getMutableNode ( element : ElementNode < T > ) : Mutable < CollectionNode < T > > {
364
- let node = element . node ;
365
- if ( ! this . mutatedNodes . has ( element ) ) {
366
- node = element . node . clone ( ) ;
367
- this . mutatedNodes . add ( element ) ;
368
- element . node = node ;
369
- }
370
- this . markDirty ( element ) ;
371
- return node ;
372
- }
373
-
374
409
private getMutableCollection ( ) {
375
- if ( ! this . isSSR && ! this . collectionMutated ) {
376
- this . collection = this . collection . clone ( ) ;
377
- this . collectionMutated = true ;
410
+ if ( ! this . nextCollection ) {
411
+ this . nextCollection = this . collection . clone ( ) ;
378
412
}
379
413
380
- return this . collection ;
414
+ return this . nextCollection ;
381
415
}
382
416
383
417
markDirty ( node : BaseNode < T > ) : void {
384
418
this . dirtyNodes . add ( node ) ;
385
419
}
386
420
387
- startTransaction ( ) : void {
388
- this . transactionCount ++ ;
389
- }
390
-
391
- endTransaction ( ) : void {
392
- this . transactionCount -- ;
393
- }
421
+ private addNode ( element : ElementNode < T > ) : void {
422
+ if ( element . isHidden ) {
423
+ return ;
424
+ }
394
425
395
- addNode ( element : ElementNode < T > ) : void {
396
426
let collection = this . getMutableCollection ( ) ;
397
427
if ( ! collection . getItem ( element . node . key ) ) {
398
- collection . addNode ( element . node ) ;
399
-
400
428
for ( let child of element ) {
401
429
this . addNode ( child ) ;
402
430
}
403
431
}
404
432
405
- this . markDirty ( element ) ;
433
+ collection . addNode ( element . node ) ;
406
434
}
407
435
408
- removeNode ( node : ElementNode < T > ) : void {
436
+ private removeNode ( node : ElementNode < T > ) : void {
409
437
for ( let child of node ) {
410
438
this . removeNode ( child ) ;
411
439
}
412
440
413
441
let collection = this . getMutableCollection ( ) ;
414
442
collection . removeNode ( node . node . key ) ;
415
- this . markDirty ( node ) ;
416
443
}
417
444
418
445
/** Finalizes the collection update, updating all nodes and freezing the collection. */
419
446
getCollection ( ) : C {
420
- if ( this . transactionCount > 0 ) {
421
- return this . collection ;
447
+ // If in a subscription update, return a clone of the existing collection.
448
+ // This ensures React will queue a render. React will call getCollection again
449
+ // during render, at which point all the updates will be complete and we can return
450
+ // the new collection.
451
+ if ( this . inSubscription ) {
452
+ return this . collection . clone ( ) ;
422
453
}
423
454
424
- this . updateCollection ( ) ;
425
-
426
455
// Reset queuedRender to false when getCollection is called during render.
427
- if ( ! this . inSubscription ) {
428
- this . queuedRender = false ;
429
- }
456
+ this . queuedRender = false ;
430
457
458
+ this . updateCollection ( ) ;
431
459
return this . collection ;
432
460
}
433
461
@@ -439,36 +467,35 @@ export class Document<T, C extends BaseCollection<T> = BaseCollection<T>> extend
439
467
440
468
// Next, update dirty collection nodes.
441
469
for ( let element of this . dirtyNodes ) {
442
- if ( element instanceof ElementNode && element . isConnected ) {
443
- element . updateNode ( ) ;
470
+ if ( element instanceof ElementNode ) {
471
+ if ( element . isConnected && ! element . isHidden ) {
472
+ element . updateNode ( ) ;
473
+ this . addNode ( element ) ;
474
+ } else {
475
+ this . removeNode ( element ) ;
476
+ }
477
+
478
+ element . isMutated = false ;
444
479
}
445
480
}
446
481
447
482
this . dirtyNodes . clear ( ) ;
448
483
449
484
// Finally, update the collection.
450
- if ( this . mutatedNodes . size || this . collectionMutated ) {
451
- let collection = this . getMutableCollection ( ) ;
452
- for ( let element of this . mutatedNodes ) {
453
- if ( element . isConnected ) {
454
- collection . addNode ( element . node ) ;
455
- }
485
+ if ( this . nextCollection ) {
486
+ this . nextCollection . commit ( this . firstVisibleChild ?. node . key ?? null , this . lastVisibleChild ?. node . key ?? null , this . isSSR ) ;
487
+ if ( ! this . isSSR ) {
488
+ this . collection = this . nextCollection ;
489
+ this . nextCollection = null ;
456
490
}
457
-
458
- this . mutatedNodes . clear ( ) ;
459
- collection . commit ( this . firstChild ?. node . key ?? null , this . lastChild ?. node . key ?? null , this . isSSR ) ;
460
491
}
461
-
462
- this . collectionMutated = false ;
463
492
}
464
493
465
494
queueUpdate ( ) : void {
466
- // Don't emit any updates if there is a transaction in progress.
467
- // queueUpdate should be called again after the transaction.
468
- if ( this . dirtyNodes . size === 0 || this . transactionCount > 0 || this . queuedRender ) {
495
+ if ( this . dirtyNodes . size === 0 || this . queuedRender ) {
469
496
return ;
470
497
}
471
-
498
+
472
499
// Only trigger subscriptions once during an update, when the first item changes.
473
500
// React's useSyncExternalStore will call getCollection immediately, to check whether the snapshot changed.
474
501
// If so, React will queue a render to happen after the current commit to our fake DOM finishes.
0 commit comments