Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 75 additions & 9 deletions components/layout/navigation/Navbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,29 +26,95 @@
)

const isOnTop = ref(true)
const isNavbarVisible = ref(true)
const lastScrollY = ref(0)

function onIntersectionObserver(entries: IntersectionObserverEntry[]) {
const [entry] = entries
const SCROLL_DELTA_THRESHOLD = 12
const TOP_VISIBILITY_THRESHOLD = 8

if (entry.isIntersecting) {
isOnTop.value = true
} else {
isOnTop.value = false
let scrollRafId: number | null = null

function onIntersectionObserver(entries: IntersectionObserverEntry[], _observer: IntersectionObserver) {
const entry = entries[0]

if (!entry) {
return
}

isOnTop.value = entry.isIntersecting
}

function onWindowScroll() {
if (!isPostsPage.value || scrollRafId !== null) {
return
}

scrollRafId = window.requestAnimationFrame(() => {
const currentScrollY = window.scrollY

if (currentScrollY <= TOP_VISIBILITY_THRESHOLD) {
isNavbarVisible.value = true
lastScrollY.value = currentScrollY
scrollRafId = null

return
}

const deltaY = currentScrollY - lastScrollY.value

if (Math.abs(deltaY) >= SCROLL_DELTA_THRESHOLD) {
isNavbarVisible.value = deltaY < 0
lastScrollY.value = currentScrollY
}

scrollRafId = null
})
}

watch(
isPostsPage,
(value) => {
if (!import.meta.client) {
return
}

if (!value) {
isNavbarVisible.value = true
}

lastScrollY.value = window.scrollY
},
{ immediate: true }
)

onMounted(() => {
lastScrollY.value = window.scrollY
window.addEventListener('scroll', onWindowScroll, { passive: true })
})

onBeforeUnmount(() => {
window.removeEventListener('scroll', onWindowScroll)

if (scrollRafId !== null) {
window.cancelAnimationFrame(scrollRafId)
scrollRafId = null
}
})
</script>

<template>
<!-- Same margin as Nav height -->
<nav class="mb-14">
<div v-intersection-observer="[onIntersectionObserver]" />
<div v-intersection-observer="onIntersectionObserver" />

<div
:class="{
'fixed!': isPostsPage,
'bg-base-1000/60 shadow-lg backdrop-blur-lg backdrop-saturate-200 md:border-b-2': isPostsPage && !isOnTop
'bg-base-1000/60 shadow-lg backdrop-blur-lg backdrop-saturate-200 md:border-b-2': isPostsPage && !isOnTop,
'-translate-y-full': isPostsPage && !isOnTop && !isNavbarVisible,
'translate-y-0': !isPostsPage || isOnTop || isNavbarVisible
}"
class="border-base-0/20 absolute inset-x-0 top-0 z-10 transition duration-200"
class="border-base-0/20 absolute inset-x-0 top-0 z-10 transform-gpu transition-[transform,background-color,box-shadow,backdrop-filter] duration-200 will-change-transform"
Comment on lines +114 to +117
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Hidden navbar remains focusable off-screen (a11y issue).

At Line 114, the navbar is only translated out of view. Menu controls can still receive focus while invisible, which is confusing for keyboard/screen-reader users.

♿ Suggested fix
     <div
+      :aria-hidden="isPostsPage && !isOnTop && !isNavbarVisible"
+      :inert="isPostsPage && !isOnTop && !isNavbarVisible"
       :class="{
         'fixed!': isPostsPage,
         'bg-base-1000/60 shadow-lg backdrop-blur-lg backdrop-saturate-200 md:border-b-2': isPostsPage && !isOnTop,
         '-translate-y-full': isPostsPage && !isOnTop && !isNavbarVisible,
-        'translate-y-0': !isPostsPage || isOnTop || isNavbarVisible
+        'translate-y-0': !isPostsPage || isOnTop || isNavbarVisible,
+        'pointer-events-none': isPostsPage && !isOnTop && !isNavbarVisible
       }"
       class="border-base-0/20 absolute inset-x-0 top-0 z-10 transform-gpu transition-[transform,background-color,box-shadow,backdrop-filter] duration-200 will-change-transform"
     >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/layout/navigation/Navbar.vue` around lines 114 - 117, The navbar
is only visually translated off-screen so its interactive elements remain
focusable; update the root element in Navbar.vue (the element using the class
bindings with isPostsPage, isOnTop, isNavbarVisible) to set accessibility
attributes when hidden: bind aria-hidden and either inert (if supported) or
tabindex behavior based on the same condition that produces '-translate-y-full'
(i.e., when isPostsPage && !isOnTop && !isNavbarVisible). Concretely, add
:aria-hidden="isPostsPage && !isOnTop && !isNavbarVisible" and
:inert="isPostsPage && !isOnTop && !isNavbarVisible" (fallback: set
tabindex="-1" on the root or programmatically remove focusability of child
interactive elements) so controls aren’t reachable by keyboard/screen readers
when the navbar is translated out.

>
<!-- Navbar -->
<div
Expand Down