Mathias Riis Sorensen
Mathias Riis Sorensen

Reputation: 880

Headless UI - Popover - Menu item state

I'm trying to implement Popover from https://headlessui.dev/react/popover, but I cannot quite figure out how I can get the menu to close when clicking the menu items.

I'm using NextJS and Link, and that is where I see the behavior. When I am only using < a > tags, then it reloads the page and the menu is closed on reload, but I would like to leverage Link from NextJS.

This is my header file simplified without class names:

import React, { Fragment, useState } from "react";
import { Popover, Transition } from "@headlessui/react";
import Link from "next/link";

export default function Header() {
  const [navbar, setNavbar] = useState(false);

  const changeNavbar = () => {
    if (window.scrollY >= 80) {
      setNavbar(true);
    } else {
      setNavbar(false);
    }
  };

  React.useEffect(() => {
    window.addEventListener("scroll", changeNavbar);
  }, []);

  return (
    <Wrapper>
      <div>
        <Popover>
          {({ open }) => (
            <>
              <div>
                <div>
                  <div>
                    <span>Name</span>
                    <Link href="/">
                      <img
                        src="/images/logos/logo_blue.png"
                        alt=""
                      />
                    </Link>
                  </div>
                  <div>
                    <Popover.Button>
                      <span>Open menu</span>
                      <MenuIcon aria-hidden="true" />
                    </Popover.Button>
                  </div>
                  <Popover.Group as="nav">
                    {main.map((item) => (
                      <Link href={item.href} key={item.name}>
                        <a key={item.name}>
                          {item.name}
                        </a>
                      </Link>
                    ))}

                    <Popover>
                      {({ open }) => (
                        <>
                          <Popover.Button>
                            <span>More</span>
                            <ChevronDownIcon
                              aria-hidden="true"
                            />
                          </Popover.Button>

                          <Transition
                            show={open}
                            as={Fragment}
                            enter="transition ease-out duration-200"
                            enterFrom="opacity-0 translate-y-1"
                            enterTo="opacity-100 translate-y-0"
                            leave="transition ease-in duration-150"
                            leaveFrom="opacity-100 translate-y-0"
                            leaveTo="opacity-0 translate-y-1"
                          >
                            <Popover.Panel
                              static>
                              <div>
                                <div>
                                  {resources.map((item) => (
                                    <a
                                      key={item.name}
                                      href={item.href}
                                      >
                                      <item.icon
                                        aria-hidden="true"
                                      />
                                      <div>
                                        <p>
                                          {item.name}
                                        </p>
                                        <p>
                                          {item.description}
                                        </p>
                                      </div>
                                    </a>
                                  ))}
                                </div>
                               </div>
                            </Popover.Panel>
                          </Transition>
                        </>
                      )}
                    </Popover>
                  </Popover.Group>
                  <div>
                    <a
                      href="/login">
                      Sign in
                    </a>
                    <a
                      href="/login"
                    >
                      Sign up
                    </a>
                  </div>
                </div>
              </div>

              <Transition
                show={open}
                as={Fragment}
                enter="duration-200 ease-out"
                enterFrom="opacity-0 scale-95"
                enterTo="opacity-100 scale-100"
                leave="duration-100 ease-in"
                leaveFrom="opacity-100 scale-100"
                leaveTo="opacity-0 scale-95"
              >
                <Popover.Panel
                  focus
                  static
                 >
                  <div>
                    <div>
                      <div>
                        <Link href="/">
                          <img
                            src="/images/logos/logo_blue.png"
                            alt="Workflow"
                          />
                        </Link>
                        <div className="-mr-2">
                          <Popover.Button>
                            <span >Close menu</span>
                            <XIcon aria-hidden="true" />
                          </Popover.Button>
                        </div>
                      </div>
                      <div>
                        <nav>
                          {main.map((item) => (
                            <a
                              key={item.name}
                              href={item.href}
                            >
                              <item.icon
                                aria-hidden="true"
                              />
                              <span>
                                {item.name}
                              </span>
                            </a>
                          ))}
                        </nav>
                      </div>
                    </div>
                    <div>
                      <div>
                        {resources.map((item) => (
                          <a
                            key={item.name}
                            href={item.href}
                          >
                            {item.name}
                          </a>
                        ))}
                      </div>
                      <div>
                        <a
                          href="/login"
                        >
                          Sign up
                        </a>

                        <p>
                          Existing customer?{" "}
                          <a href="/login">
                            Sign in
                          </a>
                        </p>
                      </div>
                    </div>
                  </div>
                </Popover.Panel>
              </Transition>
            </>
          )}
        </Popover>
      </div>
    </Wrapper>
  );
}

Upvotes: 4

Views: 14651

Answers (4)

fsalc004
fsalc004

Reputation: 23

According to the Headless UI documentation, "The Next.js Link component does not forward unknown props to the underlying a element, so it won't close the menu on click when used inside a Menu.Item."

You can use the useRouter hook on the Menu.Item component to avoid using the Link component.

import { Menu } from "@headlessui/react";
import { useRouter } from "next/router";

function MyDropdown() {
  const router = useRouter();

  return (
    <Menu>
      <Menu.Button>
        <button>More</button>
      </Menu.Button>
      <Menu.Items>
        <Menu.Item
          as="div"
          onClick={() => {
            router.push({
              pathname: href,
            });
          }}
        >
          <a>Account Settings</a>
        </Menu.Item>
      </Menu.Items>
    </Menu>
  );
}


Upvotes: 0

kiru
kiru

Reputation: 129

For anyone looking for an answer, Headless UI wrote a workaround for this problem:

https://headlessui.dev/react/menu#integrating-with-next-js

The solution is to use a custom Link wrapper:

const MyLink = forwardRef((props, ref) => {
  let { href, children, ...rest }: any = props;
  return (
    <Link href={href}>
      <a ref={ref as any} {...rest}>
        {children}
      </a>
    </Link>
  )
})

Replace the Menu with the above link:

{menuItems.map((each, ix) =>
  <Menu.Item key={ix}>
    {({ active }) => (
      <MyLink href={each.link}>
        {each.name}
      </MyLink>

    )}
  </Menu.Item>
)}

Upvotes: 3

Michael Brenndoerfer
Michael Brenndoerfer

Reputation: 4076

Wrap your button/link with the internal close state variable and then trigger the onClick close()

<Popover.Panel static focus>
  {({ close }) => (
    <span onClick={() => close()}>This button will close the panel</span>
  )}
</Popover.Panel>

Upvotes: 2

Loren K.
Loren K.

Reputation: 76

I am using TailwindCSS and HeadlessUI on a ReactJS project for my school and I was wondering the same thing.

My solution was to wrap the popover panel items in a Popover.Button and invoke a function that set the Popover's own 'open' to false and as a result the menu will close.

I'm taking only 1 example from your code:

{main.map((item) => (

    <Popover.Button onClick={() => (open = false)}>

       <Link href={item.href} key={item.name}>
         <a key={item.name}>
           {item.name}
         </a>
       </Link>

     <Popover.Button>

I hope this helps solve your case as well as it did for me :)

Upvotes: 6

Related Questions