Menu Components
The Menu Components provide dropdown menu functionality for the WorkPayCore Frontend application. These components handle various menu implementations from simple action menus to complex navigation structures with consistent styling and behavior.
Overview
This document covers menu-related components and patterns that provide dropdown menu functionality with WorkPay's branded styling and behavior patterns.
Components Overview
Core Menu Components
- CustomMenu - Flexible menu component with extensive customization options
Menu Patterns
- Action Menus - Common action menu patterns
- Navigation Menus - Sidebar and navigation menu patterns
- Context Menus - Right-click and contextual menu patterns
CustomMenu
A highly flexible menu component that supports various configurations including action menus, dropdown menus, and icon buttons with customizable styling.
Component Location
import CustomMenu from 'components/Menu/CustomMenu';
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| options | MenuOption[] | ✓ | - | Array of menu options |
| name | string | - | - | Menu button text |
| action | boolean | - | false | Show as "Actions" menu |
| leftIcon | ReactNode | - | - | Left icon for menu button |
| rightIcon | ReactNode | - | - | Right icon for menu button |
| baseIconButton | ReactNode | - | - | Icon for icon button mode |
| baseVariant | string | - | 'unstyled' | Button variant |
| baseBg | string | - | 'white' | Background color |
| baseColor | string | - | 'charcoal' | Text color |
| baseWeight | number | - | 400 | Font weight |
| baseFontSize | string | - | 'sm' | Font size |
| baseBorderRadius | string | - | 'sm' | Border radius |
| baseHover | string | - | '#F3F3F4' | Hover background color |
| optionWeight | number | - | 400 | Option font weight |
| optionFontSize | string | - | 'sm' | Option font size |
| optionBorderRadius | string | - | 'sm' | Option border radius |
| optionHover | string | - | '#F3F3F4' | Option hover background color |
TypeScript Interface
interface MenuOption {
name: string;
onClick: () => void;
leftIcon?: ReactNode;
sx?: object;
isDisabled?: boolean;
}
interface CustomMenuProps {
options: MenuOption[];
name?: string;
action?: boolean;
leftIcon?: ReactNode;
rightIcon?: ReactNode;
baseIconButton?: ReactNode;
baseVariant?: string;
baseBg?: string;
baseColor?: string;
baseWeight?: number;
baseFontSize?: string;
baseBorderRadius?: string;
baseHover?: string;
optionWeight?: number;
optionFontSize?: string;
optionBorderRadius?: string;
optionHover?: string;
}
Features
Multiple Button Types
- Regular button with text
- Icon button mode
- Action menu mode
- Custom styling options
Flexible Options
- Clickable menu items
- Disabled states
- Custom icons per option
- Custom styling per option
Responsive Design
- Hover effects
- Consistent spacing
- Proper focus handling
Usage Examples
Basic Action Menu
import CustomMenu from 'components/Menu/CustomMenu';
function ActionMenu() {
const options = [
{
name: 'Edit',
onClick: () => handleEdit(),
leftIcon: <EditIcon />,
},
{
name: 'Delete',
onClick: () => handleDelete(),
leftIcon: <DeleteIcon />,
},
{
name: 'Duplicate',
onClick: () => handleDuplicate(),
leftIcon: <CopyIcon />,
},
];
return <CustomMenu options={options} action={true} />;
}
Custom Menu with Styling
import CustomMenu from 'components/Menu/CustomMenu';
function StyledMenu() {
const options = [
{
name: 'Profile Settings',
onClick: () => navigate('/profile'),
leftIcon: <UserIcon />,
},
{
name: 'Account Settings',
onClick: () => navigate('/settings'),
leftIcon: <SettingsIcon />,
},
{
name: 'Sign Out',
onClick: () => signOut(),
leftIcon: <LogoutIcon />,
sx: { color: 'red.500' },
},
];
return (
<CustomMenu
options={options}
name='User Menu'
baseBg='blue.50'
baseColor='blue.800'
baseHover='blue.100'
optionHover='blue.50'
leftIcon={<UserIcon />}
/>
);
}
Icon Button Menu
import CustomMenu from 'components/Menu/CustomMenu';
function IconButtonMenu() {
const options = [
{
name: 'View Details',
onClick: () => showDetails(),
},
{
name: 'Edit Item',
onClick: () => editItem(),
},
{
name: 'Delete Item',
onClick: () => deleteItem(),
isDisabled: !canDelete,
},
];
return <CustomMenu options={options} baseIconButton={<MoreVerticalIcon />} />;
}
Table Row Menu
import CustomMenu from 'components/Menu/CustomMenu';
function TableRowMenu({ item, onEdit, onDelete, onView }) {
const options = [
{
name: 'View',
onClick: () => onView(item.id),
leftIcon: <EyeIcon />,
},
{
name: 'Edit',
onClick: () => onEdit(item.id),
leftIcon: <EditIcon />,
},
{
name: 'Delete',
onClick: () => onDelete(item.id),
leftIcon: <DeleteIcon />,
isDisabled: !item.canDelete,
sx: { color: 'red.500' },
},
];
return (
<CustomMenu
options={options}
baseIconButton={<DotsVerticalIcon />}
baseVariant='ghost'
/>
);
}
Conditional Menu Options
import CustomMenu from 'components/Menu/CustomMenu';
function ConditionalMenu({ user, permissions }) {
const options = [
{
name: 'View Profile',
onClick: () => viewProfile(user.id),
leftIcon: <UserIcon />,
},
permissions.canEdit && {
name: 'Edit User',
onClick: () => editUser(user.id),
leftIcon: <EditIcon />,
},
permissions.canDelete && {
name: 'Delete User',
onClick: () => deleteUser(user.id),
leftIcon: <DeleteIcon />,
sx: { color: 'red.500' },
},
].filter(Boolean);
return <CustomMenu options={options} action={true} />;
}
Menu with Custom Icons
import CustomMenu from 'components/Menu/CustomMenu';
function CustomIconMenu() {
const options = [
{
name: 'Export PDF',
onClick: () => exportPDF(),
leftIcon: <FileTextIcon color='red.500' />,
},
{
name: 'Export Excel',
onClick: () => exportExcel(),
leftIcon: <FileSpreadsheetIcon color='green.500' />,
},
{
name: 'Send Email',
onClick: () => sendEmail(),
leftIcon: <MailIcon color='blue.500' />,
},
];
return (
<CustomMenu
options={options}
name='Export Options'
leftIcon={<DownloadIcon />}
/>
);
}
Styling
Default Styling
- Button Padding: 10px 20px (regular), 8px 15px (icon button)
- Button Variant: unstyled
- Background: white
- Text Color: charcoal
- Font Weight: 400
- Font Size: sm
- Border Radius: sm
- Hover Background: #F3F3F4
Menu Items
- Padding: 2 units
- Font Weight: 400 (configurable)
- Font Size: sm (configurable)
- Border Radius: sm (configurable)
- Hover Background: #F3F3F4 (configurable)
Action Menus
Common patterns for action menus used throughout the application.
Table Actions Pattern
import CustomMenu from 'components/Menu/CustomMenu';
function useTableActions(item, permissions) {
const actions = [
{
name: 'View',
onClick: () => handleView(item),
leftIcon: <ViewIcon />,
},
permissions.canEdit && {
name: 'Edit',
onClick: () => handleEdit(item),
leftIcon: <EditIcon />,
},
permissions.canApprove &&
item.status === 'pending' && {
name: 'Approve',
onClick: () => handleApprove(item),
leftIcon: <CheckIcon />,
},
permissions.canReject &&
item.status === 'pending' && {
name: 'Reject',
onClick: () => handleReject(item),
leftIcon: <XIcon />,
},
permissions.canDelete && {
name: 'Delete',
onClick: () => handleDelete(item),
leftIcon: <DeleteIcon />,
sx: { color: 'red.500' },
},
].filter(Boolean);
return { actions };
}
function TableActionMenu({ item, permissions }) {
const { actions } = useTableActions(item, permissions);
return <CustomMenu options={actions} baseIconButton={<MoreIcon />} />;
}
Export Actions Pattern
import CustomMenu from 'components/Menu/CustomMenu';
function ExportActionsMenu({ data, onExport }) {
const exportOptions = [
{
name: 'Export as PDF',
onClick: () => onExport('pdf', data),
leftIcon: <PdfIcon />,
},
{
name: 'Export as Excel',
onClick: () => onExport('excel', data),
leftIcon: <ExcelIcon />,
},
{
name: 'Export as CSV',
onClick: () => onExport('csv', data),
leftIcon: <CsvIcon />,
},
];
return (
<CustomMenu
options={exportOptions}
name='Export'
leftIcon={<DownloadIcon />}
/>
);
}
Bulk Actions Pattern
import CustomMenu from 'components/Menu/CustomMenu';
function BulkActionsMenu({ selectedItems, onBulkAction }) {
const bulkActions = [
{
name: `Approve ${selectedItems.length} items`,
onClick: () => onBulkAction('approve', selectedItems),
leftIcon: <CheckIcon />,
},
{
name: `Reject ${selectedItems.length} items`,
onClick: () => onBulkAction('reject', selectedItems),
leftIcon: <XIcon />,
},
{
name: `Delete ${selectedItems.length} items`,
onClick: () => onBulkAction('delete', selectedItems),
leftIcon: <DeleteIcon />,
sx: { color: 'red.500' },
},
];
return (
<CustomMenu
options={bulkActions}
action={true}
isDisabled={selectedItems.length === 0}
/>
);
}
Navigation Menus
Patterns for navigation menus used in sidebars and navigation components.
Sidebar Menu Pattern
import { Menu, MenuButton, MenuList, MenuItem } from '@chakra-ui/react';
function SidebarMenu({ menuItems, activeItem, onItemClick }) {
return (
<VStack spacing={2} align='stretch'>
{menuItems.map(item => (
<Box key={item.id}>
{item.subMenus ? (
<Menu>
<MenuButton
as={Button}
variant='ghost'
width='full'
justifyContent='flex-start'
leftIcon={item.icon}
rightIcon={<ChevronDownIcon />}
isActive={activeItem === item.id}
>
{item.name}
</MenuButton>
<MenuList>
{item.subMenus.map(subItem => (
<MenuItem
key={subItem.id}
onClick={() => onItemClick(subItem)}
isDisabled={subItem.isDisabled}
>
{subItem.name}
</MenuItem>
))}
</MenuList>
</Menu>
) : (
<Button
variant='ghost'
width='full'
justifyContent='flex-start'
leftIcon={item.icon}
onClick={() => onItemClick(item)}
isActive={activeItem === item.id}
isDisabled={item.isDisabled}
>
{item.name}
</Button>
)}
</Box>
))}
</VStack>
);
}
User Profile Menu Pattern
import {
Menu,
MenuButton,
MenuList,
MenuItem,
MenuDivider,
} from '@chakra-ui/react';
function UserProfileMenu({ user, onAction }) {
return (
<Menu>
<MenuButton as={Button} variant='ghost'>
<HStack>
<Avatar size='sm' src={user.avatar} name={user.name} />
<VStack align='start' spacing={0}>
<Text fontSize='sm' fontWeight='medium'>
{user.name}
</Text>
<Text fontSize='xs' color='gray.500'>
{user.email}
</Text>
</VStack>
<ChevronDownIcon />
</HStack>
</MenuButton>
<MenuList>
<MenuItem onClick={() => onAction('profile')}>
<UserIcon mr={2} />
My Profile
</MenuItem>
<MenuItem onClick={() => onAction('settings')}>
<SettingsIcon mr={2} />
Settings
</MenuItem>
<MenuDivider />
<MenuItem onClick={() => onAction('help')}>
<HelpIcon mr={2} />
Help & Support
</MenuItem>
<MenuItem onClick={() => onAction('logout')} color='red.500'>
<LogoutIcon mr={2} />
Sign Out
</MenuItem>
</MenuList>
</Menu>
);
}
Context Menus
Patterns for context menus and right-click menus.
Right-Click Context Menu Pattern
import { Menu, MenuButton, MenuList, MenuItem } from '@chakra-ui/react';
import { useContextMenu } from '@chakra-ui/react';
function ContextMenuProvider({ children, menuItems }) {
const [contextMenu, setContextMenu] = useState(null);
const handleRightClick = event => {
event.preventDefault();
setContextMenu({
x: event.clientX,
y: event.clientY,
});
};
const handleMenuClose = () => {
setContextMenu(null);
};
return (
<Box onContextMenu={handleRightClick}>
{children}
{contextMenu && (
<Menu isOpen={true} onClose={handleMenuClose}>
<MenuList
position='fixed'
left={contextMenu.x}
top={contextMenu.y}
zIndex={1000}
>
{menuItems.map(item => (
<MenuItem
key={item.name}
onClick={() => {
item.onClick();
handleMenuClose();
}}
isDisabled={item.isDisabled}
>
{item.leftIcon && <Box mr={2}>{item.leftIcon}</Box>}
{item.name}
</MenuItem>
))}
</MenuList>
</Menu>
)}
</Box>
);
}
Dropdown Context Menu Pattern
import CustomMenu from 'components/Menu/CustomMenu';
function DropdownContextMenu({ trigger, options }) {
return (
<Menu>
<MenuButton as={Box}>{trigger}</MenuButton>
<MenuList>
{options.map(option => (
<MenuItem
key={option.name}
onClick={option.onClick}
isDisabled={option.isDisabled}
>
{option.leftIcon && <Box mr={2}>{option.leftIcon}</Box>}
{option.name}
</MenuItem>
))}
</MenuList>
</Menu>
);
}
// Usage
function ContextMenuExample() {
const contextOptions = [
{
name: 'Copy',
onClick: () => copyToClipboard(),
leftIcon: <CopyIcon />,
},
{
name: 'Cut',
onClick: () => cutToClipboard(),
leftIcon: <ScissorsIcon />,
},
{
name: 'Paste',
onClick: () => pasteFromClipboard(),
leftIcon: <ClipboardIcon />,
isDisabled: !hasClipboardContent,
},
];
return (
<DropdownContextMenu
trigger={<Box>Right-click me</Box>}
options={contextOptions}
/>
);
}
Menu Patterns Best Practices
Permission-Based Menus
function usePermissionBasedMenu(permissions, actions) {
const menuOptions = useMemo(() => {
return [
permissions.canView && {
name: 'View',
onClick: actions.view,
leftIcon: <ViewIcon />,
},
permissions.canEdit && {
name: 'Edit',
onClick: actions.edit,
leftIcon: <EditIcon />,
},
permissions.canDelete && {
name: 'Delete',
onClick: actions.delete,
leftIcon: <DeleteIcon />,
sx: { color: 'red.500' },
},
].filter(Boolean);
}, [permissions, actions]);
return menuOptions;
}
Dynamic Menu Options
function useDynamicMenu(item, status) {
const menuOptions = useMemo(() => {
const baseOptions = [
{
name: 'View Details',
onClick: () => viewItem(item.id),
leftIcon: <ViewIcon />,
},
];
if (status === 'draft') {
baseOptions.push({
name: 'Edit',
onClick: () => editItem(item.id),
leftIcon: <EditIcon />,
});
}
if (status === 'pending') {
baseOptions.push(
{
name: 'Approve',
onClick: () => approveItem(item.id),
leftIcon: <CheckIcon />,
},
{
name: 'Reject',
onClick: () => rejectItem(item.id),
leftIcon: <XIcon />,
},
);
}
return baseOptions;
}, [item, status]);
return menuOptions;
}
Async Menu Actions
function useAsyncMenuActions() {
const [isLoading, setIsLoading] = useState(false);
const createAsyncAction = action => {
return async () => {
setIsLoading(true);
try {
await action();
} catch (error) {
console.error('Menu action failed:', error);
} finally {
setIsLoading(false);
}
};
};
const menuOptions = [
{
name: 'Save',
onClick: createAsyncAction(saveItem),
leftIcon: isLoading ? <Spinner size='sm' /> : <SaveIcon />,
isDisabled: isLoading,
},
{
name: 'Delete',
onClick: createAsyncAction(deleteItem),
leftIcon: <DeleteIcon />,
isDisabled: isLoading,
},
];
return { menuOptions, isLoading };
}
Best Practices
Menu Design
- Consistent Icons: Use consistent icons for similar actions across menus
- Logical Grouping: Group related actions together
- Visual Hierarchy: Use dividers and styling to create visual hierarchy
- Dangerous Actions: Use red color for destructive actions
Performance
- Lazy Loading: Load menu content only when needed
- Memoization: Use useMemo for complex menu calculations
- Event Handling: Optimize event handlers for menu actions
- Conditional Rendering: Only render menu items that are relevant
User Experience
- Clear Labels: Use clear, descriptive labels for menu items
- Disabled States: Clearly indicate when menu items are disabled
- Keyboard Navigation: Ensure menus are keyboard accessible
- Touch Targets: Make menu items appropriately sized for touch
Accessibility
- ARIA Labels: Provide proper ARIA labels for screen readers
- Keyboard Support: Support keyboard navigation (arrows, enter, escape)
- Focus Management: Properly manage focus when opening/closing menus
- High Contrast: Ensure menu items work in high contrast mode
Testing
import { render, screen, fireEvent } from '@testing-library/react';
import CustomMenu from 'components/Menu/CustomMenu';
describe('Menu Components', () => {
it('should render menu options', () => {
const options = [
{ name: 'Edit', onClick: jest.fn() },
{ name: 'Delete', onClick: jest.fn() },
];
render(<CustomMenu options={options} action={true} />);
fireEvent.click(screen.getByText('Actions'));
expect(screen.getByText('Edit')).toBeInTheDocument();
expect(screen.getByText('Delete')).toBeInTheDocument();
});
it('should call onClick when menu item is clicked', () => {
const mockEdit = jest.fn();
const options = [{ name: 'Edit', onClick: mockEdit }];
render(<CustomMenu options={options} action={true} />);
fireEvent.click(screen.getByText('Actions'));
fireEvent.click(screen.getByText('Edit'));
expect(mockEdit).toHaveBeenCalled();
});
it('should disable menu items when isDisabled is true', () => {
const options = [{ name: 'Edit', onClick: jest.fn(), isDisabled: true }];
render(<CustomMenu options={options} action={true} />);
fireEvent.click(screen.getByText('Actions'));
expect(screen.getByText('Edit')).toHaveAttribute('aria-disabled', 'true');
});
});
This comprehensive menu system provides consistent, accessible, and flexible dropdown menu functionality for the WorkPayCore Frontend application.