@@ -22,6 +22,12 @@ interface UpdateStickyColumnsParams {
2222 stickyEndStates : boolean [ ] ;
2323}
2424
25+ interface UpdateStickRowsParams {
26+ rowsToStick : HTMLElement [ ] ;
27+ stickyStates : boolean [ ] ;
28+ position : 'top' | 'bottom' ;
29+ }
30+
2531/**
2632 * List of all possible directions that can be used for sticky positioning.
2733 * @docs -private
@@ -38,7 +44,8 @@ export class StickyStyler {
3844 ? new globalThis . ResizeObserver ( entries => this . _updateCachedSizes ( entries ) )
3945 : null ;
4046 private _updatedStickyColumnsParamsToReplay : UpdateStickyColumnsParams [ ] = [ ] ;
41- private _stickyColumnsReplayTimeout : number | null = null ;
47+ private _updatedStickRowsParamsToReplay : UpdateStickRowsParams [ ] = [ ] ;
48+ private _stickyReplayTimeout : number | null = null ;
4249 private _cachedCellWidths : number [ ] = [ ] ;
4350 private readonly _borderCellCss : Readonly < { [ d in StickyDirection ] : string } > ;
4451
@@ -206,14 +213,27 @@ export class StickyStyler {
206213 * should be stuck in the particular top or bottom position.
207214 * @param position The position direction in which the row should be stuck if that row should be
208215 * sticky.
209- *
216+ * @param replay Whether to enqueue this call for replay after a ResizeObserver update.
210217 */
211- stickRows ( rowsToStick : HTMLElement [ ] , stickyStates : boolean [ ] , position : 'top' | 'bottom' ) {
218+ stickRows (
219+ rowsToStick : HTMLElement [ ] ,
220+ stickyStates : boolean [ ] ,
221+ position : 'top' | 'bottom' ,
222+ replay = true ,
223+ ) {
212224 // Since we can't measure the rows on the server, we can't stick the rows properly.
213225 if ( ! this . _isBrowser ) {
214226 return ;
215227 }
216228
229+ if ( replay ) {
230+ this . _updateStickRowsReplayQueue ( {
231+ rowsToStick : [ ...rowsToStick ] ,
232+ stickyStates : [ ...stickyStates ] ,
233+ position,
234+ } ) ;
235+ }
236+
217237 // Coalesce with other sticky row updates (top/bottom), sticky columns updates
218238 // (and potentially other changes like column resize).
219239 this . _coalescedStyleScheduler . schedule ( ( ) => {
@@ -440,24 +460,40 @@ export class StickyStyler {
440460
441461 /**
442462 * Retreives the most recently observed size of the specified element from the cache, or
443- * meaures it directly if not yet cached.
463+ * schedules it to be measured directly if not yet cached.
444464 */
445465 private _retrieveElementSize ( element : HTMLElement ) : { width : number ; height : number } {
446466 const cachedSize = this . _elemSizeCache . get ( element ) ;
447- if ( cachedSize ) {
467+ if ( cachedSize != null ) {
448468 return cachedSize ;
449469 }
450-
451- const clientRect = element . getBoundingClientRect ( ) ;
452- const size = { width : clientRect . width , height : clientRect . height } ;
453-
454470 if ( ! this . _resizeObserver ) {
455- return size ;
471+ return this . _retrieveElementSizeImmediate ( element ) ;
456472 }
457473
458- this . _elemSizeCache . set ( element , size ) ;
459474 this . _resizeObserver . observe ( element , { box : 'border-box' } ) ;
460- return size ;
475+ setTimeout ( ( ) => {
476+ if ( this . _elemSizeCache . get ( element ) != null ) {
477+ return ;
478+ }
479+
480+ const size = this . _retrieveElementSizeImmediate ( element ) ;
481+ this . _elemSizeCache . set ( element , size ) ;
482+
483+ if ( ! this . _stickyReplayTimeout ) {
484+ this . _scheduleStickReplay ( ) ;
485+ }
486+ } , 10 ) ;
487+
488+ return { width : 0 , height : 0 } ;
489+ }
490+
491+ /**
492+ * Returns the size of the specified element by direct measurement.
493+ */
494+ private _retrieveElementSizeImmediate ( element : HTMLElement ) : { width : number ; height : number } {
495+ const clientRect = element . getBoundingClientRect ( ) ;
496+ return { width : clientRect . width , height : clientRect . height } ;
461497 }
462498
463499 /**
@@ -468,7 +504,7 @@ export class StickyStyler {
468504 this . _removeFromStickyColumnReplayQueue ( params . rows ) ;
469505
470506 // No need to replay if a flush is pending.
471- if ( this . _stickyColumnsReplayTimeout ) {
507+ if ( this . _stickyReplayTimeout ) {
472508 return ;
473509 }
474510
@@ -486,9 +522,22 @@ export class StickyStyler {
486522 ) ;
487523 }
488524
525+ private _updateStickRowsReplayQueue ( params : UpdateStickRowsParams ) {
526+ // No need to replay if a flush is pending.
527+ if ( this . _stickyReplayTimeout ) {
528+ return ;
529+ }
530+
531+ this . _updatedStickRowsParamsToReplay = this . _updatedStickRowsParamsToReplay . filter (
532+ entry => entry . position !== params . position ,
533+ ) ;
534+
535+ this . _updatedStickRowsParamsToReplay . push ( params ) ;
536+ }
537+
489538 /** Update _elemSizeCache with the observed sizes. */
490539 private _updateCachedSizes ( entries : ResizeObserverEntry [ ] ) {
491- let needsColumnUpdate = false ;
540+ let needsUpdate = false ;
492541 for ( const entry of entries ) {
493542 const newEntry = entry . borderBoxSize ?. length
494543 ? {
@@ -500,35 +549,52 @@ export class StickyStyler {
500549 height : entry . contentRect . height ,
501550 } ;
502551
552+ const cachedSize = this . _elemSizeCache . get ( entry . target as HTMLElement ) ;
503553 if (
504- newEntry . width !== this . _elemSizeCache . get ( entry . target as HTMLElement ) ?. width &&
505- isCell ( entry . target )
554+ ( newEntry . width !== cachedSize ?. width && isCell ( entry . target ) ) ||
555+ ( newEntry . height !== cachedSize ?. height && isRow ( entry . target ) )
506556 ) {
507- needsColumnUpdate = true ;
557+ needsUpdate = true ;
508558 }
509559
510560 this . _elemSizeCache . set ( entry . target as HTMLElement , newEntry ) ;
511561 }
512562
513- if ( needsColumnUpdate && this . _updatedStickyColumnsParamsToReplay . length ) {
514- if ( this . _stickyColumnsReplayTimeout ) {
515- clearTimeout ( this . _stickyColumnsReplayTimeout ) ;
516- }
563+ if ( needsUpdate ) {
564+ this . _scheduleStickReplay ( ) ;
565+ }
566+ }
517567
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 ) ;
568+ /** Schedule a defered replay of enqueued sticky column operations. */
569+ private _scheduleStickReplay ( ) {
570+ if (
571+ ! this . _updatedStickyColumnsParamsToReplay . length &&
572+ ! this . _updatedStickRowsParamsToReplay . length
573+ ) {
574+ return ;
575+ }
576+
577+ if ( this . _stickyReplayTimeout ) {
578+ clearTimeout ( this . _stickyReplayTimeout ) ;
531579 }
580+
581+ this . _stickyReplayTimeout = setTimeout ( ( ) => {
582+ for ( const update of this . _updatedStickyColumnsParamsToReplay ) {
583+ this . updateStickyColumns (
584+ update . rows ,
585+ update . stickyStartStates ,
586+ update . stickyEndStates ,
587+ true ,
588+ false ,
589+ ) ;
590+ }
591+ for ( const update of this . _updatedStickRowsParamsToReplay ) {
592+ this . stickRows ( update . rowsToStick , update . stickyStates , update . position , false ) ;
593+ }
594+ this . _updatedStickyColumnsParamsToReplay = [ ] ;
595+ this . _updatedStickRowsParamsToReplay = [ ] ;
596+ this . _stickyReplayTimeout = null ;
597+ } , 0 ) ;
532598 }
533599}
534600
@@ -537,3 +603,9 @@ function isCell(element: Element) {
537603 element . classList . contains ( klass ) ,
538604 ) ;
539605}
606+
607+ function isRow ( element : Element ) {
608+ return [ 'cdk-row' , 'cdk-header-row' , 'cdk-footer-row' ] . some ( klass =>
609+ element . classList . contains ( klass ) ,
610+ ) ;
611+ }
0 commit comments