radix-vue(Reka UI)YearRangePickerRoot 组件完全指南:年份区间选择器的 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
YearRangePickerRoot 是 radix-vue 组件库(现更名为 Reka UI)中用于构建"年份区间选择器"的根组件,适合需要按年份批量选择连续区间的场景,例如筛选 2018-2023 年度数据、生成多年期的统计报表等。读完本文,你将掌握该根组件的全部 23 个 Props、3 个自定义事件与 4 个作用域插槽的准确语义,并能结合子组件组合出一个可无障碍键盘操作的年份范围选择器。
组件定位与整体架构
YearRangePickerRoot 是整个 YearRangePicker(年份范围选择器)的唯一状态中枢。它本身不渲染任何可见的年份网格,而是通过provide/inject上下文机制向YearRangePickerGrid、YearRangePickerCell、YearRangePickerHeader、YearRangePickerNext、YearRangePickerPrev等子组件下发共享状态与回调,这与仓库内RangeCalendar、MonthRangePicker、YearPicker等日期组件共享同一套设计模式。
从源码看,Root 组件内部组合了两个核心状态逻辑:
- useYearPicker(由
YearRangePickerRoot.vue第 216 行引入):负责年份网格生成(grid)、翻页(nextPage/prevPage)、标题文本(headingValue)与格式化器(formatter); - useRangeYearPickerState:负责区间合法性校验(
isInvalid)、选中态判定(isSelected)、区间高亮(highlightedRange)以及maximumYears/fixedDate的边界约束。
组件通过 index.ts 统一导出 9 个子组件,所有组件均以YearRangePicker前缀命名,便于在 IDE 中自动补全与全局注册。
基础用法示例
首先引入组件与日期工具类:
import { CalendarDate } from '@internationalized/date' import { YearRangePickerCell, YearRangePickerCellTrigger, YearRangePickerGrid, YearRangePickerGridBody, YearRangePickerGridRow, YearRangePickerHeader, YearRangePickerHeading, YearRangePickerNext, YearRangePickerPrev, YearRangePickerRoot, } from 'reka-ui'默认选中一个 2020 至 2024 年的区间(默认每页显示 12 年,按 4 列 × 3 行布局):
<script setup lang="ts"> const defaultValue = { start: new CalendarDate(2020, 1, 1), end: new CalendarDate(2024, 1, 1), } </script> <template> <YearRangePickerRoot v-slot="{ grid }" :default-value="defaultValue"> <YearRangePickerHeader> <YearRangePickerPrev>‹</YearRangePickerPrev> <YearRangePickerHeading /> <YearRangePickerNext>›</YearRangePickerNext> </YearRangePickerHeader> <YearRangePickerGrid> <YearRangePickerGridBody> <YearRangePickerGridRow v-for="(years, index) in grid.rows" :key="`year-${index}`"> <YearRangePickerCell v-for="year in years" :key="year.toString()" :date="year"> <YearRangePickerCellTrigger :year="year" /> </YearRangePickerCell> </YearRangePickerGridRow> </YearRangePickerGridBody> </YearRangePickerGrid> </YearRangePickerRoot> </template>上述结构在官方文档示例中均有完整可运行的实现,分别位于 tailwind 版示例 与 css 版示例,可直接参考其样式写法。
Props 全量解析
YearRangePickerRoot 共暴露 23 个 Props。下表为完整清单:
| Name | 说明 | 类型 | 必填 | 默认值 |
|---|---|---|---|---|
allowNonContiguousRanges | 与isYearUnavailable配合,决定是否允许选择不连续区间 | boolean | 否 | false |
as | 指定组件渲染为的元素或组件,可被asChild覆盖 | AsTag \| Component | 否 | "div" |
asChild | 将默认渲染元素替换为传入的子元素,并合并 props 与行为 | boolean | 否 | - |
calendarLabel | 日历的可访问标签(无障碍用途) | string | 否 | - |
defaultPlaceholder | 默认占位日期 | DateValue | 否 | - |
defaultValue | 日历的默认值 | DateRange | 否 | { start: undefined, end: undefined } |
dir | 日历的阅读方向 | "ltr" \| "rtl" | 否 | - |
disabled | 是否禁用整个日历 | boolean | 否 | false |
fixedDate | 固定区间的哪一端 | "start" \| "end" | 否 | - |
initialFocus | 为 true 时,挂载时聚焦到已选中的年份 | boolean | 否 | false |
isYearDisabled | 判断某一年是否被禁用的函数 | Matcher | 否 | - |
isYearUnavailable | 判断某一年是否不可用的函数 | Matcher | 否 | - |
locale | 用于日期格式化的语言环境 | string | 否 | - |
maximumYears | 区间最多可选多少个年份 | number | 否 | - |
maxValue | 可选择的最大日期 | DateValue | 否 | - |
minValue | 可选择的最小日期 | DateValue | 否 | - |
modelValue | 受控的已选年份区间,可绑定v-model | DateRange \| null | 否 | - |
nextPage | 返回下一页(下一个年份页)的函数 | (placeholder: DateValue) => DateValue | 否 | - |
placeholder | 占位日期,用于在未选择时决定显示哪一页 | DateValue | 否 | - |
preventDeselect | 是否阻止用户在未选择新日期前取消选择 | boolean | 否 | false |
prevPage | 返回上一页(上一个年份页)的函数 | (placeholder: DateValue) => DateValue | 否 | - |
readonly | 日历是否只读 | boolean | 否 | false |
yearsPerPage | 每页显示的年份数量 | number | 否 | 12 |
受控与非受控:modelValue、defaultValue 与 placeholder
modelValue是受控值,通过v-model双向绑定;当存在modelValue时,defaultValue不再生效。Root 内部使用useVModel同步,并在外部值变化时通过watch自动同步startValue/endValue(见 YearRangePickerRoot.vue)。defaultValue为非受控初始值,类型为DateRange,其结构为{ start: DateValue | undefined, end: DateValue | undefined }。placeholder决定"当前展示的是哪一页"——当用户尚未选择任何年份时,以占位日期所在年份对应的页作为展示页;选中起始年后,placeholder会自动跟随起始年(源码中watch(startValue)会同步更新 placeholder)。
区间约束:minValue / maxValue 与 maximumYears / fixedDate
minValue/maxValue是绝对日期边界:任何超出该范围的年份既不可选中,也不会被聚焦(键盘导航时shiftFocus会先判断候选年份是否越界,见 YearRangePickerCellTrigger.vue)。maximumYears限制区间跨度的最大年份数。在 useRangeYearPicker.ts 中可以看到,当start与end都确定后,超出start ± (maximumYears - 1)范围的年份会被判为禁用;当只选中start时,focusedValue的预览高亮也会被anchor.add({ years: maximumYears - 1 })封顶,避免预览出超长区间。fixedDate与maximumYears配合:固定起始年后,end只能在start + (maximumYears - 1)范围内向后延伸;固定结束年则相反。交互上,选中完整区间后再次点击时,fixedDate决定"重开哪个端点"(详见 CellTrigger 中changeYear的分支逻辑)。
禁用与不可用:isYearDisabled / isYearUnavailable
isYearDisabled表示"硬禁用":禁用年份不可点击、不可聚焦、键盘导航会跳过。isYearUnavailable表示"软不可用":通常用于表达业务上暂时不可选的年份(如尚未发生的未来年度)。- 二者对区间有效性的影响不同:若选中的起点或终点命中
isYearDisabled,区间会被判为isInvalid(Root 会渲染data-invalid属性);而isYearUnavailable只影响区间中段——当区间内任意年份不可用时,highlightedRange返回null(即不可提交该区间),除非开启allowNonContiguousRanges。判断逻辑见 useRangeYearPicker.ts:areAllYearsBetweenValid(start, end, allowNonContiguousRanges ? () => false : isYearUnavailable, rangeIsYearDisabled)。
交互行为:preventDeselect、allowNonContiguousRanges、readonly、disabled
preventDeselect为true时,点击已选中的起点年份不会将其取消,必须先选新年份(见changeYear中!rootContext.preventDeselect.value的守卫条件)。allowNonContiguousRanges开启后,即使区间中间存在isYearUnavailable的年份,也允许该区间通过预览高亮与选中(前提是端点本身有效)。readonly只读模式下所有选择操作被拦截(changeYear第一步即检查readonly),但仍可展示数据;disabled则禁用整个交互且视觉上呈现禁用态。
渲染控制:as / asChild 与 yearsPerPage / nextPage / prevPage
as默认渲染为<div>,可通过as覆盖或asChild将根节点替换为任意子元素,用于与组件库组合(Composition 模式)。yearsPerPage控制每页年份数量(默认 12),网格行数由子组件按 4 列排布得出;nextPage/prevPage允许自定义翻页算法,例如跳过某些年份段,返回值为新的DateValue。
无障碍与国际化:calendarLabel、locale、dir
calendarLabel作为日历整体可访问标签,Root 会在渲染时把它写入aria-label,同时内部还维护一个视觉隐藏的role="heading"标题用于屏幕阅读器朗读当前页(见模板中fullCalendarLabel的用法)。locale决定年份文本的本地化格式(如中文"二〇二四年"、英文"2024");未指定时跟随全局ConfigProvider或浏览器默认。dir支持ltr/rtl,影响键盘方向键导航的语义(RTL 下左右箭头方向翻转,见handleArrowKey中sign的计算)。
事件(Emits)
| 事件名 | 说明 | 回调参数 |
|---|---|---|
update:modelValue | 每当模型值(选中区间)变化时触发 | [date: DateRange] |
update:placeholder | 每当占位日期变化时触发 | [date: DateValue] |
update:startValue | 每当起始值变化时触发 | [date: DateValue] |
update:modelValue由v-model自动监听,通常无需手动处理;在源码中,该事件由内部watch([startValue, endValue])驱动:当start/end都确定时,会按年份先后自动规整为{ start: 较小年, end: 较大年 },保证区间始终正向有序。update:placeholder用于受控占位日期;结合:placeholder.sync可在组件外部控制当前页码。update:startValue是一个便捷事件,方便在仅关心区间起点(例如级联联动到其他组件)时避免解构整个DateRange。- 交互中途按 ESC 会回滚到上一次合法的
modelValue(Root 的keydown监听基于isEditing状态实现),避免半成品区间污染受控值。
作用域插槽(Slots)
| 插槽名 | 说明 | 插槽 props |
|---|---|---|
date | 当前占位日期 | DateValue |
grid | 年份网格 | Grid<DateValue> |
locale | 日历语言环境 | string |
modelValue | 当前日期区间 | DateRange |
在官方示例中最常用的是grid插槽:通过v-slot="{ grid }"解构出grid.rows,外层用v-for渲染YearRangePickerGridRow,内层再遍历每行的年份数组渲染YearRangePickerCell。grid.rows的数据结构为二维数组,每行包含若干个DateValue,其分页与每页行数由yearsPerPage决定。
数据属性(Data Attributes)与键盘交互
尽管 Root 自身只渲染data-readonly、data-disabled、data-invalid三个状态属性,但其子组件YearRangePickerCellTrigger会依据 Root 上下文输出完整的状态属性,用于样式定制(见 YearRangePickerCellTrigger.vue):
data-selected/data-selection-start/data-selection-end:选中态与区间端点;data-highlighted/data-highlighted-start/data-highlighted-end:悬停预览高亮态(配合focusedValue);data-disabled/data-unavailable:禁用与不可用态;data-today:当前年份(年份选择器中指"今年");data-focused:当前占位年份,同时对应tabindex="0"的可聚焦项。
键盘交互同样由 CellTrigger 完成,包括方向键逐格移动、PageUp/PageDown整页翻动、Enter/Space选中、越界自动翻页与跳过禁用项等,整套行为与无障碍规范保持一致。Root 提供headingId、fullCalendarLabel等上下文,保证屏幕阅读器可朗读当前页标题。
完整可运行示例:带约束的年份区间筛选器
综合以上能力,实现一个"近十年可选、最长 5 年、禁止未来年度"的筛选器:
<script setup lang="ts"> import { CalendarDate, today, getLocalTimeZone } from '@internationalized/date' const currentYear = today(getLocalTimeZone()).year // 禁用未来年份:isYearUnavailable 命中未来年度 const isYearUnavailable = (date: CalendarDate) => date.year > currentYear // 硬性边界:最早 2010 年 const minValue = new CalendarDate(2010, 1, 1) </script> <template> <YearRangePickerRoot v-slot="{ grid }" :min-value="minValue" :is-year-unavailable="isYearUnavailable" :maximum-years="5" years-per-page="16" locale="zh-CN" > <YearRangePickerHeader> <YearRangePickerPrev>‹</YearRangePickerPrev> <YearRangePickerHeading /> <YearRangePickerNext>›</YearRangePickerNext> </YearRangePickerHeader> <YearRangePickerGrid> <YearRangePickerGridBody> <YearRangePickerGridRow v-for="(years, index) in grid.rows" :key="`year-${index}`"> <YearRangePickerCell v-for="year in years" :key="year.toString()" :date="year"> <YearRangePickerCellTrigger :year="year" /> </YearRangePickerCell> </YearRangePickerGridRow> </YearRangePickerGridBody> </YearRangePickerGrid> </YearRangePickerRoot> </template>该示例中,maximum-years="5"会阻止用户选择跨度超过 5 年的区间,is-year-unavailable会让未来年份显示删除线且不可点选,minValue则把最早可选年份限制在 2010 年。
小结
YearRangePickerRoot 是 radix-vue / Reka UI 年份区间选择能力的入口与状态核心:通过modelValue/defaultValue管理受控与非受控值,通过isYearDisabled/isYearUnavailable/maximumYears/fixedDate/minValue/maxValue组合出精细的可选区间约束,再借助grid作用域插槽与 9 个配套子组件完成完整的网格渲染与无障碍键盘交互。掌握其 Props、事件与插槽语义后,即可在任意 Vue 3 项目中快速构建专业、可访问的年份范围选择界面。
【免费下载链接】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),仅供参考