news 2026/9/18 5:58:10

radix-vue(Reka UI)YearPickerNext 组件完全指南:年份翻页按钮的 Props、Slot 与源码级实现解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
radix-vue(Reka UI)YearPickerNext 组件完全指南:年份翻页按钮的 Props、Slot 与源码级实现解析

radix-vue(Reka UI)YearPickerNext 组件完全指南:年份翻页按钮的 Props、Slot 与源码级实现解析

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

导读:YearPickerNext是 radix-vue(现已更名为 Reka UI)日期组件体系中用于"向后翻页"的导航按钮,负责让用户从当前页面向未来方向切换一页年份(默认每页 12 年)。本文以 YearPickerNext 官方 API 文档 为骨架,结合 YearPickerNext.vue 源码、根组件 YearPickerRoot.vue 与 useYearPicker.ts 组合式函数,以及 YearPicker.test.ts 测试用例,完整讲解其 Props、Slot、数据属性、禁用判定逻辑、自定义翻页函数覆盖机制,并给出可直接运行的完整示例代码。读完本文,你将能够独立使用、定制并深入理解 YearPicker 的"下一页"导航行为。

YearPickerNext 是什么

YearPickerNext是 Year Picker(年份选择器)头部导航区中的"下一页"按钮。在 radix-vue 中,YearPicker 是一套由多个可组合部件构成的组件族,YearPickerNextYearPickerPrev对称分布于 YearPickerHeader 两侧,中间是显示当前年份范围的 YearPickerHeading:

<template> <YearPickerRoot> <YearPickerHeader> <YearPickerPrev /> <YearPickerHeading /> <YearPickerNext /> <!-- 本篇文章的主角 --> </YearPickerHeader> <YearPickerGrid>...</YearPickerGrid> </YearPickerRoot> </template>

在官方文档(year-picker.md)中,它的定位被描述为:

Calendar navigation button. It navigates the calendar one page (default: 12 years) in the future.

即"日历导航按钮,将日历向前(未来方向)翻动一页,默认一页为 12 年"。当用户点击它时,网格中的年份范围会整体向后平移,例如从1980 - 1991变为1992 - 2003。这一行为在 YearPicker.test.ts 中有对应的集成测试:

it('navigates to next decade using next button', async () => { const { getByTestId, user } = setup({ pickerProps: { modelValue: calendarDate } }) const heading = getByTestId('heading') const nextBtn = getByTestId('next-button') expect(heading).toHaveTextContent('1980 - 1991') await user.click(nextBtn) expect(heading).toHaveTextContent('1992 - 2003') await user.click(nextBtn) expect(heading).toHaveTextContent('2004 - 2015') })

Props 详解

根据 YearPickerNext.md,YearPickerNext暴露 3 个 Props:asasChildnextPage

as

属性类型必填默认值
asAsTag \| Component"button"

as决定该组件最终渲染成什么元素或组件,且可以被asChild覆盖。它来自 Primitive 基础层(PrimitiveProps),因此你可以把默认的<button>换成任意 HTML 标签或自定义组件。

在 YearPickerNext.vue 源码中可以看到默认值确实为button

const props = withDefaults(defineProps<YearPickerNextProps>(), { as: 'button' })

值得注意的是,模板中对type属性做了与as联动的处理(YearPickerNext.vue):

:type="props.as === 'button' ? 'button' : undefined"

只有当渲染元素是原生<button>时才显式设置type="button",避免在表单场景中误触发提交行为;当as被改成其他元素时则不注入type

asChild

属性类型必填默认值
asChildboolean-

asChild用于"将默认渲染元素替换为传入的子元素,并合并它们的 props 与行为"。这是 radix-vue/Reka UI 的核心组合模式:当你希望用图标、链接或自定义按钮组件替代默认<button>时,把它作为子元素传入并设置asChild,本组件的点击、禁用、无障碍属性都会透传合并到子元素上。

结合源码(YearPickerNext.vue)可以看到,渲染由Primitive完成:

<Primitive :as="props.as" :as-child="props.asChild" aria-label="Next page" :type="props.as === 'button' ? 'button' : undefined" :aria-disabled="disabled || undefined" :data-disabled="disabled || undefined" :disabled="disabled" @click="handleClick" > <slot :disabled> Next page </slot> </Primitive>

因此无论渲染成什么元素,aria-label="Next page"aria-disableddata-disableddisabled与点击处理都会被统一注入。

nextPage

属性类型必填默认值
nextPage((placeholder: DateValue) => DateValue)-

nextPage是本文的核心 Props,官方文档描述为:

The function to be used for the next page. Overwrites thenextPagefunction set on theYearPickerRoot.

它的语义是"用于计算下一页的函数,覆盖在YearPickerRoot上设置的nextPage函数"。类型签名对应 YearPickerNext.vue 中的接口定义:

export interface YearPickerNextProps extends PrimitiveProps { /** The function to be used for the next page. Overwrites the `nextPage` function set on the `YearPickerRoot`. */ nextPage?: (placeholder: DateValue) => DateValue }

这实现了"两级覆盖"机制:根组件定义全局默认翻页策略,单个按钮可以局部覆盖它。测试用例 YearPicker.test.ts 演示了如何通过nextPage: date => date.add({ years: 13 })自定义翻页步长,让每次翻页前进 13 年而非默认的 12 年:

it('falls back to the nearest enabled year when paged candidate is missing on PageDown', async () => { const { getByTestId, user } = setup({ pickerProps: { placeholder: calendarDate, nextPage: date => date.add({ years: 13 }), }, }) // ... await user.keyboard(kbd.PAGE_DOWN) expect(getByTestId('heading')).toHaveTextContent('1993 - 2004') expect(getByTestId('year-1993')).toHaveFocus() })

Slot 详解

YearPickerNext暴露 1 个默认 Slot,文档定义如下:

名称描述类型
disabled(默认 slot 作用域)当前禁用状态boolean

其接口在 YearPickerNext.vue 中定义:

export interface YearPickerNextSlot { default?: (props: { /** Current disable state */ disabled: boolean }) => any }

也就是说,插槽的作用域对象上只有一个disabled布尔值。源码中的渲染方式是(YearPickerNext.vue):

<slot :disabled> Next page </slot>

当不提供插槽内容时,默认渲染文本Next page。使用插槽时可以获得实时的禁用状态,例如在禁用时切换图标颜色或样式:

<YearPickerNext v-slot="{ disabled }"> <ChevronRightIcon :class="disabled ? 'text-gray-300' : 'text-gray-700'" /> </YearPickerNext>

禁用判定与翻页逻辑的源码实现

YearPickerNext本身逻辑极简,关键行为都委托给根组件通过provide/inject注入的上下文(injectYearPickerRootContext)。完整流程如下(YearPickerNext.vue):

const rootContext = injectYearPickerRootContext() const disabled = computed(() => rootContext.disabled.value || rootContext.isNextButtonDisabled(props.nextPage)) function handleClick() { if (disabled.value) return rootContext.nextPage(props.nextPage) }

禁用状态的两层来源

disabled计算属性由两部分构成:

  1. 根组件的disabled:当整个 YearPicker 被设置为禁用时,翻页按钮必然不可用;
  2. isNextButtonDisabled(props.nextPage):根据当前网格的最后一页年份与maxValue边界判断"是否还有下一页"。

后者实现在 useYearPicker.ts:

const isNextButtonDisabled = (nextPageFunc?: (date: DateValue) => DateValue) => { if (!props.maxValue.value) return false if (props.disabled.value) return true const lastYearInView = grid.value.cells.at(-1)! if (nextPageFunc || props.nextPage.value) { const nextDate = (nextPageFunc || props.nextPage.value)!(lastYearInView) return isAfter(startOfYear(nextDate), props.maxValue.value) } const nextPageStart = startOfYear(lastYearInView.add({ years: 1 })) return isAfter(nextPageStart, props.maxValue.value) }

逻辑要点:

  • 未设置maxValue时永远可点:没有上限约束,按钮不会被禁用;
  • 自定义nextPage函数时按函数结果判定:先计算"用该函数翻页后的年份",再与该页起始年份比较是否越过maxValue
  • 默认行为按yearsPerPage平移判定:即"当前页最后一年的下一年"是否超出maxValue

当判定为禁用时,组件会同时输出aria-disableddata-disabled与原生disabled属性,测试 YearPicker.test.ts 对此进行了验证:

it('should not allow navigation after the `maxValue` (next button)', async () => { const { getByTestId, user } = setup({ pickerProps: { modelValue: calendarDate, maxValue: new CalendarDate(1991, 12, 31), }, }) const nextBtn = getByTestId('next-button') expect(nextBtn).toHaveAttribute('aria-disabled', 'true') expect(nextBtn).toHaveAttribute('data-disabled') await user.click(nextBtn) expect(getByTestId('heading')).toHaveTextContent('1980 - 1991') })

翻页函数如何更新网格

点击后调用的rootContext.nextPage(props.nextPage)实现在 useYearPicker.ts:

const nextPage = (nextPageFunc?: (date: DateValue) => DateValue) => { const firstYearInGrid = grid.value.value if (nextPageFunc || props.nextPage.value) { const newDate = (nextPageFunc || props.nextPage.value)!(firstYearInGrid) grid.value = createYearGrid({ dateObj: newDate, yearsPerPage: props.yearsPerPage.value, decadeAligned: false }) props.placeholder.value = newDate.set({ month: props.placeholder.value.month, day: props.placeholder.value.day }) return } const newDate = firstYearInGrid.add({ years: props.yearsPerPage.value }) grid.value = createYearGrid({ dateObj: newDate, yearsPerPage: props.yearsPerPage.value, decadeAligned: false }) props.placeholder.value = newDate.set({ month: props.placeholder.value.month, day: props.placeholder.value.day }) }

其工作流可以概括为:

  1. 取当前网格的第一个年份作为基准(grid.value.value);
  2. 若传入自定义nextPage(按钮级优先于根组件级),则调用它得到新基准日期;否则默认+ yearsPerPage年;
  3. 用新基准重新调用 createYearGrid 生成新网格(注意此处传入decadeAligned: false,即不再对齐到十年整段,而是以新基准年份为页首);
  4. 同步更新placeholder(保留原有的月份与日期,只替换年份),使根组件的网格、标题与聚焦逻辑保持一致。
export function createYearGrid(props: CreateSelectProps & { yearsPerPage?: number, decadeAligned?: boolean }): Grid<DateValue> { const { dateObj, yearsPerPage = 12, decadeAligned = true } = props let startYear: number if (decadeAligned) { startYear = startOfDecade(dateObj).year } else { startYear = dateObj.year } const years = Array.from({ length: yearsPerPage }, (_, i) => startOfYear(dateObj.set({ year: startYear + i }))) const firstYear = years[0] return { value: firstYear, cells: years, rows: chunk(years, 4) } }

这也解释了为什么测试中点击一次nextBtn后标题从1980 - 1991变为1992 - 2003:默认yearsPerPage为 12,网格按 3 行 × 4 列排列(chunk(years, 4)),翻页即整体平移 12 年。

与键盘交互的关系

YearPickerNext不仅支持鼠标点击,还响应键盘。根据 year-picker.md 的 Keyboard Interactions 章节:

  • Space/Enter:焦点在YearPickerNextYearPickerPrev上时,触发翻页;焦点在年份单元格上时则选中该年份;
  • PageDown/PageUp:焦点在年份单元格上时,分别翻到下一页/上一页年份(内部同样复用nextPage/prevPage逻辑)。

测试 YearPicker.test.ts 验证了PageDown/PageUp与点击按钮产生相同的翻页结果。此外,当自定义nextPage导致目标年份被禁用时,组件会自动聚焦到最近的可选年份(见 YearPicker.test.ts),保证键盘导航永不"落空"。

完整可用示例

下面是一个结合minValue/maxValue边界、自定义nextPage覆盖与插槽用法的完整示例,可直接在安装reka-ui(即 radix-vue 的新包名)与@internationalized/date的项目中运行:

<script setup lang="ts"> import { ref } from 'vue' import { CalendarDate } from '@internationalized/date' import { YearPickerCell, YearPickerCellTrigger, YearPickerGrid, YearPickerGridBody, YearPickerGridRow, YearPickerHeader, YearPickerHeading, YearPickerNext, YearPickerPrev, YearPickerRoot, } from 'reka-ui' const value = ref<CalendarDate>() // 按钮级覆盖:让"下一页"按钮每次前进 5 年(默认 12 年) function customNext(placeholder: CalendarDate) { return placeholder.add({ years: 5 }) } </script> <template> <YearPickerRoot v-model="value" :min-value="new CalendarDate(2020, 1, 1)" :max-value="new CalendarDate(2045, 12, 31)" :years-per-page="12" calendar-label="Select a year" > <YearPickerHeader> <YearPickerPrev> <template #default="{ disabled }"> <span :class="disabled ? 'text-gray-300' : 'text-gray-800'">← 上一页</span> </template> </YearPickerPrev> <YearPickerHeading /> <YearPickerNext :next-page="customNext"> <template #default="{ disabled }"> <span :class="disabled ? 'text-gray-300' : 'text-gray-800'">下一页 →</span> </template> </YearPickerNext> </YearPickerHeader> <YearPickerGrid> <YearPickerGridBody> <YearPickerGridRow v-for="(row, i) in 3" :key="`row-${i}`"> <YearPickerCell v-for="year in 4" :key="year" :date="/* 由 grid 提供 */"> <YearPickerCellTrigger :year="/* 当前年份 */" /> </YearPickerCell> </YearPickerGridRow> </YearPickerGridBody> </YearPickerGrid> </YearPickerRoot> </template>

说明:上述模板中网格行与单元格建议按官方 Anatomy 推荐的方式,通过根组件的作用域插槽v-slot="{ grid }"遍历grid.rows渲染,具体写法可参考 story/_YearPicker.vue 中的示例实现。minValue/maxValuenextPage的组合使用即可完整复现"边界自动禁用 + 自定义步长"的行为。

边界行为与易错点小结

  1. 自定义nextPage的禁用判定会变:一旦传入nextPageisNextButtonDisabled将基于"函数计算结果"而非"默认平移 12 年"来判定是否越界(useYearPicker.ts),所以自定义步长不会意外把页面翻出maxValue
  2. 按钮级优先于根组件级YearPickerNextnextPage覆盖 YearPickerRoot 的nextPage,调用时传参优先级为"组件自身 props > 根组件 props"(useYearPicker.ts);
  3. disabled是"整组禁用 + 边界禁用"的合取:根组件disabledmaxValue越界任一成立,按钮即不可点,并同步输出aria-disableddata-disabled与原生disabled
  4. 翻页不会丢失月份/日期上下文nextPage更新placeholder时保留原月份与日期,只替换年份(useYearPicker.ts);
  5. 无障碍默认值已内置:组件自带aria-label="Next page"与隐藏的role="heading"(由根组件输出完整fullCalendarLabel),并通过了vitest-axe的可访问性测试(YearPicker.test.ts)。

参考文件索引

  • API 文档主体:docs/content/meta/YearPickerNext.md
  • 组件文档(Anatomy / Keyboard Interactions):docs/content/docs/components/year-picker.md
  • 组件源码:packages/core/src/YearPicker/YearPickerNext.vue
  • 对称按钮实现:packages/core/src/YearPicker/YearPickerPrev.vue
  • 根组件(上下文提供方):packages/core/src/YearPicker/YearPickerRoot.vue
  • 翻页/禁用逻辑:packages/core/src/YearPicker/useYearPicker.ts
  • 网格生成函数:packages/core/src/date/calendar.ts
  • 集成测试:packages/core/src/YearPicker/YearPicker.test.ts
  • 可参考的完整组合示例:packages/core/src/YearPicker/story/_YearPicker.vue

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 5:56:20

把AI变成懂代码的结对程序员:Cursor上下文工程实战指南

说实话&#xff0c;我最早对 Cursor 这类 AI 编程工具是持保留态度的。用了几个月下来&#xff0c;身边很多朋友也反馈过同一个问题&#xff1a;AI 写出来的代码“时灵时不灵”&#xff0c;有时候改个十几行代码&#xff0c;它能给你引用一个根本不存在的函数&#xff0c;有时候…

作者头像 李华
网站建设 2026/9/18 5:55:37

Agent-Reach:为大模型Agent打造统一触达层,解决工具调用与数据可达性难题

1. 从一次“答非所问”说起&#xff1a;Agent-Reach到底在解决什么我大概在半年前接手过一个智能客服项目&#xff0c;当时的系统已经能流畅回答“你们公司有什么产品”“退货流程是什么”这类常见问题。但运营团队提了一个很实际的需求&#xff1a;用户如果问“我的订单现在到…

作者头像 李华
网站建设 2026/9/18 5:49:50

工业相机与镜头参数匹配实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 5:49:44

2008年的需求文档为何仍是经典?一次制造业需求分析拆解实录

简介&#xff1a;生产制造管理系统&#xff08;CCAM&#xff09;需求分析文档模板&#xff0c;面向制造企业信息化项目中的需求分析师、产品经理及开发测试人员&#xff0c;用于规范软件需求说明书的编写与评审流程。文档立足生产制造核心业务&#xff0c;覆盖基础资料、销售、…

作者头像 李华