93 lines
3.4 KiB
TypeScript
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>
|
|
);
|
|
}
|