# Menu Layer A global menu management system that allows you to open contextual menus from anywhere in your application, with support for nested submenus. ## Features - ✅ Stack-based system – Menus can open nested submenus (infinite depth) - ✅ Session-based – Prevents conflicts between multiple open/close calls - ✅ Single API – `openMenu({ anchorEl, items, menuProps })` from any component - ✅ Smooth animations – Built-in open/close transitions - ✅ Uses design-system – Renders with design-system Menu, MenuItem, and ListItem components - ✅ Optional `closeImmediate` – Close without animation when navigating or unmounting the anchor ## File Structure ``` src/components/layers/Menus/ ├── index.tsx # MenuLayerProvider and useMenuLayer ├── types.ts # MenuItemSchema, OpenMenuArgs, MenuLayerContextValue ├── stories.tsx # Storybook stories └── README.md # This file ``` ## Setup Wrap your application with the `MenuLayerProvider`: ```tsx import { MenuLayerProvider } from '@material-hu/components/layers/Menus'; function App() { return ( ); } ``` ## Usage Use the `useMenuLayer` hook to open a menu from any component. Pass the anchor element (usually the button that was clicked) and an array of items. ```tsx import { useMenuLayer } from '@material-hu/components/layers/Menus'; function MyComponent() { const { openMenu, closeMenu } = useMenuLayer(); const handleOpenOptions = (e: React.MouseEvent) => { openMenu({ anchorEl: e.currentTarget, items: [ { id: 'edit', title: 'Edit', icon: IconPencil, onSelect: () => handleEdit(), }, { id: 'delete', title: 'Delete', icon: IconTrash, onSelect: () => handleDelete(), }, ], menuProps: { position: 'left', sx: { minWidth: '200px' }, }, }); }; return ; } ``` ### openMenu args | Prop | Type | Description | | ---------- | --------------------------- | ------------------------------------------------ | | `anchorEl` | `HTMLElement` | **Required.** Element the menu is anchored to. | | `items` | `MenuItemSchema[]` | **Required.** Menu items (and optional submenus).| | `menuProps`| `Omit` | Optional. Props for the Menu wrapper (position, sx, etc.). | ### MenuItemSchema | Prop | Type | Description | | --------------- | --------------- | --------------------------------------------------------------------------- | | `id` | `string` | **Required.** Unique identifier for the item. | | `title` | `string` | **Required.** Main text displayed for the item. | | `description` | `string` | Optional. Secondary text below the title. | | `icon` | `TablerIcon` | Optional. Icon displayed to the left of the title. | | `disabled` | `boolean` | Optional. If true, the item is not clickable. | | `onSelect` | `() => void` | Optional. Callback when the item is clicked (no submenu). | | `items` | `MenuItemSchema[]` | Optional. Nested items; renders a submenu with a chevron. | | `closeImmediate`| `boolean` | Optional. If true, closes the menu immediately without animation. Use when `onSelect` navigates or unmounts the anchor. | ### useMenuLayer return value | Method | Type | Description | | ----------- | --------------------------- | ----------------------------------------------------- | | `openMenu` | `(args: OpenMenuArgs) => void` | Opens a menu at the given anchor with the given items. | | `closeMenu` | `(immediate?: boolean) => void` | Closes the current menu. Pass `true` to skip animation. | ## Examples ### Simple menu ```tsx openMenu({ anchorEl: e.currentTarget, items: [ { id: 'copy', title: 'Copy', onSelect: () => copyToClipboard() }, { id: 'paste', title: 'Paste', onSelect: () => paste() }, ], }); ``` ### Menu with icons and descriptions ```tsx openMenu({ anchorEl: e.currentTarget, items: [ { id: 'edit', title: 'Edit', description: 'Edit this item', icon: IconPencil, onSelect: () => onEdit(), }, { id: 'delete', title: 'Delete', description: 'Remove permanently', icon: IconTrash, onSelect: () => onDelete(), }, ], menuProps: { position: 'left', sx: { minWidth: 240 } }, }); ``` ### Nested submenus ```tsx openMenu({ anchorEl: e.currentTarget, items: [ { id: 'edit', title: 'Edit', onSelect: () => onEdit() }, { id: 'more', title: 'More options', icon: IconDotsVertical, items: [ { id: 'archive', title: 'Archive', onSelect: () => onArchive() }, { id: 'move', title: 'Move to...', items: [ { id: 'folder1', title: 'Folder 1', onSelect: () => moveTo('folder1') }, { id: 'folder2', title: 'Folder 2', onSelect: () => moveTo('folder2') }, ], }, ], }, ], }); ``` ### Item that navigates (close immediately) When an item triggers navigation or unmounts the anchor, use `closeImmediate: true` so the menu closes without animation and avoids stale state: ```tsx { id: 'view-details', title: 'View details', closeImmediate: true, onSelect: () => navigate(`/items/${id}`), } ``` ### Disabled item ```tsx { id: 'paste', title: 'Paste', icon: IconClipboard, disabled: true, } ``` ## Behavior Notes - **Nested submenus**: Items with an `items` array open a submenu; a chevron is shown and clicking opens the nested list. - **Session management**: Each menu session has a unique `sessionId`. Only the current session can close the menu, which avoids conflicts during transitions. - **Close behavior**: Only the top-level menu (backdrop) handles close; closing runs the exit animation then clears the stack. - **closeMenu(immediate)**: Call `closeMenu(true)` to close without animation (e.g. before navigation). ## TypeScript Support Types are exported from the layer: ```tsx import type { MenuItemSchema, OpenMenuArgs, MenuLevel, MenuLayerContextValue, } from '@material-hu/components/layers/Menus/types'; ``` ### Available exports ```tsx import { MenuLayerProvider, useMenuLayer, } from '@material-hu/components/layers/Menus'; import type { MenuItemSchema, OpenMenuArgs } from '@material-hu/components/layers/Menus/types'; ```