跳到正文
册页

浮层

Command - 命令面板

可筛选的动作列表——⌘K 那个面。

用法

TSX
import { Command } from '@misoto22/folio'

说明

可筛选的动作列表——⌘K 那个面。

建在 cmdk 上,而不是建在 Select 或菜单上,因为这个交互两者都不是:列表随你打字实时筛选,高亮行跟着方向键走而焦点留在输入框里,回车执行高亮那一行。这就是 ARIA combobox 模式,也是最不该自己手搓的部分——“aria-activedescendant 在动而焦点不动”,恰恰是自制面板在读屏软件下失灵的那一处。

结构

Command anatomy
部件说明
输入框必填CommandInput,也就是那个 combobox,带一个搜索字形,底下一条发丝线。它的高度是写死的 52px,不是 --control-h 那类 token,所以它不跟着 data-density 走。
列表必填CommandList,内联时高度上限 18rem,放进 CommandDialog 里抬到 26rem,靠自己那条六像素的发丝滚动条滚动。
行一行:一个图标元素、标签、一条可选的附注,以及印成 Kbd 的快捷键。高亮那一行拿的是 --accent-muted 加行首一道强调色竖线,因为只靠一块填充,在滚动中很难抓住。
空状态CommandEmpty。只有树里真的存在一个空状态,cmdk 才会渲染它——所以没有它,一次没匹配上的筛选留下的是输入框底下一条空白。
页脚CommandFooter 和它里面的 CommandHint 行。命令面板是一块键盘界面,而它的按键是看不见的;这里是唯一会把它们印出来的地方。

实践建议

推荐

  • 要用就用 CommandDialog,不要用光秃秃的 Command。根节点只是一个内联的带边框盒子,没有遮罩、没有焦点陷阱,自己也不处理 Escape——所以只用它搭起来的面板会一直开着,直到别的东西来关它。
  • 永远要渲染 CommandEmpty。只有它在场,cmdk 才会显示空状态——所以漏掉它的面板,对一次什么都没匹配到的筛选给出的回答,是一个输入框加一条空白,外加零解释。
  • 字形需要自己的尺寸或颜色时,给 CommandItem 传元素——icon={<Search size={16} />};不需要时就传组件——icon={Search}。两种写法在这里和在 DropdownMenuItem 里都行;它们过去隔着一次 import,彼此恰好相反。
  • ⌘K 要自己绑,然后再把它印出来:这个组件里没有任何东西在监听按键,所以在应用加上 keydown 处理之前,这个面板根本没有快捷键;而读者是从 CommandFooter 里知道它存在的。

避免

  • 除非那个值正是读者会打出来的东西,否则别给 CommandItem 传 value——cmdk 优先按 value 过滤,只有在没有 value 时才退回到行自己的文字,所以把一个 id 当 value 传进去,会让看得见的那个标签搜不到。id 必须留作 value 时,把标签里的那些词当作 keywords 传进去,SearchableMenu 就是这么做的。
  • CommandDialog 是带着 hideTitle 和 showClose={false} 渲染的,所以这个面板既没有可见标题,也没有可见的关闭:出口只有 Escape 和遮罩,两个都不会自报家门。把 Escape 印在 CommandFooter 里,别假定人人都知道。
  • 别指望这个面板跟着 data-density 走——CommandInput 是写死的 52px,不是 --control-h 那类 token,所以在 compact 下,应用里其他控件全都缩了,只有它这个输入框纹丝不动。

示例

inline

命令面板的各个部件,摊平在行内,好让它们同时可见:那个 combobox 输入框、带分组和分隔线的过滤列表,以及印着按键的页脚。在应用里要拿 CommandDialog,而不是这个光秃秃的根——这个根是一个行内的带边框盒子,没有遮罩、没有焦点陷阱、自己也不处理 Escape,所以拿它搭出来的面板会一直开着,直到别的什么东西把它关掉。永远渲染 CommandEmpty:cmdk 只有在树里存在一个空状态时才会显示它,所以一个没有它的面板,对一次匹配不上的过滤,回应的是一条空白的带子。另外注意,这里的 icon 属性收的是一个元素,和一个 import 之外的 DropdownMenuItem 正好相反。

↑↓navigate↵runescclose

dialog

命令面板几乎总是被使用的那个样子:模态、偏上居中,由这个例子自己绑定的一个按键打开。组件里没有任何东西在监听按键——只有应用加上处理器之后,这个快捷键才存在——而 CommandDialog 是带着 hideTitle 和 showClose={false} 渲染的,所以既没有可见的标题,也没有可见的关闭。Escape 和遮罩是仅有的两个出口,而且两个都不会自报家门,这也是为什么这里把快捷键印在页面上,以及为什么一个真实的面板会带一个 CommandFooter。这个处理器在 defaultPrevented 时会退出,好让它不跟另一个已经绑了同样按键的面板打架。

or press ⌘ K

values and keywords

cmdk 是按条目的 value 过滤的,只有在没有 value 时才退回用这一行自己的文字——所以把一个内部 id 当成 value 传进去,会让看得见的标签搜不出来,而这正是一个面板带着毛病交付出去最常见的方式。这里每一个 value 都是读者真的会去敲的东西,而每一行的后半段是一份 keywords,装着他们可能改敲的那些词:回滚配 revert,找人配 teammate。meta 是行尾那条安静的注记,说明这是哪一类东西——不是描述,因为一个每行都印一句话的面板,大约到第六行就不再能扫读了。

组成部分

在调用处组合,而不是通过 props 配置——所以这个组件没有预料到的布局,依然表达得出来。

CommandInput

筛选框。带着搜索图标和 combobox 语义。

这个组件没有自己的属性。

CommandList

这个组件没有自己的属性。

CommandEmpty

筛选没命中时显示。写清楚什么会命中,别写“无结果”。

这个组件没有自己的属性。

CommandFooter

底部那条键位提示。

面板是一个键盘界面,而它的键是看不见的:屏幕上没有任何东西说方向键会移动选中行、回车会执行它;一个伸手去够鼠标的读者,是被设计辜负了,而不是他自己的问题。

这个组件没有自己的属性。

CommandHint

CommandFooter 里的一组“键 + 动作”。

CommandHint props
属性类型默认值说明
children必填ReactNode它们做什么——一个动词,小写,不要写成句子。
keys必填string[]这条提示描述的按键,用 `Kbd` 键帽打印出来。

同时接受 Omit<ComponentProps<'span'>, 'children'> 里的全部属性,它们会直接透传给底层元素,不再逐条列出。

CommandGroup

这个组件没有自己的属性。

CommandSeparator

分组之间的分隔线。

标成纯装饰,因为 ARIA 只允许 listbox 里出现 option 和 group——而这个库把它渲染成 role="separator",那会给每一个用到它的面板塞进一条严重的 aria-required-children 违规。分组本身已经会被播报,所以把分隔线从无障碍树里拿掉不损失任何东西。

这个组件没有自己的属性。

CommandItem

CommandItem props
属性类型默认值说明
iconRemixiconComponentType | ReactNode前置的图标字形。两种写法都行——`icon={<Settings size={16} />}` 传的是元素,`icon={Settings}` 传的是组件,尺寸由这里来定。 它是让长列表能被扫读的关键——眼睛先按形状分类再读字,四十行长得一样的文字会让这件事失效。
metaReactNode行尾一句安静的注解——这是个什么东西,或者它当前的状态。不是描述:一个每行都印一句话的面板,大约到第六行就不能扫读了。
shortcutstring打印在行尾的快捷键。

同时接受 ComponentProps<typeof CommandPrimitive.Item> 里的全部属性,它们会直接透传给底层元素,不再逐条列出。

CommandDialog

模态里的面板,也就是它几乎总是被用的那个样子。

对话框自己的内边距被去掉了:面板是通到边缘的,而且它的输入框是焦点第一个落到的地方。

CommandDialog props
属性类型默认值说明
children必填ReactNode—
label必填string给面板起个名字,供辅助技术使用。
onOpenChange必填(open: boolean) => void—
open必填boolean—
inputLabelstring搜索框的名字需要和对话框读起来不一样时用它——比如「资料库搜索」面板里的「搜索记录」输入框。默认取 `label`。在 `CommandInput` 上写 `aria-label` 做不到这一点:输入框是按引用命名的,而引用的优先级高于 `aria-label`。
onCloseAutoFocusDialogContentProps['onCloseAutoFocus']关闭时对话框交还焦点的那一刻调用。默认会把焦点还给 `DialogTrigger`,而通过快捷键打开的面板没有触发器,焦点就掉回了页面。阻止默认行为,然后把焦点放回打开它的那个元素。
onEscapeKeyDownDialogContentProps['onEscapeKeyDown']在 Escape 关闭对话框之前调用。阻止默认行为即可让它保持打开,就像嵌套视图后退一步那样。
onOpenAutoFocusDialogContentProps['onOpenAutoFocus']对话框把焦点移进来时调用。阻止默认行为即可自己安排焦点。
shouldFilterboolean应用已经筛选过这些行时——服务端搜索,或它自己的索引——传 `false`。否则面板会拿输入再筛一遍,把应用本来要显示的结果藏起来。

键盘操作

Command keyboard interactions
按键作用
↑↓移动高亮。焦点留在输入框里,所以你打的字还能继续改。
Enter执行高亮那一项。
Escape关闭面板。

无障碍

  • 列表随打字过滤,高亮随方向键移动,焦点始终留在输入框里。最后这条是 ARIA combobox 模式,也是自制面板一定会做错的地方。