NavMenu
nav-menu侧边导航菜单 · 自研零依赖 · inline 手风琴/collapsed 图标飞出 + 树形 items + 选中/展开受控 + 纯 CSS grid 高度过渡 + WAI-ARIA tree 键盘漫游
用法
基础用法
inline 模式下子菜单内联手风琴展开,非受控用 defaultSelectedKeys 设初始选中。
<NavMenu
items={items}
mode="inline"
defaultSelectedKeys={["dashboard"]}
/>站点主导航语义(semantics="list")
默认 tree 档给行加 role=treeitem,会压过 <a> 的隐式 link role——读屏「列出页面所有链接」一条主导航都列不出来。list 档不写 role:<a> 是 link、<button> 是 button,键盘退回 Tab 逐项 + 原生激活。皮肤完全一样,改的只是无障碍树。文件树 / 大纲树留在默认 tree 档。
<NavMenu
items={items}
semantics="list"
defaultSelectedKeys={["dashboard"]}
/>默认展开子菜单
defaultOpenKeys 指定初始展开的父项,配合子项选中定位当前页。
<NavMenu
items={items}
mode="inline"
defaultOpenKeys={["users"]}
defaultSelectedKeys={["users-roles"]}
/>行尾操作
actions 槽渲染在行按钮之外(绝对覆盖右侧),可用 group-hover/nav-row 做 hover 才显。
const items = [
{
type: "group",
key: "today",
label: "今天",
children: [
{ key: "c1", label: "瑚琏组件库怎么接入", actions: <DeleteAction /> },
{ key: "c2", label: "帮我润色一封周报", actions: <DeleteAction /> },
],
},
];
<NavMenu items={items} defaultSelectedKeys={["c1"]} />收起态(图标轨)
collapsed 模式收起为图标轨,hover / 聚焦时飞出子菜单。
<NavMenu
items={items}
mode="collapsed"
defaultSelectedKeys={["dashboard"]}
/>收起态 · 多级级联
collapsed 的飞出层与 inline 一样支持无限级:子层逐级向右级联。键盘 → 进子层、← / Esc 回父层,↑↓ 在同层兄弟间移动。
const items = [
{
key: "sys",
label: "系统管理",
icon: <Settings />,
children: [
{
key: "sys-user",
label: "用户与权限",
children: [
{
key: "sys-user-role",
label: "角色",
children: [
{ key: "sys-user-role-list", label: "角色列表", href: "#role-list" },
{ key: "sys-user-role-perm", label: "权限分配", href: "#role-perm" },
],
},
],
},
],
},
];
<NavMenu items={items} mode="collapsed" defaultSelectedKeys={["sys-user-role-perm"]} />何时用
中后台左侧 Sider 树形导航(多级菜单、可收起、可受控选中/展开)用,数据驱动 items,支持 inline 手风琴与 collapsed 图标飞出两种形态,还能给每行挂行尾操作(如会话列表的删除)。横向顶栏用 Navbar,带下拉面板的导航用 NavigationMenu,右键/上下文菜单用 Menu。
导入
import { NavMenu } from "@hulianui/ui"Props
items 元素为 NavMenuNode = NavMenuItem | NavMenuGroup。NavMenuItem = { key; label; icon?; href?; disabled?; actions?; children? }(有 children 即可展开父项,有 href 渲染 <a> 否则 <button>);NavMenuGroup = { type:"group"; key; label; children }(不可折叠小标题,key 不进选中/展开态)。继承 <nav> 原生属性(onSelect 除外)。
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| items* | NavMenuNode[] | — | 树形菜单数据 |
| mode | "inline" | "collapsed" | "inline" | inline=手风琴内联展开;collapsed=Sider 收起态图标 + 悬浮飞出子菜单(两态都支持无限级) |
| semantics | "tree" | "list" | "tree" | 无障碍语义。list 时行不强加 role(保住 <a> 的 link 语义),键盘退回 Tab 逐项。站点主导航选 `list`,文件树/大纲树留 tree。见下文 |
| selectedKeys | string[] | — | 选中态(受控) |
| defaultSelectedKeys | string[] | — | 选中态(非受控初值) |
| openKeys | string[] | — | 展开态(受控) |
| defaultOpenKeys | string[] | — | 展开态(非受控初值) |
Events
| 事件 | 类型 | 说明 |
|---|---|---|
| onSelect | (key: string, item: NavMenuItem) => void | 点击叶子项触发 |
| onOpenChange | (openKeys: string[]) => void | 展开态变化回调 |
禁忌 / 坑
- 默认导航无障碍名称跟随
ConfigProvider(zhCN为“侧边导航”,enUS为 “Sidebar navigation”);显式传入aria-label时以消费方值为准。 - 行尾操作放
actions槽,组件会渲在 treeitem 按钮/链接【之外】(绝对覆盖行右侧)。别把 `<button>` 等交互元素直接塞进 `label`——嵌进 treeitem 按钮是非法 HTML,会触发 hydration 报错。actions仅 inline 态生效。 - 高度过渡用纯 CSS
grid-template-rows0fr→1fr,不靠 JS 测高,嵌套展开也不抖。参见 [[nested-collapsible-height-via-css-grid-rows-not-js-measure]]。 - 选中/展开态可受控(
selectedKeys/openKeys+ 回调)或非受控(default*),勿混用同一维度。 - collapsed 态飞出层是无限级级联(与 inline 能力对齐),整棵树 DOM 恒在、显隐纯 CSS 驱动:
:hover / :focus-within 逐层点亮。键盘走「级联菜单」语义——→ 进子层、←/Esc 回父层、
↑↓ 只在同层兄弟间移动、Home/End 落本层首尾;roving tabindex 贯穿全树(整棵树只有一个
tab 落点,深层靠方向键进出,不要指望 Tab 逐项走进飞出层)。semantics="list" 时这套键盘契约
整体让位给原生 Tab。
- `semantics` 决定的是无障碍树,不是皮肤:两档长得一模一样,选中高亮、缩进、飞出层行为都不变,
所以看不出选错,也不会有任何报错。站点主导航如果留在默认 tree 档,读屏用户按
「列出页面所有链接」就真的一条都找不到 —— 那种场景请显式传 semantics="list"。
- collapsed 的第一层飞出层用
position: fixed+ JS 实测坐标,刻意不是 `absolute`:图标轨几乎
总被放进可滚动的侧栏容器(AdminLayout 用的就是 ScrollArea),absolute 面板会被那个祖先的
overflow 整块裁掉——面板在 DOM 里、有尺寸、opacity:1,但一个像素都画不出来,
elementFromPoint 打到的是内容区。第二层起仍是 absolute(祖先已是不裁剪的面板)。
代价:坐标靠挂载时实测 + scroll(捕获阶段)/ resize 时重算;把轨道放进自定义滚动实现
(不派发 scroll 事件的那种)时位置会失准。
openKeys只作用于 inline 态;collapsed 的层级显隐由 hover/焦点驱动,不进openKeys,也不发
onOpenChange。
- SCAFFOLD 列的 menubar/SwiftUI/Tauri 原生菜单类坑均不适用本组件(它是纯 React WAI-ARIA tree,非系统托盘菜单)。
相关
Navbar · BeianFooter · NavigationMenu · Menu · Menubar · Dock
Playground
<NavMenu
mode="inline"
items={items}
defaultOpenKeys={["users"]}
selectedKeys={selected}
onSelect={(key) => setSelected([key])}
/>