Select
Select 提供锚定菜单以供选择一个或多个选项,并支持可选图标、筛选和验证文本。
字段间距由外围布局负责;请保留其预期密度,不要将所有字段组统一为相同间距。labelClearance 接受 CSS 长度,用于设置局部块起始方向的间距,也可从 fieldContextKey 继承子树默认值;该间距会叠加到父级间距上。请参阅字段布局指南。
结构
| 元素 | 部件 | 选择器 |
|---|---|---|
<div> | root | [data-scope="select"][data-part="root"]鼠标左键单击复制 |
<div> | trigger | [data-scope="select"][data-part="trigger"]鼠标左键单击复制 |
<button> | clear-button条件显示 | [data-scope="select"][data-part="clear-button"]鼠标左键单击复制 |
<Icon> | clear-icon | [data-scope="select"][data-part="clear-icon"]鼠标左键单击复制 |
<span> | indicator | [data-scope="select"][data-part="indicator"]鼠标左键单击复制 |
<Menu> | menu | [data-scope="select"][data-part="menu"] |
<TransitionGroup> | options | [data-scope="select"][data-part="options"]鼠标左键单击复制 |
<div> | empty条件显示 | [data-scope="select"][data-part="empty"]鼠标左键单击复制 |
<MenuItem> | option | [data-scope="select"][data-part="option"]鼠标左键单击复制 |
<span> | option-leading-content | [data-scope="select"][data-part="option-leading-content"]鼠标左键单击复制 |
<span> | option-check条件显示 | [data-scope="select"][data-part="option-check"]鼠标左键单击复制 |
<span> | option-icon条件显示 | [data-scope="select"][data-part="option-icon"]鼠标左键单击复制 |
属性
| 名称 | 说明 | 默认值 | 控件 |
|---|---|---|---|
shape可选 | Corner style; inherits the shared shape default. M3Shape | undefined | - | - |
label可选 | Floating label, or accessible-name fallback in external mode. Otherwise supply an external label or ARIA name. string | undefined | undefined | |
labelBehavior可选 | Floating label or externally arranged visible label; label remains an accessible-name fallback. FieldLabelBehavior | undefined | - | - |
id可选 | Optional explicit id for the trigger element. string | undefined | undefined | |
options必填 | Selectable options rendered in the menu. SelectOption[] | - | - |
filterMode可选 | Consumer-owned search results bypass local filtering. localexternal | - | |
resolveLabel可选 | Resolve selected values absent from current results; reactive caches are supported. ((value: SelectOptionValue) => string | undefined) | undefined | - | |
supportingText可选 | Supporting text shown below the field when not in error state. string | undefined | undefined | |
error可选 | Error text shown below the field. When provided, field is invalid. string | undefined | undefined | |
disabled可选 | Whether the control is disabled. boolean | undefined | false | |
required可选 | Whether the control is required. boolean | undefined | false | |
name可选 | Name used for hidden inputs during form submission. string | undefined | undefined | |
placeholder可选 | Placeholder shown when nothing is selected. string | undefined | undefined | |
size可选 | The trigger height. Matches the 32px, 40px, and 56px TextField and Button sizes. TextFieldSize | undefined | undefined | - |
labelClearance可选 | CSS length reserved at block-start for the floating label. Overrides FieldContext. string | undefined | undefined | |
multiple可选 | Enables multi-select behavior. boolean | undefined | false | - |
showSelectedIcon可选 | Show selected checkmarks; inherits Select defaults, then falls back to true. boolean | undefined | undefined | |
clearable可选 | Shows a trailing button that clears the current selection. boolean | undefined | false | |
clearLabel可选 | Accessible label for the clear button. string | undefined | undefined | |
menuColor可选 | Visual color style of the dropdown menu. MenuColor | undefined | undefined | - |
displaySeparator可选 | Separator used when showing multiple selected labels. string | undefined | ", " | |
filterable可选 | Enables typing in the field to filter the option collection. boolean | undefined | false | |
filterPlaceholder可选 | Placeholder shown in the field while filtering. string | undefined | undefined | |
filterNoResultsText可选 | Message shown when filtering returns no matching options. string | undefined | undefined | |
modelValue可选 | - SelectModelValue | - | - |
query可选 | - string | undefined | "" | |
open可选 | - boolean | undefined | false |
事件7
| 名称 | 类型 | 说明 |
|---|---|---|
update:open | [value: boolean] | - |
closed | [] | - |
update:modelValue | [value: SelectModelValue] | - |
update:query | [value: string] | - |
change | [value: SelectModelValue] | - |
focus | [event: FocusEvent] | - |
blur | [event: FocusEvent] | - |
插槽7
| 名称 | 参数 | 说明 |
|---|---|---|
list | - | - |
leading | - | - |
prefix | - | - |
suffix | - | - |
trailing | - | - |
optionLeading | - | - |
optionTrailing | - | - |
标签遵循共享的 fieldLabelBehavior 默认值(floating;compact 中为 external)。可用 label-behavior 覆盖。External 模式不显示内置标签和描边缺口,不应用 labelClearance,且仅将 label 用作无障碍名称的回退值。自定义标签布局请参阅字段布局。
菜单外观
menuColor 会覆盖共享的 Menu 颜色偏好,内置回退值为 standard。可通过插件的 menuColor 配置,或提供 menuDefaultsContextKey,让菜单共享调色板。详见Menu 颜色。
选中标记
在 Select 或 Combobox 上设置 :show-selected-icon="false",可隐藏选中标记,同时保留选中背景、前置内容和选择语义。没有前置内容的行不会预留选中标记的空间,选中时文本也不会偏移。使用自定义 list 插槽中的 SelectItem 行时同样适用。
如需设置应用级默认值,可使用 app.use(M3VuePlugin, { selectDefaults: { showSelectedIcon: false } })。如需设置子树默认值,可在 setup 中调用 provideSelectDefaults({ showSelectedIcon: ref(false) })。也可直接使用公开的 selectContextKey 提供默认值。优先级为:实例 prop → 子树默认值 → 应用默认值 → true。provideSelectDefaults 会继承未指定的字段;ref 保持响应性,将偏好值或 prop 设为 undefined 即可恢复继承。
外部虚拟列表
list 插槽只替换选项的呈现方式。Select 仍负责菜单界面、滚动容器、选择、筛选、键盘导航和关闭生命周期。使用每个条目的 option 及其在筛选结果中的索引渲染 SelectItem,并以 option.value 作为 key。选项值必须唯一且稳定。SelectItem 与内置列表使用相同的 MenuItem 呈现方式,包括图标、选中动效和首尾选项的形状。其 default、leading 和 trailing 插槽会接收 { option, selected }。
插槽提供:
entries:完整的筛选结果集合,每项为{ option, index };每项的index指向原始 options。scrollElement:现有的 MenuContainer 滚动元素;将其用作虚拟列表的滚动元素。open:弹出菜单是否打开;关闭时应禁用测量。activeIndex:筛选结果集合中的活动索引,独立于选中状态。setScrollToIndex(callback):注册虚拟器滚动回调;卸载时传入undefined注销。
请在 options 中保留完整逻辑结果集,不要只传入当前挂载窗口。虚拟列表负责测量行高和定位,不应额外创建滚动容器或维护选择状态。虚拟行不会使用内置列表的筛选过渡效果;重新挂载已选行也不会重播选中动画。Select 和 Combobox 的自定义列表均可使用 list。TanStack 仅用于本示例,并非组件库依赖。
共享虚拟列表组件
<script setup lang="ts">
import { computed, onBeforeUnmount, watch } from 'vue';
import { useVirtualizer } from '@tanstack/vue-virtual';
import { SelectItem, type SelectListSlot } from '@/components/Select';
const props = defineProps<{ list: SelectListSlot }>();
const virtualizer = useVirtualizer(
computed(() => ({
count: props.list.entries.length,
getScrollElement: () => props.list.scrollElement ?? null,
estimateSize: () => 48,
getItemKey: (index: number) => props.list.entries[index]!.option.value,
overscan: 5,
enabled: props.list.open,
})),
);
watch(
() => props.list.setScrollToIndex,
(register) => {
register((index) => virtualizer.value.scrollToIndex(index, { align: 'auto' }));
},
{ immediate: true },
);
onBeforeUnmount(() => props.list.setScrollToIndex(undefined));
</script>
<template>
<div class="virtual-select-list" :style="{ height: `${virtualizer.getTotalSize()}px` }">
<SelectItem
v-for="row in virtualizer.getVirtualItems()"
:key="list.entries[row.index]!.option.value"
:ref="
(instance) => {
if (instance) virtualizer.measureElement((instance as any).$el);
}
"
:data-index="row.index"
:index="row.index"
:option="list.entries[row.index]!.option"
class="virtual-select-row"
:style="{ transform: `translateY(${row.start}px)` }"
/>
</div>
</template>
<style scoped>
.virtual-select-list {
flex: none;
position: relative;
inline-size: 100%;
}
.virtual-select-row {
position: absolute;
inset-block-start: 0;
inset-inline-start: 0;
inline-size: 100%;
}
</style>搜索结果与选中标签
v-model:query 暴露筛选文本。默认使用本地筛选。设置 filterMode="external" 时,由调用方加载并提供当前完整结果集,并负责请求取消和过期响应处理。resolveLabel(value) 可为当前结果集中不存在的已选值提供标签。在此同步回调中读取响应式标签缓存;异步数据到达时更新该缓存。缺少标签时回退为字符串形式的值。清除和表单提交仍使用已选值,与行是否挂载无关。
键盘与无障碍
字段使用 combobox 语义,并控制包含 option 行的 listbox。焦点保留在字段中。方向键会循环导航所有未禁用的选项;菜单打开时,Home/End 移至首尾选项。Enter 选择活动选项;对于不可筛选的 Select,空格键也会选择。Escape 和 Tab 会关闭弹出菜单。导航不会选中选项。只有活动行已挂载时才会设置 aria-activedescendant。迁移自定义 DOM 查询时,应使用这些角色查询,替代之前的菜单项角色。
关闭生命周期
原生菜单关闭时会立即触发 update:open(false) 更新控件状态。菜单的视觉退出完成后才会触发 closed。退出期间会保留筛选状态,并在 closed 时重置,因此菜单关闭动画过程中选项列表和尺寸不会发生变化。
示例
尺寸与工具栏对齐
Select 和 Combobox 与 TextField 使用相同的触发器尺寸:extra-small(32px)、small(40px)和 medium(56px)。size 属性优先于 fieldContextKey 中的 size,内置回退值为 medium。可通过 M3VuePlugin 配置 fieldSize,或通过上下文配置子树。详见组件默认值。
默认
无图标
筛选
配置
Composable
useSelect
完整类型签名
function useSelect(opts: UseSelectOptions): { filterQuery: import("vue").Ref<string, string>; selectedOptions: import("vue").ComputedRef<UseSelectOption[]>; hasValue: import("vue").ComputedRef<boolean>; displayText: import("vue").ComputedRef<string>; submittedValues: import("vue").ComputedRef<string[]>; filteredOptionEntries: import("vue").ComputedRef<import("./selectModel").SelectOptionEntry[]>; hasFilteredOptions: import("vue").ComputedRef<boolean>; activeIndex: import("vue").ComputedRef<number>; activeDescendant: import("vue").ComputedRef<string | undefined>; list: import("vue").ComputedRef<{ entries: import("./selectModel").SelectOptionEntry[]; scrollElement: HTMLElement | undefined; activeIndex: number; open: boolean; setScrollToIndex: (callback: ((index: number) => void) | undefined) => void; }>; optionProps: (index: number, option: UseSelectOption) => { id: string; 'aria-posinset': number; 'aria-setsize': number; 'data-active': string | undefined; 'data-keyboard-active': string | undefined; 'data-first': string | undefined; 'data-last': string | undefined; ref: (instance: Element | ComponentPublicInstance | null) => void; onMousedown: (event: MouseEvent) => void; onPointermove: () => void; onClick: () => void; }; isOptionSelected: (option: UseSelectOption) => boolean; toggleOption: (option: UseSelectOption) => void; clearValue: () => void; focus: () => void; blur: () => void; show: () => Promise<void>; hide: () => void; toggle: () => void; openMenu: (target?: "first" | "last") => void; closeMenu: () => void; handleFieldClick: () => void; handleFieldInput: () => void; handleFilterKeydown: (event: KeyboardEvent) => void; handleMenuClosed: () => void; }来源:src/composables/useSelect.ts
| 字段 | 类型 | 必填 | 说明 | 默认值 |
|---|---|---|---|---|
options | () => UseSelectOption[] | 是 | Unique, stable values identify the complete logical result set. | - |
multiple | () => boolean | 是 | - | - |
filterable | () => boolean | 是 | - | - |
disabled | () => boolean | 是 | - | - |
displaySeparator | () => string | 是 | - | - |
locale | (() => string | undefined) | undefined | 否 | - | - |
filterMode | (() => "local" | "external") | undefined | 否 | Disable local filtering when the consumer supplies search results. | - |
resolveLabel | ((value: UseSelectOptionValue) => string | undefined) | undefined | 否 | Resolve values missing from current results. Falls back to String(value). | - |
query | Ref<string, string> | undefined | 否 | - | - |
modelValue | Ref<UseSelectModelValue, UseSelectModelValue> | 是 | - | - |
open | Ref<boolean, boolean> | 是 | - | - |
onChange | ((value: UseSelectModelValue) => void) | undefined | 否 | - | - |
onClosed | (() => void) | undefined | 否 | - | - |
id | () => string | 是 | Stable listbox id, normally derived from useId(). | - |
focus | () => void | 是 | - | - |
blur | () => void | 是 | - | - |
showMenu | () => void | 是 | Use the shared Menu overlay, including source anchoring and close completion. | - |
hideMenu | () => void | 是 | - | - |
scrollElement | () => HTMLElement | undefined | 是 | The MenuContainer is the single scroll owner. | - |
| 字段 | 类型 | 必填 | 说明 | 默认值 |
|---|---|---|---|---|
filterQuery | import("vue").Ref<string, string> | 是 | - | - |
selectedOptions | import("vue").ComputedRef<UseSelectOption[]> | 是 | - | - |
hasValue | import("vue").ComputedRef<boolean> | 是 | - | - |
displayText | import("vue").ComputedRef<string> | 是 | - | - |
submittedValues | import("vue").ComputedRef<string[]> | 是 | - | - |
filteredOptionEntries | import("vue").ComputedRef<import("./selectModel").SelectOptionEntry[]> | 是 | - | - |
hasFilteredOptions | import("vue").ComputedRef<boolean> | 是 | - | - |
activeIndex | import("vue").ComputedRef<number> | 是 | - | - |
activeDescendant | import("vue").ComputedRef<string | undefined> | 是 | - | - |
list | import("vue").ComputedRef<{ entries: import("./selectModel").SelectOptionEntry[]; scrollElement: HTMLElement | undefined; activeIndex: number; open: boolean; setScrollToIndex: (callback: ((index: number) => void) | undefined) => void; }> | 是 | - | - |
optionProps | (index: number, option: UseSelectOption) => { id: string; 'aria-posinset': number; 'aria-setsize': number; 'data-active': string | undefined; 'data-keyboard-active': string | undefined; 'data-first': string | undefined; 'data-last': string | undefined; ref: (instance: Element | ComponentPublicInstance | null) => void; onMousedown: (event: MouseEvent) => void; onPointermove: () => void; onClick: () => void; } | 是 | - | - |
isOptionSelected | (option: UseSelectOption) => boolean | 是 | - | - |
toggleOption | (option: UseSelectOption) => void | 是 | - | - |
clearValue | () => void | 是 | - | - |
focus | () => void | 是 | - | - |
blur | () => void | 是 | - | - |
show | () => Promise<void> | 是 | - | - |
hide | () => void | 是 | - | - |
toggle | () => void | 是 | - | - |
openMenu | (target?: "first" | "last") => void | 是 | - | - |
closeMenu | () => void | 是 | - | - |
handleFieldClick | () => void | 是 | - | - |
handleFieldInput | () => void | 是 | - | - |
handleFilterKeydown | (event: KeyboardEvent) => void | 是 | - | - |
handleMenuClosed | () => void | 是 | - | - |