Skip to content

Menu 菜单 ​

由 Menu 与 MenuItem 组合而成:Menu 是弹出层容器,每个 MenuItem 描述一个菜单项。

  • 菜单面板渲染到 body,底色 bg-ds-elevated、阴影 shadow-ds-2、圆角 rounded-ds-sm、上下内边距 py-2——与 Select 的弹层同一套规格。
  • 触发元素由 #trigger 插槽描述,也可以省略插槽、用 anchor 指定贴靠元素,或用 x / y 直接定位(上下文菜单)。
  • MenuItem 支持前置图标、辅助说明、尾部快捷键 / 自定义内容、禁用、勾选态。
  • 菜单项里放 #submenu 插槽即成为二级菜单,悬停、点击或 → 展开。
  • 键盘遵循 ARIA menu 模式:方向键、Home / End 移动焦点,→ / Enter 进入子菜单,← 返回上级,Escape 关闭整棵菜单。

基础用法 ​

#trigger 插槽里的元素会被自动补上 aria-haspopup / aria-expanded,点击即可展开;Menu 的 select 事件带上被选中项的标识(value,省略时是该项的索引)。

当前选中:—

vue
<script setup lang="ts">
import { ref } from "vue";
import { Button, Menu, MenuItem } from "@sharpdotnut/dotsharp";
import deleteIcon from "@iconify-icons/material-symbols/delete";
import redo from "@iconify-icons/material-symbols/redo";
import undo from "@iconify-icons/material-symbols/undo";

const picked = ref<string | number>("—");
</script>

<template>
  <Menu @select="picked = $event">
    <template #trigger>
      <Button variant="tonal">更多操作</Button>
    </template>
    <MenuItem label="撤销" :icon="undo" shortcut="⌘Z" />
    <MenuItem label="重做" :icon="redo" shortcut="⇧⌘Z" disabled />
    <MenuItem label="删除" :icon="deleteIcon" />
  </Menu>
</template>

Menu 的属性与事件透传到菜单面板;触发元素自己的类名与事件照常写在自己的标签上。

受控展开 ​

Menu 用 v-model:open 暴露展开状态,需要在代码里开关时绑定它:

vue
<script setup lang="ts">
import { ref } from "vue";
import { Button, Menu, MenuItem } from "@sharpdotnut/dotsharp";
import settings from "@iconify-icons/material-symbols/settings";

const open = ref(false);
</script>

<template>
  <Menu v-model:open="open">
    <template #trigger>
      <Button>更多操作</Button>
    </template>
    <MenuItem label="设置" :icon="settings" />
  </Menu>
</template>

点击外部、按 Escape、选中菜单项都会写回 open。

图标、说明文字与快捷键 ​

  • icon:前置图标,与 Icon 的 icon 同源;
  • description:主文本下方的辅助说明,自动切成两行排版;
  • shortcut:尾部快捷键文本;
  • #trailing 插槽:角标、自定义元素等尾部内容,排在快捷键之后。
vue
<Menu>
  <template #trigger>
    <Button variant="outlined">文件</Button>
  </template>
  <MenuItem label="创建副本" :icon="contentCopy" description="保存到同一文件夹" shortcut="⌘D" />
  <MenuItem label="重命名" :icon="create">
    <template #trailing>
      <span class="rounded-ds-sm bg-ds-tonal px-1.5 py-0.5 text-xs text-ds-on-tonal">新</span>
    </template>
  </MenuItem>
  <MenuItem label="分享" :icon="share" disabled />
</Menu>

需要完全自定义行内容时用默认插槽,插槽优先于 label:

vue
<MenuItem value="custom">
  <span class="font-medium">保存全部</span>
</MenuItem>

勾选态 ​

给 checked 一个布尔值,菜单项即成为 role="menuitemcheckbox",前置位置显示勾选标记(checked 为 false 时保留空位,文字不会跳动)。勾选标记优先于 icon。

可勾选项与普通菜单项的区别在点击后的收尾:切换勾选态后菜单保持展开,方便连续开关多项;普通菜单项选中即收起整棵菜单。

离线模式:开

vue
<script setup lang="ts">
import { ref } from "vue";
import { Button, Menu, MenuItem } from "@sharpdotnut/dotsharp";

const offline = ref(true);
</script>

<template>
  <Menu>
    <template #trigger>
      <Button variant="tonal">偏好设置</Button>
    </template>
    <MenuItem label="离线模式" :checked="offline" shortcut="⌘O" @select="offline = !offline" />
    <MenuItem label="显示网格" :checked="false" disabled />
  </Menu>
</template>

分隔线 ​

菜单里可直接放 Divider(以及任何普通元素,例如分组标题):

vue
<Menu>
  <template #trigger>
    <Button variant="outlined">整理</Button>
  </template>
  <MenuItem label="撤销" :icon="undo" shortcut="⌘Z" />
  <MenuItem label="重做" :icon="redo" shortcut="⇧⌘Z" />
  <Divider class="my-2" />
  <MenuItem label="设置" :icon="settings" />
  <MenuItem label="帮助与反馈" :icon="help" />
</Menu>

二级菜单 ​

MenuItem 的 #submenu 插槽里放菜单项,该行尾部自动出现展开箭头。子菜单贴在本项右侧、上对齐,空间不足时翻到左侧。

vue
<Menu>
  <template #trigger>
    <Button variant="tonal">新建</Button>
  </template>
  <MenuItem label="创建" :icon="create">
    <template #submenu>
      <MenuItem label="文档" :icon="description" />
      <MenuItem label="图片" :icon="image" />
      <MenuItem label="幻灯片" :icon="slideshow" />
    </template>
  </MenuItem>
  <MenuItem label="下载" :icon="download">
    <template #submenu>
      <MenuItem label="原始文件" />
      <MenuItem label="PDF" />
    </template>
  </MenuItem>
  <Divider class="my-2" />
  <MenuItem label="分享" :icon="share" />
</Menu>
  • 指针悬停 120ms 展开,移出菜单项后 200ms 收起;
  • 点击本项(或 → / Enter)展开,焦点进入子菜单第一项;
  • 子菜单里的 ← 收起到父菜单项,Escape 同样只收起这一级;
  • 选中任意层级的菜单项会收起整棵菜单,并把焦点还给最初的触发元素。

贴靠元素定位 ​

不使用 #trigger 时,用 anchor 指定贴靠元素(通常给触发按钮套一层带 ref 的容器)。placement 控制方向:bottom-start(默认)、bottom-end、right-start。

当前选中:—

vue
<script setup lang="ts">
import { ref } from "vue";
import { Button, Menu, MenuItem } from "@sharpdotnut/dotsharp";
import contentCopy from "@iconify-icons/material-symbols/content-copy";
import deleteIcon from "@iconify-icons/material-symbols/delete";

const anchor = ref<HTMLElement | null>(null);
const open = ref(false);
</script>

<template>
  <span ref="anchor" class="inline-flex">
    <Button variant="outlined" @click="open = !open">贴靠定位</Button>
  </span>
  <Menu v-model:open="open" :anchor="anchor">
    <MenuItem label="复制" :icon="contentCopy" shortcut="⌘C" />
    <MenuItem label="删除" :icon="deleteIcon" shortcut="⌫" />
  </Menu>
</template>

键盘与可访问性 ​

  • 面板是 role="menu",菜单项是 role="menuitem",可勾选项是 role="menuitemcheckbox"(同步 aria-checked)。
  • 打开后焦点进入第一个可用菜单项;↑ / ↓ 循环移动,Home / End 跳到两端,禁用的菜单项不参与。
  • → 展开子菜单,← 收起子菜单,Escape 逐级收起并把焦点交还(子菜单还给父菜单项,根菜单还给触发元素)。
  • Tab 关闭菜单,不拦截焦点离开;鼠标点击菜单外部同样关闭。
  • 面板贴边时自动翻转 / 夹到视口内,并在高度不足时滚动。

API ​

属性类型默认值说明
v-model:openbooleanfalse菜单是否展开
anchorHTMLElement | nullnull贴靠元素;省略时使用 #trigger 里的触发元素
placement"bottom-start" | "bottom-end" | "right-start""bottom-start"相对锚点的展开方向,空间不足时自动翻转
xnumber—视口 x 坐标;给出时忽略 anchor(上下文菜单使用)
ynumber—视口 y 坐标
autoFocusbooleantrue展开时是否把焦点移入第一个可用菜单项
  • 插槽:default(菜单项与分隔线等)、trigger(触发元素,可选)。
  • 事件:select(value),value 是被选中项的 value 或索引。子菜单里的选中同样冒泡到根 Menu。
  • 可勾选项(checked 为布尔值)触发 select 后不收起菜单,普通菜单项会收起。
  • 暴露:focusFirst(),把焦点移到第一个可用菜单项(子菜单内部使用)。
  • 根级属性与事件透传到菜单面板。
属性类型默认值说明
labelstring—菜单项文本;存在默认插槽时被插槽覆盖
iconIconifyIcon | string—前置图标,尺寸 1.5em 跟随字号
descriptionstring—主文本下方的辅助说明,切换为两行排版
shortcutstring—尾部快捷键文本
valuestring | number所在位置索引菜单项标识,select 事件携带
checkedboolean—给出布尔值即可勾选:role="menuitemcheckbox",显示勾选标记
disabledbooleanfalse禁用:不参与键盘移动,点击 / 悬停不响应
  • 插槽:default(行内容,优先于 label)、leading(前置内容,优先于 icon 与勾选标记)、trailing(尾部内容,排在快捷键之前)、submenu(子菜单的菜单项)。
  • 事件:select(value);同时冒泡到所属 Menu。
  • 点击可勾选项只切换勾选态,菜单保持展开;点击普通项收起整棵菜单。
  • 根元素是原生 button,其余属性与事件透传到该按钮。
  • 单行高度由 px-3 py-2 与 leading-5 决定(约 36px);紧凑用法可用 class="py-1" 之类调整,但会牺牲触控面积。

子项约定 ​

Menu / MenuItem 沿用父子组件以及插槽统一范式的取值规则:value 省略时使用 Menu 注入的渲染顺序索引, v-for 与 v-if 会按最终顺序重新编号;label 只做纯文本回退,默认插槽优先。