@@ -22,6 +22,12 @@ interface UpdateStickyColumnsParams {
22
22
stickyEndStates : boolean [ ] ;
23
23
}
24
24
25
+ interface UpdateStickRowsParams {
26
+ rowsToStick : HTMLElement [ ] ;
27
+ stickyStates : boolean [ ] ;
28
+ position : 'top' | 'bottom' ;
29
+ }
30
+
25
31
/**
26
32
* List of all possible directions that can be used for sticky positioning.
27
33
* @docs -private
@@ -38,7 +44,10 @@ export class StickyStyler {
38
44
? new globalThis . ResizeObserver ( entries => this . _updateCachedSizes ( entries ) )
39
45
: null ;
40
46
private _updatedStickyColumnsParamsToReplay : UpdateStickyColumnsParams [ ] = [ ] ;
41
- private _stickyColumnsReplayTimeout : number | null = null ;
47
+ private _updatedStickRowsParamsToReplay : UpdateStickRowsParams [ ] = [ ] ;
48
+ private _stickyReplayTimeout : number | null = null ;
49
+ private _readSizeQueue : HTMLElement [ ] = [ ] ;
50
+ private _readSizeTimeout : number | null = null ;
42
51
private _cachedCellWidths : number [ ] = [ ] ;
43
52
private readonly _borderCellCss : Readonly < { [ d in StickyDirection ] : string } > ;
44
53
@@ -206,14 +215,27 @@ export class StickyStyler {
206
215
* should be stuck in the particular top or bottom position.
207
216
* @param position The position direction in which the row should be stuck if that row should be
208
217
* sticky.
209
- *
218
+ * @param replay Whether to enqueue this call for replay after a ResizeObserver update.
210
219
*/
211
- stickRows ( rowsToStick : HTMLElement [ ] , stickyStates : boolean [ ] , position : 'top' | 'bottom' ) {
220
+ stickRows (
221
+ rowsToStick : HTMLElement [ ] ,
222
+ stickyStates : boolean [ ] ,
223
+ position : 'top' | 'bottom' ,
224
+ replay = true ,
225
+ ) {
212
226
// Since we can't measure the rows on the server, we can't stick the rows properly.
213
227
if ( ! this . _isBrowser ) {
214
228
return ;
215
229
}
216
230
231
+ if ( replay ) {
232
+ this . _updateStickRowsReplayQueue ( {
233
+ rowsToStick : [ ...rowsToStick ] ,
234
+ stickyStates : [ ...stickyStates ] ,
235
+ position,
236
+ } ) ;
237
+ }
238
+
217
239
// Coalesce with other sticky row updates (top/bottom), sticky columns updates
218
240
// (and potentially other changes like column resize).
219
241
this . _coalescedStyleScheduler . schedule ( ( ) => {
@@ -440,24 +462,55 @@ export class StickyStyler {
440
462
441
463
/**
442
464
* Retreives the most recently observed size of the specified element from the cache, or
443
- * meaures it directly if not yet cached.
465
+ * schedules it to be measured directly if not yet cached.
444
466
*/
445
467
private _retrieveElementSize ( element : HTMLElement ) : { width : number ; height : number } {
446
468
const cachedSize = this . _elemSizeCache . get ( element ) ;
447
- if ( cachedSize ) {
469
+ if ( cachedSize != null ) {
448
470
return cachedSize ;
449
471
}
450
-
451
- const clientRect = element . getBoundingClientRect ( ) ;
452
- const size = { width : clientRect . width , height : clientRect . height } ;
453
-
454
472
if ( ! this . _resizeObserver ) {
455
- return size ;
473
+ return this . _retrieveElementSizeImmediate ( element ) ;
456
474
}
457
475
458
- this . _elemSizeCache . set ( element , size ) ;
459
476
this . _resizeObserver . observe ( element , { box : 'border-box' } ) ;
460
- return size ;
477
+ this . _enqueueReadSize ( element ) ;
478
+
479
+ return { width : 0 , height : 0 } ;
480
+ }
481
+
482
+ private _enqueueReadSize ( element : HTMLElement ) : void {
483
+ this . _readSizeQueue . push ( element ) ;
484
+
485
+ if ( ! this . _readSizeTimeout ) {
486
+ this . _readSizeTimeout = setTimeout ( ( ) => {
487
+ this . _readSizeTimeout = null ;
488
+
489
+ let needsReplay = false ;
490
+ for ( const e of this . _readSizeQueue ) {
491
+ if ( this . _elemSizeCache . get ( e ) != null ) {
492
+ continue ;
493
+ }
494
+
495
+ const size = this . _retrieveElementSizeImmediate ( e ) ;
496
+ this . _elemSizeCache . set ( e , size ) ;
497
+ needsReplay = true ;
498
+ }
499
+ this . _readSizeQueue = [ ] ;
500
+
501
+ if ( needsReplay && ! this . _stickyReplayTimeout ) {
502
+ this . _scheduleStickReplay ( ) ;
503
+ }
504
+ } , 10 ) ;
505
+ }
506
+ }
507
+
508
+ /**
509
+ * Returns the size of the specified element by direct measurement.
510
+ */
511
+ private _retrieveElementSizeImmediate ( element : HTMLElement ) : { width : number ; height : number } {
512
+ const clientRect = element . getBoundingClientRect ( ) ;
513
+ return { width : clientRect . width , height : clientRect . height } ;
461
514
}
462
515
463
516
/**
@@ -468,7 +521,7 @@ export class StickyStyler {
468
521
this . _removeFromStickyColumnReplayQueue ( params . rows ) ;
469
522
470
523
// No need to replay if a flush is pending.
471
- if ( this . _stickyColumnsReplayTimeout ) {
524
+ if ( this . _stickyReplayTimeout ) {
472
525
return ;
473
526
}
474
527
@@ -486,9 +539,22 @@ export class StickyStyler {
486
539
) ;
487
540
}
488
541
542
+ private _updateStickRowsReplayQueue ( params : UpdateStickRowsParams ) {
543
+ // No need to replay if a flush is pending.
544
+ if ( this . _stickyReplayTimeout ) {
545
+ return ;
546
+ }
547
+
548
+ this . _updatedStickRowsParamsToReplay = this . _updatedStickRowsParamsToReplay . filter (
549
+ entry => entry . position !== params . position ,
550
+ ) ;
551
+
552
+ this . _updatedStickRowsParamsToReplay . push ( params ) ;
553
+ }
554
+
489
555
/** Update _elemSizeCache with the observed sizes. */
490
556
private _updateCachedSizes ( entries : ResizeObserverEntry [ ] ) {
491
- let needsColumnUpdate = false ;
557
+ let needsUpdate = false ;
492
558
for ( const entry of entries ) {
493
559
const newEntry = entry . borderBoxSize ?. length
494
560
? {
@@ -500,35 +566,52 @@ export class StickyStyler {
500
566
height : entry . contentRect . height ,
501
567
} ;
502
568
569
+ const cachedSize = this . _elemSizeCache . get ( entry . target as HTMLElement ) ;
503
570
if (
504
- newEntry . width !== this . _elemSizeCache . get ( entry . target as HTMLElement ) ?. width &&
505
- isCell ( entry . target )
571
+ ( newEntry . width !== cachedSize ?. width && isCell ( entry . target ) ) ||
572
+ ( newEntry . height !== cachedSize ?. height && isRow ( entry . target ) )
506
573
) {
507
- needsColumnUpdate = true ;
574
+ needsUpdate = true ;
508
575
}
509
576
510
577
this . _elemSizeCache . set ( entry . target as HTMLElement , newEntry ) ;
511
578
}
512
579
513
- if ( needsColumnUpdate && this . _updatedStickyColumnsParamsToReplay . length ) {
514
- if ( this . _stickyColumnsReplayTimeout ) {
515
- clearTimeout ( this . _stickyColumnsReplayTimeout ) ;
516
- }
580
+ if ( needsUpdate ) {
581
+ this . _scheduleStickReplay ( ) ;
582
+ }
583
+ }
517
584
518
- this . _stickyColumnsReplayTimeout = setTimeout ( ( ) => {
519
- for ( const update of this . _updatedStickyColumnsParamsToReplay ) {
520
- this . updateStickyColumns (
521
- update . rows ,
522
- update . stickyStartStates ,
523
- update . stickyEndStates ,
524
- true ,
525
- false ,
526
- ) ;
527
- }
528
- this . _updatedStickyColumnsParamsToReplay = [ ] ;
529
- this . _stickyColumnsReplayTimeout = null ;
530
- } , 0 ) ;
585
+ /** Schedule a defered replay of enqueued sticky column operations. */
586
+ private _scheduleStickReplay ( ) {
587
+ if (
588
+ ! this . _updatedStickyColumnsParamsToReplay . length &&
589
+ ! this . _updatedStickRowsParamsToReplay . length
590
+ ) {
591
+ return ;
531
592
}
593
+
594
+ if ( this . _stickyReplayTimeout ) {
595
+ clearTimeout ( this . _stickyReplayTimeout ) ;
596
+ }
597
+
598
+ this . _stickyReplayTimeout = setTimeout ( ( ) => {
599
+ for ( const update of this . _updatedStickyColumnsParamsToReplay ) {
600
+ this . updateStickyColumns (
601
+ update . rows ,
602
+ update . stickyStartStates ,
603
+ update . stickyEndStates ,
604
+ true ,
605
+ false ,
606
+ ) ;
607
+ }
608
+ for ( const update of this . _updatedStickRowsParamsToReplay ) {
609
+ this . stickRows ( update . rowsToStick , update . stickyStates , update . position , false ) ;
610
+ }
611
+ this . _updatedStickyColumnsParamsToReplay = [ ] ;
612
+ this . _updatedStickRowsParamsToReplay = [ ] ;
613
+ this . _stickyReplayTimeout = null ;
614
+ } , 0 ) ;
532
615
}
533
616
}
534
617
@@ -537,3 +620,9 @@ function isCell(element: Element) {
537
620
element . classList . contains ( klass ) ,
538
621
) ;
539
622
}
623
+
624
+ function isRow ( element : Element ) {
625
+ return [ 'cdk-row' , 'cdk-header-row' , 'cdk-footer-row' ] . some ( klass =>
626
+ element . classList . contains ( klass ) ,
627
+ ) ;
628
+ }
0 commit comments