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 @@