Skip to content

Commit

Permalink
report: fix sticky header toggling too late (#13279)
Browse files Browse the repository at this point in the history
  • Loading branch information
paulirish authored Nov 4, 2021
1 parent 5d8ea1a commit 68ba77a
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 32 deletions.
70 changes: 38 additions & 32 deletions report/renderer/topbar-features.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,15 @@ export class TopbarFeatures {
/** @type {HTMLElement} */
this.topbarEl; // eslint-disable-line no-unused-expressions
/** @type {HTMLElement} */
this.scoreScaleEl; // eslint-disable-line no-unused-expressions
/** @type {HTMLElement} */
this.categoriesEl; // eslint-disable-line no-unused-expressions
/** @type {HTMLElement?} */
this.stickyHeaderEl; // eslint-disable-line no-unused-expressions
/** @type {HTMLElement} */
this.highlightEl; // eslint-disable-line no-unused-expressions
this.onDropDownMenuClick = this.onDropDownMenuClick.bind(this);
this.onKeyUp = this.onKeyUp.bind(this);
this.onCopy = this.onCopy.bind(this);
this.collapseAllDetails = this.collapseAllDetails.bind(this);
this._updateStickyHeaderOnScroll = this._updateStickyHeaderOnScroll.bind(this);
}

/**
Expand All @@ -54,25 +53,7 @@ export class TopbarFeatures {
const topbarLogo = this._dom.find('.lh-topbar__logo', this._dom.rootEl);
topbarLogo.addEventListener('click', () => toggleDarkTheme(this._dom));

// There is only a sticky header when at least 2 categories are present.
if (Object.keys(this.lhr.categories).length >= 2) {
this._setupStickyHeaderElements();
const reportRootEl = this._dom.rootEl;
const elToAddScrollListener = this._getScrollParent(reportRootEl);
elToAddScrollListener.addEventListener('scroll', this._updateStickyHeaderOnScroll);

// Use ResizeObserver where available.
// TODO: there is an issue with incorrect position numbers and, as a result, performance
// issues due to layout thrashing.
// See https://github.com/GoogleChrome/lighthouse/pull/9023/files#r288822287 for details.
// For now, limit to DevTools.
if (this._dom.isDevTools()) {
const resizeObserver = new window.ResizeObserver(this._updateStickyHeaderOnScroll);
resizeObserver.observe(reportRootEl);
} else {
window.addEventListener('resize', this._updateStickyHeaderOnScroll);
}
}
this._setupStickyHeader();
}

/**
Expand Down Expand Up @@ -233,7 +214,7 @@ export class TopbarFeatures {
/**
* Finds the first scrollable ancestor of `element`. Falls back to the document.
* @param {Element} element
* @return {Node}
* @return {Element | Document}
*/
_getScrollParent(element) {
const {overflowY} = window.getComputedStyle(element);
Expand Down Expand Up @@ -271,20 +252,45 @@ export class TopbarFeatures {
}
}

_setupStickyHeaderElements() {
_setupStickyHeader() {
// Cache these elements to avoid qSA on each onscroll.
this.topbarEl = this._dom.find('div.lh-topbar', this._dom.rootEl);
this.scoreScaleEl = this._dom.find('div.lh-scorescale', this._dom.rootEl);
this.stickyHeaderEl = this._dom.find('div.lh-sticky-header', this._dom.rootEl);
this.categoriesEl = this._dom.find('div.lh-categories', this._dom.rootEl);

// Defer behind rAF to avoid forcing layout.
window.requestAnimationFrame(() => window.requestAnimationFrame(() => {
// Only present in the DOM if it'll be used (>=2 categories)
try {
this.stickyHeaderEl = this._dom.find('div.lh-sticky-header', this._dom.rootEl);
} catch {
return;
}

// Highlighter will be absolutely positioned at first gauge, then transformed on scroll.
this.highlightEl = this._dom.createChildOf(this.stickyHeaderEl, 'div', 'lh-highlighter');
// Highlighter will be absolutely positioned at first gauge, then transformed on scroll.
this.highlightEl = this._dom.createChildOf(this.stickyHeaderEl, 'div', 'lh-highlighter');

// Update sticky header visibility and highlight when page scrolls/resizes.
const scrollParent = this._getScrollParent(this._dom.rootEl);
// The 'scroll' handler must be should be on {Element | Document}...
scrollParent.addEventListener('scroll', () => this._updateStickyHeader());
// However resizeObserver needs an element, *not* the document.
const resizeTarget = scrollParent instanceof window.Document
? document.documentElement
: scrollParent;
new window.ResizeObserver(() => this._updateStickyHeader()).observe(resizeTarget);
}));
}

_updateStickyHeaderOnScroll() {
// Show sticky header when the score scale begins to go underneath the topbar.
/**
* Toggle visibility and update highlighter position
*/
_updateStickyHeader() {
if (!this.stickyHeaderEl) return;

// Show sticky header when the main 5 gauges clear the topbar.
const topbarBottom = this.topbarEl.getBoundingClientRect().bottom;
const scoreScaleTop = this.scoreScaleEl.getBoundingClientRect().top;
const showStickyHeader = topbarBottom >= scoreScaleTop;
const categoriesTop = this.categoriesEl.getBoundingClientRect().top;
const showStickyHeader = topbarBottom >= categoriesTop;

// Highlight mini gauge when section is in view.
// In view = the last category that starts above the middle of the window.
Expand Down
5 changes: 5 additions & 0 deletions report/test/clients/bundle-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,17 @@ describe('lighthouseRenderer bundle', () => {
document = window.document;

global.window = global.self = window;
global.window.requestAnimationFrame = fn => fn();
// Stub out matchMedia for Node.
global.self.matchMedia = function() {
return {
addListener: function() {},
};
};
global.window.ResizeObserver = class ResizeObserver {
observe() { }
unobserve() { }
};
});

afterAll(() => {
Expand Down
6 changes: 6 additions & 0 deletions report/test/renderer/report-ui-features-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,20 @@ describe('ReportUIFeatures', () => {

global.HTMLElement = document.window.HTMLElement;
global.HTMLInputElement = document.window.HTMLInputElement;
global.HTMLInputElement = document.window.HTMLInputElement;

global.window = document.window;
global.window.requestAnimationFrame = fn => fn();
global.window.getComputedStyle = function() {
return {
marginTop: '10px',
height: '10px',
};
};
global.window.ResizeObserver = class ResizeObserver {
observe() { }
unobserve() { }
};

dom = new DOM(document.window.document);
sampleResults = Util.prepareReportResult(sampleResultsOrig);
Expand Down

0 comments on commit 68ba77a

Please sign in to comment.