Skip to main content

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

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

PropTypeRequiredDefaultDescription
optionsMenuOption[]-Array of menu options
namestring--Menu button text
actionboolean-falseShow as "Actions" menu
leftIconReactNode--Left icon for menu button
rightIconReactNode--Right icon for menu button
baseIconButtonReactNode--Icon for icon button mode
baseVariantstring-'unstyled'Button variant
baseBgstring-'white'Background color
baseColorstring-'charcoal'Text color
baseWeightnumber-400Font weight
baseFontSizestring-'sm'Font size
baseBorderRadiusstring-'sm'Border radius
baseHoverstring-'#F3F3F4'Hover background color
optionWeightnumber-400Option font weight
optionFontSizestring-'sm'Option font size
optionBorderRadiusstring-'sm'Option border radius
optionHoverstring-'#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} />;
}
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
  • 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}
/>
);
}

Patterns for navigation menus used in sidebars and navigation components.

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 ? (
&lt;Menu&gt;
<MenuButton
as={Button}
variant='ghost'
width='full'
justifyContent='flex-start'
leftIcon={item.icon}
rightIcon={<ChevronDownIcon />}
isActive={activeItem === item.id}
>
{item.name}
</MenuButton>
&lt;MenuList&gt;
{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 (
&lt;Menu&gt;
<MenuButton as={Button} variant='ghost'>
&lt;HStack&gt;
<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>
&lt;MenuList&gt;
<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>
);
}
import CustomMenu from 'components/Menu/CustomMenu';

function DropdownContextMenu({ trigger, options }) {
return (
&lt;Menu&gt;
<MenuButton as={Box}>{trigger}</MenuButton>
&lt;MenuList&gt;
{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={&lt;Box&gt;Right-click me</Box>}
options={contextOptions}
/>
);
}

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

  1. Consistent Icons: Use consistent icons for similar actions across menus
  2. Logical Grouping: Group related actions together
  3. Visual Hierarchy: Use dividers and styling to create visual hierarchy
  4. Dangerous Actions: Use red color for destructive actions

Performance

  1. Lazy Loading: Load menu content only when needed
  2. Memoization: Use useMemo for complex menu calculations
  3. Event Handling: Optimize event handlers for menu actions
  4. Conditional Rendering: Only render menu items that are relevant

User Experience

  1. Clear Labels: Use clear, descriptive labels for menu items
  2. Disabled States: Clearly indicate when menu items are disabled
  3. Keyboard Navigation: Ensure menus are keyboard accessible
  4. Touch Targets: Make menu items appropriately sized for touch

Accessibility

  1. ARIA Labels: Provide proper ARIA labels for screen readers
  2. Keyboard Support: Support keyboard navigation (arrows, enter, escape)
  3. Focus Management: Properly manage focus when opening/closing menus
  4. 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.