@@ -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,10 @@ 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 ;
49+ private _readSizeQueue : HTMLElement [ ] = [ ] ;
50+ private _readSizeTimeout : number | null = null ;
4251 private _cachedCellWidths : number [ ] = [ ] ;
4352 private readonly _borderCellCss : Readonly < { [ d in StickyDirection ] : string } > ;
4453
@@ -206,14 +215,27 @@ export class StickyStyler {
206215 * should be stuck in the particular top or bottom position.
207216 * @param position The position direction in which the row should be stuck if that row should be
208217 * sticky.
209- *
218+ * @param replay Whether to enqueue this call for replay after a ResizeObserver update.
210219 */
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+ ) {
212226 // Since we can't measure the rows on the server, we can't stick the rows properly.
213227 if ( ! this . _isBrowser ) {
214228 return ;
215229 }
216230
231+ if ( replay ) {
232+ this . _updateStickRowsReplayQueue ( {
233+ rowsToStick : [ ...rowsToStick ] ,
234+ stickyStates : [ ...stickyStates ] ,
235+ position,
236+ } ) ;
237+ }
238+
217239 // Coalesce with other sticky row updates (top/bottom), sticky columns updates
218240 // (and potentially other changes like column resize).
219241 this . _coalescedStyleScheduler . schedule ( ( ) => {
@@ -440,24 +462,53 @@ export class StickyStyler {
440462
441463 /**
442464 * 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.
444466 */
445467 private _retrieveElementSize ( element : HTMLElement ) : { width : number ; height : number } {
446468 const cachedSize = this . _elemSizeCache . get ( element ) ;
447- if ( cachedSize ) {
469+ if ( cachedSize != null ) {
448470 return cachedSize ;
449471 }
450-
451- const clientRect = element . getBoundingClientRect ( ) ;
452- const size = { width : clientRect . width , height : clientRect . height } ;
453-
454472 if ( ! this . _resizeObserver ) {
455- return size ;
473+ return this . _retrieveElementSizeImmediate ( element ) ;
456474 }
457475
458- this . _elemSizeCache . set ( element , size ) ;
459476 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+ for ( const e of this . _readSizeQueue ) {
490+ if ( this . _elemSizeCache . get ( e ) != null ) {
491+ continue ;
492+ }
493+
494+ const size = this . _retrieveElementSizeImmediate ( e ) ;
495+ this . _elemSizeCache . set ( e , size ) ;
496+ }
497+ this . _readSizeQueue = [ ] ;
498+
499+ if ( ! this . _stickyReplayTimeout ) {
500+ this . _scheduleStickReplay ( ) ;
501+ }
502+ } , 10 ) ;
503+ }
504+ }
505+
506+ /**
507+ * Returns the size of the specified element by direct measurement.
508+ */
509+ private _retrieveElementSizeImmediate ( element : HTMLElement ) : { width : number ; height : number } {
510+ const clientRect = element . getBoundingClientRect ( ) ;
511+ return { width : clientRect . width , height : clientRect . height } ;
461512 }
462513
463514 /**
@@ -468,7 +519,7 @@ export class StickyStyler {
468519 this . _removeFromStickyColumnReplayQueue ( params . rows ) ;
469520
470521 // No need to replay if a flush is pending.
471- if ( this . _stickyColumnsReplayTimeout ) {
522+ if ( this . _stickyReplayTimeout ) {
472523 return ;
473524 }
474525
@@ -486,9 +537,22 @@ export class StickyStyler {
486537 ) ;
487538 }
488539
540+ private _updateStickRowsReplayQueue ( params : UpdateStickRowsParams ) {
541+ // No need to replay if a flush is pending.
542+ if ( this . _stickyReplayTimeout ) {
543+ return ;
544+ }
545+
546+ this . _updatedStickRowsParamsToReplay = this . _updatedStickRowsParamsToReplay . filter (
547+ entry => entry . position !== params . position ,
548+ ) ;
549+
550+ this . _updatedStickRowsParamsToReplay . push ( params ) ;
551+ }
552+
489553 /** Update _elemSizeCache with the observed sizes. */
490554 private _updateCachedSizes ( entries : ResizeObserverEntry [ ] ) {
491- let needsColumnUpdate = false ;
555+ let needsUpdate = false ;
492556 for ( const entry of entries ) {
493557 const newEntry = entry . borderBoxSize ?. length
494558 ? {
@@ -500,35 +564,52 @@ export class StickyStyler {
500564 height : entry . contentRect . height ,
501565 } ;
502566
567+ const cachedSize = this . _elemSizeCache . get ( entry . target as HTMLElement ) ;
503568 if (
504- newEntry . width !== this . _elemSizeCache . get ( entry . target as HTMLElement ) ?. width &&
505- isCell ( entry . target )
569+ ( newEntry . width !== cachedSize ?. width && isCell ( entry . target ) ) ||
570+ ( newEntry . height !== cachedSize ?. height && isRow ( entry . target ) )
506571 ) {
507- needsColumnUpdate = true ;
572+ needsUpdate = true ;
508573 }
509574
510575 this . _elemSizeCache . set ( entry . target as HTMLElement , newEntry ) ;
511576 }
512577
513- if ( needsColumnUpdate && this . _updatedStickyColumnsParamsToReplay . length ) {
514- if ( this . _stickyColumnsReplayTimeout ) {
515- clearTimeout ( this . _stickyColumnsReplayTimeout ) ;
516- }
578+ if ( needsUpdate ) {
579+ this . _scheduleStickReplay ( ) ;
580+ }
581+ }
517582
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 ) ;
583+ /** Schedule a defered replay of enqueued sticky column operations. */
584+ private _scheduleStickReplay ( ) {
585+ if (
586+ ! this . _updatedStickyColumnsParamsToReplay . length &&
587+ ! this . _updatedStickRowsParamsToReplay . length
588+ ) {
589+ return ;
531590 }
591+
592+ if ( this . _stickyReplayTimeout ) {
593+ clearTimeout ( this . _stickyReplayTimeout ) ;
594+ }
595+
596+ this . _stickyReplayTimeout = setTimeout ( ( ) => {
597+ for ( const update of this . _updatedStickyColumnsParamsToReplay ) {
598+ this . updateStickyColumns (
599+ update . rows ,
600+ update . stickyStartStates ,
601+ update . stickyEndStates ,
602+ true ,
603+ false ,
604+ ) ;
605+ }
606+ for ( const update of this . _updatedStickRowsParamsToReplay ) {
607+ this . stickRows ( update . rowsToStick , update . stickyStates , update . position , false ) ;
608+ }
609+ this . _updatedStickyColumnsParamsToReplay = [ ] ;
610+ this . _updatedStickRowsParamsToReplay = [ ] ;
611+ this . _stickyReplayTimeout = null ;
612+ } , 0 ) ;
532613 }
533614}
534615
@@ -537,3 +618,9 @@ function isCell(element: Element) {
537618 element . classList . contains ( klass ) ,
538619 ) ;
539620}
621+
622+ function isRow ( element : Element ) {
623+ return [ 'cdk-row' , 'cdk-header-row' , 'cdk-footer-row' ] . some ( klass =>
624+ element . classList . contains ( klass ) ,
625+ ) ;
626+ }
0 commit comments