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)
没有单独的 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 变量覆盖,弹窗本体去掉内边距:
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;
}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-height | 40px | 输入框高度 |
--pt-command-icon-size | 16px | 搜索图标与项内图标的尺寸 |
--pt-command-item-padding-block | 6px | 项的上下内边距 |
--pt-command-list-max-height | 300px | 列表最大高度,超出后内部滚动 |
面板本身不带描边与阴影(与源包一致),直接放在页面上时按需给宿主加 border。
API
属性
| 属性 | Attribute | 类型 | 默认值 | 说明 |
|---|---|---|---|---|
emptyText | empty-text | string | undefined | — | 没有匹配项时的提示,默认取 locale 的 command.empty。要放更复杂的内容用 `empty` 插槽 |
filter | — | ((query: string, item: PtCommandFilterItem) => boolean) | undefined | — | 自定义过滤函数,替掉内置的子串匹配(只能用 JS 赋值)。`shouldFilter` 为 false 时不调用 |
label | label | string | undefined | — | 输入框与列表的无障碍名称(不显示),默认取 locale 的 command.label。外部 pt-label 关联进来时以它为准 |
loop | loop | boolean | false | 方向键到头后是否回到另一端。默认不循环(与 cmdk 一致) |
placeholder | placeholder | string | undefined | — | 输入框的占位文案 |
query | query | string | '' | 输入框里的查询。用户打字时组件自己改写它并发 `ptQueryChange`;外部改写时重新过滤、高亮第一项 |
shouldFilter | should-filter | boolean | true | 是否用内置规则过滤。关掉后查询只是个输入框,列表显示全部项,由使用方按 `ptQueryChange` 自己增删 |
value | value | string | undefined | — | 高亮项的 value(缺省为项的文案)。键盘、指针、过滤移动高亮时组件自己改写它; 外部写入(含初始声明)时高亮第一个 value 相同、可见且可用的项;找不到就不高亮、value 原样保留, 之后这一项出现(异步加入、解禁)时再高亮它。用户移动高亮之前都不会退回首项。组件自己改写时,没有高亮项就是 `undefined` |
事件
| 事件 | detail 类型 | 说明 |
|---|---|---|
ptQueryChange | string | 查询因用户操作而改变(打字、选中后清空),detail 为新的查询。外部改 `query` 不发 |
ptSelect | string | 某一项被选中,detail 为它的 `value`,`event.target` 是那一项。 这是 `pt-command-item` 发出、冒泡上来的事件,本元素**自己不发**;声明在这里只为让 React / Vue / Angular 包装层生成绑定(`onPtSelect` / `@pt-select` / `(ptSelect)`),可以在面板上统一监听,不必写在每一项上。 在这里 `preventDefault()` 同样阻止随后的清空查询。 静态类型的限制:`PtCommandCustomEvent` 把 `target` 标成本元素,运行时却是项;要读项时写 `event.target as HTMLPtCommandItemElement`。 |
ptValueChange | string | 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 | 类型 | 默认值 | 说明 |
|---|---|---|---|---|
label | label | string | undefined | — | 组标题(源包 cmdk 的 `heading`)。不可选、方向键跳过;同时是这一组的无障碍名称。不给时不渲染标题行 |
插槽
| 名称 | 说明 |
|---|---|
(默认) | 这一组的 `pt-command-item`(可夹 `pt-separator`) |
可定制的内部元素(::part())
| part | 说明 |
|---|---|
base | 分组容器(role="group") |
label | 组标题 |
pt-command-item
属性
| 属性 | Attribute | 类型 | 默认值 | 说明 |
|---|---|---|---|---|
active | active | boolean | false | 键盘 / 指针当前高亮项。由所属的 pt-command 维护,不要手动设置 |
disabled | disabled | boolean | false | 禁用:搜得到、看得见,但方向键跳过、点击与 Enter 都不会选中,并标注 aria-disabled |
keywords | keywords | string | undefined | — | 额外的搜索关键词,空格分隔(如 `"settings preferences 偏好"`):别名、拼音、英文名。 任一关键词包含查询即命中,不影响渲染 |
value | value | string | undefined | — | 这一项的值:`ptSelect` 的 detail,也是 pt-command `value`(高亮项)对应的值。省略时取文案 |
事件
| 事件 | detail 类型 | 说明 |
|---|---|---|
ptSelect | string | 选中时触发(点击,或高亮时按 Enter),detail 为 `value`(缺省为文案)。 冒泡:pt-command 声明了同名事件,可以在面板上统一监听(框架包装层里同样能绑)。 可取消:`event.preventDefault()` 后 pt-command 不清空查询(默认选中后清空,见 pt-command)。 |
插槽
| 名称 | 说明 |
|---|---|
(默认) | 文案 |
end | 行尾的快捷键提示,如 `⌘P` |
start | 文案前的图标(默认 16px,随 --pt-command-icon-size) |
可定制的内部元素(::part())
| part | 说明 |
|---|---|
base | 这一行 |