@@ -16,6 +16,12 @@ import {StickyPositioningListener} from './sticky-position-listener';
16
16
17
17
export type StickyDirection = 'top' | 'bottom' | 'left' | 'right' ;
18
18
19
+ interface UpdateStickyColumnsParams {
20
+ rows : HTMLElement [ ] ;
21
+ stickyStartStates : boolean [ ] ;
22
+ stickyEndStates : boolean [ ] ;
23
+ }
24
+
19
25
/**
20
26
* List of all possible directions that can be used for sticky positioning.
21
27
* @docs -private
@@ -27,6 +33,12 @@ export const STICKY_DIRECTIONS: StickyDirection[] = ['top', 'bottom', 'left', 'r
27
33
* @docs -private
28
34
*/
29
35
export 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 ;
30
42
private _cachedCellWidths : number [ ] = [ ] ;
31
43
private readonly _borderCellCss : Readonly < { [ d in StickyDirection ] : string } > ;
32
44
@@ -68,6 +80,10 @@ export class StickyStyler {
68
80
* @param stickyDirections The directions that should no longer be set as sticky on the rows.
69
81
*/
70
82
clearStickyPositioning ( rows : HTMLElement [ ] , stickyDirections : StickyDirection [ ] ) {
83
+ if ( stickyDirections . includes ( 'left' ) || stickyDirections . includes ( 'right' ) ) {
84
+ this . _removeFromStickyColumnReplayQueue ( rows ) ;
85
+ }
86
+
71
87
const elementsToClear : HTMLElement [ ] = [ ] ;
72
88
for ( const row of rows ) {
73
89
// If the row isn't an element (e.g. if it's an `ng-container`),
@@ -100,13 +116,23 @@ export class StickyStyler {
100
116
* in this index position should be stuck to the end of the row.
101
117
* @param recalculateCellWidths Whether the sticky styler should recalculate the width of each
102
118
* column cell. If `false` cached widths will be used instead.
119
+ * @param replay Whether to enqueue this call for replay after a ResizeObserver update.
103
120
*/
104
121
updateStickyColumns (
105
122
rows : HTMLElement [ ] ,
106
123
stickyStartStates : boolean [ ] ,
107
124
stickyEndStates : boolean [ ] ,
108
125
recalculateCellWidths = true ,
126
+ replay = true ,
109
127
) {
128
+ if ( replay ) {
129
+ this . _updateStickyColumnReplayQueue ( {
130
+ rows : [ ...rows ] ,
131
+ stickyStartStates : [ ...stickyStartStates ] ,
132
+ stickyEndStates : [ ...stickyEndStates ] ,
133
+ } ) ;
134
+ }
135
+
110
136
if (
111
137
! rows . length ||
112
138
! this . _isBrowser ||
@@ -213,7 +239,7 @@ export class StickyStyler {
213
239
? ( Array . from ( row . children ) as HTMLElement [ ] )
214
240
: [ row ] ;
215
241
216
- const height = row . getBoundingClientRect ( ) . height ;
242
+ const height = this . _retrieveElementSize ( row ) . height ;
217
243
stickyOffset += height ;
218
244
stickyCellHeights [ rowIndex ] = height ;
219
245
}
@@ -366,8 +392,8 @@ export class StickyStyler {
366
392
const cellWidths : number [ ] = [ ] ;
367
393
const firstRowCells = row . children ;
368
394
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 ) ;
371
397
}
372
398
373
399
this . _cachedCellWidths = cellWidths ;
@@ -411,4 +437,103 @@ export class StickyStyler {
411
437
412
438
return positions ;
413
439
}
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
+ ) ;
414
539
}
0 commit comments