diff --git a/src/app/Nav.tsx b/src/app/Nav.tsx index 5e71384e..538242bf 100644 --- a/src/app/Nav.tsx +++ b/src/app/Nav.tsx @@ -10,6 +10,7 @@ import { isPathAdmin, isPathFeed, isPathGrid, + isPathTopLevel, isPathProtected, isPathSignIn, } from '@/app/paths'; @@ -19,6 +20,8 @@ import { HAS_DEFINED_SITE_DESCRIPTION, SITE_DESCRIPTION, } from './config'; +import { useRef } from 'react'; +import useStickyNav from './useStickyNav'; const NAV_HEIGHT_CLASS = HAS_DEFINED_SITE_DESCRIPTION ? 'min-h-[4rem] sm:min-h-[5rem]' @@ -29,9 +32,17 @@ export default function Nav({ }: { siteDomainOrTitle: string; }) { - const pathname = usePathname(); + const ref = useRef(null); + const pathname = usePathname(); const showNav = !isPathSignIn(pathname); + const isHome = isPathTopLevel(pathname); + + const { + isNavSticky, + shouldHideStickyNav, + shouldAnimateStickyNav, + } = useStickyNav(ref, isHome); const renderLink = ( text: string, @@ -55,6 +66,9 @@ export default function Nav({ return ( export const checkPathPrefix = (pathname = '', prefix: string) => pathname.toLowerCase().startsWith(prefix); +export const isPathRoot = (pathname?: string) => + pathname === PATH_ROOT; + export const isPathGrid = (pathname?: string) => checkPathPrefix(pathname, PATH_GRID); export const isPathFeed = (pathname?: string) => checkPathPrefix(pathname, PATH_FEED); +export const isPathTopLevel = (pathname?: string) => + isPathRoot(pathname)|| + isPathGrid(pathname) || + isPathFeed(pathname); + export const isPathSignIn = (pathname?: string) => checkPathPrefix(pathname, PATH_SIGN_IN); diff --git a/src/app/useStickyNav.ts b/src/app/useStickyNav.ts new file mode 100644 index 00000000..3d1531c6 --- /dev/null +++ b/src/app/useStickyNav.ts @@ -0,0 +1,33 @@ +import useScrollDirection from '@/utility/useScrollDirection'; +import { RefObject } from 'react'; + +export default function useStickyNav( + ref: RefObject, + isEnabled = true, +) { + const { scrollDirection, lastScrollY } = useScrollDirection(); + + const navHeight = ref.current?.clientHeight ?? 0; + + const hasScrolledPastNav = lastScrollY > navHeight; + + const isNavSticky = isEnabled && ( + hasScrolledPastNav || + scrollDirection === 'up' + ); + + const shouldHideStickyNav = + isNavSticky && + scrollDirection === 'down'; + + const shouldAnimateStickyNav = + isNavSticky && + lastScrollY > navHeight * 2 || + scrollDirection === 'up'; + + return { + isNavSticky, + shouldHideStickyNav, + shouldAnimateStickyNav, + }; +}; diff --git a/src/utility/useScrollDirection.ts b/src/utility/useScrollDirection.ts new file mode 100644 index 00000000..46ce7fa8 --- /dev/null +++ b/src/utility/useScrollDirection.ts @@ -0,0 +1,21 @@ +import { useEffect, useState } from 'react'; + +export default function useScrollDirection() { + const [lastScrollY, setLastScrollY] = useState(0); + const [scrollDirection, setScrollDirection] = useState<'up' | 'down'>('up'); + + useEffect(() => { + const handleScroll = () => { + setScrollDirection(window.scrollY > lastScrollY ? 'down' : 'up'); + setLastScrollY(window.scrollY); + }; + + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, [lastScrollY]); + + return { + scrollDirection, + lastScrollY, + }; +}