Cadenza
EN

可键入的日期输入框 + 日历弹层——打字即生效,点选即提交

日期选择的完整控件。中间是真 <input>:直接键入「2026-08-16」解析成功就立即 生效,日历同步跳到那个月;也可以点日历按钮(或按 ArrowDown)打开弹层点选。 Base UI 没有日期组件,这套行为是封装层自己的——输入壳复用 InputGroup, 弹层是 Base UI Popover,面板是同样导出的 Calendar(react-day-picker)。

非法输入不落值:合法前值一直保留,离开字段时非法文本回退成上一次提交值的 格式化显示;清空文本离开则清值。

使用

import { DatePicker } from '@gedatou/cadenza-ui'
<DatePicker aria-label="日期" placeholder="选择日期" />

不写 children 就有完整默认组合。标签走普通通道:根上的 id 转发给内层输入框, FieldLabel htmlFor 直连;没有可见标签时传 aria-label,默认组合会转交给 输入框。

组成

写了 children 就替换输入侧(输入框、清除、日历按钮);弹层默认在场,组合了 DatePickerPopup 才让位。标记部件必须是根的直接子节点或裹在 Fragment 里。

DatePicker
├─ InputGroup
│  ├─ DatePickerInput
│  ├─ DatePickerClear
│  └─ DatePickerTrigger
└─ DatePickerPopup
   ├─ Calendar(默认)
   └─ DatePickerFooter(可选,组合即进入确认模式)

输入侧的可见部件是 InputGroup 的组框——自己组合时照常用它的原名:

<DatePicker aria-label="日期">
  <InputGroup>
    <DatePickerInput placeholder="选择日期" />
    <DatePickerTrigger />
  </InputGroup>
</DatePicker>

格式与本地化

format 是 date-fns token,同时管显示与解析;locale 一并喂给格式化和日历。

locale 覆盖面板的每一处可见文案:星期行、年月下拉、以及 输入框里格式化后的值——同一条 date-fns 通道,不会有一处跟着浏览器语言、 另一处留在英文默认。不传就整体是 react-day-picker 的英文。

无障碍文案是另一条通道:react-day-picker 的 labelslocale 只能翻译 格式化出来的文本,而 rdp 有一批写死的英文字面量——labelYearDropdown (Choose the Year)、labelMonthDropdownlabelPrevious(Go to the Previous Month)、labelNextlabelWeekNumberlabelWeekNumberHeader, 共 6 个,没有任何 locale 能碰到。本库不往 DOM 塞任何语言的文案,所以不替你 翻译,只把通道留着。

它只影响面板、不影响输入框,所以和 startMonth 那些一样走 自定义日历,不在根上:

<DatePickerPopup>
  {calendar => (
    <Calendar
      {...calendar}
      labels={{ labelYearDropdown: () => '选择年份', labelMonthDropdown: () => '选择月份' }}
    />
  )}
</DatePickerPopup>

Partial:只写要改的那几个。其余的 label(labelGrid / labelWeekday / labelDayButton)本来就走 locale,不用管。

根上只放同时影响输入框和面板的东西:locale 要参与解析键入的文本、 format 管显示与解析、disabledDates 连键入一起拒。纯面板的配置一律走弹层的 函数 children——这样「这个 prop 该写在哪」永远只有一个答案,不用背表。

表单序列化不跟随 format——hidden input 始终是 yyyy-MM-dd(见表单)。 注意 token 大小写:月份是 MM,小写 mm 在 date-fns 语义里是分钟, 传错不会报错、只会安静地显示错东西(从 dayjs 迁移的 YYYY/DD 反而会被 date-fns 直接 throw 提醒)。

format 是严格匹配——键入必须完全按它来。要宽容收多种写法时,用 inputToValue 只换解析侧:显示仍走 format,失焦后统一格式化。

返回 null 表示「还不是日期」(草稿保留,失焦回退);返回的 Date 照常被 归一到当天零点、过 disabledDates 检查。解析器必须是纯函数。

禁用日期

disabledDates 收 react-day-picker 的 matcher(单日、区间、before/after、 函数都行)。日历里点不了,键入同样被拒。

年月选择

面板顶上的年和月本身就是下拉,默认在场——跳到三年前不用点 36 次箭头。 下拉用的是本库的 Select,不是原生 <select>: 弹层里按下鼠标不夺焦是这个控件的既定行为(输入框的光标不会闪),而原生 下拉正是靠那个被取消掉的默认动作展开的,放着不管整行标题会点不动。

年份范围默认是今年 ±100 年。 react-day-picker 自己的默认是「今年往前 100 年 到今年年底」——生日选择器的量法,连明年都选不到,所以这里把上界补成对称的。 真实场景多半更窄:startMonth / endMonth 收窄它,两个都是日历的 props, 走弹层的函数 children 传(见上面的 demo)。它们一旦给出,箭头也走不出这个范围。

下拉文案跟随 locale,与星期行、输入框格式化同一条通道,见 格式与本地化。不传 locale 时整个面板都是 react-day-picker 的英文默认,不会只有月份跟着浏览器语言跑。

要回到不可点的纯标题,spread 之后传 captionLayout="label"

<DatePickerPopup>
  {calendar => <Calendar {...calendar} captionLayout="label" />}
</DatePickerPopup>

自定义日历

DatePickerPopup 的函数 children 拿到接好线的日历 props,spread 进自己的 <Calendar> 再往上叠配置。captionLayout 也在这份 props 里,所以叠配置 不会把年月下拉弄丢。

确认模式

往弹层里组合 DatePickerFooter,点选和键入就从「即时提交」变成「暂存」: 输入框实时预览暂存值,DatePickerClose(或 Enter)才提交并关闭——reason 是 close-pressDatePickerCancel、Esc、点击外部都丢弃暂存。

Footer 是纯布局壳(AlertDialogFooter 同款):动作部件、文案、variant 都由使用方组合——库不带任何语言的默认文案。三个动作部件: DatePickerFooterClear 暂存 null 不关闭(名字里的 Footer 只为与输入侧 DatePickerClear 消歧);DatePickerCancel 关闭并丢弃暂存; DatePickerClose 提交并关闭——Base UI 词汇里「关闭并提交」的正名。 通常应包含一个 DatePickerClose,否则只有 Enter 能提交。

受控

value / defaultValue / onValueChange(value, details)——受控空值是 nulldetails.reason 区分来源(键入 input-change、点选 item-press、 清除 clear-press / input-clear),details.cancel() 拒绝这次变更。 弹层开合是并列的一套:open / defaultOpen / onOpenChange(open, details)

表单

name 就渲染一个常驻 hidden input:空值序列化成 ''、有值是 yyyy-MM-dd,与显示格式无关。hidden input 对原生约束校验不可见,必填校验 放表单层做;视觉星号交给 FieldLabel required。与 TanStack Form 的绑定见 表单指南

禁用

disabled 禁整个控件(hidden input 一并 disabled,不参与提交)。 readOnly 只锁编辑:默认组合会摘掉清除与日历按钮——不是省事,是因为 InputGroup 会为任何 disabled 后代整体变暗(has-disabled:opacity-50), 留一个禁用按钮会让只读字段看起来像禁用字段。

什么时候用 DatePicker

选单个日期用它;选一段区间(入住—退房、起止日期)用 DateRangePicker——两端输入框 + 双月 日历,值是 { from, to }。只要日历面板不要输入框时直接用 Calendar

状态与 className

根是普通 <div>className 就是字符串;状态从根上的 data 属性读。

属性出现时机
data-empty没有值
data-open弹层开着
data-disableddisabled
data-readonlyreadOnly
data-slot是什么
date-picker
date-picker-trigger / -addon日历按钮及其 addon
date-picker-clear / -addon清除按钮及其 addon
date-picker-popup弹层
date-picker-footer确认模式的动作行
date-picker-footer-clear / date-picker-cancel / date-picker-close三个动作按钮

DatePickerInput 刻意不带自己的 data-slot:InputGroupInputdata-slot="input-group-control"InputGroup 画焦点环的契约,覆盖它 整个组框就不亮环了。

键盘交互

按键效果
ArrowDown在输入框上打开弹层
Escape关闭弹层(值不动,事件不冒泡给外层 dialog)
Enter结算当前输入;弹层开着时同时关闭且不提交表单(确认模式下就是「确定」)
日历内方向键react-day-picker 自带的日期漫游

清除按钮与日历按钮都在 Tab 序之外——键盘路径是输入框:清空文本即清值, ArrowDown 即开日历。

Props

顺序规则:必填 → 非受控默认值 → 受控值 → 回调 → 行为开关 → 外观 → className

DatePicker

根。不写 children 渲染完整默认组合;有 name 时在弹层外渲染常驻 hidden input。

Prop类型默认值说明
defaultValueDate | null非受控初始值
valueDate | null受控值;null 是受控空值
onValueChange(value, details) => void每次提交变更;details.cancel() 拒绝
defaultOpenbooleanfalse弹层初始开合
openboolean受控弹层
onOpenChange(open, details) => void弹层开合;details.cancel() 保持原样
onOpenChangeComplete(open) => void开合动画完成后
actionsRefBase UI Popover 的 actions命令式 unmount / close
formatstring'yyyy-MM-dd'显示与解析格式(date-fns token)
localedate-fns Locale格式化、解析与日历文案
inputToValue(text) => Date | nullformat 严格解析覆盖解析侧;null = 还不是日期
disabledDatesMatcher | Matcher[]禁选日期;键入同样被拒
namestring有则渲染 hidden input(yyyy-MM-dd
disabledbooleanfalse禁用
readOnlybooleanfalse只读;默认组合摘掉两个按钮
clearablebooleantrue清除总开关;false 连显式组合的也关
modalBase UI 的 modalfalse钉为 false:行内控件不锁页面
placeholderstring默认组合输入框的占位
idstring转发给输入框,FieldLabel htmlFor 直连
aria-labelstring默认组合输入框的无障碍名
childrenReactNode | (state & { defaultChildren }) => ReactNode替换输入侧
其余ComponentProps<'div'>透传到根

DatePickerInput

<input>,从 context 读文本与接线,组合进 InputGroup 任意位置。聚焦时 显示原始草稿,失焦显示格式化值。收 ComponentProps<typeof InputGroupInput>

DatePickerTrigger

日历按钮(含自己的 InputGroupAddon),点击开合弹层。默认日历图标 + 英文 aria-label(Open calendar),tabIndex={-1}。收 ComponentProps<typeof InputGroupButton>

DatePickerClear

清除按钮(含自己的 addon),空值时靠根的 data-empty 隐藏;clearablefalse 时整个不渲染。默认 ✕ 图标 + 英文 aria-label(Clear date), tabIndex={-1}。收 ComponentProps<typeof InputGroupButton>

DatePickerPopup

Portal → Positioner → Popup 一体,锚定在根的盒子上。定位四件 (align / alignOffset / side / sideOffset)给 Positioner,其余给 Popup;initialFocus / finalFocus 由封装层钉死(焦点留在输入框、点选后 回输入框)。普通 children 渲染在默认日历下方(放 Footer 的位置); 函数 children 收 DatePickerCalendarProps 替换日历(见 自定义日历)。

DatePickerFooter

日历下方的动作行,纯布局壳,组合即进入确认模式。收 ComponentProps<'div'>

DatePickerFooterClear / DatePickerCancel / DatePickerClose

确认模式的三个动作按钮,都收 ComponentProps<typeof Button>(文案是 children,variant 自定):FooterClear 暂存 null 不关闭、Cancel 丢弃暂存关闭、Close 提交暂存(close-press)关闭。

15 个类型一并导出:DatePickerProps / DatePickerState / DatePickerInputProps / DatePickerTriggerProps / DatePickerClearProps / DatePickerPopupProps / DatePickerFooterProps / DatePickerFooterClearProps / DatePickerCancelProps / DatePickerCloseProps / DatePickerCalendarProps / DatePickerChangeEventReason / DatePickerChangeEventDetails / DatePickerOpenChangeEventReason / DatePickerOpenChangeEventDetails