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
Menu
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| v-model:open | boolean | false | 菜单是否展开 |
| anchor | HTMLElement | null | null | 贴靠元素;省略时使用 #trigger 里的触发元素 |
| placement | "bottom-start" | "bottom-end" | "right-start" | "bottom-start" | 相对锚点的展开方向,空间不足时自动翻转 |
| x | number | — | 视口 x 坐标;给出时忽略 anchor(上下文菜单使用) |
| y | number | — | 视口 y 坐标 |
| autoFocus | boolean | true | 展开时是否把焦点移入第一个可用菜单项 |
- 插槽:
default(菜单项与分隔线等)、trigger(触发元素,可选)。 - 事件:
select(value),value是被选中项的value或索引。子菜单里的选中同样冒泡到根Menu。 - 可勾选项(
checked为布尔值)触发select后不收起菜单,普通菜单项会收起。 - 暴露:
focusFirst(),把焦点移到第一个可用菜单项(子菜单内部使用)。 - 根级属性与事件透传到菜单面板。
MenuItem
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| label | string | — | 菜单项文本;存在默认插槽时被插槽覆盖 |
| icon | IconifyIcon | string | — | 前置图标,尺寸 1.5em 跟随字号 |
| description | string | — | 主文本下方的辅助说明,切换为两行排版 |
| shortcut | string | — | 尾部快捷键文本 |
| value | string | number | 所在位置索引 | 菜单项标识,select 事件携带 |
| checked | boolean | — | 给出布尔值即可勾选:role="menuitemcheckbox",显示勾选标记 |
| disabled | boolean | false | 禁用:不参与键盘移动,点击 / 悬停不响应 |
- 插槽:
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 只做纯文本回退,默认插槽优先。