# 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';
```