@@ -29,16 +29,15 @@ export class TopbarFeatures {
29
29
/** @type {HTMLElement } */
30
30
this . topbarEl ; // eslint-disable-line no-unused-expressions
31
31
/** @type {HTMLElement } */
32
- this . scoreScaleEl ; // eslint-disable-line no-unused-expressions
33
- /** @type {HTMLElement } */
32
+ this . categoriesEl ; // eslint-disable-line no-unused-expressions
33
+ /** @type {HTMLElement? } */
34
34
this . stickyHeaderEl ; // eslint-disable-line no-unused-expressions
35
35
/** @type {HTMLElement } */
36
36
this . highlightEl ; // eslint-disable-line no-unused-expressions
37
37
this . onDropDownMenuClick = this . onDropDownMenuClick . bind ( this ) ;
38
38
this . onKeyUp = this . onKeyUp . bind ( this ) ;
39
39
this . onCopy = this . onCopy . bind ( this ) ;
40
40
this . collapseAllDetails = this . collapseAllDetails . bind ( this ) ;
41
- this . _updateStickyHeaderOnScroll = this . _updateStickyHeaderOnScroll . bind ( this ) ;
42
41
}
43
42
44
43
/**
@@ -54,25 +53,7 @@ export class TopbarFeatures {
54
53
const topbarLogo = this . _dom . find ( '.lh-topbar__logo' , this . _dom . rootEl ) ;
55
54
topbarLogo . addEventListener ( 'click' , ( ) => toggleDarkTheme ( this . _dom ) ) ;
56
55
57
- // There is only a sticky header when at least 2 categories are present.
58
- if ( Object . keys ( this . lhr . categories ) . length >= 2 ) {
59
- this . _setupStickyHeaderElements ( ) ;
60
- const reportRootEl = this . _dom . rootEl ;
61
- const elToAddScrollListener = this . _getScrollParent ( reportRootEl ) ;
62
- elToAddScrollListener . addEventListener ( 'scroll' , this . _updateStickyHeaderOnScroll ) ;
63
-
64
- // Use ResizeObserver where available.
65
- // TODO: there is an issue with incorrect position numbers and, as a result, performance
66
- // issues due to layout thrashing.
67
- // See https://github.com/GoogleChrome/lighthouse/pull/9023/files#r288822287 for details.
68
- // For now, limit to DevTools.
69
- if ( this . _dom . isDevTools ( ) ) {
70
- const resizeObserver = new window . ResizeObserver ( this . _updateStickyHeaderOnScroll ) ;
71
- resizeObserver . observe ( reportRootEl ) ;
72
- } else {
73
- window . addEventListener ( 'resize' , this . _updateStickyHeaderOnScroll ) ;
74
- }
75
- }
56
+ this . _setupStickyHeader ( ) ;
76
57
}
77
58
78
59
/**
@@ -233,7 +214,7 @@ export class TopbarFeatures {
233
214
/**
234
215
* Finds the first scrollable ancestor of `element`. Falls back to the document.
235
216
* @param {Element } element
236
- * @return {Node }
217
+ * @return {Element | Document }
237
218
*/
238
219
_getScrollParent ( element ) {
239
220
const { overflowY} = window . getComputedStyle ( element ) ;
@@ -271,20 +252,45 @@ export class TopbarFeatures {
271
252
}
272
253
}
273
254
274
- _setupStickyHeaderElements ( ) {
255
+ _setupStickyHeader ( ) {
256
+ // Cache these elements to avoid qSA on each onscroll.
275
257
this . topbarEl = this . _dom . find ( 'div.lh-topbar' , this . _dom . rootEl ) ;
276
- this . scoreScaleEl = this . _dom . find ( 'div.lh-scorescale' , this . _dom . rootEl ) ;
277
- this . stickyHeaderEl = this . _dom . find ( 'div.lh-sticky-header' , this . _dom . rootEl ) ;
258
+ this . categoriesEl = this . _dom . find ( 'div.lh-categories' , this . _dom . rootEl ) ;
259
+
260
+ // Defer behind rAF to avoid forcing layout.
261
+ window . requestAnimationFrame ( ( ) => window . requestAnimationFrame ( ( ) => {
262
+ // Only present in the DOM if it'll be used (>=2 categories)
263
+ try {
264
+ this . stickyHeaderEl = this . _dom . find ( 'div.lh-sticky-header' , this . _dom . rootEl ) ;
265
+ } catch {
266
+ return ;
267
+ }
278
268
279
- // Highlighter will be absolutely positioned at first gauge, then transformed on scroll.
280
- this . highlightEl = this . _dom . createChildOf ( this . stickyHeaderEl , 'div' , 'lh-highlighter' ) ;
269
+ // Highlighter will be absolutely positioned at first gauge, then transformed on scroll.
270
+ this . highlightEl = this . _dom . createChildOf ( this . stickyHeaderEl , 'div' , 'lh-highlighter' ) ;
271
+
272
+ // Update sticky header visibility and highlight when page scrolls/resizes.
273
+ const scrollParent = this . _getScrollParent ( this . _dom . rootEl ) ;
274
+ // The 'scroll' handler must be should be on {Element | Document}...
275
+ scrollParent . addEventListener ( 'scroll' , ( ) => this . _updateStickyHeader ( ) ) ;
276
+ // However resizeObserver needs an element, *not* the document.
277
+ const resizeTarget = scrollParent instanceof window . Document
278
+ ? document . documentElement
279
+ : scrollParent ;
280
+ new window . ResizeObserver ( ( ) => this . _updateStickyHeader ( ) ) . observe ( resizeTarget ) ;
281
+ } ) ) ;
281
282
}
282
283
283
- _updateStickyHeaderOnScroll ( ) {
284
- // Show sticky header when the score scale begins to go underneath the topbar.
284
+ /**
285
+ * Toggle visibility and update highlighter position
286
+ */
287
+ _updateStickyHeader ( ) {
288
+ if ( ! this . stickyHeaderEl ) return ;
289
+
290
+ // Show sticky header when the main 5 gauges clear the topbar.
285
291
const topbarBottom = this . topbarEl . getBoundingClientRect ( ) . bottom ;
286
- const scoreScaleTop = this . scoreScaleEl . getBoundingClientRect ( ) . top ;
287
- const showStickyHeader = topbarBottom >= scoreScaleTop ;
292
+ const categoriesTop = this . categoriesEl . getBoundingClientRect ( ) . top ;
293
+ const showStickyHeader = topbarBottom >= categoriesTop ;
288
294
289
295
// Highlight mini gauge when section is in view.
290
296
// In view = the last category that starts above the middle of the window.
0 commit comments