Files
rainblogweb/frontend/src/tools/workbench/components/MenuBar.jsx
T

119 lines
4.2 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useRef, useState } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import Divider from '@mui/material/Divider';
import Typography from '@mui/material/Typography';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
/* ============================================================
* MenuBarVSCode 风格菜单栏(MUI Menu,配置数组驱动)
*
* menus = [{ label, items: [{ label, Icon?, action?, shortcut?, divider? }] }]
* - role=menubar / menuitem,顶层 roving tabindex + 方向键导航:
* ← → / Home / End 移动焦点,↓ / Enter / 空格 打开菜单
* - 菜单内部导航由 MUI Menu 自带(方向键 + Enter/Esc
* ============================================================ */
export default function MenuBar({ menus = [] }) {
const [openIdx, setOpenIdx] = useState(null);
const [anchorEl, setAnchorEl] = useState(null);
const [focusIdx, setFocusIdx] = useState(0); // roving tabindex 停靠点
const barRef = useRef(null);
const btnEls = () => Array.from(barRef.current?.querySelectorAll('[data-menu-btn]') || []);
const focusBtn = (i) => {
const n = menus.length;
if (n === 0) return;
const j = ((i % n) + n) % n;
setFocusIdx(j);
btnEls()[j]?.focus();
};
const openMenu = (i) => {
setFocusIdx(i);
setAnchorEl(btnEls()[i]);
setOpenIdx(i);
};
const close = () => { setOpenIdx(null); setAnchorEl(null); };
const handleTopKeyDown = (e, i) => {
switch (e.key) {
case 'ArrowRight': e.preventDefault(); focusBtn(i + 1); break;
case 'ArrowLeft': e.preventDefault(); focusBtn(i - 1); break;
case 'Home': e.preventDefault(); focusBtn(0); break;
case 'End': e.preventDefault(); focusBtn(menus.length - 1); break;
case 'ArrowDown':
case 'Enter':
case ' ':
e.preventDefault();
openMenu(i); // MUI Menu 打开后自动聚焦首个菜单项
break;
default: break;
}
};
return (
<Box
ref={barRef}
component="nav"
role="menubar"
aria-label="主菜单"
sx={{ display: 'flex', alignItems: 'center' }}
>
{menus.map((menu, i) => (
<React.Fragment key={menu.label}>
<Button
data-menu-btn
role="menuitem"
aria-haspopup="true"
aria-expanded={openIdx === i}
tabIndex={focusIdx === i ? 0 : -1}
size="small"
onClick={(e) => {
setFocusIdx(i);
if (openIdx === i) { close(); return; }
setAnchorEl(e.currentTarget);
setOpenIdx(i);
}}
onKeyDown={(e) => handleTopKeyDown(e, i)}
sx={{
minWidth: 0, px: 1, height: 30, borderRadius: 1,
fontSize: 13, color: openIdx === i ? 'primary.main' : 'text.primary',
'&:hover': { bgcolor: openIdx === i ? 'action.selected' : 'action.hover' },
}}
>
{menu.label}
<KeyboardArrowDownIcon sx={{ fontSize: 14, ml: 0.25, color: 'text.disabled' }} />
</Button>
<Menu
open={openIdx === i}
anchorEl={anchorEl}
onClose={close}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
MenuListProps={{ sx: { py: 0.5, minWidth: 200 } }}
>
{menu.items.map((item, j) => (
item.divider ? (
<Divider key={`d-${j}`} sx={{ my: 0.5 }} />
) : (
<MenuItem
key={item.label + j}
dense
onClick={() => { close(); item.action?.(); }}
>
{item.Icon && <item.Icon sx={{ fontSize: 16, mr: 1.25, color: 'text.secondary' }} />}
<span style={{ flex: 1 }}>{item.label}</span>
{item.shortcut && (
<Typography variant="caption" sx={{ ml: 3, color: 'text.disabled' }}>{item.shortcut}</Typography>
)}
</MenuItem>
)
))}
</Menu>
</React.Fragment>
))}
</Box>
);
}