184 lines
5.3 KiB
JavaScript
184 lines
5.3 KiB
JavaScript
import ExpandMore from "@mui/icons-material/ExpandMore";
|
|
import { useEffect, useRef, useState } from "react";
|
|
|
|
export function Sidebar({ activePath, isCollapsed, items, onNavigate }) {
|
|
const [expandedGroupId, setExpandedGroupId] = useState(() => findParentGroupId(activePath, items));
|
|
const [flyoutGroupId, setFlyoutGroupId] = useState(null);
|
|
const sidebarRef = useRef(null);
|
|
|
|
useEffect(() => {
|
|
setExpandedGroupId(findParentGroupId(activePath, items));
|
|
setFlyoutGroupId(null);
|
|
}, [activePath, items]);
|
|
|
|
useEffect(() => {
|
|
setFlyoutGroupId(null);
|
|
}, [isCollapsed]);
|
|
|
|
useEffect(() => {
|
|
if (!flyoutGroupId) {
|
|
return undefined;
|
|
}
|
|
|
|
const closeFlyout = (event) => {
|
|
if (sidebarRef.current?.contains(event.target)) {
|
|
return;
|
|
}
|
|
|
|
setFlyoutGroupId(null);
|
|
};
|
|
|
|
const closeOnEscape = (event) => {
|
|
if (event.key === "Escape") {
|
|
setFlyoutGroupId(null);
|
|
}
|
|
};
|
|
|
|
document.addEventListener("pointerdown", closeFlyout);
|
|
document.addEventListener("keydown", closeOnEscape);
|
|
|
|
return () => {
|
|
document.removeEventListener("pointerdown", closeFlyout);
|
|
document.removeEventListener("keydown", closeOnEscape);
|
|
};
|
|
}, [flyoutGroupId]);
|
|
|
|
const handleNavigate = (path) => {
|
|
if (path) {
|
|
onNavigate(path);
|
|
}
|
|
setFlyoutGroupId(null);
|
|
};
|
|
|
|
return (
|
|
<aside className={`sidebar ${isCollapsed ? "sidebar--collapsed" : ""}`} ref={sidebarRef}>
|
|
<nav className="nav-list" aria-label="主导航">
|
|
{items.map((item) => (
|
|
<NavGroup
|
|
activePath={activePath}
|
|
isExpanded={expandedGroupId === item.id}
|
|
isCollapsed={isCollapsed}
|
|
isFlyoutOpen={flyoutGroupId === item.id}
|
|
item={item}
|
|
key={item.id}
|
|
onNavigate={handleNavigate}
|
|
onToggleExpanded={() => setExpandedGroupId((current) => (current === item.id ? null : item.id))}
|
|
onToggleFlyout={() => setFlyoutGroupId((current) => (current === item.id ? null : item.id))}
|
|
/>
|
|
))}
|
|
</nav>
|
|
</aside>
|
|
);
|
|
}
|
|
|
|
function NavGroup({
|
|
activePath,
|
|
isCollapsed,
|
|
isExpanded,
|
|
isFlyoutOpen,
|
|
item,
|
|
onNavigate,
|
|
onToggleExpanded,
|
|
onToggleFlyout
|
|
}) {
|
|
const Icon = item.icon;
|
|
const children = item.children || [];
|
|
const hasChildren = children.length > 0;
|
|
const isActive = Boolean(item.path && activePath.startsWith(item.path));
|
|
const isChildActive = children.some((child) => child.path && activePath.startsWith(child.path));
|
|
const isOpen = isCollapsed ? isFlyoutOpen : isExpanded;
|
|
|
|
const handleParentClick = () => {
|
|
if (hasChildren && isCollapsed) {
|
|
onToggleFlyout();
|
|
return;
|
|
}
|
|
|
|
if (hasChildren) {
|
|
onToggleExpanded();
|
|
return;
|
|
}
|
|
|
|
onNavigate(item.path);
|
|
};
|
|
|
|
return (
|
|
<div className="nav-group">
|
|
<button
|
|
aria-expanded={hasChildren ? isOpen : undefined}
|
|
aria-haspopup={hasChildren && isCollapsed ? "menu" : undefined}
|
|
className={[
|
|
"nav-item",
|
|
hasChildren ? "nav-item--parent" : "",
|
|
isOpen ? "nav-item--expanded" : "",
|
|
isActive ? "nav-item--active" : "",
|
|
isChildActive || isOpen ? "nav-item--open" : "",
|
|
isFlyoutOpen ? "nav-item--flyout-open" : ""
|
|
]
|
|
.filter(Boolean)
|
|
.join(" ")}
|
|
type="button"
|
|
onClick={handleParentClick}
|
|
>
|
|
<Icon fontSize="small" />
|
|
<span className="nav-label">{item.label}</span>
|
|
{hasChildren ? <ExpandMore className="nav-expand" fontSize="small" /> : null}
|
|
</button>
|
|
|
|
{hasChildren && !isCollapsed ? (
|
|
<div className={`nav-children ${isOpen ? "nav-children--open" : ""}`}>
|
|
<div className="nav-children__inner">
|
|
{children.map((child) => {
|
|
const ChildIcon = child.icon;
|
|
|
|
return (
|
|
<button
|
|
className={`nav-item nav-item--child ${child.path && activePath.startsWith(child.path) ? "nav-item--active" : ""}`}
|
|
key={child.id}
|
|
type="button"
|
|
onClick={() => onNavigate(child.path)}
|
|
>
|
|
<ChildIcon fontSize="small" />
|
|
<span className="nav-label">{child.label}</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
{hasChildren && isCollapsed && isFlyoutOpen ? (
|
|
<div className="nav-flyout" role="menu">
|
|
<div className="nav-flyout__title">{item.label}</div>
|
|
{children.map((child) => {
|
|
const ChildIcon = child.icon;
|
|
|
|
return (
|
|
<button
|
|
className={`nav-flyout__item ${child.path && activePath.startsWith(child.path) ? "nav-flyout__item--active" : ""}`}
|
|
key={child.id}
|
|
role="menuitem"
|
|
type="button"
|
|
onClick={() => onNavigate(child.path)}
|
|
>
|
|
<ChildIcon fontSize="small" />
|
|
<span>{child.label}</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function findParentGroupId(activePath, items) {
|
|
for (const item of items) {
|
|
if (item.children?.some((child) => child.path && activePath.startsWith(child.path))) {
|
|
return item.id;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|