Change top bar to only appear whene scrolling up
This commit is contained in:
@@ -29,11 +29,14 @@ function onFilterChange(event) {
|
||||
}
|
||||
|
||||
// Scroll-driven show/hide: the header slides out of view on scroll-down and
|
||||
// back in on scroll-up. This is show/hide via `transform` (not the old
|
||||
// resize behaviour) — the header is position:fixed, so translating it never
|
||||
// reflows content, and the app's programmatic scrolls resolve to sensible
|
||||
// states: scrollTo(0, 0) lands near the top → shown; the list-view
|
||||
// read-correction scrollBy moves only a few px → stays under the threshold.
|
||||
// back in only on scroll-up — never merely by proximity to the top. This is
|
||||
// show/hide via `transform` (not the old resize behaviour) — the header is
|
||||
// position:fixed, so translating it never reflows content. The app's
|
||||
// programmatic scrolls resolve to sensible states without any top-of-page
|
||||
// special case: scrollTo(0, 0) (article paging, layout toggle) is itself a
|
||||
// large upward jump, so it crosses REVEAL_THRESHOLD like a real scroll-up and
|
||||
// reveals the header that way; the list-view read-correction scrollBy moves
|
||||
// only a few px → stays under the threshold and leaves a hidden header hidden.
|
||||
const hidden = ref(false)
|
||||
const REVEAL_THRESHOLD = 12 // px of accumulated travel before toggling
|
||||
// When the feed list mutates itself (read-correction scrollBy + array-splice
|
||||
@@ -45,25 +48,13 @@ const REVEAL_THRESHOLD = 12 // px of accumulated travel before toggling
|
||||
const PROGRAMMATIC_SUPPRESS_MS = 300
|
||||
let lastY = 0
|
||||
let accumulated = 0
|
||||
// Cached in onMounted() below instead of re-read via headerRef.offsetHeight
|
||||
// on every scroll event. The header is a fixed size (see onMounted), so
|
||||
// re-measuring it here was pure waste — worse, a DevTools performance trace
|
||||
// showed that offsetHeight read landing right after an unrelated DOM write
|
||||
// elsewhere on the page (e.g. the article view swapping in a new article)
|
||||
// forces the browser into a synchronous, forced-reflow layout pass right
|
||||
// there mid-script, instead of its normal scheduled one. Stays 0 until
|
||||
// onMounted() below sets it (before the scroll listener is attached, so
|
||||
// onScroll() never runs against the unset value) — keep that ordering if
|
||||
// this ever changes.
|
||||
let headerHeight = 0
|
||||
|
||||
function onScroll() {
|
||||
const y = Math.max(0, window.scrollY)
|
||||
|
||||
// Always reveal near the very top, and keep it visible while the menu is
|
||||
// open (the dropdown is anchored to the header, so hiding it would slide the
|
||||
// open menu off-screen).
|
||||
if (y <= headerHeight || menuOpen.value) {
|
||||
// Keep the header visible while the menu is open (the dropdown is anchored
|
||||
// to the header, so hiding it would slide the open menu off-screen).
|
||||
if (menuOpen.value) {
|
||||
hidden.value = false
|
||||
accumulated = 0
|
||||
lastY = y
|
||||
@@ -99,11 +90,9 @@ function onScrollRaf() {
|
||||
onMounted(() => {
|
||||
// Drives #app's padding-top / RssFeeds' scroll-margin-top so content below
|
||||
// the fixed header isn't hidden behind it at scroll position 0. The header is
|
||||
// a fixed size, so this is measured once on mount and never changes — and
|
||||
// onScroll() reuses this same measurement instead of re-reading it.
|
||||
// a fixed size, so this is measured once on mount and never changes.
|
||||
const h = headerRef.value?.getBoundingClientRect().height ?? 0
|
||||
document.documentElement.style.setProperty('--app-nav-height', `${h}px`)
|
||||
headerHeight = h
|
||||
|
||||
lastY = Math.max(0, window.scrollY)
|
||||
window.addEventListener('scroll', onScrollRaf, { passive: true })
|
||||
|
||||
@@ -330,10 +330,9 @@ describe('AppNav', () => {
|
||||
Object.defineProperty(window, 'scrollY', { value: 0, configurable: true, writable: true })
|
||||
vi.stubGlobal('requestAnimationFrame', (cb) => { cb(); return 0 })
|
||||
// getBoundingClientRect() is all-zero in jsdom; give the header a real
|
||||
// height so onMounted()'s one-time measurement (which onScroll() then
|
||||
// reuses instead of re-reading offsetHeight — see AppNav.vue) has
|
||||
// something to compare against for the "near the top" guard. This is a
|
||||
// prototype-wide spy (scoped to this describe block only, restored in
|
||||
// height so onMounted()'s one-time measurement (used for the
|
||||
// --app-nav-height CSS var — see AppNav.vue) has a realistic value.
|
||||
// This is a prototype-wide spy (scoped to this describe block only, restored in
|
||||
// afterEach), so it returns a full DOMRect-shaped object rather than a
|
||||
// partial one — no test here happens to call getBoundingClientRect() on
|
||||
// anything but the header, but a bare `{ height: 50 }` would silently
|
||||
@@ -373,19 +372,38 @@ describe('AppNav', () => {
|
||||
expect(wrapper.find('header').classes()).not.toContain('app-nav--hidden')
|
||||
})
|
||||
|
||||
it('always shows the header near the top of the page', async () => {
|
||||
it('reveals the header on a large upward jump that lands near the top', async () => {
|
||||
const wrapper = mountNav()
|
||||
scrollTo(400)
|
||||
await nextTick()
|
||||
expect(wrapper.find('header').classes()).toContain('app-nav--hidden')
|
||||
|
||||
// Back within the header's own height of the top → always revealed.
|
||||
// A jump this large (e.g. article paging's scrollTo(0, 0)) crosses
|
||||
// REVEAL_THRESHOLD on its own — no top-of-page special case needed.
|
||||
scrollTo(10)
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('header').classes()).not.toContain('app-nav--hidden')
|
||||
})
|
||||
|
||||
it('does not reveal the header merely for landing near the top', async () => {
|
||||
const wrapper = mountNav()
|
||||
// Hide it with a jump just past the header's own height (50px, mocked
|
||||
// above) plus the reveal threshold.
|
||||
scrollTo(62)
|
||||
await nextTick()
|
||||
expect(wrapper.find('header').classes()).toContain('app-nav--hidden')
|
||||
|
||||
// Move back to right at the header's own height — within the "near the
|
||||
// top" zone the old code always revealed for — but the upward delta
|
||||
// (12px) doesn't exceed REVEAL_THRESHOLD, so a hidden header stays
|
||||
// hidden: proximity to the top is no longer sufficient on its own.
|
||||
scrollTo(50)
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('header').classes()).toContain('app-nav--hidden')
|
||||
})
|
||||
|
||||
it('does not let a programmatic upward jump reveal the header mid-read', async () => {
|
||||
const { markProgrammaticScroll } = useFeeds()
|
||||
const nowSpy = vi.spyOn(performance, 'now').mockReturnValue(1000)
|
||||
|
||||
Reference in New Issue
Block a user