菜单
本页内容
菜单就是一个带样式面板的 <details>。这是有意不选 popover API 的结果:popover 活在顶层,没有锚点定位就没法相对触发器摆放,而锚点定位还没到处都有。<details> 今天就能摆对位置,而且不需要加载任何东西。
触发器就是那个 <summary>,只是打扮成按钮的样子——所以你把文字和按钮属性交给 menu(),而不是塞一个已经渲染好的 button() 进去。summary 本身已经可交互,在里面再放按钮,就是在只有一个动作的地方套了两个控件:既是非法标记,又让一件事有了两个 Tab 停靠点。
点击外部关闭和按 Esc 关闭来自一个 ontoggle 处理函数,它在菜单第一次打开时才去导入这些行为——而且仅此一次。就算那个模块始终没到,菜单靠自己的 summary 照样能开能关。
基础菜单
menu({ trigger: '操作' },
menuItem({ href: '#edit' }, '编辑'),
menuItem({ href: '#duplicate' }, '复制'),
menuSeparator(),
menuItem({ href: '#delete' }, '删除'),
)对齐
菜单从触发器的起始边展开。align: 'end' 会把它翻过来,靠近栏右端的菜单正需要这样。
stack({ direction: 'row', gap: 'xl', justify: 'space-between', style: 'width: 100%' },
menu({ trigger: '起始侧对齐', variant: 'soft' },
menuItem({ href: '#a' }, '第一项'),
menuItem({ href: '#b' }, '第二项'),
),
menu({ trigger: '末尾侧对齐', variant: 'soft', align: 'end' },
menuItem({ href: '#c' }, '第一项'),
menuItem({ href: '#d' }, '第二项'),
),
)图标触发器
没有 trigger 文字的图标需要一个 label——它会成为图标本身给不了的无障碍名称。
stack({ direction: 'row', gap: 'md' },
menu({
align: 'end',
label: '更多操作',
variant: 'ghost',
icon: icon('more-horizontal'),
},
menuItem({ href: '#rename' }, '重命名'),
menuItem({ href: '#move' }, '移动'),
menuSeparator(),
menuItem({ href: '#archive' }, '归档'),
),
)带图标的菜单项
menu({ trigger: '文件' },
menuItem({
href: '#new',
icon: icon('plus'),
}, '新建页面'),
menuItem({
href: '#open',
icon: icon('folder'),
}, '打开…'),
menuSeparator(),
menuItem({
href: '#build',
icon: icon('zap'),
}, '构建站点'),
)用按钮而不是链接
没有 href 的菜单项会渲染成 <button>——适合在页面上发生的操作,而不是跳转。
menu({ trigger: '导出', variant: 'soft', color: 'primary' },
menuItem({ onclick: "import('/su/toast.js').then(m=>m.toast('已导出为 JSON。',{color:'success'}))" }, '导出为 JSON'),
menuItem({ onclick: "import('/su/toast.js').then(m=>m.toast('已导出为 CSV。',{color:'success'}))" }, '导出为 CSV'),
)在应用栏中
appBar({ brand: 'sitelo' },
appBarSpacer(),
appBarActions(
themeToggle(),
menu({
align: 'end',
label: '更多',
variant: 'ghost',
icon: icon('more-horizontal'),
},
menuItem({ href: '/zh/docs' }, '文档'),
menuItem({ href: '/zh/examples' }, '示例'),
menuSeparator(),
menuItem({ href: 'https://github.com/paul-browne/sitelo' }, 'GitHub'),
),
),
)无障碍
面板是 role="menu",其中各项是 role="menuitem",summary 上带着 aria-haspopup。<details> 并不是原生的菜单控件,所以这只是一个合理的近似,而非完美方案——如果只是一串链接,把 nav 放进 details 里同样成立,而且承诺得更少。
属性
menu()——触发器的属性就是按钮那一套:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
trigger | Child | — | 可见文字。传文本,别传已经渲染好的 button()。 |
icon | Child | — | 放在文字前面的标记;单独给它就是纯图标触发器。 |
label | string | — | 无障碍名称。有图标且没有 trigger 文字时必填。 |
variant | 'solid' | 'soft' | 'outline' | 'ghost' | 'link' | 'outline' | 触发器的样式。 |
color | 'primary' | 'neutral' | 'success' | 'warning' | 'danger' | 'neutral' | 触发器取用哪一套配色。 |
size | 'sm' | 'md' | 'lg' | 'md' | 触发器的尺寸。 |
align | 'start' | 'end' | 'start' | 面板与触发器的哪一边对齐。 |
triggerClass | string | — | 额外的类加在触发器上,而不是外层 details 上。 |
menuItem():
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
href | string | — | 渲染成锚点;不给就是按钮。 |
icon | Child | — | 放在文字前面的标记。 |
as | string | 'button' | 没有 href 时渲染成哪个元素。 |
menuSeparator() 不接受任何属性——它就是分组之间的那道细线。