菜单

菜单就是一个带样式面板的 <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()——触发器的属性就是按钮那一套:

属性类型默认值说明
triggerChild—可见文字。传文本,别传已经渲染好的 button()。
iconChild—放在文字前面的标记;单独给它就是纯图标触发器。
labelstring—无障碍名称。有图标且没有 trigger 文字时必填。
variant'solid' | 'soft' | 'outline' | 'ghost' | 'link''outline'触发器的样式。
color'primary' | 'neutral' | 'success' | 'warning' | 'danger''neutral'触发器取用哪一套配色。
size'sm' | 'md' | 'lg''md'触发器的尺寸。
align'start' | 'end''start'面板与触发器的哪一边对齐。
triggerClassstring—额外的类加在触发器上,而不是外层 details 上。

menuItem():

属性类型默认值说明
hrefstring—渲染成锚点;不给就是按钮。
iconChild—放在文字前面的标记。
asstring'button'没有 href 时渲染成哪个元素。

menuSeparator() 不接受任何属性——它就是分组之间的那道细线。