import React, { useEffect, useRef, useState } from 'react';
/**
* Interface for dropdown menu items
*/
export interface DropdownMenuItem {
/**
* Unique identifier for the menu item.
* Used as the React key and for accessibility purposes.
*/
id: string;
/**
* Display text for the menu item.
* Should be descriptive and action-oriented (e.g., "Edit Message", "Delete").
*/
label: string;
/**
* Optional icon to display before the label.
* Can be an emoji, Unicode character, or React component.
* Will be rendered with gray color styling.
*/
icon?: string;
/**
* Callback function triggered when the menu item is clicked.
* The dropdown will automatically close after this function is called.
*
* @remarks
* This function should handle the specific action for the menu item.
* Any async operations should be handled within this callback.
*/
onClick: () => void;
}
/**
* Props for the DropdownMenu component
*/
export interface DropdownMenuProps {
/**
* React node that will trigger the dropdown when clicked.
* Can be any clickable element like buttons, icons, text, or custom components.
* Will automatically receive click and keyboard event handlers.
*
* @example
* ```tsx
* // Button trigger
* trigger={}
*
* // Icon trigger
* trigger={}
*
* // Custom trigger
* trigger={
⋮
}
* ```
*/
trigger: React.ReactNode;
/**
* Array of menu items to display in the dropdown.
* Items will be rendered in the order provided.
* Each item must have a unique ID within the dropdown.
*/
items: DropdownMenuItem[];
/**
* Horizontal alignment of the dropdown relative to the trigger element.
* - `left`: Dropdown aligns to the left edge of the trigger
* - `right`: Dropdown aligns to the right edge of the trigger
*
* @default "right"
*/
align?: 'left' | 'right';
}
/**
* DropdownMenu component displays a toggleable menu with customizable items
*
* Features:
* - Custom trigger element (button, icon, etc.)
* - Configurable alignment (left or right)
* - Support for icons in menu items
* - Automatically closes when clicking outside
* - Accessible keyboard navigation
* - Escape key support
* - Focus management
*
* Behavior:
* - Clicking outside the dropdown closes it automatically
* - Clicking any menu item closes the dropdown after executing the onClick handler
* - Dropdown is positioned absolutely relative to the trigger
* - Uses z-index of 50 for the dropdown and 40 for the backdrop
* - Supports both light and dark themes
*
*
* @example
* // Basic dropdown with button trigger
* Options}
* items={[
* { id: 'edit', label: 'Edit', onClick: () => handleEdit() },
* { id: 'delete', label: 'Delete', onClick: () => handleDelete() },
* ]}
* />
*
* @example
* // Dropdown with icon trigger and icons in items
* }
* align="left"
* items={[
* {
* id: 'share',
* label: 'Share',
* icon: '🔗',
* onClick: () => handleShare()
* },
* {
* id: 'copy',
* label: 'Copy Link',
* icon: '📋',
* onClick: () => handleCopy()
* },
* {
* id: 'report',
* label: 'Report',
* icon: '⚠️',
* onClick: () => handleReport()
* },
* ]}
* />
*
* @example
* // User profile dropdown
*
*
* {user.name}
*
* }
* align="right"
* items={[
* { id: 'profile', label: 'Profile', onClick: () => navigate('/profile') },
* { id: 'settings', label: 'Settings', onClick: () => navigate('/settings') },
* { id: 'logout', label: 'Sign Out', onClick: () => handleLogout() },
* ]}
* />
*/
export const DropdownMenu = ({ trigger, items, align = 'right' }: DropdownMenuProps) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
const handleEscapeKey = (event: KeyboardEvent) => {
if (event.key === 'Escape' && isOpen) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleEscapeKey);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('keydown', handleEscapeKey);
};
}, [isOpen]);
const handleItemClick = (item: DropdownMenuItem) => {
item.onClick();
setIsOpen(false);
};
return (