跳到正文
册页

容器

Calendar - 日历

一个月,画成一格一格的天。

用法

什么时候用它

单独用来看范围或排期;放进 DatePicker 里用来选一天。
TSX
import { Calendar } from '@misoto22/folio'

说明

一个月份,画成一张日期网格。

包了 react-day-picker 而不是自己造一个日历,原因在长尾:月份网格很简单,围着它的一圈都不简单——键盘约定(方向键走一天,Page 走一个月,Home 和 End 到这一周的两端)、月份变化时的 aria-live 播报、周数、每个地区一周从周几开始不一样,以及“今天”和“选中”是读屏软件必须能听出区别的两种状态。

每个类名都是替换而不是叠加在库的样式表上,所以这里没有任何东西依赖一个本包管不到的 CSS 文件——但库自己的类名在每个插槽上都保留了下来,因为传类名是替换,而下游每一个 .rdp-* 选择器都依赖它们。

月份名跟随 locale。选择器自己的那层外壳——两个面板,以及浏览它们的四个箭头——跟随 CalendarLabels,因为 locale 供不出这个包自己发明的字符串。

结构

Calendar anatomy
部件说明
日期网格必填这个月:一行等宽的星期名,下面每天一个 36px、胶囊圆角的按钮,多数月份五行,有些月份六行。相邻月份的日子照样画出来、只是压暗,而不是留成窟窿。
月份标题必填「September 2026」是一个带 aria-expanded 的按钮,不是两个下拉——日期本来就是这么说出口的,而拆开会在一行 250px、还得塞下两个箭头的地方放进四个控件。
月份箭头必填库自带的 nav,被抬出文档流、以同样的 36px 高度铺在月份标题这一行上,所以两个箭头正好落在月份名的两头。选择面板打开时两个都退场:改月份有两条路、其中一条还藏在面板后面,就是多了一条。
年月选择面板一个 role="dialog",画在日期网格原来的位置上,而不是盖在它上面——十二个月排成 3×4,最多二十四个年份排成 4×6,尺寸和它替下去的那张网格完全一样。打开时焦点移进来,Tab 在里面循环;Escape 关掉它,并把焦点送回月份标题。
日期标记必填今天是一圈描边,选中是一块填充。在区间里,淡底铺在单元格上,填充落在两端那两个按钮上,正是这一点让区间读起来是一条两头圆的带子——也让只有一天、两端重合的区间照样是圆的。

实践建议

推荐

  • locale 要传,别指望页面的 lang:月份标题是 Intl 按 locale.code 格式化出来的,兜底是 en-US,所以在这个属性设上之前,一个法语日历说的是「September」。
  • 通过 classNames 覆盖某个槽位时,要把库自己的类名加回去——你的是替换掉我们的,而我们那份带着下游每个选择器都依赖的 .rdp-* 钩子。覆盖 root 时漏掉 rdp-root,就把那个钩子从树上摘掉了。
  • 标注可用性要走 classNames 的槽位,不要去给日期按钮加样式:淡底属于单元格,标记属于按钮,而加在按钮上的背景会把区间那个圆头压平。
  • 把范围放宽之前,先知道代价:年份网格一次翻 24 个,所以默认前后各十年正好一页、不用翻,而一个宽到能选生日的范围,意味着读者要一页一页翻到 1974。
  • 选择面板上的那圈文字要和 locale 一起翻。locale 只管到月份名就到头了,所以一个设好了这个属性的法语日历,在 CalendarLabels 那几个属性传进来之前,两个面板和四个箭头叫的仍然是「Month and year」「Previous year」和「Earlier years」。

避免

  • 不要用宽度类去把它撑开:它是 w-fit,列宽固定在 36px,所以 w-full 只是替掉了 w-fit,同一张网格照旧贴在一个更宽的盒子的行首那一侧。
  • 不要把它切成固定高度。网格多数月份五周、有些月份六周,而选择面板是按网格取的尺寸、自己没有高度——照着一个五周的月份量出来的盒子,会把第六周裁掉。
  • 不要用 components 去替掉 MonthCaption:年月选择面板就住在我们这个里面,所以自定义的标题会让读者只剩两个箭头,一次挪不过一个月。

示例

a range

一个月,画成一张日期网格,上面横着一段区间。淡色的底落在单元格上,而两端的填充落在按钮上,正是这一点让一段区间读起来像一条两头是圆的带子——也正是这一点让一段只有一天、两端合一的区间仍然是圆的。要通过 classNames 的插槽去标注可用性,别去给日期按钮加样式,否则那个端点的背景会被压平。标题栏是一个按钮,不是两个下拉框:一个日期本来就是这么说出来的,而把它拆开,等于在一个 250px、还要装下两个箭头的行里塞进四个控件。点开它,月份和年份的选择器会原地画在网格的位置上、尺寸分毫不差,按 Escape 焦点回到标题栏。labels.labelNav 为月份箭头那个工具栏命名。不管它,页面上每个日历都是一个叫「Navigation bar」的 nav——那是 react-day-picker 自带的字符串——而一页上三个例子就是三个没人分得出的地标。

one day

一次一天,以及那两个绝不能长得一样的记号:today 是一圈环,selected 是一块填充。一个是关于这本日历的事实,另一个是读者做出的选择,所以一套把两者画成同一个样子的系统,会让读者分不出自己到底选了什么。相邻月份的日期会显示出来并调暗,而不是留成空洞。别给这张网格一个固定高度——大多数月份是五周,有些是六周——也别用 w-full 把它拉开:它是 w-fit,排的是固定 36px 的列,所以一个宽度类只会让同样这张网格坐在一个更宽盒子的起始边上。要在表单里选日期,这东西属于 DatePicker。labels.labelNav 为月份箭头那个工具栏命名。不管它,页面上每个日历都是一个叫「Navigation bar」的 nav——那是 react-day-picker 自带的字符串——而一页上三个例子就是三个没人分得出的地标。

Thu Sep 17 2026

disabled days

周末和 7 号之前的一切都被拒掉,网格也不会让你提交这样一个日期:disabled 收的是匹配器——星期几、一个日期区间、一个判定函数——它会把命中的按钮调暗、而不是把它们移走,于是这个月保住了自己的形状,读者也看得见某一天为什么不可用。这里也是一个可用性视图的起点:通过 classNames 的插槽去标记单元格,别去给日期按钮加样式,这样那些标记就仍然和区间的淡色底兼容。startMonth 和 endMonth 限定标题栏的选择器能走多远;默认是前后各十年,正好是年份网格的一页,完全不用翻页。labels.labelNav 为月份箭头那个工具栏命名。不管它,页面上每个日历都是一个叫「Navigation bar」的 nav——那是 react-day-picker 自带的字符串——而一页上三个例子就是三个没人分得出的地标。

键盘操作

Calendar keyboard interactions
按键作用
←→移动一天。
↑↓移动一周。
Page UpPage Down移动一个月。
HomeEnd跳到本周的第一天或最后一天。
EnterSpace选中当前聚焦的那一天。

无障碍

  • 方向键走一天,Page 键走一个月,Home 和 End 到这一周的两端。
  • “今天”是描边,“选中”是填充——一个是日历自身的事实,一个是读者做的选择,两者不能长得一样。
  • 年和月是一整面按钮,不是原生的 select:平台给的一百年列表是在滚动而不是在选择,而且样式由操作系统决定。
  • Tab 在打开的面板里循环。面板是不透明的,日期网格还挂在它下面,所以一个走出去的 Tab 会把读者放到一个他看不见的日子上——而且越过了那个持有 Escape 处理逻辑的月份标题。
  • 默认前后各十年。生日需要更宽的范围,用 startMonth 去要。