-
Notifications
You must be signed in to change notification settings - Fork 9.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
report(redesign): add sticky scores header #8524
Conversation
Nice!! EDIT: Yep! it's flagged as #8524 (comment) |
// Show sticky header when the score scale begins to go underneath the topbar. | ||
const showStickyHeader = | ||
topbarEl.getBoundingClientRect().bottom - scoreScaleEl.getBoundingClientRect().top >= 0; | ||
stickyHeaderEl.classList.toggle('stuck', showStickyHeader); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we use lh-sticky-header--stuck
? :)
// is the one "in view". | ||
let highlightIndex = 0; | ||
let highlightIndexDistance = Number.POSITIVE_INFINITY; | ||
for (const [index, categoryEl] of Object.entries(document.querySelectorAll('.lh-category'))) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this index-based approach is what is causing the behavior I was seeing with plugin and PWA flipped. Do we want the gauges to match their appearance order in the report instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just confirming you are correct in your next comment - this code will work as expected when the gauge order is as expected.
} | ||
|
||
// Category order matches gauge order in sticky header. | ||
// TODO(hoten): not 100% true yet, need to order gauges like: core, pwa, plugins. Remove |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, I see :)
historically comments without a direct issue link tend to be forgotten, is that PR like the next one though?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a task entry in the report-redesign issue for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah of course!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry shouldn't have prematurely flushed!
// Use the middle of the viewport as an anchor - the closest category to the middle | ||
// is the one "in view". | ||
let highlightIndex = 0; | ||
let highlightIndexDistance = Number.POSITIVE_INFINITY; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just curious, why not Infinity
? :)
// Normalize to middle of viewport. | ||
const distanceToMiddle = categoryEl.getBoundingClientRect().top - window.innerHeight / 2; | ||
// Closest negative distance to zero wins. | ||
if (distanceToMiddle < 0 && highlightIndexDistance > distanceToMiddle) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems like we could simplify some of this but maybe I'm misunderstanding
highlightIndexDistance
will only ever update whendistanceToMiddle
is negativehighlightIndexDistance
is-distanceToMiddle
, sohighlightIndexDistance
is always positivehighlightIndexDistance
will always be greater thandistanceToMiddle
ifdistanceToMiddle
is negative, so it's really justif (distanceToMiddle < 0)
this seems equivalent to "we highlight the last category that starts above the middle of the window" which at least for me is a lot easier to understand.
maybe something like the below?
const categoriesAboveTheMiddle = categories.filter(el => el.getBoundingClientRect().top - window.innerHeight / 2)
const categoryToHighlight = categoriesAboveTheMiddle[categoriesAboveTheMiddle.length - 1]
const guageToHighlight = gauges[categories.indexOf(categoryToHighlight)]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this works:
// Highlight mini gauge when section is in view.
// In view = the last category that starts above the middle of the window.
const categoryEls = Array.from(this._document.querySelectorAll('.lh-category'));
const categoriesAboveTheMiddle =
categoryEls.filter(el => el.getBoundingClientRect().top - window.innerHeight / 2 < 0);
const highlightIndex =
categoriesAboveTheMiddle.length > 0 ? categoriesAboveTheMiddle.length - 1 : 0;
// Category order matches gauge order in sticky header.
// TODO(hoten): not 100% true yet, need to order gauges like: core, pwa, plugins. Remove
// this comment when that is done.
const gaugeToHighlight = stickyHeaderEl.querySelectorAll('.lh-gauge__wrapper')[highlightIndex];
// @ts-ignore
highlightEl.style.left = gaugeToHighlight.getBoundingClientRect().left + 'px';
thanks for making more declarative, i'll update
// TODO(hoten): not 100% true yet, need to order gauges like: core, pwa, plugins. Remove | ||
// this comment when that is done. | ||
const gaugeToHighlight = stickyHeaderEl.querySelectorAll('.lh-gauge__wrapper')[highlightIndex]; | ||
// @ts-ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what are we ignoring?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Property 'style' does not exist on type 'Element'
idk why
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
needs to be HTMLElement
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
// TODO(hoten): not 100% true yet, need to order gauges like: core, pwa, plugins. Remove | ||
// this comment when that is done. | ||
const gaugeToHighlight = stickyHeaderEl.querySelectorAll('.lh-gauge__wrapper')[highlightIndex]; | ||
(/** @type {HTMLElement} */ (highlightEl)).style.left = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah I guess querySelector
can return things that aren't HTMLElements, bummer! Maybe make the assertion at the top where it's declared?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just refactored to use _dom + make member vars
this._setUpCollapseDetailsAfterPrinting(); | ||
this._resetUIState(); | ||
this._document.addEventListener('keydown', this.printShortCutDetect); | ||
this._document.addEventListener('copy', this.onCopy); | ||
this._document.addEventListener('scroll', this._handleStickyHeader); | ||
// window.addEventListener is undefined in jest tests. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
which test file(s) fails? This sounds like jsdom wasn't set up correctly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yup that was it, fixed.
@@ -80,7 +80,7 @@ describe('ReportRenderer', () => { | |||
assert.ok(output.querySelector('.lh-header-sticky'), 'has a header'); | |||
assert.ok(output.querySelector('.lh-report'), 'has report body'); | |||
assert.equal(output.querySelectorAll('.lh-gauge__wrapper, .lh-gauge--pwa__wrapper').length, | |||
sampleResults.reportCategories.length * 2, 'renders category gauges'); | |||
sampleResults.reportCategories.length * 3, 'renders category gauges'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe add a comment for where this multiplier comes from
|
||
// The sticky header is just the score gauges, but styled to be smaller. Just | ||
// clone the gauges from the score header. | ||
for (const gaugeWrapperEl of this._dom.findAll('.lh-gauge__wrapper', scoreHeader)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this feels pretty squicky to me :)
Can we make a renderScoresHeader()
method and use it in both places?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
@@ -380,6 +402,29 @@ class ReportUIFeatures { | |||
this._document.body.removeChild(a); | |||
setTimeout(_ => URL.revokeObjectURL(href), 500); | |||
} | |||
|
|||
_handleStickyHeader() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe rename to make it clear this is for adjusting the sticky header on scroll events?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It handles sticky header visibility, and the highlighting. It also fires on scroll and resize.
_handleStickyHeaderVisibilityAndHighlighterOnScrollAndOnResize
? :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
haha, I was thinking more like _updateStickyHeaderOnScroll
or whatever :)
Arguably (it's a stretch :) resize is a special case of scrolling for this element, resizing is uncommon compared to scrolling, and the main thing is the name can communicate something about what it's up to and when
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sold, to _updateStickyHeaderOnScroll
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a few more things, but LGTM!
/** | ||
* @param {LH.ReportResult} report | ||
* @param {CategoryRenderer} categoryRenderer | ||
* @param {Record<string, CategoryRenderer>} specificCategoryRenderers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
categoryRenderer
and specificCategoryRenderers
probably need docstrings (sorry for that second name :)
scoreHeader.append(...defaultGauges, ...customGauges, ...pluginGauges); | ||
|
||
scoreHeader.append( | ||
...this._renderScoreGauges(report, categoryRenderer, specificCategoryRenderers)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
kind of hard to read the spread after a line break like this. Maybe save to a local var first?
this._dom.createChildOf(stickyHeader, 'div', 'lh-highlighter'); | ||
|
||
// The sticky header is just the score gauges, but styled to be smaller. Just | ||
// clone the gauges from the score header. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not cloned anymore
// The sticky header is just the score gauges, but styled to be smaller. Just | ||
// clone the gauges from the score header. | ||
stickyHeader.append( | ||
...this._renderScoreGauges(report, categoryRenderer, specificCategoryRenderers)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same thing with the line break and spread as above
@@ -380,6 +399,29 @@ class ReportUIFeatures { | |||
this._document.body.removeChild(a); | |||
setTimeout(_ => URL.revokeObjectURL(href), 500); | |||
} | |||
|
|||
_updateStickyHeaderOnScroll() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this need to be called once on startup?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this need to be called once on startup?
I guess not since it starts out hidden
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TIL - https://stackoverflow.com/a/34095654
The scroll event does not fire on every load, only when refreshing a page that was scrolled, or when navigating to an anchor directly.
So fresh view (top of page), state should be hidden, and the default is just that. Refresh after scrolling a bit, or click on a link w/ an anchor to the report, and a scroll event fires.
const showStickyHeader = topbarBottom >= scoreScaleTop; | ||
this.stickyHeaderEl.classList.toggle('lh-sticky-header--visible', showStickyHeader); | ||
|
||
// Highlight mini gauge when section is in view. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
early return if showStickyHeader === false
?
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'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we need a test for this, though jsdom (AFAIK) doesn't support scrolling, so it will have to be a limited one. Since we have a nice complete "renders score gauges in this order" test, maybe just a test that .lh-sticky-header
has child gauges that match the ones in .lh-scores-header
?
Or something like that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a test that .lh-sticky-header has child gauges that match the ones in .lh-scores-header?
imo that doesn't seem like a useful test, since these gauges are created with the same function.
jsdom (AFAIK) doesn't support scrolling
jsdom/jsdom#1422 (comment) this makes me think it should be possible to fake it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok it'd be way too much faking, basically all the getBoundingClientRect
needs to be mocked
will also need a PR rename. @paulirish do you feel comfortable with the overhead of this scroll handler? Seems comparable to the one we had before |
Attempted a rebase - it's a lot of conflicts over many commits. Resolving them is not realistic imo. I ran all the tests locally and they passed. |
just merge? a rebase isn't necessary
I got burned the last two times I overrode the Travis check, so I'd rather not do that anymore |
Ah, failed to mention that I did merge master. Still failing.
…On Wed, Apr 24, 2019, 6:38 PM Brendan Kenny ***@***.***> wrote:
Attempted a rebase - it's a lot of conflicts over many commits. Resolving
them is not realistic imo.
just merge? a rebase isn't necessary
I ran all the tests locally and they passed.
I got burned the last two times I overrode the Travis check, so I'd rather
not do that anymore
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#8524 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AA7CAMRU6EYF7TOSNGXOYS3PSEDQBANCNFSM4HHSUZPQ>
.
|
Looks like it was the NO_TRACING_STARTED flakiness. Green now |
@paulirish had some performance feedback |
const topbarBottom = this.topbarEl.getBoundingClientRect().bottom; | ||
const scoreScaleTop = this.scoreScaleEl.getBoundingClientRect().top; | ||
const showStickyHeader = topbarBottom >= scoreScaleTop; | ||
this.stickyHeaderEl.classList.toggle('lh-sticky-header--visible', showStickyHeader); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting... The order is the same, so is the issue that code in between these two changes allows for the browser to jump in and do an extra layout? Idk how layouts work
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
got it, makes sense.
AFAICT from the performance tab w/ this code, there are no extra layouts. One change I saw for when the sticky header toggles - "Recalculate Styles" was within the update function, but w/ your changes it ran just after on the main thread. Would like to know if I'm reading the performance timeline wrong.
Even if there technically isn't a layout cost, moving the DOM mutation to the bottom SGTM b/c it makes it obvious there is no layout thrashing.
#8185
sticky header w/ mini gauges
...appears when scrolling past score scale
...current category in view is underlined