Radix Vue Tabs 组件 TabsRoot 完整指南:受控状态、方向、激活模式与键盘导航实现解析
【免费下载链接】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
导读
Tabs(标签页)是一组分层排列的内容区块——即 tab panel(标签面板),同一时刻只展示其中一个。在 radix-vue(Radix Vue,本仓库即其源码实现,现已演进为 reka-ui)中,TabsRoot是整套 Tabs 组件的状态中枢:它持有当前激活值、方向与激活模式等共享上下文,并通过 Context 注入给TabsList、TabsTrigger、TabsContent、TabsIndicator等子部件。读完本文,你将掌握TabsRoot全部 Props / Events / Slots 的含义与默认值,理解受控与非受控模式、自动/手动激活、横向/纵向布局与 RTL 支持在源码中的落点,并能在实际项目中写出可复现、可运行、符合 WAI-ARIA Tabs 模式的完整标签页。
组件定位与 Anatomy
TabsRoot位于packages/core/src/Tabs/TabsRoot.vue,是 Tabs 组件的根容器,"Contains all the tabs component parts"。完整装配通常包含五个部分:
<script setup> import { TabsContent, TabsIndicator, TabsList, TabsRoot, TabsTrigger } from 'reka-ui' </script> <template> <TabsRoot> <TabsList> <TabsIndicator /> <TabsTrigger /> </TabsList> <TabsContent /> </TabsRoot> </template>组件结构一览(见 packages/core/src/Tabs):
| 部件 | 文件 | 职责 |
|---|---|---|
TabsRoot | TabsRoot.vue | 状态中枢,提供共享 Context |
TabsList | TabsList.vue | 沿激活内容边缘排列触发器的容器,role="tablist" |
TabsTrigger | TabsTrigger.vue | 激活对应内容的按钮,role="tab" |
TabsIndicator | TabsIndicator.vue | 高亮当前激活标签的指示器 |
TabsContent | TabsContent.vue | 与触发器关联的内容面板,role="tabpanel" |
在根节点上,TabsRoot会把dir与data-orientation透传到最外层渲染元素;同时通过provideTabsRootContext注入modelValue、orientation、dir、unmountOnHide、activationMode、baseId、tabsList、contentIds及注册/注销 content 的回调(TabsRoot.vue),子部件全部通过injectTabsRootContext()获取这些状态。
Props 全面解析
TabsRootProps<T>继承自PrimitiveProps,泛型T extends StringOrNumber(即string | number)决定了标签 value 的类型。以下表格来自 docs/content/meta/TabsRoot.md,并结合源码(TabsRoot.vue)补充了默认值语义:
| Name | Description | Type | Required | Default |
|---|---|---|---|---|
activationMode | 标签是在聚焦时自动激活,还是在点击时手动激活。 | "automatic" \| "manual" | No | "automatic" |
as | 组件渲染成的元素或组件,可被asChild覆盖。 | AsTag \| Component | No | "div" |
asChild | 将默认渲染元素改为传入的子元素,合并其 props 与行为。详见 Composition 指南。 | boolean | No | - |
defaultValue | 初始渲染时激活的标签值。用于不需要控制 tabs 状态(非受控)的场景。 | T | No | - |
dir | 阅读方向。省略时继承自全局ConfigProvider,否则假定 LTR。 | "ltr" \| "rtl" | No | - |
modelValue | 受控模式下要激活的标签值,可通过v-model绑定。 | T | No | - |
orientation | 标签的布局方向,主要用于决定箭头导航方向(左右 vs 上下)。 | "vertical" \| "horizontal" | No | "horizontal" |
unmountOnHide | 为true时,关闭(隐藏)状态下元素会被卸载。 | boolean | No | true |
受控与非受控模式(modelValue / defaultValue)
源码中通过useVModel统一处理两种模式(TabsRoot.vue):
const modelValue = useVModel<TabsRootProps<T>, 'modelValue', 'update:modelValue'>(props, 'modelValue', emits, { defaultValue: props.defaultValue, passive: (props.modelValue === undefined) as false, })- 传入
modelValue并配合v-model即为受控模式:激活值完全由外部状态决定,切换标签会触发update:modelValue事件; - 不传
modelValue、只传defaultValue即为非受控模式:内部自行维护状态,defaultValue仅作为初始值,后续切换不再同步回外部。
泛型T允许 value 为字符串或数字。注意官方示例中存在:value="1"(数字)与value="tab2"(字符串)混用的写法(见 story/_Tabs.vue),源码对二者均支持,但同一组标签内应保持 value 类型一致,以免触发值比较问题。
方向 dir:继承 ConfigProvider、支持 RTL
dir的取值逻辑在 TabsRoot.vue:
const { orientation, unmountOnHide, dir: propDir } = toRefs(props) const dir = useDirection(propDir)useDirection会在未显式传入dir时向全局ConfigProvider查询阅读方向,缺省假定 LTR。该方向随后被传递给TabsList内部的RovingFocusGroup,决定左右方向键的移动顺序,并作为dir属性渲染到根元素上。
渲染控制:as / asChild
TabsRoot默认渲染为div,并通过Primitive组件对外渲染(TabsRoot.vue):
<Primitive :dir="dir" :data-orientation="orientation" :as-child="asChild" :as="as" > <slot :model-value="modelValue" /> </Primitive>as:指定渲染为任意元素或组件,如as="section";asChild:将根元素替换为你传入的单个子元素,并把组件的行为、属性合并到该元素上,适合与现有语义化标记或自定义组件组合。
卸载策略 unmountOnHide
默认true,即标签内容在隐藏时会被卸载(配合Presence组件实现)。若设为false,内容面板仅通过hidden属性隐藏而保留在 DOM 中,可用于需要保持内部状态(如表单输入)或配合动画库的场景。其实际消费点在 TabsContent.vue:<slot v-if="rootContext.unmountOnHide.value ? present : true" />。
Events 与 Slots
TabsRoot对外仅暴露一个事件与一个插槽(见 docs/content/meta/TabsRoot.md):
Events
| Name | Description | Type |
|---|---|---|
update:modelValue | 值变化时调用的事件处理器 | [payload: T] |
该事件由useVModel驱动,受控模式下任何切换(点击、聚焦自动激活、键盘操作)最终都会走到changeModelValue并写入modelValue,进而触发update:modelValue(TabsRoot.vue)。
Slots
| Name | Description | Type |
|---|---|---|
modelValue | 当前输入值 | T |
默认插槽接收modelValue作为 slot prop,方便在根内部访问当前激活值,例如<TabsRoot v-slot="{ modelValue }">。
源码级实现细节
子部件的共享上下文
TabsRoot提供的关键 Context 成员及消费方:
tabsList:由TabsList挂载时通过context.tabsList = currentElement回写(TabsList.vue),供TabsIndicator查询激活 tab 的位置;contentIds/registerContent/unregisterContent:TabsContent在onMounted时注册自己的 value、在卸载前注销(TabsContent.vue)。这样TabsTrigger可以只对"确实存在对应内容"的触发器输出aria-controls——测试 Tabs.test.ts 专门验证了这一行为;baseId:由useId(undefined, 'reka-tabs')生成,随后通过 utils.ts 的makeTriggerId/makeContentId派生出${baseId}-trigger-${value}与${baseId}-content-${value}关联 ID。
自动激活 activationMode 的落点
activationMode本身在 Root 中只做透传(TabsRoot.vue),真正消费它的是TabsTrigger的@focus处理器(TabsTrigger.vue):
const isAutomaticActivation = rootContext.activationMode !== 'manual'; if (!isSelected && !disabled && isAutomaticActivation) { rootContext.changeModelValue(value); }automatic(默认):键盘聚焦或 Tab 键移入时即激活对应标签,符合移动端与多数桌面端的预期;manual:仅当用户点击或按 Enter / Space 时才激活(@keydown.enter.space="rootContext.changeModelValue(value)"),聚焦本身不改变激活值。
鼠标点击的精细处理
TabsTrigger对mousedown.left做了特殊处理(TabsTrigger.vue):仅响应左键且未按住 Ctrl(避开 macOS 右键触发的mousedown),否则preventDefault()阻止聚焦,避免误激活。
与 RovingFocusGroup 的协作
TabsList将自身包裹在RovingFocusGroup中(TabsList.vue),把orientation、dir、loop(默认true,支持在首尾标签间循环)传入;每个TabsTrigger则由RovingFocusItem包裹(TabsTrigger.vue)。这构成了完整的键盘焦点管理链路:上下左右箭头按方向移动焦点、Home/End 跳到首尾、Tab 键在 tablist 与 tabpanel 间切换。
数据属性与无障碍
- 根与列表渲染
[data-orientation="vertical|horizontal"]; - 触发器渲染
[data-state="active|inactive"]、禁用时渲染[data-disabled],并输出aria-selected、aria-controls(仅当有匹配内容时)与role="tab"; - 内容渲染
[data-state]与[data-orientation],输出role="tabpanel"、aria-labelledby指向对应触发器,且tabindex="0"使面板可聚焦(TabsContent.vue)。
以上属性使组件整体遵循 WAI-ARIA Tabs 设计模式。测试套件通过axe自动检查无障碍违规(Tabs.test.ts),并验证了 ArrowRight 切换焦点与内容渲染行为(Tabs.test.ts)。
实战示例
基础用法(非受控 + 自动激活)
<script setup> import { TabsContent, TabsList, TabsRoot, TabsTrigger } from 'reka-ui' </script> <template> <TabsRoot default-value="account" class="flex flex-col w-[300px]" > <TabsList class="flex border-b" aria-label="Manage your account" > <TabsTrigger class="px-5 h-[45px] flex-1" value="account" > Account </TabsTrigger> <TabsTrigger class="px-5 h-[45px] flex-1" value="password" > Password </TabsTrigger> </TabsList> <TabsContent value="account"> Make changes to your account here. </TabsContent> <TabsContent value="password"> Change your password here. </TabsContent> </TabsRoot> </template>完整可运行示例可参考 story/_Tabs.vue 与 TabsVertical.story.vue。
受控模式
<script setup> import { ref } from 'vue' import { TabsContent, TabsList, TabsRoot, TabsTrigger } from 'reka-ui' const activeTab = ref('tab1') </script> <template> <TabsRoot v-model="activeTab"> <TabsList aria-label="controlled tabs"> <TabsTrigger value="tab1">One</TabsTrigger> <TabsTrigger value="tab2">Two</TabsTrigger> </TabsList> <TabsContent value="tab1">Tab one content</TabsContent> <TabsContent value="tab2">Tab two content</TabsContent> </TabsRoot> </template>纵向布局
设置orientation="vertical"后,箭头导航自动切换为上下方向(左右键逻辑由内部RovingFocusGroup按方向重新映射),并渲染data-orientation="vertical":
<TabsRoot default-value="tab1" orientation="vertical" > <TabsList aria-label="tabs example"> <TabsTrigger value="tab1">One</TabsTrigger> <TabsTrigger value="tab2">Two</TabsTrigger> <TabsTrigger value="tab3">Three</TabsTrigger> </TabsList> <TabsContent value="tab1">Tab one content</TabsContent> <TabsContent value="tab2">Tab two content</TabsContent> <TabsContent value="tab3">Tab three content</TabsContent> </TabsRoot>手动激活模式
将activationMode设为"manual",聚焦只移动焦点、不切换面板,点击或按 Enter/Space 才激活:
<TabsRoot default-value="tab1" activation-mode="manual" > <!-- ... --> </TabsRoot>键盘交互参考
根据官方文档(docs/content/docs/components/tabs.md)中列出的键盘表,并对照RovingFocusGroup/RovingFocusItem的实现:
| 按键 | 行为 |
|---|---|
Tab | 焦点进入 tabs 时聚焦当前激活的触发器;触发器聚焦时,将焦点移至激活的内容面板 |
ArrowDown | 依orientation将焦点移至下一个触发器并激活其内容 |
ArrowRight | 依orientation将焦点移至下一个触发器并激活其内容 |
ArrowUp | 依orientation将焦点移至上一个触发器并激活其内容 |
ArrowLeft | 依orientation将焦点移至上一个触发器并激活其内容 |
Home | 焦点移至第一个触发器并激活其内容 |
End | 焦点移至最后一个触发器并激活其内容 |
TabsList的loop属性默认为true,即在首尾之间循环移动焦点;将其设为false可禁用循环。
更多阅读
- 组件主文档:docs/content/docs/components/tabs.md
- API 元数据:docs/content/meta/TabsRoot.md
- 源码目录:packages/core/src/Tabs
- 测试用例:packages/core/src/Tabs/Tabs.test.ts
- 指南文档:
docs/content/docs/guides下的 Composition、Styling、Animation 等章节,覆盖asChild组合、数据属性样式与 Presence 动画相关主题。
【免费下载链接】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),仅供参考