radix-vue Time Field 组件完全指南:基于 reka-ui 构建可本地化的分段式时间输入
2026/9/17 5:47:56 网站建设 项目流程

radix-vue Time Field 组件完全指南:基于 reka-ui 构建可本地化的分段式时间输入

【免费下载链接】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

本文以 reka-ui(原 Radix Vue)官方文档 Time Field 为骨架,结合仓库内 TimeFieldRoot.vue、TimeFieldInput.vue 源码及 TimeField.test.ts 测试用例,全面讲解如何在 Vue 3 项目中实现支持完整键盘导航、可受控/非受控、默认无障碍的分段式(segment-based)时间输入组件。

Time Field(<Badge>Alpha</Badge>状态)是一个专门用于输入具体时间的字段组件,它把时间拆解为时、分、秒、上下午(day period)、时区等多个可独立聚焦、编辑的"段(segment)",替代传统文本框,让用户以最自然的方式逐段录入时间。本文将带你从安装、组件拼装到 API 全量解析,再深入到源码级实现原理与测试验证,让你能直接在生产项目中正确使用并二次定制它。

核心特性

官方文档明确了该组件的六大设计目标:

  • Full keyboard navigation:完整键盘导航,支持方向键逐段移动、数字键入、退格删除等;
  • Can be controlled or uncontrolled:受控与非受控均可,通过v-model绑定值;
  • Focus is fully managed:焦点完全托管,单击任意段即可聚焦,标签点击聚焦首段;
  • Localization support:完整的本地化支持,基于Intl格式化,随locale切换段顺序与显示;
  • Highly composable:高度可组合,Root 与 Input 拆分为独立部分,可自由定制 DOM 结构;
  • Accessible by default:默认无障碍,通过role="group"、隐藏原生 input、aria-*属性与视觉隐藏元素实现。

前置依赖:@internationalized/date

官方文档明确指出,Time Field 组件依赖@internationalized/date包(Adobe React Spectrum 生态的国际化日期工具库),它解决了 JavaScript 中处理日期时间的大量痛点:不可变时间对象、时区运算、日历换算、格式化与解析等。reka-ui 中所有 date 相关组件(DateField、DatePicker、Calendar、RangeCalendar 等)均建立在该包之上。

从源码看,TimeFieldRoot.vue 直接导入了getLocalTimeZoneisEqualDayTimetoCalendarDateTimetoday等工具,测试文件 TimeField.test.ts 也使用了CalendarDateTimenowparseAbsoluteToLocalTimetoZoned五种时间值类型,因此强烈建议先通读该包文档,理解TimeCalendarDateTimeZonedDateTime的区别后再上手组件。

安装

首先安装日期基础包:

npm install @internationalized/date

然后从命令行安装组件(reka-ui 为当前项目发布名,即原 Radix Vue 的现名):

npm install reka-ui

组件解剖(Anatomy)

Time Field 由两个部分构成:

  • TimeFieldRoot:容器,持有全部状态与逻辑;
  • TimeFieldInput:渲染时间字段的某一个"段"(segment),需传入part指定渲染哪一部分。

最简拼装如下:

<script setup> import { TimeFieldInput, TimeFieldRoot, } from 'reka-ui' </script> <template> <TimeFieldRoot> <TimeFieldInput /> </TimeFieldRoot> </template>

注意:上面是最小骨架,实际使用时需要为TimeFieldInput指定part,或通过 Root 的segments插槽自动遍历渲染(见下文实战示例)。

实战示例:带标签的完整时间字段

仓库演示代码 docs/components/demo/TimeField/css/index.vue 给出了贴近生产的完整写法:使用 Root 的默认插槽接收segments数组,v-for遍历每个段,区分literal(冒号等字面分隔符)与可编辑段分别套用样式,同时通过granularity="second"把粒度设为秒:

<script setup lang="ts"> import { Label, TimeFieldInput, TimeFieldRoot } from 'reka-ui' import './styles.css' </script> <template> <div class="TimeFieldWrapper"> <Label class="TimeFieldLabel" for="time-field">Appointment</Label> <TimeFieldRoot id="time-field" v-slot="{ segments }" granularity="second" class="TimeField" > <template v-for="item in segments" :key="item.part"> <TimeFieldInput v-if="item.part === 'literal'" :part="item.part" class="TimeFieldLiteral" > {{ item.value }} </TimeFieldInput> <TimeFieldInput v-else :part="item.part" class="TimeFieldSegment" > {{ item.value }} </TimeFieldInput> </template> </TimeFieldRoot> </div> </template>

要点:

  • Root 的v-slot="{ segments }"提供段内容数组,每项为{ part, value }(见 TimeFieldRoot.vue 的segmentContents计算属性);
  • part === 'literal'的段(如:分隔符)只读展示,不参与编辑;
  • Labelfor指向 Root 的id,点击标签会将焦点移到首个段(测试focuses first segment on label click验证了该行为)。

API Reference:Root

Root 是包含整个时间字段全部部分的容器,渲染为role="group"的 div(默认标签),提供状态注入、键盘导航与无障碍属性。

Root Props

NameDescriptionTypeRequiredDefault
as组件应渲染成的元素或组件,可被asChild覆盖AsTag \| ComponentNo"div"
asChild将默认渲染元素改为传入的子元素,合并其 props 与行为booleanNo-
defaultPlaceholder默认占位时间TimeValueNo-
defaultValue默认值(非受控模式)TimeValueNo-
dir阅读方向,省略时继承全局ConfigProvider,否则假定 LTR"ltr" \| "rtl"No-
disabled是否禁用整个时间字段booleanNofalse
granularity格式化时间的粒度,字段将渲染到该粒度为止的所有段"hour" \| "minute" \| "second"No-
hideTimeZone是否隐藏时区段booleanNo-
hourCycle格式化时间使用的小时制,默认跟随本地偏好12 \| 24No-
id元素 idstringNo-
locale格式化日期使用的区域stringNo-
maxValue可选择的最大时间TimeValueNo-
minValue可选择的最小时间TimeValueNo-
modelValue受控状态值,可绑定v-modelTimeValue \| nullNo-
name字段名,随所属表单以 name/value 对提交stringNo-
placeholder占位时间,用于确定未选中时间时的显示,随用户导航实时更新TimeValueNo-
readonly是否只读booleanNofalse
required为 true 时表示用户必须在表单提交前设置值booleanNo-
step步进间隔,默认1DateStepNo-
stepSnapping是否在输入后将值吸附到最近的步进增量,默认falsebooleanNofalse

Root Events

NameDescriptionType
update:modelValue模型值变化时触发[date: TimeValue]
update:placeholder占位值变化时触发[date: TimeValue]

Root Slots

NameDescriptionType
modelValue字段当前时间TimeValue \| undefined
segments时间字段的段内容{ part: SegmentPart; value: string; }[]
isInvalid输入是否无效boolean

Root Methods

NameDescriptionType
setFocusedElement设置 DateField 内部聚焦元素的辅助方法(el: HTMLElement) => void

关键 Props 的源码级解读

granularity(粒度):决定渲染哪些段。从 TimeFieldRoot.vue 可见,未显式传入时默认推断为minute;底层 shared/date/utils.ts 的getOptsByGranularity会按粒度裁剪Intl.DateTimeFormatOptions(如granularity: 'hour'时删除minute/second'minute'时删除second)。测试overrides the default displayed segments with the granularity prop系列验证了hour/minute粒度下的段显隐。

hourCycle(小时制):接受12 | 24。源码通过normalizeHourCycle映射为h23/h11传给Intl格式化器(shared/date/utils.ts)。12 小时制下,内部 24 小时值会被转换为展示值:hour === 0 → 12hour > 12 → hour - 12(见 TimeFieldRoot.vue 的segmentContents转换逻辑)。测试覆盖了午夜显示12 AM、正午显示12 PM、下午 13-23 点转换,以及 11 PM 上箭头循环到 12 AM 等边界情况。

stepstepSnappingstep的类型DateStep定义于 shared/date/types.ts,允许按hour/minute/second/millisecond分别设置步长;未传时normalizeDateStepdefu合并出全为 1 的默认值(shared/date/utils.ts)。stepSnapping开启后,键入的值会吸附到最近步进增量:测试中step: { minute: 15 }时键入23吸附为30、键入17吸附为15、键入58因越界吸附为45;而方向键调整始终按step走,与stepSnapping无关。

minValue/maxValue:用于合法性校验。Root 的isInvalid计算属性会比对当前值与上下界(TimeFieldRoot.vue),越界时组件挂上data-invalidaria-invalid

modelValue的类型兼容:组件接受TimeValue,即TimeCalendarDateTimeZonedDateTime三者之一。convertValue会把纯时间值通过toCalendarDateTime与"今天"合并(TimeFieldRoot.vue);测试分别验证了三种类型都能正确回填段内容,且ZonedDateTime会额外渲染dayPeriodtimeZoneName段(如PMEST)。

API Reference:Input

TimeFieldInput渲染时间字段的某个段,是实际可聚焦、可编辑的最小单元。

Input Props

NameDescriptionTypeRequiredDefault
as组件应渲染成的元素或组件,可被asChild覆盖AsTag \| ComponentNo"div"
asChild将默认渲染元素改为传入的子元素,合并其 props 与行为booleanNo-
part要渲染的时间段部分"day" \| "month" \| "year" \| "hour" \| "minute" \| "second" \| "dayPeriod" \| "literal" \| "timeZoneName"Yes-

从实现看,TimeFieldInput.vue 会为该段设置data-reka-time-field-segment="part"contenteditable(禁用/只读或literal时为false),并把disabled/readonly/invalid同步为aria-*data-*属性;所有按键、输入、合成事件处理由useDateField组合式函数统一提供,Root 上下文通过injectTimeFieldRootContext注入。

数据属性(Data Attributes)

Root

AttributeValues
[data-readonly]Present when readonly
[data-disabled]Present when disabled
[data-invalid]Present when invalid

Input

AttributeValues
[data-disabled]Present when disabled
[data-invalid]Present when invalid
[data-placeholder]Present when no value is set

此外每个 Input 段还带data-reka-time-field-segment,其值即part(见 TimeFieldInput.vue),Root 正是靠它来识别、排序并定位各段(TimeFieldRoot.vue)。

无障碍与键盘交互

无障碍设计

  • Root 渲染role="group",禁用时附加aria-disabled
  • Root 内部放置一个VisuallyHidden的隐藏原生input(带name/required/disabled/value),用于随表单提交与屏幕阅读器交互(TimeFieldRoot.vue),其聚焦时自动把焦点转交给首段;
  • 每个段带aria-disabledaria-readonlyaria-invalid,无值时呈现占位内容;
  • 测试套件通过vitest-axeaxe(container)断言无无障碍违规(TimeField.test.ts)。

键盘交互

KeysDescription
Tab焦点移入时间字段时,聚焦第一个段
ArrowLeft/ArrowRight在时间字段的各段之间导航
ArrowUp/ArrowDown递增/改变当前段的值(按step步进,12 小时制下小时段会联动上下行切换)
0-9焦点在数值型TimeFieldInput上时输入数字,若下一个输入将产生无效值则自动跳到下一段
Backspace从聚焦的数值段删除一位数字
A/P焦点在 day period(上下午)段时,分别设为 AM 或 PM

源码层面的键盘实现要点:

  • 段间导航:Root 监听keydown.left.right(TimeFieldRoot.vue),通过data-reka-time-field-segment计算当前段索引并移动焦点;方向键导航感知书写方向dir === 'rtl'时左右键语义互换),而数字键入后的自动前进始终按 DOM 顺序(即 locale 的格式顺序)进行——测试advances focus through segments in DOM order when typing in RTL专门验证了 RTL 下键入数字时焦点仍按段顺序推进;
  • 键入行为:数字段支持连击,1连打会一路走完 hour → minute → second → dayPeriod(测试takes you all the way through the segment),0的焦点推进规则(段值为 0 时不过早切换焦点)也有专门测试;
  • 合成输入保护:e.isComposing时跳过导航处理,避免与 IME 候选词导航冲突;
  • 12 小时制联动:在dayPeriod段按A/P或方向键会同步换算modelValue的小时(如 10 PM 内部存为 22 时、12 AM 内部存为 0 时),对应测试updates the hour on the modelValue if the dayPeriod is updated及 12-hour input handling 全套用例。

本地化行为

  • 未传locale时继承ConfigProvider或回退到浏览器环境;
  • 切换 locale 会重建段顺序与显示:Root 的watch(locale)会重新抓取段元素(因为不同 locale 下格式顺序可能不同),并同步更新 formatter(TimeFieldRoot.vue);
  • 测试验证了en-UK等不使用上下午制的 locale 不渲染dayPeriod段,而默认 locale 下会渲染;zh-TW下开启hideTimeZone时不再显示时区括号;
  • 12 小时制显示与 locale 结合:hourCycle: 12时即使传入 24 小时制内部值也会正确展示为 12 小时格式并联动 AM/PM。

禁用、只读与表单集成

  • disabled:整场禁用。段不可聚焦、不响应输入,Root 挂data-disabled,隐藏 input 同步disabled;测试prevents interaction when disabled验证所有段点击无焦点、无tabindex
  • readonly:可聚焦但不可修改。contenteditablefalse,方向键等不会改变值;测试prevents modification when readonly验证了这一点;
  • 表单提交:通过 Root 的name+ 隐藏 input 完成原生表单集成,required标记必填约束。

源码结构速览

文件作用
TimeFieldRoot.vueRoot 组件:状态管理、段注册、键盘导航、locale/hourCycle 格式化、无障碍属性、上下文注入
TimeFieldInput.vueInput 段组件:按part渲染单个可编辑段,复用useDateField处理输入
shared/date/types.tsTimeValueDateStepSegmentPartHourCycleDayPeriod等核心类型
shared/date/utils.ts粒度选项构建、步长归一化、小时制映射等工具函数
TimeField.test.ts覆盖无障碍、RTL、三种时间值类型、粒度、小时制、步进吸附、禁用只读等 30+ 场景的测试套件

组件通过createContext/provideTimeFieldRootContext建立 Root ↔ Input 的上下文通信(TimeFieldRoot.vue),并在 index.ts 统一导出TimeFieldRootTimeFieldInput及其类型。

使用建议

  • 若只需时分输入,保持默认granularity: 'minute';需要秒级精度时显式设为'second'
  • 跨时区场景(如预约系统)优先使用ZonedDateTime作为modelValue,并配合hideTimeZone控制时区段显隐;
  • 需要约束可选时间范围时同时设置minValue/maxValue,组件会自动计算isInvalid并反映在data-invalid与插槽参数上;
  • 步进场景(如每 5 分钟、每 15 分钟选择)设置step: { minute: 5 },需要强制吸附键入值时再开启stepSnapping
  • 需要上下午制展示时设置hourCycle: 12,组件会自动处理午夜/正午与 AM/PM 的往返换算。

该组件当前处于 Alpha 阶段,API 可能随版本演进微调,建议以仓库 docs/content/meta/TimeFieldRoot.md 与 docs/content/meta/TimeFieldInput.md 的自动生成元数据为准核对当前版本参数。

【免费下载链接】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),仅供参考

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

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

立即咨询