devtools: enable sticky header, top bar, and report ui features (#9023)
This commit is contained in:
Родитель
ffaea60094
Коммит
a11dec42ce
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 <templates> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -159,7 +159,6 @@ limitations under the License.
|
|||
.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 @@ limitations under the License.
|
|||
--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 @@ limitations under the License.
|
|||
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 @@ limitations under the License.
|
|||
height: 1px;
|
||||
background: var(--color-highlighter-bg);
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
bottom: -5px;
|
||||
}
|
||||
|
||||
.lh-gauge__wrapper:first-of-type {
|
||||
contain: none;
|
||||
}
|
||||
</style>
|
||||
<div class="lh-scores-wrapper">
|
||||
|
@ -219,7 +226,7 @@ limitations under the License.
|
|||
<template id="tmpl-lh-topbar">
|
||||
<style>
|
||||
.lh-topbar {
|
||||
position: fixed;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
@ -256,6 +263,15 @@ limitations under the License.
|
|||
cursor: pointer;
|
||||
margin-right: 5px;
|
||||
}
|
||||
/*
|
||||
Some features in the top right drop down menu don't work in the DevTools
|
||||
client. They could with some tweaks, but currently they don't. For example:
|
||||
Saving as HTML/JSON - does not bring up a file dialog, as one would expect in DevTools.
|
||||
also, it saves the AuditsPanel HTML, which is funky.
|
||||
*/
|
||||
.lh-devtools .lh-export__button {
|
||||
display: none;
|
||||
}
|
||||
.lh-export__button svg {
|
||||
fill: var(--lh-export-icon-color);
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@ describe('ReportRenderer', () => {
|
|||
it('should render a report', () => {
|
||||
const container = renderer._dom._document.body;
|
||||
const output = renderer.renderReport(sampleResults, container);
|
||||
assert.ok(output.querySelector('.lh-header-sticky'), 'has a header');
|
||||
assert.ok(output.querySelector('.lh-header-container'), 'has a header');
|
||||
assert.ok(output.querySelector('.lh-report'), 'has report body');
|
||||
// 3 sets of gauges - one in sticky header, one in scores header, and one in each section.
|
||||
assert.equal(output.querySelectorAll('.lh-gauge__wrapper, .lh-gauge--pwa__wrapper').length,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"extends": "../tsconfig",
|
||||
"compilerOptions": {
|
||||
"typeRoots": [
|
||||
"@types",
|
||||
"../node_modules/@types",
|
||||
"../types",
|
||||
"./types",
|
||||
],
|
||||
|
|
|
@ -87,6 +87,7 @@
|
|||
"@types/node": "*",
|
||||
"@types/opn": "^3.0.28",
|
||||
"@types/raven": "^2.5.1",
|
||||
"@types/resize-observer-browser": "^0.1.1",
|
||||
"@types/rimraf": "^2.0.2",
|
||||
"@types/semver": "^5.5.0",
|
||||
"@types/uglify-es": "^3.0.0",
|
||||
|
@ -183,7 +184,7 @@
|
|||
},
|
||||
{
|
||||
"path": "./dist/viewer/src/viewer.js",
|
||||
"threshold": "70 Kb"
|
||||
"threshold": "75 Kb"
|
||||
}
|
||||
],
|
||||
"nyc": {
|
||||
|
|
|
@ -585,6 +585,11 @@
|
|||
"@types/events" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/resize-observer-browser@^0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.1.tgz#9b7cdae9cdc8b1a7020ca7588018dac64c770866"
|
||||
integrity sha512-5/bJS/uGB5kmpRrrAWXQnmyKlv+4TlPn4f+A2NBa93p+mt6Ht+YcNGkQKf8HMx28a9hox49ZXShtbGqZkk41Sw==
|
||||
|
||||
"@types/rimraf@^2.0.2":
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-2.0.2.tgz#7f0fc3cf0ff0ad2a99bb723ae1764f30acaf8b6e"
|
||||
|
|
Загрузка…
Ссылка в новой задаче