diff --git a/lighthouse-core/report/html/renderer/report-renderer.js b/lighthouse-core/report/html/renderer/report-renderer.js index 3c7e61051669..86559958f4b6 100644 --- a/lighthouse-core/report/html/renderer/report-renderer.js +++ b/lighthouse-core/report/html/renderer/report-renderer.js @@ -91,17 +91,6 @@ class ReportRenderer { return el; } - /** - * @return {Element} - */ - _renderReportShortHeader() { - const shortHeaderContainer = this._dom.createElement('div', 'lh-header-container'); - const wrapper = this._dom.cloneTemplate('#tmpl-lh-scores-wrapper', this._templateContext); - shortHeaderContainer.appendChild(wrapper); - return shortHeaderContainer; - } - - /** * @param {LH.ReportResult} report * @return {DocumentFragment} @@ -195,16 +184,8 @@ class ReportRenderer { * @return {DocumentFragment} */ _renderReport(report) { - let header; const headerContainer = this._dom.createElement('div'); - if (this._dom.isDevTools()) { - headerContainer.classList.add('lh-header-plain'); - header = this._renderReportShortHeader(); - } else { - headerContainer.classList.add('lh-header-sticky'); - header = this._renderReportHeader(); - } - headerContainer.appendChild(header); + headerContainer.appendChild(this._renderReportHeader()); const container = this._dom.createElement('div', 'lh-container'); const reportSection = container.appendChild(this._dom.createElement('div', 'lh-report')); @@ -242,38 +223,27 @@ class ReportRenderer { wrapper.appendChild(renderer.render(category, report.categoryGroups)); } + const reportFragment = this._dom.createFragment(); + const topbarDocumentFragment = this._renderReportTopbar(report); + reportFragment.appendChild(topbarDocumentFragment); + if (scoreHeader) { - const scoreGauges = - this._renderScoreGauges(report, categoryRenderer, specificCategoryRenderers); - scoreHeader.append(...scoreGauges); const scoreScale = this._dom.cloneTemplate('#tmpl-lh-scorescale', this._templateContext); const scoresContainer = this._dom.find('.lh-scores-container', headerContainer); + scoreHeader.append( + ...this._renderScoreGauges(report, categoryRenderer, specificCategoryRenderers)); scoresContainer.appendChild(scoreHeader); scoresContainer.appendChild(scoreScale); - } - - reportSection.appendChild(this._renderReportFooter(report)); - - const reportFragment = this._dom.createFragment(); - if (!this._dom.isDevTools()) { - const topbarDocumentFragment = this._renderReportTopbar(report); - reportFragment.appendChild(topbarDocumentFragment); - } - - if (scoreHeader && !this._dom.isDevTools()) { const stickyHeader = this._dom.createElement('div', 'lh-sticky-header'); - this._dom.createChildOf(stickyHeader, 'div', 'lh-highlighter'); - - const scoreGauges = - this._renderScoreGauges(report, categoryRenderer, specificCategoryRenderers); - stickyHeader.append(...scoreGauges); - + stickyHeader.append( + ...this._renderScoreGauges(report, categoryRenderer, specificCategoryRenderers)); reportFragment.appendChild(stickyHeader); } reportFragment.appendChild(headerContainer); reportFragment.appendChild(container); + reportSection.appendChild(this._renderReportFooter(report)); return reportFragment; } diff --git a/lighthouse-core/report/html/renderer/report-ui-features.js b/lighthouse-core/report/html/renderer/report-ui-features.js index 8a2db2697921..b403805b5610 100644 --- a/lighthouse-core/report/html/renderer/report-ui-features.js +++ b/lighthouse-core/report/html/renderer/report-ui-features.js @@ -44,6 +44,8 @@ class ReportUIFeatures { this._dom = dom; /** @type {Document} */ this._document = this._dom.document(); + /** @type {ParentNode} */ + this._templateContext = this._dom.document(); /** @type {boolean} */ this._copyAttempt = false; /** @type {HTMLElement} */ @@ -76,9 +78,8 @@ class ReportUIFeatures { * @param {LH.Result} report */ initFeatures(report) { - if (this._dom.isDevTools()) return; - this.json = report; + this._setupMediaQueryListeners(); this._setupExportButton(); this._setupThirdPartyFilter(); @@ -86,6 +87,7 @@ class ReportUIFeatures { this._resetUIState(); this._document.addEventListener('keyup', this.onKeyUp); this._document.addEventListener('copy', this.onCopy); + const topbarLogo = this._dom.find('.lh-topbar__logo', this._document); topbarLogo.addEventListener('click', () => this._toggleDarkTheme()); @@ -98,13 +100,9 @@ class ReportUIFeatures { const scoresAll100 = Object.values(report.categories).every(cat => cat.score === 1); const hasAllCoreCategories = Object.keys(report.categories).filter(id => !Util.isPluginCategory(id)).length >= 5; - if (!this._dom.isDevTools() && scoresAll100 && hasAllCoreCategories) { + if (scoresAll100 && hasAllCoreCategories) { turnOffTheLights = true; - const scoresContainer = this._dom.find('.lh-scores-container', this._document); - scoresContainer.classList.add('score100'); - scoresContainer.addEventListener('click', _ => { - scoresContainer.classList.toggle('fireworks-paused'); - }); + this._enableFireworks(); } if (turnOffTheLights) { @@ -114,8 +112,21 @@ class ReportUIFeatures { // There is only a sticky header when at least 2 categories are present. if (Object.keys(this.json.categories).length >= 2) { this._setupStickyHeaderElements(); - this._document.addEventListener('scroll', this._updateStickyHeaderOnScroll); - window.addEventListener('resize', this._updateStickyHeaderOnScroll); + const containerEl = this._dom.find('.lh-container', this._document); + const elToAddScrollListener = this._getScrollParent(containerEl); + 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(containerEl); + } else { + window.addEventListener('resize', this._updateStickyHeaderOnScroll); + } } // Show the metric descriptions by default when there is an error. @@ -128,6 +139,43 @@ class ReportUIFeatures { } } + /** + * Define a custom element for to be extracted from. For example: + * this.setTemplateContext(new DOMParser().parseFromString(htmlStr, 'text/html')) + * @param {ParentNode} context + */ + setTemplateContext(context) { + this._templateContext = context; + } + + /** + * Finds the first scrollable ancestor of `element`. Falls back to the document. + * @param {HTMLElement} element + * @return {Node} + */ + _getScrollParent(element) { + const {overflowY} = window.getComputedStyle(element); + const isScrollable = overflowY !== 'visible' && overflowY !== 'hidden'; + + if (isScrollable) { + return element; + } + + if (element.parentElement) { + return this._getScrollParent(element.parentElement); + } + + return document; + } + + _enableFireworks() { + const scoresContainer = this._dom.find('.lh-scores-container', this._document); + scoresContainer.classList.add('score100'); + scoresContainer.addEventListener('click', _ => { + scoresContainer.classList.toggle('fireworks-paused'); + }); + } + /** * Fires a custom DOM event on target. * @param {string} name Name of the event. @@ -188,13 +236,13 @@ class ReportUIFeatures { if (thirdPartyRows.size === urlItems.length || !thirdPartyRows.size) return; // create input box - const filterTemplate = this._dom.cloneTemplate('#tmpl-lh-3p-filter', this._document); + const filterTemplate = this._dom.cloneTemplate('#tmpl-lh-3p-filter', this._templateContext); const filterInput = this._dom.find('input', filterTemplate); const id = `lh-3p-filter-label--${index}`; filterInput.id = id; filterInput.addEventListener('change', e => { - // Remove rows from the dom and keep track of them to readd on uncheck. + // Remove rows from the dom and keep track of them to re-add on uncheck. // Why removing instead of hiding? To keep nth-child(even) background-colors working. if (e.target instanceof HTMLInputElement && !e.target.checked) { for (const row of thirdPartyRows.values()) { @@ -263,7 +311,10 @@ class ReportUIFeatures { this.topbarEl = this._dom.find('.lh-topbar', this._document); this.scoreScaleEl = this._dom.find('.lh-scorescale', this._document); this.stickyHeaderEl = this._dom.find('.lh-sticky-header', this._document); - this.highlightEl = this._dom.find('.lh-highlighter', this._document); + + // Position highlighter at first gauge; will be transformed on scroll. + const firstGauge = this._dom.find('.lh-gauge__wrapper', this.stickyHeaderEl); + this.highlightEl = this._dom.createChildOf(firstGauge, 'div', 'lh-highlighter'); } /** @@ -554,7 +605,15 @@ class ReportUIFeatures { * @param {boolean} [force] */ _toggleDarkTheme(force) { - this._document.body.classList.toggle('dark', force); + const el = this._dom.find('.lh-vars', this._document); + // This seems unnecessary, but in DevTools, passing "undefined" as the second + // parameter acts like passing "false". + // https://github.com/ChromeDevTools/devtools-frontend/blob/dd6a6d4153647c2a4203c327c595692c5e0a4256/front_end/dom_extension/DOMExtension.js#L809-L819 + if (typeof force === 'undefined') { + el.classList.toggle('dark'); + } else { + el.classList.toggle('dark', force); + } } _updateStickyHeaderOnScroll() { @@ -574,11 +633,12 @@ class ReportUIFeatures { // Category order matches gauge order in sticky header. const gaugeWrapperEls = this.stickyHeaderEl.querySelectorAll('.lh-gauge__wrapper'); const gaugeToHighlight = gaugeWrapperEls[highlightIndex]; - const offset = gaugeToHighlight.getBoundingClientRect().left + 'px'; + const origin = gaugeWrapperEls[0].getBoundingClientRect().left; + const offset = gaugeToHighlight.getBoundingClientRect().left - origin; // Mutate at end to avoid layout thrashing. + this.highlightEl.style.transform = `translate(${offset}px)`; this.stickyHeaderEl.classList.toggle('lh-sticky-header--visible', showStickyHeader); - this.highlightEl.style.left = offset; } } diff --git a/lighthouse-core/report/html/report-styles.css b/lighthouse-core/report/html/report-styles.css index 8be7d1206f03..c4c4faaeeed5 100644 --- a/lighthouse-core/report/html/report-styles.css +++ b/lighthouse-core/report/html/report-styles.css @@ -261,6 +261,24 @@ --audit-indent: 16px; --expandable-indent: 16px; + --gauge-circle-size-big: 72px; + --gauge-circle-size: 64px; + + --audits-margin-bottom: 20px; + --env-name-min-width: 120px; + --header-padding: 16px 0 16px 0; + --plugin-icon-size: 75%; + --pwa-icon-margin: 0 7px 0 -3px; + --score-container-width: 92px; + --score-number-font-size-big: 34px; + --score-number-font-size: 26px; + --score-shape-margin-left: 2px; + --score-shape-size: 10px; + --score-title-font-size-big: 22px; + --score-title-font-size: 14px; + --score-title-line-height-big: 26px; + --score-title-line-height: 20px; + --lh-audit-vpadding: 4px; --lh-audit-hgap: 12px; --lh-audit-group-vpadding: 12px; @@ -555,6 +573,9 @@ .lh-column:first-of-type { margin-right: 0px; } + .lh-column:first-of-type .lh-metric:last-of-type { + border-bottom: 0; + } } @@ -952,22 +973,6 @@ } /* Report */ - -.lh-header-sticky { - /** TODO: Redesigned report has a small sticky header. - For now, disable the current sticky behavior. */ - /* position: -webkit-sticky; - position: sticky; */ - top: 0; - width: 100%; - min-width: var(--report-min-width); - z-index: 2; - will-change: transform; -} -.lh-header-plain { - margin-top: var(--section-padding); -} - .lh-list > div:not(:last-child) { padding-bottom: 20px; } diff --git a/lighthouse-core/report/html/templates.html b/lighthouse-core/report/html/templates.html index 7032ea4b83c1..ee4b388c69af 100644 --- a/lighthouse-core/report/html/templates.html +++ b/lighthouse-core/report/html/templates.html @@ -159,7 +159,6 @@ .lh-scores-container { display: flex; flex-direction: column; - margin-top: var(--topbar-height); padding: var(--header-padding); position: relative; width: 100%; @@ -171,12 +170,12 @@ --plugin-icon-size: 75%; --score-container-width: 60px; --score-number-font-size: 13px; - position: fixed; + position: sticky; left: 0; right: 0; top: var(--topbar-height); font-weight: 700; - display: flex; + display: none; justify-content: center; background-color: var(--color-sticky-header-bg); border-bottom: 1px solid var(--color-black-200); @@ -184,12 +183,17 @@ padding-bottom: 4px; z-index: 1; pointer-events: none; - opacity: 0; } .lh-sticky-header--visible { + display: flex; pointer-events: auto; - opacity: 1; + } + + /* Disable the gauge arc animation for the sticky header, so toggling display: none + does not play the animation. */ + .lh-sticky-header .lh-gauge-arc { + animation: none; } .lh-sticky-header .lh-gauge__label { @@ -201,8 +205,11 @@ height: 1px; background: var(--color-highlighter-bg); position: absolute; - bottom: -1px; - left: 0; + bottom: -5px; + } + + .lh-gauge__wrapper:first-of-type { + contain: none; }
@@ -219,7 +226,7 @@