构建自定义 UI

本指南将引导您了解在 headless hooks 之上构建自己的日期选择器 UI 的模式。

模式

每个 headless hook 都遵循相同的模式:

  1. 调用 hook 并传入您的选项(valueonChangeconfiguration
  2. 解构返回值以获取状态、计算值和处理程序
  3. 使用返回的值渲染您自己的标记
  4. 通过调用提供的处理程序来连接交互

hook 管理了所有的复杂性——日历生成、日期计算、打开/关闭状态、键盘导航、范围逻辑——而您只需专注于呈现。

示例:自定义日期选择器

import { useState } from "react";
import { useDatePicker } from "react-date-range-picker-headless";
function CustomDatePicker() {
const [value, setValue] = useState<Date | null>(null);
const {
isOpen,
calendar,
getDayProps,
displayValue,
hasValue,
canConfirm,
locale,
handleToggle,
handleDateClick,
handleConfirm,
handleCancel,
handleClear,
handlePrevMonth,
handleNextMonth,
handleKeyDown,
containerRef,
popupRef,
} = useDatePicker({ value, onChange: setValue });
return (
<div ref={containerRef} style={{ position: "relative", display: "inline-block" }}>
{/* Trigger */}
<button
onClick={handleToggle}
style={{
padding: "8px 16px",
border: "1px solid #d1d5db",
borderRadius: 6,
background: "white",
cursor: "pointer",
minWidth: 180,
textAlign: "left",
}}
>
{displayValue || locale.placeholder}
</button>
{/* Popup */}
{isOpen && (
<div
ref={popupRef}
onKeyDown={handleKeyDown}
style={{
position: "absolute",
top: "100%",
left: 0,
marginTop: 4,
padding: 16,
background: "white",
border: "1px solid #e5e7eb",
borderRadius: 8,
boxShadow: "0 4px 12px rgba(0,0,0,0.1)",
zIndex: 50,
}}
>
{/* Header */}
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 12,
}}
>
<button onClick={handlePrevMonth}>{locale.prevMonth}</button>
<span style={{ fontWeight: 600 }}>{locale.formatMonthYear(calendar.month)}</span>
<button onClick={handleNextMonth}>{locale.nextMonth}</button>
</div>
{/* Weekday headers */}
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(7, 36px)",
gap: 2,
textAlign: "center",
}}
>
{locale.weekdays.map((wd) => (
<span key={wd} style={{ fontSize: 12, color: "#9ca3af", padding: 4 }}>
{wd}
</span>
))}
</div>
{/* Calendar grid */}
<div style={{ display: "grid", gridTemplateColumns: "repeat(7, 36px)", gap: 2 }}>
{calendar.weeks.flat().map((day, i) => {
if (!day) return <span key={i} />;
const dp = getDayProps(day);
return (
<button
key={i}
onClick={() => handleDateClick(day)}
disabled={dp.isDisabled}
style={{
width: 36,
height: 36,
borderRadius: "50%",
border: dp.isFocused ? "2px solid #0ea5e9" : "none",
background: dp.isSelected ? "#0ea5e9" : dp.isToday ? "#f0f9ff" : "transparent",
color: dp.isSelected ? "white" : dp.isDisabled ? "#d1d5db" : "inherit",
cursor: dp.isDisabled ? "not-allowed" : "pointer",
fontWeight: dp.isToday ? 600 : 400,
}}
>
{dp.day}
</button>
);
})}
</div>
{/* Footer */}
<div style={{ display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 12 }}>
{hasValue && (
<button onClick={handleClear} style={{ color: "#ef4444" }}>
{locale.clear}
</button>
)}
<button onClick={handleCancel}>{locale.cancel}</button>
<button
onClick={handleConfirm}
disabled={!canConfirm}
style={{
background: canConfirm ? "#0ea5e9" : "#e5e7eb",
color: canConfirm ? "white" : "#9ca3af",
padding: "4px 16px",
borderRadius: 4,
border: "none",
}}
>
{locale.confirm}
</button>
</div>
</div>
)}
</div>
);
}

核心概念

getDayProps

getDayProps 函数是日历渲染的核心。对于每个日期单元格,它返回一个 DayProps 对象,其中包含 18 个布尔值/数字标志,用于描述单元格的状态:

const dp = getDayProps(day);
// 选择状态
dp.isSelected; // 当前选定的日期
dp.isToday; // 今天的日期
// 范围状态(适用于范围选择器)
dp.isInRange; // 在开始和结束日期之间
dp.isRangeStart; // 范围的第一个日期
dp.isRangeEnd; // 范围的最后一个日期
dp.isInHoverRange; // 在悬停预览范围内
dp.isHoverTarget; // 悬停的日期本身
dp.isRangeSingle; // 范围的开始和结束都是同一天(单日范围)
// 视觉连接辅助
dp.hasLeftConnection; // 连接到前一天(用于范围背景)
dp.hasRightConnection; // 连接到后一天
dp.isConsecutiveRange; // 多日范围的一部分
// 状态
dp.isDisabled; // 被 minDate/maxDate/isDateUnavailable 禁用
dp.isFocused; // 具有键盘焦点
dp.isOutsideDay; // 属于相邻月份(当 showOutsideDays 启用时)
dp.isHighlighted; // 在 highlightDates 数组中
// 数据
dp.date; // Date 对象
dp.day; // 月份中的天数
dp.dayOfWeek; // 0-6 (周日-周六)

用于外部点击的 Refs

这些 hooks 返回 containerRefpopupRef。将它们附加到您的包装器和弹出元素上——hook 使用它们来检测外部点击并自动关闭弹出窗口。

键盘导航

handleKeyDown 处理程序支持箭头键、Enter、Escape 和 Tab 键,以实现完整的键盘导航。请将其附加到弹出容器上。

本地化

所有面向用户的字符串都来自 locale 对象。您可以通过传递一个部分的 locale 选项来自定义任何字符串:

const picker = useDatePicker({
value,
onChange: setValue,
locale: {
confirm: "确定",
cancel: "返回",
placeholder: "选择一个日期...",
},
});

后续步骤

  • Hook 参考 — 每个 hook 的完整选项和返回值
  • Contexts — 为复合组件模式使用 providers
  • 日期工具 — 用于日期操作的辅助函数