Skip to content

Menu 下拉菜单 ​

点触发元素,在它旁边弹出一列命令:编辑 / 复制 / 删除、切换视图、账户操作。选一项就执行,菜单随即关闭。 要从一组值里选定一个值并显示在控件上用 Select;弹出的是表单或说明卡片用 Popover。 在一块区域上右键弹出的菜单用 ContextMenu,下面这些菜单内容元素两者通用。

基础用法

触发元素放进 trigger 插槽;菜单项的 end 插槽放快捷键提示,pt-menu-label 与 pt-separator 给项分段。选中一项后菜单关闭。

<pt-menu aria-label="文件">
  <pt-button slot="trigger" variant="secondary">文件</pt-button>
  <pt-menu-label>文件</pt-menu-label>
  <pt-menu-item value="new">
    新建
    <span slot="end">⌘N</span>
  </pt-menu-item>
  <pt-menu-item value="save" disabled>
    保存
    <span slot="end">⌘S</span>
  </pt-menu-item>
  <pt-separator></pt-separator>
  <pt-menu-item value="delete">删除</pt-menu-item>
</pt-menu>
勾选项与单选组

type="checkbox" 的项选中时切换 checked;type="radio" 的项放进 type="radio" 的 pt-menu-group,由组的 value 决定选中哪一项。

<pt-menu aria-label="视图">
  <pt-button slot="trigger" variant="secondary">视图</pt-button>
  <pt-menu-item type="checkbox" checked>状态栏</pt-menu-item>
  <pt-menu-item type="checkbox">活动栏</pt-menu-item>
  <pt-separator></pt-separator>
  <pt-menu-group type="radio" value="comfortable" label="密度">
    <pt-menu-item type="radio" value="compact">紧凑</pt-menu-item>
    <pt-menu-item type="radio" value="comfortable">默认</pt-menu-item>
  </pt-menu-group>
</pt-menu>
子菜单

pt-menu-sub 的 trigger 插槽是这一行的文字,默认插槽是子菜单的内容。指针停留或按 → 打开,← / Esc 回到上一层。

<pt-menu align="start" aria-label="分享">
  <pt-button slot="trigger" variant="secondary">分享</pt-button>
  <pt-menu-item value="link">复制链接</pt-menu-item>
  <pt-menu-sub>
    <span slot="trigger">发送给</span>
    <pt-menu-item value="email">邮件</pt-menu-item>
    <pt-menu-item value="message">消息</pt-menu-item>
  </pt-menu-sub>
</pt-menu>

组成 ​

元素作用
pt-menu根:trigger 插槽放触发元素,默认插槽放菜单内容
pt-menu-item一项。type 为 default(命令)/ checkbox(勾选项)/ radio(单选项)
pt-menu-group一组项,可带组标题(label);type="radio" 时管理组内单选项的选中态
pt-menu-label一行不可选的小标题
pt-menu-sub子菜单:trigger 插槽是这一行的文字,默认插槽是子菜单的内容,可以再嵌 pt-menu-sub
pt-separator分隔线,直接放进菜单或组里即可(菜单会把它的颜色换成更淡的 bg-secondary,并贯通到面板两边)

菜单项的 start 插槽放 16px 图标,end 插槽放快捷键提示(⌘K)。没有图标、也不是勾选项的项想与它们的文字对齐, 加 inset(pt-menu-label 与 pt-menu-sub 同样支持)。

选中与拦截 ​

点击一项、或聚焦时按 Enter / Space,该项先发 ptSelect(冒泡,detail 为它的 value), 然后整棵菜单关闭。ptSelect 是可取消的:event.preventDefault() 后菜单保持打开 —— 常用于勾选项, 让用户连续勾好几项再关(对应 Radix 在 onSelect 里 preventDefault())。

在菜单上统一监听,按 detail 分派命令:<PtMenu onPtSelect={…}>(React)、<PtMenu @pt-select="…">(Vue)、 <pt-menu (ptSelect)="…">(Angular)、原生 HTML 里 menu.addEventListener('ptSelect', …)。ptSelect 由菜单项发出、 冒泡上来,pt-menu 自己不发,只是声明了同名事件好让包装层生成绑定 —— 所以每次选择只收到一次,event.target 是那个菜单项, 在菜单上 preventDefault() 同样让菜单保持打开。写在单个 PtMenuItem 上也照样可以。

类型提示:生成的事件类型把 event.target 标成 pt-menu(Stencil 按声明事件的元素定 target 类型),运行时它是菜单项。 要读菜单项的属性时写 event.target as HTMLPtMenuItemElement;只按 detail 分派命令则不受影响。

  • 勾选项(type="checkbox"):选中时切换自己的 checked 并发 ptChange(detail 为新的 checked)。
  • 单选项(type="radio"):放进 type="radio" 的 pt-menu-group,选中态由组按它的 value 管理; 用户选了另一项时组改写 value 并发 ptChange(detail 为新的 value)。组登记在受控值清单里, Vue 可以 v-model、Angular 可以 [(ngModel)]。
  • 禁用项(disabled)可以用方向键停留(读屏能读到它),但不会被选中。

pt-menu-item 与 pt-menu-group 的 ptChange 都不冒泡:两者同名、detail 类型不同(布尔 / 字符串), 勾选项又常常放在组里,冒泡的话组上的 onPtChange 会收到子项的布尔值;组还能经子菜单嵌套,内层的选择会改写外层的绑定。 要在外层统一监听,用捕获阶段(addEventListener('ptChange', fn, true))。

打开、关闭与焦点 ​

  • 点触发元素切换。打开后焦点落在菜单面板上,此时按 ↓ 到第一项、↑ 到最后一项。
  • 触发元素聚焦时按 ↓ / Enter / Space 打开并聚焦第一项,按 ↑ 打开并聚焦最后一项。
  • 菜单里 ↑ ↓ 在项之间移动、到头回绕,Home / End 跳首尾, 按字母跳到以它开头的项。指针悬停在哪一项,焦点就跟到哪一项,键盘接着从那里走。
  • 以下操作关闭菜单并发 ptClose,detail.reason 分别是:选中一项 select、按 Esc escape、 在外面按下指针 outside、再点一次触发元素 trigger、按 Tab tab。
  • 关闭时焦点若在菜单里,回到触发元素;点外面关闭时不抢焦点;按 Tab 时焦点照常移到触发元素之后的元素 (Shift+Tab 停在触发元素上)。
  • ptOpen 在面板每次渲染出来之后发;外部把 open 写成 false 只关闭,不发 ptClose(与 Popover / Modal 一致)。

与其它弹层共用层栈:Modal 里的菜单按 Esc 只关菜单,第二下才关 Modal。

子菜单 ​

  • 指针停在子菜单那一行上约 100ms 打开,点击立即打开;触发行聚焦时按 → / Enter / Space 打开并聚焦子菜单的第一项。
  • 子菜单里按 ← 或 Esc 只关子菜单,焦点回到那一行。RTL 下左右方向键对调,子菜单开在左侧。
  • 选中子菜单里的项,整棵菜单一起关闭。同一层同时只开一个子菜单。
  • 指针从那一行斜着移进子菜单时,途中扫过父菜单的其它项不会立刻把子菜单关掉:离开后约 300ms 才关,宽限期内进了子菜单就取消。 已知限制:没有做 Radix 的「安全三角形」判定,慢慢斜移并在别的项上停超过 300ms 时子菜单会先关掉。

位置 ​

placement 是首选方位(默认 bottom),视口放不下时翻到对面;align 是交叉轴对齐(默认 center,与源组件 DropdownMenu 一致),offset 是面板与触发元素的间距(默认 4px)。子菜单固定贴着那一行的行尾一侧、顶边对齐,放不下时翻到另一侧。

面板经 popover API 进顶层:放在 overflow: hidden 的容器、表格单元格或带 transform 的祖先里都不会被裁切。 窄屏下不切换成底部抽屉(与 Select / Popover 不同):菜单项少、面板窄,贴着触发元素弹出在手机上照样好点。

无障碍 ​

按 WAI-ARIA Menu Button 模式:触发元素上由组件维护 aria-haspopup="menu" 与 aria-expanded; 面板 role="menu",用 aria-label 给它一个名称(包装层里属性名是 ariaLabelText)。菜单项宿主本身就是可聚焦的 menuitem / menuitemcheckbox / menuitemradio(后两种带 aria-checked),不进 Tab 序列,由方向键移动。

面板在 pt-menu 的影子树里、触发元素在 light DOM,IDREF 形式的 aria-controls 跨不过影子边界,所以组件不写这个 attribute,而是在支持 ARIA 元素反射的浏览器里把触发元素的 ariaControlsElements 指向面板。 子菜单那一行与子面板在同一棵影子树里,aria-controls / aria-labelledby 照常生效。

API ​

属性

属性Attribute类型默认值说明
alignalign"center" | "end" | "start"'center'交叉轴上的对齐:top / bottom 时 start 是行首一侧(RTL 下是右),left / right 时 start 是上。 默认 center(源包 DropdownMenuContent 未设 align,取 Radix 的默认值)
ariaLabelTextaria-labelstring | undefined—菜单面板(role="menu")的无障碍名称:原生 HTML 里写 aria-label, React / Vue / Angular 包装层里属性名是 ariaLabelText。
offsetoffsetnumber4面板与触发元素之间的距离(px)
openopenbooleanfalse是否展开。用户交互时组件自己改写;外部直接赋值立即生效,不发 ptClose
placementplacement"bottom" | "left" | "right" | "top"'bottom'相对触发元素的方位。视口放不下时翻到对面,反射的属性仍是这里的首选值

事件

事件detail 类型说明
ptClose{ reason: PtMenuCloseReason; }用户交互关闭后触发,detail 说明是怎么关的。外部把 open 写成 false **不发** **不冒泡**:弹层常互相嵌套(弹窗里的菜单、气泡卡片里的下拉),冒泡的话外层上的监听会收到内层的开关。
ptOpenvoid面板渲染出来之后发(交互或属性打开都发,与 pt-popover 一致) **不冒泡**:弹层常互相嵌套(弹窗里的菜单、气泡卡片里的下拉),冒泡的话外层上的监听会收到内层的开关。
ptSelectstring | undefined某个菜单项被选中,detail 为它的 `value`,`event.target` 是那个菜单项。 这是 `pt-menu-item` 发出、冒泡上来的事件,本元素**自己不发**;声明在这里只为让 React / Vue / Angular 包装层生成绑定(`onPtSelect` / `@pt-select` / `(ptSelect)`),可以在菜单上统一监听,不必写在每一项上。 在这里 `preventDefault()` 同样阻止菜单关闭(菜单项在事件派发完之后才判断要不要关)。 静态类型的限制:Stencil 生成的 `PtMenuCustomEvent` 把 `target` 标成声明事件的元素(本元素),运行时却是菜单项; 要读菜单项时写 `event.target as HTMLPtMenuItemElement`。只按 `detail` 分派命令则无需关心。

插槽

名称说明
(默认)菜单内容:`pt-menu-item`、`pt-menu-group`、`pt-menu-label`、`pt-menu-sub`、`pt-separator`
trigger触发元素,点击切换展开(通常是 `pt-button` 或 `<button>`)

可定制的内部元素(::part())

part说明
panel菜单面板(role="menu")

pt-menu-item ​

属性

属性Attribute类型默认值说明
checkedcheckedbooleanfalse是否勾选,checkbox / radio 型才有意义。checkbox 型选中时组件自己切换它并发 `ptChange`; radio 型由所属 `pt-menu-group` 按它的 `value` 回写,不要手动设置
disableddisabledbooleanfalse禁用:方向键仍可停留(读屏能读到它),但点击、Enter / Space 都不会选中
insetinsetbooleanfalse行首缩进 32px,与带勾选指示位的项、带图标的项对齐文字
typetype"checkbox" | "default" | "radio"'default'类型:普通项 / 勾选项 / 单选项(单选项放在 `type="radio"` 的 pt-menu-group 里)
valuevaluestring | undefined—这一项的值:`ptSelect` 的 detail;radio 型时与所属组的 `value` 比较决定是否选中

事件

事件detail 类型说明
ptChangebooleancheckbox 型被用户切换后触发,detail 为新的 `checked`。外部改 `checked` 不发。 **不冒泡**(与其它组件的默认相反):`pt-menu-group` 也有同名的 `ptChange`(type="radio" 时,detail 是字符串), 勾选项常常就放在组里。冒泡的话组上的 `onPtChange`(React)/ `@pt-change`(Vue)会收到子项的布尔值, 双向绑定挂在组上时还会被写坏。要在外层统一监听,用捕获阶段(`addEventListener('ptChange', fn, true)`)。
ptSelectstring | undefined选中时触发(点击,或聚焦时按 Enter / Space),detail 为 `value`。冒泡:pt-menu / pt-context-menu 声明了同名事件,可以在菜单上统一监听(框架包装层里同样能绑)。 可取消:`event.preventDefault()` 后菜单不关闭(勾选项仍会切换)。

插槽

名称说明
(默认)菜单项文字
end行尾的快捷键提示,如 `⌘K`
start文字前的图标(16px)

可定制的内部元素(::part())

part说明
base菜单项这一行
indicatorcheckbox / radio 型的勾选指示位

pt-menu-group ​

属性

属性Attribute类型默认值说明
labellabelstring | undefined—组标题(样式同 `pt-menu-label`),同时是这一组的无障碍名称。不给时不渲染标题行
typetype"default" | "radio"'default'类型:`default` 只是分组;`radio` 时组内的单选项按 `value` 互斥选中
valuevaluestring''`type="radio"` 时选中项的 `value`。用户选中时组件自己改写它并发 `ptChange`;外部直接赋值不发

事件

事件detail 类型说明
ptChangestring`type="radio"` 时用户选中了另一项后触发,detail 为新的 `value`。外部改 `value`、再选一次已选中的项都不发。 **不冒泡**(与其它组件的默认相反):组能经子菜单嵌套(组 → pt-menu-sub → 另一个组),而三个框架的双向绑定 都挂在宿主上 —— Vue 的 v-model 只按 tagName 过滤冒泡事件,内外层同是 PT-MENU-GROUP 拦不住, 冒泡的话选中内层的项会连带改写外层绑定。要在外层统一监听,用捕获阶段(`addEventListener('ptChange', fn, true)`)。

方法

方法说明
refresh() => Promise<void>按 `value` 重新同步单选项的选中态。组里的项增删、改 value 时组件自己会调,一般不需要手动调用

插槽

名称说明
(默认)这一组的菜单项(可夹 `pt-separator`)

可定制的内部元素(::part())

part说明
base分组容器(role="group")
label组标题

pt-menu-label ​

属性

属性Attribute类型默认值说明
insetinsetbooleanfalse行首缩进 32px,与带勾选指示位或 `inset` 的菜单项文字对齐

插槽

名称说明
(默认)标题文字

可定制的内部元素(::part())

part说明
base标题行

pt-menu-sub ​

属性

属性Attribute类型默认值说明
disableddisabledbooleanfalse禁用:触发行仍可用方向键停留,但不会打开子菜单
insetinsetbooleanfalse触发行行首缩进 32px,与带勾选指示位或 `inset` 的菜单项文字对齐
openopenbooleanfalse子菜单是否展开。由菜单在交互时维护(外层菜单关闭时一并收起),一般不需要手动设置

插槽

名称说明
(默认)子菜单的内容
trigger触发行的文字(可带图标)

可定制的内部元素(::part())

part说明
chevron触发行尾部的箭头
panel子菜单面板(role="menu")
trigger触发行(role="menuitem")

Apache-2.0 协议开源