0

I have a header with an off-canvas menu that comes in from the right. It works as expected, except for one issue: When the menu is visible and open, the mouse/touch events go right through it below to the content underneath.

Here is the code:

import { forwardRef, useCallback, useEffect, useRef, useState } from "react"
import Link from "next/link"

type ScrollDir = 'UP' | 'DOWN'

const LandingHeader = forwardRef((_, ref: React.Ref<HTMLDivElement>) => {
  const [activeLink, setActiveLink] = useState('')
  const sectionsRef = useRef([])
  const { clientHeight } = (ref as React.MutableRefObject<HTMLDivElement>)?.current ?? { clientHeight: 0 }
  const [navShowing, setNavShowing] = useState<boolean>(true)
  const lastScrollPos = useRef<number>(0)
  const [uBound, setUBound] = useState<number>(0)
  const [lBound, setLBound] = useState<number>(0)
  const [mobileMenuOpen, setMobileMenuOpen] = useState<boolean>(false)

  const handleMobileMenu = (shouldOpen?: boolean) => {
    setMobileMenuOpen(prev => shouldOpen ?? !prev)
  }

  const trackScrollPos = useCallback(() => {
    const { pageYOffset, scrollY } = window
    const { clientHeight: headerHeight } = (ref as React.MutableRefObject<HTMLDivElement>)?.current ?? { clientHeight: 0 }

    // Set active classes for nav links

    sectionsRef.current.forEach(section => {
      const sectionId = (section as HTMLElement).getAttribute('id')
      const sectionOffsetTop = (section as HTMLElement).offsetTop - clientHeight
      const sectionHeight = (section as HTMLElement).offsetHeight

      if (
        scrollY >= sectionOffsetTop &&
        scrollY < sectionOffsetTop + sectionHeight
      ) {
        setActiveLink(`/#${sectionId || ''}`);
      }
    });

    // Base case (if pageYOffset is near top, show nav)
    if ((pageYOffset <= headerHeight ?? 0) && !!!navShowing) {
      //setNavShowing(true)
      setUBound(pageYOffset)
      setLBound(pageYOffset)
    }

    // Set Direction of scroll
    const newDir: ScrollDir = pageYOffset > lastScrollPos.current ? 'DOWN' : 'UP'

    // Check direction states
    switch(newDir) {
      case "DOWN":
        setLBound(pageYOffset)
        if (!mobileMenuOpen && pageYOffset >= (uBound + headerHeight) && !!navShowing) {
          //setNavShowing(false)
        }
        break;
        
        case "UP":
          setUBound(pageYOffset)
          if (pageYOffset <= (lBound - headerHeight) && !!!navShowing) {
            //setNavShowing(true)
          }
          break;
        }

    // Always set lastScrollPos to current pageYOffset -- TICK
    lastScrollPos.current = pageYOffset
  }, [lBound, uBound, navShowing, mobileMenuOpen])

  const headerLinks: { name: string, href: string }[] = [
    {
      name: "Mission",
      href: "/#mission"
    },
    {
      name: "Support",
      href: "/#support"
    },
    {
      name: "Causes",
      href: "/#causes"
    },
    {
      name: "Platform",
      href: "/#how-it-works"
    }
  ]

  useEffect(() => {
    document.documentElement.style.setProperty('scroll-padding-top', `${clientHeight - 1}px`)
  }, [clientHeight])

  useEffect(() => {
    // Cache all sections
    sectionsRef.current = Array.from(document.querySelectorAll('div[id]'))

    // Header scroll tracker for hide/show
    window.addEventListener('scroll', trackScrollPos)

    // Cleanup function
    return () => window.removeEventListener('scroll', trackScrollPos)
  }, [trackScrollPos, activeLink])

  return (
    <div ref={ref} style={{transform: `translateY(${navShowing ? "0" : "-100%"})` }} className="z-50 py-6 px-[calc(min(10vw,1rem))] bg-[rgba(255,255,255,.85)] backdrop-blur-md sticky top-0 border-b-2 shadow-sm translate-all duration-300 overflow-x-clip">
      <nav>
        <div className="flex flex-1 justify-between items-center max-w-7xl m-auto gap-6">
          <div className="max-w-[200px] flex-shrink-0 mr-5">
            <Link href="/"><img className="w-full h-auto hidden md:block object-contain" src="/images/logos/solution_logo_black.svg" alt="logo" /></Link>
            <Link href="/"><img className="w-[30px] h-auto block md:hidden object-contain" src="/images/logos/solution-circle.svg" alt="logo" /></Link>
          </div>
          {/* Full-size menu */}
          <div className="hidden md:flex flex-1 ml-auto justify-around items-center max-w-2xl text-black font-normal whitespace-nowrap">
            {
              headerLinks.map((link) => (
                <Link className={`${link.href == activeLink ? "!bg-neutralBlue" : ""} group relative font-bold hover:bg-neutralBlue transition-all duration-500 ease-in-out rounded-full px-3 py-3`} passHref href={link.href} key={link.name}>
                  {link.name}
                  <span className="absolute bottom-3 left-[14px] right-0 transform w-0 h-[1px] bg-black transition-all duration-500 ease-in-out group-hover:w-8/12"></span>
                </Link>))
            }
            <Link href="/#get-involved">
              <button className={`${activeLink == "/#get-involved" ? "bg-transparent !text-black" : ""} btn btn-outline rounded-3xl text-white active:text-black  hover:text-black bg-black active:bg-transparent hover:bg-transparent font-normal`}>Get Involved</button>
            </Link>
          </div>
          {/* Mobile menu */}
          <div className="md:hidden flex justify-end gap-5 items-center flex-grow">
            <Link href="#get-involved">
              <button className={`px-[calc(max(1rem,2rem))] whitespace-nowrap max-w-[40vw] text-[calc(max(12px,2vmin))]  ${activeLink == "/#get-involved" ? "bg-transparent !text-black" : ""} btn btn-outline rounded-3xl text-white active:text-black  hover:text-black bg-black active:bg-transparent hover:bg-transparent font-normal white`}>Get Involved</button>
            </Link>
            <button className="flex-shrink-0" onClick={() => handleMobileMenu()}>
              <img src="/images/icons/hamburger.svg" alt="hamburger menu" />
            </button>
          </div>
        </div>
      </nav>
      <div className={`absolute ${ mobileMenuOpen ? "translate-x-0" : "translate-x-full" } md:hidden top-0 bottom-0 left-0 right-0 flex flex-col justify-stretch items-stretch bg-[rgba(255,255,255,0.75)] backdrop-blur-xl w-full h-screen transition-all duration-300 ease-in-out z-[9999]`}>
        <div className="flex flex-col items-stretch h-screen max-w-md ml-auto bg-white px-5 py-6 z-[9999]">
          <div className="flex justify-between items-center mb-10 w-full">
            <img className="object-contain w-full h-auto max-w-[50%]" src="/images/logos/solution_logo_black.svg" alt="solution logo" />
            <button className="btn btn-outline btn-sm btn-circle text-sm font-bold aspect-square" onClick={() => handleMobileMenu(false)}>
              <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" /></svg>
            </button>
          </div>
          <div className="flex flex-col gap-10 text-3xl font-bold [&_a:hover]:opacity-50">
            {
              headerLinks.map(link => <Link className="w-full h-full" onClick={() => handleMobileMenu(false)} href={link.href} key={link.name}>{link.name}</Link>)
            }
          </div>
          <div className="">Footer bottom</div>
        </div>
      </div>
    </div>
  )
})

export default LandingHeader

LandingHeader.displayName = "LandingHeader"

It's been years since I've hand-coded an off-canvas menu, so I'm sure I'm missing something.

Joel Hager
  • 2,990
  • 3
  • 15
  • 44

1 Answers1

0

You can try the following steps:

  1. Add the CSS property pointer-events: none; to the off-canvas menu container when closed. This prevents mouse/touch events from being registered on the menu, allowing them to pass through to the underlying content.

  2. When the off-canvas menu is open, set pointer-events: auto; to enable interaction with the menu.

  3. Attach an event listener to the content area below the off-canvas menu. This listener will close the menu when the user interacts with the content. You can do this by detecting clicks or touches on the content area and triggering the menu close action.

By implementing these changes, you should be able to prevent mouse/touch events from passing through the off-canvas menu and interacting with the content below it.

Emre Turan
  • 108
  • 3