日期选择的完整控件。中间是真 <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 的 labels。locale 只能翻译
格式化出来的文本,而 rdp 有一批写死的英文字面量——labelYearDropdown
(Choose the Year)、labelMonthDropdown、labelPrevious(Go to the
Previous Month)、labelNext、labelWeekNumber、labelWeekNumberHeader,
共 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-press;DatePickerCancel、Esc、点击外部都丢弃暂存。
Footer 是纯布局壳(AlertDialogFooter 同款):动作部件、文案、variant
都由使用方组合——库不带任何语言的默认文案。三个动作部件:
DatePickerFooterClear 暂存 null 不关闭(名字里的 Footer 只为与输入侧
DatePickerClear 消歧);DatePickerCancel 关闭并丢弃暂存;
DatePickerClose 提交并关闭——Base UI 词汇里「关闭并提交」的正名。
通常应包含一个 DatePickerClose,否则只有 Enter 能提交。
受控
value / defaultValue / onValueChange(value, details)——受控空值是
null;details.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 属性读。
DatePickerInput 刻意不带自己的 data-slot:InputGroupInput 的
data-slot="input-group-control" 是 InputGroup 画焦点环的契约,覆盖它
整个组框就不亮环了。
键盘交互
清除按钮与日历按钮都在 Tab 序之外——键盘路径是输入框:清空文本即清值, ArrowDown 即开日历。
Props
顺序规则:必填 → 非受控默认值 → 受控值 → 回调 → 行为开关 → 外观 → className。
DatePicker
根。不写 children 渲染完整默认组合;有 name 时在弹层外渲染常驻 hidden input。
DatePickerInput
真 <input>,从 context 读文本与接线,组合进 InputGroup 任意位置。聚焦时
显示原始草稿,失焦显示格式化值。收 ComponentProps<typeof InputGroupInput>。
DatePickerTrigger
日历按钮(含自己的 InputGroupAddon),点击开合弹层。默认日历图标 +
英文 aria-label(Open calendar),tabIndex={-1}。收
ComponentProps<typeof InputGroupButton>。
DatePickerClear
清除按钮(含自己的 addon),空值时靠根的 data-empty 隐藏;clearable
为 false 时整个不渲染。默认 ✕ 图标 + 英文 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。