@@ -16,6 +16,12 @@ import {StickyPositioningListener} from './sticky-position-listener';
1616
1717export type StickyDirection = 'top' | 'bottom' | 'left' | 'right' ;
1818
19+ interface UpdateStickyColumnsParams {
20+ rows : HTMLElement [ ] ;
21+ stickyStartStates : boolean [ ] ;
22+ stickyEndStates : boolean [ ] ;
23+ }
24+
1925/**
2026 * List of all possible directions that can be used for sticky positioning.
2127 * @docs -private
@@ -27,6 +33,12 @@ export const STICKY_DIRECTIONS: StickyDirection[] = ['top', 'bottom', 'left', 'r
2733 * @docs -private
2834 */
2935export class StickyStyler {
36+ private _elemSizeCache = new WeakMap < HTMLElement , { width : number ; height : number } > ( ) ;
37+ private _resizeObserver = globalThis ?. ResizeObserver
38+ ? new globalThis . ResizeObserver ( entries => this . _updateCachedSizes ( entries ) )
39+ : null ;
40+ private _updatedStickyColumnsParamsToReplay : UpdateStickyColumnsParams [ ] = [ ] ;
41+ private _stickyColumnsReplayTimeout : number | null = null ;
3042 private _cachedCellWidths : number [ ] = [ ] ;
3143 private readonly _borderCellCss : Readonly < { [ d in StickyDirection ] : string } > ;
3244
@@ -68,6 +80,10 @@ export class StickyStyler {
6880 * @param stickyDirections The directions that should no longer be set as sticky on the rows.
6981 */
7082 clearStickyPositioning ( rows : HTMLElement [ ] , stickyDirections : StickyDirection [ ] ) {
83+ if ( stickyDirections . includes ( 'left' ) || stickyDirections . includes ( 'right' ) ) {
84+ this . _removeFromStickyColumnReplayQueue ( rows ) ;
85+ }
86+
7187 const elementsToClear : HTMLElement [ ] = [ ] ;
7288 for ( const row of rows ) {
7389 // If the row isn't an element (e.g. if it's an `ng-container`),
@@ -100,13 +116,23 @@ export class StickyStyler {
100116 * in this index position should be stuck to the end of the row.
101117 * @param recalculateCellWidths Whether the sticky styler should recalculate the width of each
102118 * column cell. If `false` cached widths will be used instead.
119+ * @param replay Whether to enqueue this call for replay after a ResizeObserver update.
103120 */
104121 updateStickyColumns (
105122 rows : HTMLElement [ ] ,
106123 stickyStartStates : boolean [ ] ,
107124 stickyEndStates : boolean [ ] ,
108125 recalculateCellWidths = true ,
126+ replay = true ,
109127 ) {
128+ if ( replay ) {
129+ this . _updateStickyColumnReplayQueue ( {
130+ rows : [ ...rows ] ,
131+ stickyStartStates : [ ...stickyStartStates ] ,
132+ stickyEndStates : [ ...stickyEndStates ] ,
133+ } ) ;
134+ }
135+
110136 if (
111137 ! rows . length ||
112138 ! this . _isBrowser ||
@@ -213,7 +239,7 @@ export class StickyStyler {
213239 ? ( Array . from ( row . children ) as HTMLElement [ ] )
214240 : [ row ] ;
215241
216- const height = row . getBoundingClientRect ( ) . height ;
242+ const height = this . _retrieveElementSize ( row ) . height ;
217243 stickyOffset += height ;
218244 stickyCellHeights [ rowIndex ] = height ;
219245 }
@@ -366,8 +392,8 @@ export class StickyStyler {
366392 const cellWidths : number [ ] = [ ] ;
367393 const firstRowCells = row . children ;
368394 for ( let i = 0 ; i < firstRowCells . length ; i ++ ) {
369- let cell : HTMLElement = firstRowCells [ i ] as HTMLElement ;
370- cellWidths . push ( cell . getBoundingClientRect ( ) . width ) ;
395+ const cell = firstRowCells [ i ] as HTMLElement ;
396+ cellWidths . push ( this . _retrieveElementSize ( cell ) . width ) ;
371397 }
372398
373399 this . _cachedCellWidths = cellWidths ;
@@ -411,4 +437,103 @@ export class StickyStyler {
411437
412438 return positions ;
413439 }
440+
441+ /**
442+ * Retreives the most recently observed size of the specified element from the cache, or
443+ * meaures it directly if not yet cached.
444+ */
445+ private _retrieveElementSize ( element : HTMLElement ) : { width : number ; height : number } {
446+ const cachedSize = this . _elemSizeCache . get ( element ) ;
447+ if ( cachedSize ) {
448+ return cachedSize ;
449+ }
450+
451+ const clientRect = element . getBoundingClientRect ( ) ;
452+ const size = { width : clientRect . width , height : clientRect . height } ;
453+
454+ if ( ! this . _resizeObserver ) {
455+ return size ;
456+ }
457+
458+ this . _elemSizeCache . set ( element , size ) ;
459+ this . _resizeObserver . observe ( element , { box : 'border-box' } ) ;
460+ return size ;
461+ }
462+
463+ /**
464+ * Conditionally enqueue the requested sticky update and clear previously queued updates
465+ * for the same rows.
466+ */
467+ private _updateStickyColumnReplayQueue ( params : UpdateStickyColumnsParams ) {
468+ this . _removeFromStickyColumnReplayQueue ( params . rows ) ;
469+
470+ // No need to replay if a flush is pending.
471+ if ( this . _stickyColumnsReplayTimeout ) {
472+ return ;
473+ }
474+
475+ this . _updatedStickyColumnsParamsToReplay . push ( params ) ;
476+ }
477+
478+ /** Remove updates for the specified rows from the queue. */
479+ private _removeFromStickyColumnReplayQueue ( rows : HTMLElement [ ] ) {
480+ const rowsSet = new Set ( rows ) ;
481+ for ( const update of this . _updatedStickyColumnsParamsToReplay ) {
482+ update . rows = update . rows . filter ( row => ! rowsSet . has ( row ) ) ;
483+ }
484+ this . _updatedStickyColumnsParamsToReplay = this . _updatedStickyColumnsParamsToReplay . filter (
485+ update => ! ! update . rows . length ,
486+ ) ;
487+ }
488+
489+ /** Update _elemSizeCache with the observed sizes. */
490+ private _updateCachedSizes ( entries : ResizeObserverEntry [ ] ) {
491+ let needsColumnUpdate = false ;
492+ for ( const entry of entries ) {
493+ const newEntry = entry . borderBoxSize ?. length
494+ ? {
495+ width : entry . borderBoxSize [ 0 ] . inlineSize ,
496+ height : entry . borderBoxSize [ 0 ] . blockSize ,
497+ }
498+ : {
499+ width : entry . contentRect . width ,
500+ height : entry . contentRect . height ,
501+ } ;
502+
503+ if (
504+ newEntry . width !== this . _elemSizeCache . get ( entry . target as HTMLElement ) ?. width &&
505+ isCell ( entry . target )
506+ ) {
507+ needsColumnUpdate = true ;
508+ }
509+
510+ this . _elemSizeCache . set ( entry . target as HTMLElement , newEntry ) ;
511+ }
512+
513+ if ( needsColumnUpdate && this . _updatedStickyColumnsParamsToReplay . length ) {
514+ if ( this . _stickyColumnsReplayTimeout ) {
515+ clearTimeout ( this . _stickyColumnsReplayTimeout ) ;
516+ }
517+
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 ) ;
531+ }
532+ }
533+ }
534+
535+ function isCell ( element : Element ) {
536+ return [ 'cdk-cell' , 'cdk-header-cell' , 'cdk-footer-cell' ] . some ( klass =>
537+ element . classList . contains ( klass ) ,
538+ ) ;
414539}
0 commit comments