Menu Compound Component

Overview

The Menu compound component provides a flexible navigation system with support for nested menus, grouping, and active item highlighting. It uses React Context to share state between the menu hierarchy.

Example

Code Implementation

// Menu.tsx
import React, { createContext, useContext, useState, ReactNode } from 'react';

// Create context for the menu
type MenuContextType = {
  activeItem: string | null;
  expandedSubMenus: Record<string, boolean>;
  setActiveItem: (id: string | null) => void;
  toggleSubMenu: (id: string) => void;
};

const MenuContext = createContext<MenuContextType | undefined>(undefined);

// Hook to use menu context
const useMenu = () => {
  const context = useContext(MenuContext);
  if (!context) {
    throw new Error('Menu components must be used within a Menu');
  }
  return context;
};

// Main Menu component
const Menu = ({ children, defaultActiveItem = null, className = '' }) => {
  const [activeItem, setActiveItem] = useState(defaultActiveItem);
  const [expandedSubMenus, setExpandedSubMenus] = useState({});

  const toggleSubMenu = (id) => {
    setExpandedSubMenus(prev => ({ ...prev, [id]: !prev[id] }));
  };

  return (
    <MenuContext.Provider value={{ activeItem, expandedSubMenus, setActiveItem, toggleSubMenu }}>
      <nav className={`${className}`}>
        <ul className="space-y-1">
          {children}
        </ul>
      </nav>
    </MenuContext.Provider>
  );
};

// Group component for organizing menu items
const Group = ({ children, title, className = '' }) => {
  return (
    <li className={className}>
      {title && (
        <div className="px-3 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wider">
          {title}
        </div>
      )}
      <ul className="space-y-1">
        {children}
      </ul>
    </li>
  );
};

// Item component
const Item = ({ children, id, icon, onClick, className = '' }) => {
  const { activeItem, setActiveItem } = useMenu();
  const isActive = activeItem === id;

  const handleClick = () => {
    setActiveItem(id);
    if (onClick) onClick();
  };

  return (
    <li>
      <div
        className={`flex items-center px-3 py-2 text-sm rounded-md cursor-pointer ${
          isActive 
            ? 'bg-gray-100 text-gray-900' 
            : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
        } ${className}`}
        onClick={handleClick}
      >
        {icon && <span className="mr-3">{icon}</span>}
        <span>{children}</span>
      </div>
    </li>
  );
};

// SubMenu component
const SubMenu = ({ children, id, title, icon, className = '' }) => {
  const { expandedSubMenus, toggleSubMenu } = useMenu();
  const isExpanded = !!expandedSubMenus[id];

  return (
    <li className={className}>
      <div
        className="flex items-center justify-between px-3 py-2 text-sm rounded-md cursor-pointer"
        onClick={() => toggleSubMenu(id)}
      >
        <div className="flex items-center">
          {icon && <span className="mr-3">{icon}</span>}
          <span>{title}</span>
        </div>
        <span className="ml-2">
          {isExpanded ? '▼' : '►'}
        </span>
      </div>

      {isExpanded && (
        <ul className="pl-6 mt-1 space-y-1">
          {children}
        </ul>
      )}
    </li>
  );
};

// Attach sub-components to Menu
Menu.Group = Group;
Menu.Item = Item;
Menu.SubMenu = SubMenu;

export default Menu;

Key Features

  • Active item tracking with visual feedback
  • Expandable/collapsible submenus
  • Logical grouping of related menu items
  • Support for icons and custom content in menu items
  • Accessible navigation structure with proper semantics
  • Shared state through React Context for coordinated menu behavior