Vue TagsInputItem 组件深度解析:radix-vue 标签项的 Props、选中态与可访问性实现
2026/9/17 22:09:23 网站建设 项目流程

Vue TagsInputItem 组件深度解析:radix-vue 标签项的 Props、选中态与可访问性实现

【免费下载链接】radix-vueAn open-source UI component library for building high-quality, accessible design systems and web apps for Vue. Previously Radix Vue项目地址: https://gitcode.com/GitHub_Trending/ra/radix-vue

本文以 radix-vue(即 Reka UI 的 Vue 实现)官方 API 参考文档docs/content/meta/TagsInputItem.md为骨架,结合 TagsInputItem 源码 与配套的 TagsInputRoot 源码、测试用例及官方组件文档,系统讲解 TagsInputItem 在标签输入(Tags Input)组件中的定位、全部 Props、渲染与选中态机制、与 ItemText/ItemDelete 的协作方式,以及键盘导航与无障碍实现。读完本文,你将能理解每个标签项背后的数据流,并能够熟练定制单个标签的样式、禁用与删除行为。

一、TagsInputItem 在组件家族中的定位

Tags Input 是一类"在输入框内渲染标签、并在其后跟随真实文本输入框"的复合组件。在 radix-vue 中,它由五个可组合部件构成(见官方文档 tags-input.md 的 Anatomy 一节,部件从reka-ui包导入):

  • TagsInputRoot:根容器,管理标签数组modelValue与全部交互逻辑;
  • TagsInputItem单个标签的容器,即本文的主角;
  • TagsInputItemText:标签的文本部分,对无障碍至关重要;
  • TagsInputItemDelete:删除当前标签的按钮;
  • TagsInputInput:文本输入框;
  • TagsInputClear:一键清空所有标签的按钮。

其中 TagsInputItem 处于中间枢纽位置:它向上从 Root 注入上下文(injectTagsInputRootContext),向下通过provideTagsInputItemContext把当前标签的valuedisplayValueisSelecteddisabled等状态提供给 ItemText 与 ItemDelete 使用。源码位于 packages/core/src/TagsInput/TagsInputItem.vue,并在 index.ts 中对外导出TagsInputItem及类型TagsInputItemProps

二、Props 完整参考:继承自官方 API 文档

官方 API 参考(docs/content/meta/TagsInputItem.md)为 TagsInputItem 定义了 4 个 Props:

NameDescriptionTypeRequiredDefault
asThe element or component this component should render as. Can be overwritten by asChild.AsTag \| ComponentNo"div"
asChildChange the default rendered element for the one passed as a child, merging their props and behavior. Read our Composition guide for more details.booleanNo-
disabledWhen true, prevents the user from interacting with the tags input.booleanNo-
valueValue associated with the tagsstring \| number \| bigint \| Record<string, any>Yes-

对应到源码中,TagsInputItemProps接口继承了PrimitiveProps,并声明了唯一必填的value与可选的disabled(TagsInputItem.vue):

export interface TagsInputItemProps extends PrimitiveProps { /** Value associated with the tags */ value: AcceptableInputValue /** When `true`, prevents the user from interacting with the tags input. */ disabled?: boolean }

1.value:必填的标签值

value是每个标签项唯一必填的属性,其类型AcceptableInputValue定义在 TagsInputRoot.vue 中:

export type AcceptableInputValue = string | number | bigint | Record<string, any>

也就是说,标签既可以承载普通字符串/数字,也可以承载任意对象。使用对象值时需要特别注意两点(详见下文"对象值"示例):

  • Root 上必须提供convertValue函数,把输入框文本转换为对象;否则运行时会抛出You must provide a 'convertValue' function when using objects as values.(该行为在 TagsInput.test.ts 中有专门用例验证);
  • 可以配合 Root 的displayValue函数,把对象格式化为要展示的文本,例如Person: {name}

每个 Item 的value会被登记到 Root 的 Collection(useCollection)中,Root 正是通过该集合遍历、定位、删除标签的。

2.disabled:禁用单个标签

disabledtrue时阻止用户与该标签交互。值得注意的实现细节是:Item 的禁用态是自身禁用与整体禁用取并集得到的(TagsInputItem.vue):

const disabled = computed(() => props.disabled || context.disabled.value)

即只要 Item 自身声明disabled,或者 Root 整体处于禁用态,该标签即视为禁用。禁用后的标签:

  • 在根元素上渲染data-disabled属性;
  • 会被 Root 的键盘导航集合过滤掉(Root 中onInputKeydown使用filter(i => i.dataset.disabled !== '')构建可导航集合),因此方向键导航与 Backspace/Delete 删除都会跳过禁用标签——这一点在测试用例"given TagsInput with a disabled item before a removable one"中得到了验证:连续按 Backspace 只会删除可移除的标签,禁用标签永远无法被键盘删除(见 TagsInput.test.ts);
  • 子组件TagsInputItemDelete同样会读取该禁用态,点击删除按钮时直接返回(TagsInputItemDelete.vue)。

3.asasChild:Primitive 组合能力

与 radix-vue 其他组件一致,TagsInputItem 继承PrimitiveProps,默认渲染为divas允许把标签改成任意元素或组件(如spanli、自定义组件);asChild则让标签直接合并到传入子元素的 props 与行为上(Radix 风格的 Slot 组合模式)。这一点使标签可以无缝融入既有的列表结构或设计系统。渲染由Primitive组件完成(TagsInputItem.vue):

<template> <CollectionItem :value="value"> <Primitive :ref="forwardRef" :as="as" :as-child="asChild" :aria-labelledby="itemContext.textId" :aria-current="isSelected" :data-disabled="disabled ? '' : undefined" :data-state="isSelected ? 'active' : 'inactive'" > <slot /> </Primitive> </CollectionItem> </template>

三、选中态(data-state)与可访问性属性

官方组件文档为 Item 定义了如下 Data Attributes 表(见 tags-input.md 的### Item小节):

属性
[data-state]active|inactive
[data-disabled]Present when disabled

结合源码可以精确还原它们的来源:

  • data-state:由isSelected计算属性驱动。isSelected的判断逻辑是context.selectedElement.value === currentElement.value(TagsInputItem.vue),即当前标签的 DOM 元素是否正是 Root 记录的"当前选中标签"。选中时输出active,否则为inactive。这也是官方样式示例中data-[state=active]aria-[selected=true]类选择器能够生效的根本原因。
  • data-disabled:仅在disabled为真时输出空字符串(''),否则为undefined(即不渲染该属性)。
  • aria-current:选中标签会输出aria-current="true",配合视觉高亮,向屏幕阅读器标明当前聚焦的标签。
  • aria-labelledby:指向itemContext.textId,该 id 由TagsInputItemText生成(见下文)。

此外 Root 根元素还带有data-invaliddata-disableddata-focused等状态属性(TagsInputRoot.vue),可与 Item 的属性配合做整体样式控制。

四、与子组件的上下文协作

Item 通过provideTagsInputItemContext向内部子组件提供上下文(TagsInputItem.vue):

export interface TagsInputItemContext { value: Ref<AcceptableInputValue> displayValue: ComputedRef<string> isSelected: Ref<boolean> disabled?: Ref<boolean> textId: string }

TagsInputItemText:文本渲染与 id 生成

TagsInputItemText默认渲染为spanas默认值),从 Item 上下文读取displayValue作为默认插槽内容,并负责生成textId(TagsInputItemText.vue):

itemContext.textId ||= useId(undefined, 'reka-tags-input-item-text')
<template> <Primitive v-bind="props" :id="itemContext.textId" > <slot>{{ itemContext.displayValue.value }}</slot> </Primitive> </template>

因此"先有 ItemText、再有 Item"的顺序是安全且推荐的:ItemText 一旦挂载,就会把生成的 id 回填到 Item 上下文的textId,Item 据此渲染aria-labelledby,保证每个标签的可访问名称与其可见文本一一对应。官方 Anatomy 中TagsInputItemText被标注为 "Important for accessibility"。

TagsInputItemDelete:删除按钮

TagsInputItemDelete默认渲染为button,并强制设置tabindex="-1"(避免键盘 Tab 进入标签内层按钮,删除统一交给键盘导航),同时继承aria-labelledbyaria-currentdata-statedata-disabled(TagsInputItemDelete.vue)。点击删除时的调用链为:

function handleDelete() { if (disabled.value) return const index = context.modelValue.value.findIndex(i => isEqual(i, itemContext.value.value)) context.onRemoveValue(index) }

它使用ohashisEqual做深度相等比较(因此对象值也能正确匹配),找到当前标签在modelValue数组中的下标后,调用 Root 提供的onRemoveValue。Root 的handleRemoveTag会从数组中过滤掉该值并触发removeTag事件(TagsInputRoot.vue)。

五、键盘交互:选中、删除与导航

虽然键盘逻辑主要由 Root 的onInputKeydown承担,但它操作的对象正是 Item 的选中态(selectedElement)。官方文档的 Keyboard Table 定义了如下交互(tags-input.md 的### Keyboard Interactions):

KeysDescription
DeleteWhen tag is active, remove it and set the tag on right active.
BackspaceWhen tag is active, remove it and set the tag on left active. If there are no tags to the left, either the next tag gets focus, or the input.
ArrowRightSet the next tag active.
ArrowLeftSet the previous tag active.
HomeSet the first tag active.
EndSet the last tag active.

源码层面的关键细节(TagsInputRoot.vue):

  • 当输入框光标位于首字符(selectionStart === 0)时,按ArrowLeft会先选中最后一个标签;连续按方向键通过useArrowNavigation在标签集合中移动,selectedElement随之更新,Item 的data-state同步切换;
  • 末位标签上按ArrowRight会取消选中、回到输入框;
  • 标签处于选中态时按Backspace/Delete会先执行handleRemoveTag,再把选中态迁移到相邻标签(删除方向遵循 LTR/RTL 语义);
  • 使用 IME 输入法组合(composition)期间,所有导航与删除逻辑都会提前返回,避免干扰候选词选择——测试中专门验证了"输入法组合期间输入分隔符不应创建标签"的场景(TagsInput.test.ts)。

六、完整实战示例

基础用法:字符串标签 + 删除按钮

参考仓库 story 组件 _TagsInput.vue:

<script setup lang="ts"> import { ref } from 'vue' import { TagsInputInput, TagsInputItem, TagsInputItemDelete, TagsInputItemText, TagsInputRoot } from 'reka-ui' const modelValue = ref(['Test']) </script> <template> <TagsInputRoot v-model="modelValue" class="flex flex-wrap items-center gap-2 ..."> <TagsInputItem v-for="item in modelValue" :key="item" :value="item" class="...><TagsInputRoot v-model="people" :display-value="(item) => `Person: ${item.name}`" :convert-value="(text) => ({ name: text, id: Math.random() })" > <TagsInputItem v-for="(item, i) in people" :key="i" :value="item"> <TagsInputItemText class="text-sm" /> <TagsInputItemDelete><XIcon /></TagsInputItemDelete> </TagsInputItem> <TagsInputInput placeholder="Add a person..." /> </TagsInputRoot>

禁用单个标签

参考 _TagsInputDisabled.vue:通过给特定 Item 传disabled即可锁定该标签,同时可配合v-if隐藏其删除按钮;Root 的键盘导航会自动跳过它,且测试保证它永远不会被 Backspace 删除。

七、测试如何验证这些行为

TagsInput.test.ts 覆盖了与 TagsInputItem 直接相关的三类关键行为,可作为功能契约参考:

  1. 默认渲染与新增:初始标签正确渲染;回车新增后addTag事件按序触发、输入框被清空并保持焦点;
  2. 键盘导航与删除ArrowLeft选中末位标签(data-state="active")、Home/End跳到首/末位、ArrowRight取消选中、Backspace触发removeTag并选中新末位标签;
  3. 对象值与禁用:对象标签正确显示格式化文本;缺少convertValue时抛出指定错误信息;禁用标签前的可移除标签能被删除而禁用标签始终保留。

整套测试还通过axe做了无障碍断言,从侧面印证了aria-labelledbyaria-currentdata-state等属性设计满足无违规(accessibility violations)要求。

八、总结

TagsInputItem 是整个 Tags Input 组件中负责"单个标签"渲染与状态的核心部件:value决定了标签的身份(支持字符串、数字与对象),disabled提供了细粒度的单项禁用能力,as/asChild保留了 Primitive 的组合自由度,而data-state(active/inactive)与aria-*属性则把选中态无缝暴露给样式层与无障碍层。理解它向上注入 Root 上下文、向下供给 ItemText/ItemDelete 的协作链路,就能在此基础上轻松实现带自动补全、富文本样式与复杂对象数据的标签输入交互。

【免费下载链接】radix-vueAn open-source UI component library for building high-quality, accessible design systems and web apps for Vue. Previously Radix Vue项目地址: https://gitcode.com/GitHub_Trending/ra/radix-vue

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询