@@ -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,55 @@ 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+ 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 } ;
461514 }
462515
463516 /**
@@ -468,7 +521,7 @@ export class StickyStyler {
468521 this . _removeFromStickyColumnReplayQueue ( params . rows ) ;
469522
470523 // No need to replay if a flush is pending.
471- if ( this . _stickyColumnsReplayTimeout ) {
524+ if ( this . _stickyReplayTimeout ) {
472525 return ;
473526 }
474527
@@ -486,49 +539,79 @@ export class StickyStyler {
486539 ) ;
487540 }
488541
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+
489555 /** Update _elemSizeCache with the observed sizes. */
490556 private _updateCachedSizes ( entries : ResizeObserverEntry [ ] ) {
491- let needsColumnUpdate = false ;
557+ let needsUpdate = false ;
492558 for ( const entry of entries ) {
493559 const newEntry = entry . borderBoxSize ?. length
494560 ? {
495561 width : entry . borderBoxSize [ 0 ] . inlineSize ,
496562 height : entry . borderBoxSize [ 0 ] . blockSize ,
497563 }
498564 : {
499- width : entry . contentRect . width ,
500- height : entry . contentRect . height ,
565+ width : ( entry . target as HTMLElement ) . offsetWidth ,
566+ height : ( entry . target as HTMLElement ) . offsetHeight ,
501567 } ;
502568
569+ const cachedSize = this . _elemSizeCache . get ( entry . target as HTMLElement ) ;
503570 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 ) )
506573 ) {
507- needsColumnUpdate = true ;
574+ needsUpdate = true ;
508575 }
509576
510577 this . _elemSizeCache . set ( entry . target as HTMLElement , newEntry ) ;
511578 }
512579
513- if ( needsColumnUpdate && this . _updatedStickyColumnsParamsToReplay . length ) {
514- if ( this . _stickyColumnsReplayTimeout ) {
515- clearTimeout ( this . _stickyColumnsReplayTimeout ) ;
516- }
580+ if ( needsUpdate ) {
581+ this . _scheduleStickReplay ( ) ;
582+ }
583+ }
517584
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 ;
531592 }
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 ) ;
532615 }
533616}
534617
@@ -537,3 +620,9 @@ function isCell(element: Element) {
537620 element . classList . contains ( klass ) ,
538621 ) ;
539622}
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