GitHubopen_in_new

Select ​

Select 提供锚定菜单以供选择一个或多个选项,并支持可选图标、筛选和验证文本。

字段间距由外围布局负责;请保留其预期密度,不要将所有字段组统一为相同间距。labelClearance 接受 CSS 长度,用于设置局部块起始方向的间距,也可从 fieldContextKey 继承子树默认值;该间距会叠加到父级间距上。请参阅字段布局指南。

结构

元素部件选择器
<div>
root
[data-scope="select"][data-part="root"]
left_click鼠标左键单击复制
<div>
trigger
[data-scope="select"][data-part="trigger"]
left_click鼠标左键单击复制
<button>
clear-button条件显示
[data-scope="select"][data-part="clear-button"]
left_click鼠标左键单击复制
<Icon>
clear-icon
[data-scope="select"][data-part="clear-icon"]
left_click鼠标左键单击复制
<span>
indicator
[data-scope="select"][data-part="indicator"]
left_click鼠标左键单击复制
<Menu>
menu
[data-scope="select"][data-part="menu"]
<TransitionGroup>
options
[data-scope="select"][data-part="options"]
left_click鼠标左键单击复制
<div>
empty条件显示
[data-scope="select"][data-part="empty"]
left_click鼠标左键单击复制
<MenuItem>
option
[data-scope="select"][data-part="option"]
left_click鼠标左键单击复制
<span>
option-leading-content
[data-scope="select"][data-part="option-leading-content"]
left_click鼠标左键单击复制
<span>
option-check条件显示
[data-scope="select"][data-part="option-check"]
left_click鼠标左键单击复制
<span>
option-icon条件显示
[data-scope="select"][data-part="option-icon"]
left_click鼠标左键单击复制

属性

名称说明默认值控件
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 仅用于本示例,并非组件库依赖。

共享虚拟列表组件
vue
<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,或通过上下文配置子树。详见组件默认值。

默认 ​

无图标 ​

筛选 ​

配置 ​

Single selection
Multiple selection stays open after each pick.
Selection required

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).-
queryRef<string, string> | undefined否--
modelValueRef<UseSelectModelValue, UseSelectModelValue>是--
openRef<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.-
字段类型必填说明默认值
filterQueryimport("vue").Ref<string, string>是--
selectedOptionsimport("vue").ComputedRef<UseSelectOption[]>是--
hasValueimport("vue").ComputedRef<boolean>是--
displayTextimport("vue").ComputedRef<string>是--
submittedValuesimport("vue").ComputedRef<string[]>是--
filteredOptionEntriesimport("vue").ComputedRef<import("./selectModel").SelectOptionEntry[]>是--
hasFilteredOptionsimport("vue").ComputedRef<boolean>是--
activeIndeximport("vue").ComputedRef<number>是--
activeDescendantimport("vue").ComputedRef<string | undefined>是--
listimport("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是--