浮层
ContextMenu - 右键菜单
右键打开的菜单。
用法
什么时候用它
import { ContextMenu } from '@misoto22/folio'说明
Radix ContextMenu 的根节点和触发器,带类型的原样透传。
结构
| 部件 | 说明 |
|---|---|
| 触发区必填 | 次级点击会在其上打开菜单的那块区域。不写 asChild,Radix 就把 children 包进自己那个 inline 的 span,并在上面设 -webkit-touch-callout: none,免得系统的文本气泡先弹出来。 |
| 面板必填 | portal 出去的那个菜单,落点是指针的位置,而不是贴着触发区——这里没有 side 或 align 可设,只有那 8px 的碰撞内边距,把它留在视口或者 OverlayContainer 的框里面。 |
| 菜单项 | 一行,收的 icon、destructive 和 disabled 属性都和 DropdownMenuItem 一样——icon 两种写法都行——高亮也走同一个 data-highlighted。 |
| 分组标签 | 一行孤零零的等宽小标,而且只是视觉上的。真正给若干行当标题的是 ContextMenuGroup:它渲染 role="group",并把自己的 aria-labelledby 指向这一个。 |
| 分组 | 一个有名字的区段:role="group" 包住那些行,label 在它内部渲染成 Label,再通过 aria-labelledby 完成命名。 |
| 分隔线 | 组与组之间的一条发丝线,而且是真正的 role="separator"。 |
实践建议
推荐
- 写 asChild,把元素本身交给它:不写,Radix 就在你和你的子元素之间插进一个 span,成为 flex 项或 grid 项的是那个 span,而你的卡片是作为行内内容排在它里面的。
- 把同一份动作数组也做成一个藏在溢出按钮后面的 DropdownMenu——两者收的 icon、destructive 和 disabled 属性完全一样,所以一份列表喂两边,右键就成了快捷方式,而不是唯一那扇门。
- 可右键的区域住在一个会滚动、或者有边界的框里时,用 OverlayContainer 把这棵子树包住:这是唯一一个位置由读者亲手指定的面板,而对着一条他看不见的视口边翻过去,会把它落在他没有指的地方。
避免
- 别把「不支持触摸」当成事情的全部:对触摸和触控笔,Radix 是靠 700ms 长按打开菜单的,但指针一动就取消——所以在一份能滚动的列表上,长按和滚动手势彼此竞争,而通常是滚动赢。
- 光把一个 ContextMenuLabel 摆在几行上面,得到的是一张标题的图片:Radix 的 MenuLabel 是一个没有 role、也没有任何东西把它和后面内容绑起来的普通 div,于是看得见的读者眼里那几个分区,到另一边就是一份没有分段的列表。ContextMenuGroup 会把两半都渲染出来,并且接在一起。
- 这里 Radix 的 modal 默认同样是 true,所以菜单开着时,后面的页面被锁住滚动、指针也是惰性的——开在一份长列表上的右键菜单,会让列表在它底下动不了;这对一份短动作列表是对的,对任何需要读者滚动才能作答的东西都是错的。
示例
on a region
一次右键打开的菜单,位置定在指针处,而不是贴着触发器——这里没有 side 或 align 可设,只有那点把它留在框内的碰撞留白。永远不要让它成为够到某个动作的唯一途径:在平台支持的地方,Shift+F10 可以从键盘打开它,而 Radix 对触摸和触控笔会在长按 700ms 时打开、指针一动就取消,所以在一个可滚动的列表上,通常是滚动手势赢。那个分组标签是一个不带 role 的普通 div,和 DropdownMenu 的一样——一个视觉上的小标题,读屏软件一行行走过去时听不到它。
two doors
一个动作数组,两扇进去的门。ContextMenuItem 和 DropdownMenuItem 收的 icon、destructive 和 disabled 属性完全一致,所以同一份列表可以同时喂给两边,于是右键成了快捷方式、而不是唯一的入口——这很要紧,因为一个触摸用户、一个触控板用户和一个键盘用户,可能根本没有办法打开一个右键菜单。列表只写一次:两份靠手维护的副本,正是那个溢出按钮最后漏掉某人加进右键里的动作的经过。
kyoto-february.tif
a card trigger
asChild 把 Card 本身交给了 Radix。没有它,Radix 会在触发器和子元素之间插一个 span,而那个 span 会成为网格项,卡片则作为行内内容排在它里面——一个看起来坏掉了、而页面上没有任何东西指向原因的布局。这个菜单是模态的,所以打开期间背后的页面被锁住滚动、也不接受指针:对着一个对象的一小串动作,它是对的;对着任何需要读者滚动页面才能回答的东西,它是错的。
api.misoto22.com
live组成部分
在调用处组合,而不是通过 props 配置——所以这个组件没有预料到的布局,依然表达得出来。
ContextMenuContent
右键打开的那个菜单。
永远不能是通往某个动作的唯一路径。右键菜单靠指针的次要按键或者键盘上的菜单键打开,而相当一部分读者两者都没有——触屏用户、没配置过触控板的人、纯键盘操作的人。这里放的任何东西,都必须在别处也有能够得着的入口:一行的溢出按钮,或者一条工具栏。
这个组件没有自己的属性。
ContextMenuItem
一行。高亮跟着 data-highlighted,悬停和键盘都覆盖到了。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| destructive | boolean | false | 把这一行画成破坏性的。用于删除、吊销、断开连接。 |
| icon | RemixiconComponentType | ReactNode | 可选的前置图标。两种写法都行——`icon={Copy}` 传的是组件,尺寸由这里来定;`icon={<Copy size={16} />}` 传的是元素,位置由这里来放。 |
同时接受 ComponentProps<typeof ContextMenuPrimitive.Item> 里的全部属性,它们会直接透传给底层元素,不再逐条列出。
ContextMenuSeparator
分组之间的细线分隔。
这个组件没有自己的属性。
ContextMenuLabel
一条独立的等宽小标题。
只是视觉上的:Radix 把它渲染成一个没有 role 的光秃秃 <div>。当这条小标题是若干行的标题时,改用 ContextMenuGroup;这一个适合的是一条什么都不统领的文字。
这个组件没有自己的属性。
ContextMenuGroup
菜单里一段带名字的区块。
只有一条小标题时,那不过是一张标题的图片:Radix 的 MenuLabel 既不带 role,也不带 aria-labelledby 接线,于是看得见的读者看到的是若干区块,而读屏用户拿到的是一整条不分段的列表。这个组件会渲染出分组、把标签渲染在分组里面,并且让前者指向后者。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| label | ReactNode | 这些行上方的那条小标题,同时也是这一组的可访问名称。 |
同时接受 ComponentProps<typeof ContextMenuPrimitive.Group> 里的全部属性,它们会直接透传给底层元素,不再逐条列出。
再导出
ContextMenu = ContextMenuPrimitive.RootRadix ContextMenu 的根节点和触发器,带类型的原样透传。
ContextMenuTrigger = ContextMenuPrimitive.Trigger键盘操作
| 按键 | 作用 |
|---|---|
| ShiftF10 | 在平台支持的地方,用键盘打开菜单。 |
| ↑↓ | 在菜单项之间移动。 |
| Escape | 关闭它。 |