Files
OpsLog/frontend/src/components/Menubar.tsx
T
2026-05-28 18:35:22 +02:00

93 lines
3.4 KiB
TypeScript

import { useState } from 'react';
import {
DropdownMenu, DropdownMenuTrigger, DropdownMenuContent,
DropdownMenuItem, DropdownMenuSeparator, DropdownMenuShortcut,
} from '@/components/ui/dropdown-menu';
import { cn } from '@/lib/utils';
export type MenuItem =
| { type: 'item'; label: string; action: string; shortcut?: string; disabled?: boolean }
| { type: 'separator' };
export interface Menu {
name: string;
label: string;
items: MenuItem[];
}
interface Props {
menus: Menu[];
onAction: (action: string) => void;
}
export function Menubar({ menus, onAction }: Props) {
// Track which menu is open so hover-to-switch works like a desktop menubar.
const [openMenu, setOpenMenu] = useState<string | null>(null);
return (
<nav className="flex items-stretch h-full">
{menus.map((menu) => (
<DropdownMenu
key={menu.name}
open={openMenu === menu.name}
onOpenChange={(o) => setOpenMenu(o ? menu.name : null)}
modal={false}
>
<DropdownMenuTrigger
onMouseEnter={() => {
// Only switch on hover if a menu is already open.
if (openMenu !== null && openMenu !== menu.name) setOpenMenu(menu.name);
}}
onPointerDown={(e) => {
// Desktop-menubar behaviour: when another menu is already
// open, a click on a different trigger should switch to it
// in one click. Without this Radix consumes the click to
// close the current menu first, requiring a second click
// to open the new one. We pre-empt by setting open state
// synchronously and stopping the event from reaching the
// default Radix toggle.
if (openMenu !== null && openMenu !== menu.name) {
e.preventDefault();
e.stopPropagation();
setOpenMenu(menu.name);
}
}}
className={cn(
'px-3 text-sm rounded-md text-muted-foreground hover:bg-muted hover:text-foreground transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring',
openMenu === menu.name && 'bg-muted text-primary',
)}
>
{menu.label}
</DropdownMenuTrigger>
<DropdownMenuContent
align="start" sideOffset={4} className="min-w-[240px]"
onCloseAutoFocus={(e) => {
// Radix re-focuses the trigger after close. Combined with our
// focus-visible:ring style this leaves an orange outline around
// the previously-clicked menu — looks like a stuck "selected"
// state. We swallow the auto-focus and let the next interaction
// decide where focus belongs.
e.preventDefault();
}}
>
{menu.items.map((item, i) =>
item.type === 'separator' ? (
<DropdownMenuSeparator key={i} />
) : (
<DropdownMenuItem
key={i}
disabled={item.disabled}
onSelect={() => onAction(item.action)}
>
<span>{item.label}</span>
{item.shortcut && <DropdownMenuShortcut>{item.shortcut}</DropdownMenuShortcut>}
</DropdownMenuItem>
),
)}
</DropdownMenuContent>
</DropdownMenu>
))}
</nav>
);
}