Skip to content

Command 命令面板 ​

基础用法

打字即过滤:文案与 keywords 任一包含查询即命中,保持原顺序;组里全被滤掉时整组藏起。方向键移动高亮、Enter 选中,禁用项搜得到但选不了。

<pt-command label="命令" placeholder="输入命令或搜索…" style="max-width: 450px; border: 1px solid var(--pt-border-default)">
  <pt-command-group label="建议">
    <pt-command-item value="calendar" keywords="rili">日历</pt-command-item>
    <pt-command-item value="emoji" keywords="biaoqing">搜索表情</pt-command-item>
    <pt-command-item value="calculator" disabled>计算器</pt-command-item>
  </pt-command-group>
  <pt-separator></pt-separator>
  <pt-command-group label="设置">
    <pt-command-item value="profile">
      个人资料
      <span slot="end">⌘P</span>
    </pt-command-item>
    <pt-command-item value="billing">
      账单
      <span slot="end">⌘B</span>
    </pt-command-item>
    <pt-command-item value="settings" keywords="preferences 偏好">
      设置
      <span slot="end">⌘S</span>
    </pt-command-item>
  </pt-command-group>
</pt-command>

顶上一个搜索框、下面一列可过滤命令的面板:⌘K 命令菜单、快速跳转、「选一个动作」这类场景。 项写成 pt-command-item,可以用 pt-command-group 分组(label 是组标题)、夹 pt-separator。 行尾的快捷键提示放进 end 插槽,图标放进 start 插槽。

和 Combobox 怎么选 ​

需要用
表单里选一个值(有选中态、参与表单提交)pt-combobox
执行一个动作:列表常驻展开,选完就去做那件事了pt-command

两者的过滤规则、「焦点留在输入框 + 高亮项」的键盘模型是同一套(utils/filter)。

过滤规则 ​

  • 查询先去首尾空白、不分大小写;项的文案(默认插槽里的文字,不含 start / end 插槽)与 keywords (空格分隔)任一包含查询即命中。value 不参与匹配。
  • 保持原顺序,不按匹配度重排。 这里有意偏离了 cmdk(源包 Command 的底层)的打分排序:顺序可预期, 禁用项也不会因为不参与排序浮到顶上。与 pt-combobox 一致。
  • 禁用项命中时照样可见(半透明),只是方向键跳过、点不动。
  • 组里的项全被滤掉时整组藏起;有查询时分隔线藏起。一条都没命中时显示 empty-text (不给时取 locale 的 command.empty,「无匹配结果」),要放更复杂的内容用 empty 插槽。
  • 换规则:给 filter 属性一个函数(只能用 JS 赋值),(query, { value, label, keywords, element }) => boolean, query 是输入框里原样的文字。
  • 异步搜索:should-filter="false" 关掉内置过滤,监听 ptQueryChange 自己增删项。项的增删、文案与 value / keywords / disabled 的变化会被自动察觉,不需要手动 refresh()。

选中之后 ​

点击一项,或高亮时按 Enter,项发出 ptSelect(detail 是它的 value,缺省为文案),冒泡到面板上 —— 在 pt-command 上统一监听即可,框架里写 onPtSelect / @pt-select / (ptSelect)。

ptSelect 可取消。没被 preventDefault() 时,面板随后清空查询、高亮回到第一项 —— 相当于「重新打开」: 放在弹窗里时,下次打开不会停在上次的搜索结果上(源包的 Dialog 关闭时卸载内容,天然如此;这里的元素不卸载,由这一步补上)。 要连续选几项、保留查询时 preventDefault()。

value 是高亮项 ​

value 是当前高亮项的值,不是「选中值」:方向键、指针、打字后重新过滤都会改写它,并发 ptValueChange (适合做「高亮哪一项就在旁边预览哪一项」)。外部写 value 会把高亮移到对应的项上。 因此它不登记双向绑定(没有 v-model / ngModel):选中走 ptSelect。

query 同理:用户打字(以及选中后清空)时改写并发 ptQueryChange,外部写入会重新过滤。

放进弹窗(⌘K) ​

放进弹窗(⌘K)

没有单独的 CommandDialog:pt-modal 包 pt-command,尺寸差异用 pt-command 上的 CSS 变量覆盖(输入框 48px、项上下 12px、图标 20px);auto-focus="false" 后在 ptOpen 里调 setFocus() 把焦点放进搜索框。弹窗初始关闭,这里只展示结构。

弹窗默认关闭,页面上看不到它;下面的代码就是它的全部结构。
<pt-modal class="command-dialog" aria-label="命令面板" auto-focus="false">
  <pt-command placeholder="输入命令或搜索…" style="--pt-command-input-height: 48px; --pt-command-item-padding-block: 12px; --pt-command-icon-size: 20px">
    <pt-command-group label="建议">
      <pt-command-item value="calendar">日历</pt-command-item>
      <pt-command-item value="emoji">搜索表情</pt-command-item>
    </pt-command-group>
  </pt-command>
</pt-modal>

没有单独的 CommandDialog 元素:用 pt-modal 包 pt-command。源包 CommandDialog 里的尺寸差异(输入框 48px、 项上下内边距 12px、图标 20px)用 pt-command 上的 CSS 变量覆盖,弹窗本体去掉内边距:

css
pt-modal.command-dialog::part(panel) {
  padding: 0;
  overflow: hidden;
}

pt-modal.command-dialog::part(header) {
  display: none;
}

pt-modal.command-dialog pt-command {
  --pt-command-input-height: 48px;
  --pt-command-item-padding-block: 12px;
  --pt-command-icon-size: 20px;
}
js
const dialog = document.querySelector('pt-modal.command-dialog');
const command = dialog.querySelector('pt-command');
/* pt-modal 默认把焦点给面板本身;写了 auto-focus="false" 就由这里把焦点放进搜索框 */
dialog.addEventListener('ptOpen', () => command.setFocus());
document.addEventListener('keydown', event => {
  if (event.key === 'k' && (event.metaKey || event.ctrlKey)) {
    event.preventDefault();
    dialog.open = !dialog.open;
  }
});
dialog.addEventListener('ptSelect', event => {
  run(event.detail);
  dialog.open = false;
});

pt-modal 写 auto-focus="false"、在它的 ptOpen 里调 pt-command 的 setFocus():弹窗默认把焦点给面板容器本身, 那样一打开还得先点一下搜索框才能打字。标题行不用(::part(header) 藏起),无障碍名称给 pt-modal 的 aria-label。 Esc 由弹窗处理(面板本身不响应 Esc)。 ptSelect 冒泡,在弹窗上监听同样收得到。

键盘 ​

按键行为
打字过滤,高亮第一个可用项
↓ / ↑移动高亮,跳过禁用项;默认到头停住,loop 时回到另一端
Home / End跳到第一项 / 最后一项(与 cmdk 一致)
Enter选中高亮项

输入法组合中(中文 / 日文候选框开着)的回车与方向键是选字、翻候选,组件不响应。 cmdk 的 vim 键位(Ctrl+N / J / P / K)与按组跳转(Alt+方向键)没有做。

无障碍 ​

  • 焦点始终在输入框上(role="combobox"、aria-expanded="true"),列表是 role="listbox", 高亮项经 aria-activedescendant 播报;组是 role="group",组标题是组名。
  • label 是输入框与列表的无障碍名称(不显示),缺省取 locale 的 command.label;也可以用 pt-label for 关联。
  • 没有匹配项时的提示在一个 role="status" 的区域里,读屏会播报。

样式定制 ​

CSS 变量默认说明
--pt-command-radius--pt-radius-md面板圆角
--pt-command-input-height40px输入框高度
--pt-command-icon-size16px搜索图标与项内图标的尺寸
--pt-command-item-padding-block6px项的上下内边距
--pt-command-list-max-height300px列表最大高度,超出后内部滚动

面板本身不带描边与阴影(与源包一致),直接放在页面上时按需给宿主加 border。

API ​

属性

属性Attribute类型默认值说明
emptyTextempty-textstring | undefined—没有匹配项时的提示,默认取 locale 的 command.empty。要放更复杂的内容用 `empty` 插槽
filter—((query: string, item: PtCommandFilterItem) => boolean) | undefined—自定义过滤函数,替掉内置的子串匹配(只能用 JS 赋值)。`shouldFilter` 为 false 时不调用
labellabelstring | undefined—输入框与列表的无障碍名称(不显示),默认取 locale 的 command.label。外部 pt-label 关联进来时以它为准
looploopbooleanfalse方向键到头后是否回到另一端。默认不循环(与 cmdk 一致)
placeholderplaceholderstring | undefined—输入框的占位文案
queryquerystring''输入框里的查询。用户打字时组件自己改写它并发 `ptQueryChange`;外部改写时重新过滤、高亮第一项
shouldFiltershould-filterbooleantrue是否用内置规则过滤。关掉后查询只是个输入框,列表显示全部项,由使用方按 `ptQueryChange` 自己增删
valuevaluestring | undefined—高亮项的 value(缺省为项的文案)。键盘、指针、过滤移动高亮时组件自己改写它; 外部写入(含初始声明)时高亮第一个 value 相同、可见且可用的项;找不到就不高亮、value 原样保留, 之后这一项出现(异步加入、解禁)时再高亮它。用户移动高亮之前都不会退回首项。组件自己改写时,没有高亮项就是 `undefined`

事件

事件detail 类型说明
ptQueryChangestring查询因用户操作而改变(打字、选中后清空),detail 为新的查询。外部改 `query` 不发
ptSelectstring某一项被选中,detail 为它的 `value`,`event.target` 是那一项。 这是 `pt-command-item` 发出、冒泡上来的事件,本元素**自己不发**;声明在这里只为让 React / Vue / Angular 包装层生成绑定(`onPtSelect` / `@pt-select` / `(ptSelect)`),可以在面板上统一监听,不必写在每一项上。 在这里 `preventDefault()` 同样阻止随后的清空查询。 静态类型的限制:`PtCommandCustomEvent` 把 `target` 标成本元素,运行时却是项;要读项时写 `event.target as HTMLPtCommandItemElement`。
ptValueChangestring | undefined高亮项因用户操作而改变(方向键、指针、打字后重新过滤、选中),detail 为新的 `value`(没有高亮项时 `undefined`)。 外部改 `value`、增删项引起的变化不发。适合做「高亮哪一项就预览哪一项」

方法

方法说明
refresh() => Promise<void>重新过滤并同步高亮。项的增删与文案、value、keywords、disabled 的变化在浏览器里会自动触发(MutationObserver), 一般不用手动调;项是否可见由 `filter` 里读的外部数据决定、而那份数据变了时调它
setFocus(options?: FocusOptions) => Promise<void>输入框获得焦点

插槽

名称说明
(默认)`pt-command-item` 列表,可混放 `pt-command-group` 与 `pt-separator`
empty没有匹配项时显示的内容,替换默认的 `emptyText`

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

part说明
empty没有匹配项时的提示
input输入框
input-wrapper输入区(搜索图标 + 输入框,带底边)
list列表的滚动容器

CSS 变量

变量说明
--pt-command-icon-size搜索图标与项内图标的尺寸,默认 16px(放进弹窗时源包是 20px)
--pt-command-input-height输入框高度,默认 40px(放进弹窗时源包是 48px)
--pt-command-item-padding-block项的上下内边距,默认 6px(放进弹窗时源包是 12px)
--pt-command-list-max-height列表最大高度,超出后列表内滚动,默认 300px
--pt-command-radius面板圆角,默认 --pt-radius-md

pt-command-group ​

属性

属性Attribute类型默认值说明
labellabelstring | undefined—组标题(源包 cmdk 的 `heading`)。不可选、方向键跳过;同时是这一组的无障碍名称。不给时不渲染标题行

插槽

名称说明
(默认)这一组的 `pt-command-item`(可夹 `pt-separator`)

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

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

pt-command-item ​

属性

属性Attribute类型默认值说明
activeactivebooleanfalse键盘 / 指针当前高亮项。由所属的 pt-command 维护,不要手动设置
disableddisabledbooleanfalse禁用:搜得到、看得见,但方向键跳过、点击与 Enter 都不会选中,并标注 aria-disabled
keywordskeywordsstring | undefined—额外的搜索关键词,空格分隔(如 `"settings preferences 偏好"`):别名、拼音、英文名。 任一关键词包含查询即命中,不影响渲染
valuevaluestring | undefined—这一项的值:`ptSelect` 的 detail,也是 pt-command `value`(高亮项)对应的值。省略时取文案

事件

事件detail 类型说明
ptSelectstring选中时触发(点击,或高亮时按 Enter),detail 为 `value`(缺省为文案)。 冒泡:pt-command 声明了同名事件,可以在面板上统一监听(框架包装层里同样能绑)。 可取消:`event.preventDefault()` 后 pt-command 不清空查询(默认选中后清空,见 pt-command)。

插槽

名称说明
(默认)文案
end行尾的快捷键提示,如 `⌘P`
start文案前的图标(默认 16px,随 --pt-command-icon-size)

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

part说明
base这一行

Apache-2.0 协议开源