跳到主要内容

Calendar 年份下拉范围改为按 fromDate/toDate 推导 (WEB-1312)

版本: 0.9.1 · 类型: 🐛 Bug 修复

WEB-1312 · 来源走查 FISH-85

问题​

Calendar / DatePicker 的年份下拉始终列出「当前年 ± 50」,完全无视调用方传入的 fromDate / toDate。比如传 fromDate={今天} 想把可选范围限制在今天及以后,年份下拉里却仍然列着几十年的历史年份,看起来可以选——选中后日历确实会跳过去,但当月格子全部被禁用,用户体验上是明显的 bug。

DatePicker 的 disabledDate(透传成 Calendar 的 disabled matcher)存在同样的问题:design-site「Disabled Dates」示例用的是 disabledDate={{ before: new Date() }},年份下拉里仍然列出回溯到 1970 的历史年份,看起来可以选。

翻页箭头存在同样的盲区:此前完全没有边界感知,传 fromDate={今天}(或等价的 disabled={{ before: 今天 }})时,上一月箭头仍能一路点回 1969 年之前——比刚修完的年份下拉范围还要越界。

根因​

fromDate / toDate 在 Calendar.tsx 里只被用来禁用单个日期格子。CalendarMonthCaption 的年份列表来自模块级常量(当前年 ± 50),完全不知道这两个边界的存在;月份下拉同样没有边界感知。

disabled 的 { before } / { after } matcher 形式在语义上与 fromDate / toDate 完全等价(见 Calendar.tsx 的 matchDate),但从未接入同一套推导逻辑,所以只要调用方用 disabledDate 而不是 fromDate/toDate,同样的年份范围异常就会重现。

翻页箭头的 goToPrevMonth / goToNextMonth 完全没有边界判断——不管 minDate / maxDate 是什么,永远无条件把 currentMonth 平移一个月。

改动文件​

  • packages/design/src/components/Calendar/CalendarMonthCaption.tsx
  • packages/design/src/components/Calendar/Calendar.tsx
  • packages/design/src/components/Calendar/styles.ts
  • packages/design/src/components/Calendar/__tests__/Calendar.test.tsx

改动内容​

  1. CalendarMonthCaption 现在从 fromDate 推导年份下拉的下界(未传时回退到新增的 DEFAULT_START_YEAR,即 1970),从 toDate 推导上界(未传时回退到 DEFAULT_END_YEAR,即「当前年 + 50」),按此裁剪列表而不是禁用条目。若算出的下界高于上界,会退化成只保留下界这一年,不抛错、也不会渲染出空列表。
  2. 在边界年份,月份下拉会禁用早于 fromDate 所在月(或晚于 toDate 所在月)的项,但仍保持 12 项全部渲染,不裁剪列表位置。
  3. 选中年份时,会先把目标月份 clamp 到该年份的合法范围内,再调用 goToMonth,确保切到边界年份不会落进一个全禁用的月份。
  4. Calendar 把 fromDate / toDate 透传给 CalendarMonthCaption(此前仅用于日期格子禁用)。
  5. 新增单测覆盖:默认 1970 下界、fromDate/toDate 推导出的范围边界、边界年份的月份禁用、切换年份时的月份 clamp。
  6. Calendar 新增 resolveSelectableBounds 辅助函数,把 disabled matcher 也归一进同一套推导:{ before } / { after }(单个或数组)会被折算成传给 CalendarMonthCaption 的同一对 minDate/maxDate。由于多个 matcher 之间是「任一命中即禁用」的并集,可选范围就是各约束的交集——下界取 fromDate 与所有 { before } 值中的最大值,上界取 toDate 与所有 { after } 值中的最小值。非单调 matcher(裸 Date、{ from, to }、函数式)无法推导出可靠边界,有意忽略。
  7. 新增单测覆盖 disabled matcher 驱动的边界:单个 { before } / { after }、数组同时收敛上下界、fromDate/toDate 与 disabled 冲突时取更严的一方、非单调 matcher(函数、裸 Date、{ from, to })不影响默认的 1970 下界。
  8. 翻页箭头新增边界感知:新增 toYearMonthIndex 辅助函数按「年月粒度」比较日期,显示的月份范围到达归一后的 minDate / maxDate 时自动禁用——上一月箭头在第一个显示月已到达或早于 minDate 所在月时禁用;下一月箭头在最后一个显示月(addMonths(currentMonth, numberOfMonths - 1),兼容 RangePicker 双日历场景)已到达或晚于 maxDate 所在月时禁用。所有导航箭头(含 navPerMonth 模式下两个日历各自的箭头)共用同一对禁用状态,因为它们都作用于同一个 currentMonth 位移。
  9. goToPrevMonth / goToNextMonth 新增早退守卫,各自越界时直接返回,防止键盘或编程式调用绕过按钮的 disabled 属性。
  10. CALENDAR_NAV_BUTTON_CLASS 新增 disabled:cursor-not-allowed、disabled:text-(--Labels-Disabled),以及 disabled:hover:bg-transparent disabled:hover:text-(--Labels-Disabled) 压掉禁用态下仍会漏出的 hover 样式(做法与 Button 组件的禁用态一致)。
  11. 新增单测覆盖翻页箭头越界禁用:无约束时的基线表现、fromDate/toDate 落在当前显示月、disabled={{ before }} matcher 与 fromDate 表现一致、点击已禁用箭头不触发 onMonthChange、numberOfMonths={2} 时禁用判据基于最后一个显示月而非第一个、未越界时箭头保持可点击。

影响​

  • 传了 fromDate / toDate 的调用方,年份下拉现在会与实际可选范围一致。
  • 未传 fromDate / toDate 的调用方,上界仍是「当前年 + 50」不变;下界从跟随当前年浮动的「当前年 - 50」改为固定的 1970。
  • 使用 disabledDate(Calendar 的 disabled prop)传入 { before } / { after } matcher 的调用方,年份/月份下拉现在与 fromDate / toDate 一样能正确收敛,包括 design-site 的「Disabled Dates」示例。
  • 翻页箭头现在无法点击超出 minDate / maxDate 的范围,无论该边界来自 fromDate/toDate 还是 disabled={{ before }}/{ after } matcher。