Skip to content

A useful headless component (hook) that gives you all the functions you need to create a multi-level menu using your own components!

License

Notifications You must be signed in to change notification settings

cdes/react-headless-nested-menu

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Oct 9, 2020
640ed0e · Oct 9, 2020

History

32 Commits
Sep 18, 2020
Oct 9, 2020
Sep 30, 2020
Sep 18, 2020
Sep 15, 2020
Sep 18, 2020
Sep 15, 2020
Sep 18, 2020
Sep 18, 2020
Sep 18, 2020
Oct 9, 2020
Sep 18, 2020
Sep 21, 2020
Sep 15, 2020
Sep 30, 2020

Repository files navigation

React Headless Nested Menu Logo

React Headless Nested Menu

A useful headless component (hook) that gives you all the functions you need to create a multi-level menu using your own components!

Features

  • Only functionality, no need to fight with CSS classes and overrides to customize your menu.
  • Created in TypeScript, so you get types out of the box.
  • Fully configurable behavior (open on click or hover).

React Headless Nested Menu Logo

Installation

yarn add react-headless-nested-menu

Usage

You can import the generated bundle to use the whole library generated by this starter:

import React from "react";
import { useNestedMenu } from "react-headless-nested-menu";

function App() {
  const {
    getToggleButtonProps,
    getMenuProps,
    getItemProps,
    getOpenTriggerProps,
    getCloseTriggerProps,
    getMenuOffsetStyles,
    isOpen,
    isSubMenuOpen,
    toggleMenu
  } = useNestedMenu({
    items
  });

  const [item, setItem] = useState<MenuItem>();

  // your custom function to render items
  const renderItem = (item: MenuItem) => (
    <div
      {...getItemProps(item)}
      className="relative my-1 first:mt-0 last:mb-0"
      {...getOpenTriggerProps("onPointerEnter", item)}
      onClick={(event) => {
        event.stopPropagation();
        setItem(item);
        toggleMenu();
      }}
    >
      <div
        className={classnames(
          "flex flex-row justify-between items-center rounded-lg flex-1 h-8 flex items-center px-2",
          {
            "text-gray-600 hover:text-gray-800 hover:bg-gray-200": !isSubMenuOpen(
              item
            ),
            "text-gray-800 bg-gray-200": isSubMenuOpen(item)
          }
        )}
      >
        {item.label}
        {item.subMenu && <Chevron />}
      </div>

      {/* Only show submenu when there's a submenu & it's open */}
      {item.subMenu && isSubMenuOpen(item) && renderMenu(item.subMenu, item)}
    </div>
  );

  // your custom function to render menus (root menu & sub-menus)
  const renderMenu = (items: Items, parentItem?: MenuItem) => (
    <div
      {...getMenuProps(parentItem)}
      style={{
        position: "absolute",
        ...getMenuOffsetStyles(parentItem)
      }}
      className={classnames(
        "bg-white p-2 shadow-lg rounded-lg select-none border border-gray-100 relative z-10",
        {
          "ms-2": typeof parentItem === "undefined", //for root menu
          "-mt-3": typeof parentItem !== "undefined" //for submenus only
        }
      )}
      {...getCloseTriggerProps("onPointerLeave", parentItem)}
    >
      <div>{items.map((item) => renderItem(item))}</div>

      {/* add hit area */}
      {parentItem && (
        <div
          style={{
            position: "absolute",
            top: -8,
            bottom: -8,
            left: -8,
            right: -8,
            zIndex: -1
          }}
        ></div>
      )}
    </div>
  );

  return (
    <div className="w-64 p-4 rounded-lg flex flex-col ms-4 mt-4">
      <button
        className="text-gray-600 border-2 border-gray-600 rounded-lg h-10 focus:outline-none"
        {...getToggleButtonProps()}
      >
        {item ? item.label : "Open Menu"}
      </button>
      {isOpen && renderMenu(items)}
    </div>
  );
}

const rootElement = document.getElementById("root");
React.render(<App />, rootElement);

To do

  • Improve documentation.
  • Add more example.
  • Add tests.
  • Use popper for positioning menus.

Examples

Credits