Right-click context menu for rows.
Context menus provide right-click actions for rows. Define custom menu items with icons, variants, and conditional logic.
type ContextMenuItem<TData> =
| {
label: string;
action: (row: TData) => void;
icon?: LucideIcon;
variant?: 'default' | 'destructive';
disabled?: boolean;
}
| 'separator';
type ContextMenuBuilder<TData> = (row: TData) => ContextMenuItem<TData>[];<ActiveGrid
columns={columns}
data={data}
contextMenu={(row) => [
{
label: 'Edit',
action: () => handleEdit(row),
icon: Pencil,
},
{
label: 'Delete',
action: () => handleDelete(row),
icon: Trash,
variant: 'destructive',
},
]}
{...}
/>{
label: 'Edit User',
action: (row) => {
router.push(`/users/${row.id}/edit`);
},
}import { Pencil, Trash, Copy, Eye } from 'lucide-react';
{
label: 'Edit',
action: (row) => handleEdit(row),
icon: Pencil,
}{
label: 'Delete',
action: (row) => handleDelete(row),
icon: Trash,
variant: 'destructive', // Red text and icon
}{
label: 'Archive',
action: (row) => handleArchive(row),
disabled: row.status === 'archived',
}[
{ label: 'Edit', action: handleEdit },
'separator',
{ label: 'Delete', action: handleDelete, variant: 'destructive' },
]import { Pencil, Trash, Copy, Download, Archive, Eye } from 'lucide-react';
<ActiveGrid
columns={columns}
data={users}
contextMenu={(row) => [
{
label: 'View Details',
action: () => router.push(`/users/${row.id}`),
icon: Eye,
},
{
label: 'Edit',
action: () => handleEdit(row),
icon: Pencil,
},
{
label: 'Duplicate',
action: () => handleDuplicate(row),
icon: Copy,
},
'separator',
{
label: 'Export',
action: () => handleExport(row),
icon: Download,
},
{
label: 'Archive',
action: () => handleArchive(row),
icon: Archive,
disabled: row.status === 'archived',
},
'separator',
{
label: 'Delete',
action: () => handleDelete(row),
icon: Trash,
variant: 'destructive',
},
]}
/>Return different items based on row data:
contextMenu={(row) => {
const items: ContextMenuItem<User>[] = [
{
label: 'View',
action: () => viewUser(row),
icon: Eye,
},
];
// Only show edit for non-admin users
if (row.role !== 'admin') {
items.push({
label: 'Edit',
action: () => editUser(row),
icon: Pencil,
});
}
// Show different delete options
if (row.status === 'active') {
items.push('separator');
items.push({
label: 'Deactivate',
action: () => deactivate(row),
icon: Ban,
});
} else {
items.push('separator');
items.push({
label: 'Delete Permanently',
action: () => deleteUser(row),
icon: Trash,
variant: 'destructive',
});
}
return items;
}}contextMenu={(row) => [
{
label: 'Delete',
action: async () => {
if (confirm('Are you sure you want to delete this user?')) {
await deleteUser(row.id);
toast.success('User deleted');
}
},
icon: Trash,
variant: 'destructive',
},
]}contextMenu={(row) => [
{
label: 'Copy ID',
action: () => {
navigator.clipboard.writeText(row.id);
toast.success('ID copied to clipboard');
},
icon: Copy,
},
{
label: 'Copy Email',
action: () => {
navigator.clipboard.writeText(row.email);
toast.success('Email copied to clipboard');
},
icon: Mail,
},
]}contextMenu={(row) => [
{
label: 'Export',
action: async () => {
try {
toast.loading('Exporting...');
await exportUser(row.id);
toast.success('Export complete');
} catch (error) {
toast.error('Export failed');
}
},
icon: Download,
},
]}import { useRouter } from 'next/navigation';
function UsersTable() {
const router = useRouter();
return (
<ActiveGrid
contextMenu={(row) => [
{
label: 'View Profile',
action: () => router.push(`/users/${row.id}`),
icon: User,
},
{
label: 'View Orders',
action: () => router.push(`/users/${row.id}/orders`),
icon: ShoppingCart,
},
]}
{...}
/>
);
}Use separators to group related actions:
contextMenu={(row) => [
// View actions
{
label: 'View Details',
action: () => view(row),
icon: Eye,
},
{
label: 'View History',
action: () => viewHistory(row),
icon: History,
},
'separator',
// Edit actions
{
label: 'Edit',
action: () => edit(row),
icon: Pencil,
},
{
label: 'Duplicate',
action: () => duplicate(row),
icon: Copy,
},
'separator',
// Destructive actions
{
label: 'Archive',
action: () => archive(row),
icon: Archive,
},
{
label: 'Delete',
action: () => del(row),
icon: Trash,
variant: 'destructive',
},
]}function usePermissions() {
return {
canEdit: true,
canDelete: true,
canExport: false,
};
}
function UsersTable() {
const { canEdit, canDelete, canExport } = usePermissions();
return (
<ActiveGrid
contextMenu={(row) => {
const items: ContextMenuItem<User>[] = [];
if (canEdit) {
items.push({
label: 'Edit',
action: () => edit(row),
icon: Pencil,
});
}
if (canExport) {
items.push({
label: 'Export',
action: () => exportUser(row),
icon: Download,
});
}
if (canDelete) {
if (items.length > 0) items.push('separator');
items.push({
label: 'Delete',
action: () => del(row),
icon: Trash,
variant: 'destructive',
});
}
return items;
}}
{...}
/>
);
}type Order = {
id: string;
status: 'pending' | 'processing' | 'completed' | 'cancelled';
};
contextMenu={(row) => {
const baseItems: ContextMenuItem<Order>[] = [
{
label: 'View Details',
action: () => view(row),
icon: Eye,
},
];
// Status-specific actions
switch (row.status) {
case 'pending':
baseItems.push('separator');
baseItems.push({
label: 'Start Processing',
action: () => startProcessing(row),
icon: Play,
});
baseItems.push({
label: 'Cancel Order',
action: () => cancel(row),
icon: X,
variant: 'destructive',
});
break;
case 'processing':
baseItems.push('separator');
baseItems.push({
label: 'Mark Complete',
action: () => complete(row),
icon: Check,
});
break;
case 'completed':
baseItems.push('separator');
baseItems.push({
label: 'View Invoice',
action: () => viewInvoice(row),
icon: FileText,
});
break;
}
return baseItems;
}}import {
Eye,
Pencil,
Copy,
Download,
Mail,
Lock,
Unlock,
Archive,
Trash,
} from 'lucide-react';
type User = {
id: string;
name: string;
email: string;
status: 'active' | 'inactive' | 'locked';
role: 'admin' | 'user' | 'guest';
};
function UsersTable() {
const router = useRouter();
const { user: currentUser } = useAuth();
const handleAction = async (action: string, user: User) => {
try {
switch (action) {
case 'edit':
router.push(`/users/${user.id}/edit`);
break;
case 'duplicate':
await duplicateUser(user.id);
toast.success('User duplicated');
break;
case 'export':
await exportUser(user.id);
toast.success('Export complete');
break;
case 'email':
window.location.href = `mailto:${user.email}`;
break;
case 'lock':
await lockUser(user.id);
toast.success('User locked');
break;
case 'unlock':
await unlockUser(user.id);
toast.success('User unlocked');
break;
case 'archive':
if (confirm('Archive this user?')) {
await archiveUser(user.id);
toast.success('User archived');
}
break;
case 'delete':
if (confirm('Permanently delete this user?')) {
await deleteUser(user.id);
toast.success('User deleted');
}
break;
}
} catch (error) {
toast.error('Action failed');
}
};
return (
<ActiveGrid
columns={columns}
data={users}
contextMenu={(row) => {
const items: ContextMenuItem<User>[] = [
{
label: 'View Profile',
action: () => router.push(`/users/${row.id}`),
icon: Eye,
},
];
// Don't allow editing yourself or admins (unless you're admin)
const canEdit = row.id !== currentUser.id &&
(currentUser.role === 'admin' || row.role !== 'admin');
if (canEdit) {
items.push({
label: 'Edit',
action: () => handleAction('edit', row),
icon: Pencil,
});
items.push({
label: 'Duplicate',
action: () => handleAction('duplicate', row),
icon: Copy,
});
}
items.push('separator');
items.push({
label: 'Export Data',
action: () => handleAction('export', row),
icon: Download,
});
items.push({
label: 'Send Email',
action: () => handleAction('email', row),
icon: Mail,
});
if (canEdit) {
items.push('separator');
if (row.status === 'locked') {
items.push({
label: 'Unlock Account',
action: () => handleAction('unlock', row),
icon: Unlock,
});
} else {
items.push({
label: 'Lock Account',
action: () => handleAction('lock', row),
icon: Lock,
});
}
items.push('separator');
items.push({
label: 'Archive',
action: () => handleAction('archive', row),
icon: Archive,
disabled: row.status === 'inactive',
});
items.push({
label: 'Delete',
action: () => handleAction('delete', row),
icon: Trash,
variant: 'destructive',
});
}
return items;
}}
/>
);
}Context menu opens on right-click or Shift + F10
Context menus include:
import { ContextMenuItem } from '@workspace/active-grid';
type User = {
id: string;
name: string;
};
const menuItems: ContextMenuItem<User>[] = [
{
label: 'Edit',
action: (row: User) => console.log(row.id),
icon: Pencil,
},
];
<ActiveGrid<User>
contextMenu={(row: User) => menuItems}
{...}
/>