浮层
DropdownMenu - 下拉菜单
一组动作构成的菜单。
用法
什么时候用它
import { DropdownMenu } from '@misoto22/folio'说明
Radix DropdownMenu 的根节点和触发器,以带类型的原样透传再导出。
结构
| 部件 | 说明 |
|---|---|
| 触发器必填 | 直接透传给 Radix;不写 asChild,它就渲染自己那个光秃秃的 button。它带着 aria-haspopup 和 data-state,Button 能表现出自己的菜单是开着的,靠的就是这个。 |
| 面板必填 | portal 出去的那个菜单:至少 11rem 宽,离触发器 6px,以 8px 的内边距去撞视口或者 OverlayContainer 的框。它没有最大高度,所以它是翻边,不是滚动。 |
| 菜单项 | 一行。icon 两种写法都收——图标组件本身,在这里被定成 16px;或者一个渲染好的元素,你给什么就摆什么;destructive 把这一行画成 --danger;disabled 去掉指针事件并压低透明度。 |
| 分组标签 | 一行孤零零的等宽小标。纯视觉——Radix 把它渲染成一个普通 div,方向键会跳过它。真正给若干行当标题的是 DropdownMenuGroup:它渲染 role="group",并把自己的 aria-labelledby 指向这一个。 |
| 分组 | 一个有名字的区段:role="group" 包住那些行,label 在它内部渲染成 Label,再通过 aria-labelledby 完成命名。这套接线由组件来做,是因为调用方自己动手就得凭空造一个 id。 |
| 分隔线 | 组与组之间的一条发丝线,而且是真正的 role="separator"——菜单允许这么写,Command 面板赖以搭建的那个 listbox 不允许。 |
实践建议
推荐
- icon 要传组件——icon={Settings}——把定尺寸交给行本身,这样一个十行的菜单画出来的是十个同一尺寸的图标,而不是十个尺寸。元素那种写法现在也收;它过去和隔着一次 import 的 CommandItem 所要的东西恰好相反。
- 触发器要写 asChild 并交给它一个真正的 Button:不写,Radix 就渲染自己那个没有样式的 button,菜单最后挂在一个不属于这套系统的控件上。
- 要开对话框,就自己拿着它的 open 状态,并在这一项的 onSelect 里调 event.preventDefault()——选中一行会关掉菜单,而 Radix 的关闭会把焦点送回触发器,这一下发生在对话框已经接过焦点之后,读者刚进去就被拽了出来。
- 大概十几行就该停。面板没有最大高度,更长的菜单会一直长到撞上碰撞内边距,然后翻到触发器上方——SearchableMenu 就是同一份列表长过这里之后的样子。
避免
- Radix 的 modal 默认就是 true,这里没有任何东西改它,所以菜单开着的时候,后面的页面被锁住滚动、指针事件也关着——需要读者一边用一边翻页面的东西,不该放进菜单。
- 光把一个 DropdownMenuLabel 摆在几行上面,得到的是一张标题的图片:Radix 的 MenuLabel 是一个没有 role、也没有任何东西把它和后面内容绑起来的普通 div,于是看得见的读者眼里那几个分区,到另一边就是一份没有分段的列表。DropdownMenuGroup 会把两半都渲染出来,并且接在一起。
- 这个包的导出里没有复选项、没有单选项,也没有二级菜单——需要勾选状态的菜单只能直接从 @radix-ui/react-dropdown-menu import,而那样拿到的行,一点这个文件里的样式都没有。
示例
an account menu
一个动作菜单,而它也只该是这个:会导航的行属于 nav,会设定某个值的行是 Select 或 RadioGroup。有两处细节容易搞错。icon 收的是图标组件本身——icon={RiSettings3Line},绝不是 icon={<RiSettings3Line />}——而这和一个 import 之外的 CommandItem 恰好相反。还有,触发器需要 asChild 配一个真的 Button,否则 Radix 会渲染一个自己的、没有样式的 button,于是菜单挂在了一个不属于这套系统的控件上。那个分组标签是一个不带 role 的普通 div:它为眼睛把几行归拢在一起,而一个按 role 走菜单的读屏软件永远不会听到它。
an overflow button
行尾那个溢出控件,用在文字只会把这一行已经说过的话再说一遍的地方。触发器是一个 iconOnly 的 Button,所以 aria-label 不是可选项——没有它,这个控件会被播报成一个没有名字的按钮。align 用 end,因为面板至少 11rem 宽,否则会朝回展开、盖住它自己的触发器;它以 8px 的留白与外框碰撞,然后翻边而不是滚动,因为这个面板没有最大高度。此刻不可用的行要做成 disabled,而不是干脆不放,好让这份列表不会在两次访问之间改变形状。
kyoto-february.tif
where it opens
工具栏两端各一个菜单,各自对齐它所在的那一边。align 决定面板和触发器的哪一侧对齐,而面板至少 11rem 宽——所以一个位于末端、却留在默认 center 上的菜单,会朝回展开盖住自己的触发器,然后被那 8px 的碰撞留白推开。下面没地方时,side 会把整个面板翻到上面去,而一个很长的菜单也是同样的下场:这里没有最大高度,所以它会一直长到撞上为止。超过十来行,这份列表就不再能扫读了,而答案是 SearchableMenu——同样那些行,上面加一个过滤器——不是子菜单。
组成部分
在调用处组合,而不是通过 props 配置——所以这个组件没有预料到的布局,依然表达得出来。
DropdownMenuContent
一块菜单表面。
菜单是一串动作。如果这些项是去往某处的,它们属于导航;如果它们是在设置一个值,那是 Select 或 RadioGroup——这个包不提供勾选式或单选式的菜单项,而一个假装自己是选项的普通菜单项,会丢掉读屏软件需要的勾选状态。
同时接受 ComponentProps<typeof DropdownMenuPrimitive.Content> 里的全部属性,它们会直接透传给底层元素,不再逐条列出。
DropdownMenuItem
一个菜单行。
高亮由 Radix 的 data-highlighted 驱动,指针悬停和键盘聚焦都覆盖到了——只写 :hover 会让键盘用户看不见自己在哪。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| destructive | boolean | false | 把这一行画成破坏性的。用于删除、吊销、断开连接。 |
| icon | RemixiconComponentType | ReactNode | 可选的前置图标,渲染在标签之前。 两种写法都行:`icon={Settings}` 传的是组件,尺寸由这里来定;`icon={<Settings size={16} />}` 传的是元素,位置由这里来放。这两者过去在只隔一行 import 的地方含义正好相反——`CommandItem.icon` 收元素,而这个收组件——传错了,类型检查报不出任何可据以行动的东西,它是在渲染时才炸。 |
同时接受 ComponentProps<typeof DropdownMenuPrimitive.Item> 里的全部属性,它们会直接透传给底层元素,不再逐条列出。
DropdownMenuSeparator
菜单分组之间的细线分隔。
这个组件没有自己的属性。
DropdownMenuLabel
一条独立的等宽小标题。
只是视觉上的:Radix 把它渲染成一个没有 role 的光秃秃 <div>,所以它只为看得见的读者分隔菜单,别人一概沾不上。当这条小标题是若干行的标题时,改用 DropdownMenuGroup;这一个适合的是一条什么都不统领的文字——比如账号菜单顶上那个已登录的邮箱地址。
这个组件没有自己的属性。
DropdownMenuGroup
菜单里一段带名字的区块。
只有一条小标题时,那不过是一张标题的图片。Radix 的 MenuLabel 既不带 role,也不带 aria-labelledby 接线;而真正带着 role="group" 的 MenuGroup,这个包压根就没再导出过——于是看得见的读者看到的是三个带标题的区块,而读屏用户拿到的是一整条不分段的列表。这个组件会渲染出分组、把标签渲染在分组里面,并且让前者指向后者,这就是这次修复的全部,而不该是调用方需要自己记住的事。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| label | ReactNode | 这些行上方的那条小标题,同时也是这一组的可访问名称。 |
同时接受 ComponentProps<typeof DropdownMenuPrimitive.Group> 里的全部属性,它们会直接透传给底层元素,不再逐条列出。
再导出
DropdownMenu = DropdownMenuPrimitive.RootRadix DropdownMenu 的根节点和触发器,以带类型的原样透传再导出。
DropdownMenuTrigger = DropdownMenuPrimitive.Trigger键盘操作
| 按键 | 作用 |
|---|---|
| EnterSpace↓ | 打开菜单并落到第一项。 |
| ↑↓ | 在菜单项之间移动。 |
| a–z | 跳到下一个以该字母开头的项。 |
| Escape | 关闭菜单并把焦点还给触发器。 |
无障碍
- 在有边界的框里——设备预览、内嵌控制台——用 `<OverlayContainer container={el}>` 包住这棵子树。面板会渲染进那个元素,按它的边界翻转,而不是按视口;框上设的 `dir` 和 `data-density` 也就跟着生效了。
- 高亮由 data-highlighted 驱动,同时覆盖悬停和键盘焦点——只写 :hover 会让键盘用户看不见自己在哪。
- DropdownMenuGroup 是同时为读屏软件和眼睛把菜单分段的;光一个 Label 只为眼睛分。