Combobox 可搜索下拉
触发框就是输入框,打字即过滤。文案与 keywords(别名、拼音)任一包含查询即命中;禁用项搜得到但选不了。
<pt-combobox placeholder="选择成员" aria-label="负责人" style="max-width: 320px">
<pt-option value="u1" keywords="zhangsan zs">张三</pt-option>
<pt-option value="u2" keywords="lisi ls">李四</pt-option>
<pt-option value="u3" keywords="wangwu" disabled>王五(已离职)</pt-option>
<pt-option value="u4" keywords="zhaoliu">赵六</pt-option>
</pt-combobox>可搜索的单选下拉:触发框本身就是输入框,打字即过滤选项,方向键移动高亮、回车选中、Esc 收起。 选项写法与 Select 选择器 相同 —— pt-option、pt-option-group、pt-separator。
和 Select 怎么选
| 需要 | 用 |
|---|---|
| 选项不多(一屏放得下)、不需要搜索 | pt-select |
| 选项多、要按名字 / 别名 / 拼音快速找到一项 | pt-combobox |
| 系统原生的选择体验 | pt-native-select |
过滤规则
- 查询先去首尾空白、不分大小写;选项的文案与
keywords(空格分隔)任一包含查询即命中。 搜「zs」能命中keywords="zhangsan zs"的「张三」。 - 保持原顺序,不按匹配度重排 —— 结果列表的顺序可预期。
- 禁用项命中时照样可见(置灰),只是方向键跳过、点不动。
- 分组里的选项全被滤掉时整组藏起;有查询时分隔线藏起。一条都没命中时显示
empty-text(不给时取 locale 的combobox.empty,「无匹配结果」)。 - 面板收起时查询清空。
value:未选与空串
不写 value 是未选(显示占位);value 为空串是选了「全部」那一项,可以高亮、选中,表单提交空串。
<pt-combobox value="" aria-label="环境" style="max-width: 320px">
<pt-option value="">全部环境</pt-option>
<pt-option value="prod">生产</pt-option>
<pt-option value="dev">开发</pt-option>
</pt-combobox>value 区分两种状态,两者不等价:
| value | 含义 | 表单提交 | required |
|---|---|---|---|
undefined | 未选:显示占位文案(不写 value 属性即是) | 字段不出现 | 报「必填」 |
'' | 选了 value 为空串的那一项(「全部」这类兜底项) | 提交空串 | 算已选 |
null 视同 undefined。三个框架的双向绑定都保留这一区分:Vue v-model、Angular ngModel (null / undefined 写进去是未选,不会被转成空串)、React value + onPtChange。
value 相同的选项可以有多个:高亮、打勾与触发框上的文案都按被选中的那一行走,不会串到第一个同值的选项上。
分组与宽度
可以用 pt-option-group 分组、夹 pt-separator;组内全被滤掉时整组藏起。宿主 width: fit-content 时按已选文案自适应,面板至少与触发框等宽。
<pt-combobox value="me" aria-label="发件人" empty-text="没有这个地址" style="width: fit-content">
<pt-option value="me">我</pt-option>
<pt-separator></pt-separator>
<pt-option-group label="团队">
<pt-option value="team">team-notifications@example.com</pt-option>
<pt-option value="ops">ops@example.com</pt-option>
</pt-option-group>
</pt-combobox>默认撑满父级宽度;给宿主 width: fit-content 时按已选文案(或占位)自适应。已选文案画在输入框下面一层, 打字时只淡出不移除,触发框的宽度不跳。面板至少与触发框等宽,按内容最多撑到 min(420px, 90vw),长邮箱、长路径不会被切掉; 触发框本身比这更宽时(撑满宽表单、移动端全宽)面板与触发框等宽,但不超出视口两侧 8px 留白。
窄屏下不像 pt-select 那样切成底部抽屉:输入框就是触发框,抽屉会盖住它,打字时看不到自己打了什么。 面板照常贴着触发框,下方放不下时翻到上方。
尺寸与状态
三档尺寸与 pt-select 对齐(24 / 32 / 40);invalid 描边转危险色并标注 aria-invalid。
<div style="display: grid; gap: 12px; max-width: 320px">
<pt-combobox size="sm" value="a" aria-label="小">
<pt-option value="a">小号 24</pt-option>
</pt-combobox>
<pt-combobox size="lg" value="a" aria-label="大">
<pt-option value="a">大号 40</pt-option>
</pt-combobox>
<pt-combobox invalid placeholder="出错态" aria-label="出错态">
<pt-option value="a">选项</pt-option>
</pt-combobox>
<pt-combobox disabled value="a" aria-label="禁用">
<pt-option value="a">禁用</pt-option>
</pt-combobox>
</div>键盘
| 按键 | 行为 |
|---|---|
| 打字 | 展开并过滤,高亮第一个可用项 |
| ↓ / ↑ | 收起时展开(高亮已选项);展开时移动高亮,跳过禁用项、到头循环 |
| PageDown / PageUp | 展开时一次跨 10 项 |
| Enter | 展开时选中高亮项;收起时提交所在的表单(与原生单行输入框一致) |
| Esc | 收起(放在弹窗、抽屉里时只收起面板,不关外层) |
| Tab | 收起,焦点照常移走 |
| Home / End | 留给输入框移动光标,不跳到首尾项 |
输入法组合中(中文 / 日文候选框开着)的回车与方向键是选字、翻候选,组件不响应。
无障碍
- 焦点始终在输入框上(
role="combobox",aria-autocomplete="list"),高亮项经aria-activedescendant播报。 - 输入框的 value 只是查询,已选文案经
aria-describedby播报。外部关联了描述(pt-field 的说明文字)时, 描述优先,已选文案不再重复播报 —— 这是「输入框只放查询」这种形态的固有代价。 - 没有可见标签时写
aria-label(包装层里是ariaLabelText),或用pt-label for关联。 - 触屏上触发框的命中区不小于 44px。
事件
ptChange:选中了一项(点选或回车),detail是它的 value。外部改value不发。冒泡(不会自我嵌套)。ptOpen/ptClose:面板开关,ptClose的detail.reason是select/escape/outside/tab/programmatic。 与其它弹层一样不冒泡,外层统一监听要走捕获阶段。
API
属性
| 属性 | Attribute | 类型 | 默认值 | 说明 |
|---|---|---|---|---|
ariaLabelText | aria-label | string | undefined | — | 无障碍名称:原生 HTML 里写 aria-label,React / Vue / Angular 包装层里属性名是 ariaLabelText。 没有可见标签(pt-label for / pt-field)时必须给 |
disabled | disabled | boolean | false | 禁用。展开中改成禁用会收起面板并清空查询。只反映使用方自己写的值:外层原生 `<fieldset disabled>`、 pt-field / pt-field-set 的禁用另经继承通道生效,不改写它 |
emptyText | empty-text | string | undefined | — | 没有匹配项时的提示,默认取 locale 的 combobox.empty |
inheritedDisabled | inherited-disabled | boolean | false | 外层 pt-field / pt-field-set 下发的禁用。内部继承通道:由它们以 attribute 维护,不建议使用方直接写。 有效禁用是 `disabled` 与它的并集;父元素只写这一条、不碰 `disabled`,读 `el.disabled` 得到的始终是使用方自己写的值 |
inheritedInvalid | inherited-invalid | boolean | false | 外层 pt-field 下发的出错态。内部继承通道:由 pt-field 维护,不建议使用方直接写。有效出错是 `invalid` 与它的并集 |
inheritedRequired | inherited-required | boolean | false | 外层 pt-field 下发的必填。内部继承通道:由 pt-field 维护,不建议使用方直接写。有效必填是 `required` 与它的并集 |
invalid | invalid | boolean | false | 出错态:描边转危险色,标注 aria-invalid,并置表单校验的 customError |
maxHeight | max-height | number | 320 | 选项列表的最大高度(px),超出后列表内滚动 |
name | name | string | undefined | — | 提交表单时的字段名 |
open | open | boolean | false | 面板是否展开。可读可写,一般不用手动设置 |
placeholder | placeholder | string | undefined | — | 未选中时的占位文案,默认取 locale 的 select.placeholder |
required | required | boolean | false | 必填:未选(`undefined`)时表单校验报 valueMissing。选了空串算已选 |
size | size | "lg" | "md" | "sm" | 'md' | 尺寸档位。三档对应 24 / 32 / 40 的控件刻度,与 pt-select 一致 |
value | value | string | undefined | — | 当前值,与某个 pt-option 的 value 对应。`undefined` 为未选(`null` 视同 `undefined`); 空串是合法的**已选**值,两者不等价 |
事件
| 事件 | detail 类型 | 说明 |
|---|---|---|
ptBlur | void | 输入框失去焦点 |
ptChange | string | 选中了一项(点选或回车),detail 是它的 value。外部改 value 不发 |
ptClose | { reason: PtComboboxCloseReason; } | 面板收起,detail 说明是怎么收起的(与 pt-select 的 ptClose 同形) **不冒泡**:理由同 ptOpen。 |
ptFocus | void | 输入框获得焦点 |
ptOpen | void | 面板展开 **不冒泡**:弹层常互相嵌套(弹窗里的下拉、抽屉里的下拉),冒泡的话外层上的监听会收到内层的开关。 |
方法
| 方法 | 说明 |
|---|---|
hide() => Promise<void> | 收起面板 |
refresh() => Promise<void> | 选项变动时重新同步(动态增删、改文案后调用;增删直接子元素时会自动同步) |
setFocus(options?: FocusOptions) => Promise<void> | |
show() => Promise<void> | 展开面板 |
插槽
| 名称 | 说明 |
|---|---|
(默认) | `pt-option` 列表,可混放 `pt-option-group` 与 `pt-separator` |
可定制的内部元素(::part())
| part | 说明 |
|---|---|
base | 触发框(带描边的外框) |
empty | 没有匹配项时的提示 |
input | 输入框 |
listbox | 选项列表(滚动容器) |
popup | 弹出的面板 |
value | 已选文案 / 占位文案 |
CSS 变量
| 变量 | 说明 |
|---|---|
--pt-combobox-radius | 触发框与面板的圆角,默认 --pt-radius-md(sm 档触发框默认 --pt-radius-sm) |