Skip to content
69 changes: 61 additions & 8 deletions nuxt-app/components/Header.vue
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,10 @@
tabindex="-1"
>
<div
class="flex min-h-screen flex-col space-y-16 px-6 pb-8 pt-28 lg:pl-8 lg:pr-24 lg:pt-36 xl:pr-32 2xl:pr-48"
class="flex min-h-screen flex-col space-y-8 px-6 pb-8 pt-28 lg:pl-8 lg:pr-24 lg:pt-32 xl:pr-32 2xl:pr-48"
>
<!-- Main menu -->
<ul class="flex flex-col space-y-4 lg:flex-grow lg:items-end lg:space-y-0">
<ul class="flex flex-col space-y-4 lg:items-end lg:space-y-0">
Comment on lines 84 to +88

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description and title have been updated to also cover the tightened menu spacing, the desktop legal/social footer alignment, and the background scroll-lock changes.


Generated by Claude Code

<li
v-for="(mainMenuItem, index) in mainMenuItems"
:key="mainMenuItem.href"
Expand All @@ -108,9 +108,7 @@
</li>
</ul>

<div
class="flex flex-grow flex-col justify-between space-y-16 lg:flex-grow-0 lg:flex-row-reverse lg:items-end lg:space-y-0"
>
<div class="flex flex-col space-y-8 lg:flex-row-reverse lg:items-end lg:justify-between lg:space-y-0">
<!-- Social networks -->
<SocialNetworks class="h-8 self-end" />

Expand Down Expand Up @@ -139,8 +137,8 @@ import SearchSVG from '~/assets/icons/search.svg'
import BrandIcon from '~/assets/images/brand-icon.svg'
import BrandLogo from '~/assets/images/brand-logo.svg'
import PrimaryPbButton from '~/components/PrimaryPbButton.vue'
import { nextTick, onMounted, ref, watch } from 'vue'
import { useDocument, useEventListener } from '../composables'
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useBodyElement, useDocument, useEventListener } from '../composables'
import { CLOSE_MENU_EVENT_ID, CLOSE_SEARCH_EVENT_ID, OPEN_MENU_EVENT_ID, OPEN_SEARCH_EVENT_ID } from '../config'
import { trackGoal } from '../helpers'
import SocialNetworks from './SocialNetworks.vue'
Expand All @@ -149,7 +147,6 @@ const FLAG_SHOW_LOGIN = useRuntimeConfig().public.FLAG_SHOW_LOGIN
const FLAG_SHOW_NEWS = useRuntimeConfig().public.FLAG_SHOW_NEWS

const mainMenuItems = [
{ label: 'Home', href: '/' },
{ label: 'Podcast', href: '/podcast' },
...(FLAG_SHOW_NEWS ? [{ label: 'News', href: '/news' }] : []),
{ label: 'Meetup', href: '/meetup' },
Expand Down Expand Up @@ -180,6 +177,21 @@ const searchPlaceholder = ref('')
const searchInputElement = ref<HTMLInputElement>()
const menuElement = ref<HTMLElement>()

// Create body element reference for locking background scroll
const bodyElement = useBodyElement()

// Remember the body's inline overflow value so it can be restored when the
// scroll lock is released, instead of clobbering a pre-existing value
const previousBodyOverflow = ref('')

// Remember the body's inline padding-right for the same reason, since it is
// adjusted to compensate for the width of the hidden scrollbar
const previousBodyPaddingRight = ref('')

// Track whether the scroll lock is actually applied, set synchronously with
// the overflow change so it stays consistent regardless of watcher timing
const scrollLocked = ref(false)

// Track analytic menu events
watch(menuIsOpen, () => {
if (menuIsOpen.value) {
Expand All @@ -189,6 +201,47 @@ watch(menuIsOpen, () => {
}
})

// Lock background scroll while the menu is open so the underlying
// page can't move behind the overlay, and release it when it closes
watch(menuIsOpen, () => {
if (bodyElement.value) {
if (menuIsOpen.value) {
// Measure the scrollbar width before hiding overflow (afterwards
// the scrollbar is gone and the measurement would be 0)
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth
// Save the current inline overflow and padding-right before locking
// so values set elsewhere aren't lost, then lock the scroll
previousBodyOverflow.value = bodyElement.value.style.overflow
previousBodyPaddingRight.value = bodyElement.value.style.paddingRight
bodyElement.value.style.overflow = 'hidden'
// Compensate the hidden scrollbar with padding-right so the fixed
// header doesn't shift when the scrollbar disappears
if (scrollbarWidth > 0) {
const currentPaddingRight = parseFloat(getComputedStyle(bodyElement.value).paddingRight) || 0
bodyElement.value.style.paddingRight = `${currentPaddingRight + scrollbarWidth}px`
}
scrollLocked.value = true
} else if (scrollLocked.value) {
// Restore the previously saved overflow and padding-right on close
bodyElement.value.style.overflow = previousBodyOverflow.value
bodyElement.value.style.paddingRight = previousBodyPaddingRight.value
scrollLocked.value = false
}
}
})
Comment on lines +206 to +231

// Release the lock if the component is unmounted while it still holds it
// (e.g. a route change unmounts before the close watcher has run), so the
// page can't stay permanently locked. Drive this off the actual lock state,
// not menuIsOpen, since the watcher is async and may not have run yet
onBeforeUnmount(() => {
if (bodyElement.value && scrollLocked.value) {
bodyElement.value.style.overflow = previousBodyOverflow.value
bodyElement.value.style.paddingRight = previousBodyPaddingRight.value
scrollLocked.value = false
}
})
Comment on lines 180 to +243

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. The onBeforeUnmount restore is now guarded with menuIsOpen.value, so it only runs when the scroll lock is actually held and won't touch a pre-existing overflow we never captured.


Generated by Claude Code

Comment on lines +237 to +243

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: the lock is now tracked with a dedicated scrollLocked flag that's set synchronously in the same callback that mutates overflow, and both the close-restore and the onBeforeUnmount restore are driven off that flag instead of menuIsOpen. So unmount always releases the lock regardless of the async watcher timing.


Generated by Claude Code


// Track analytic search events
watch(searchIsOpen, () => {
if (searchIsOpen.value) {
Expand Down
Loading