Xây dựng Giao diện Người dùng Tùy chỉnh

Hướng dẫn này trình bày mẫu để xây dựng giao diện người dùng bộ chọn ngày của riêng bạn trên các hook headless.

Mẫu thiết kế

Mọi hook headless đều tuân theo cùng một mẫu:

  1. Gọi hook với các tùy chọn của bạn (giá trị, onChange, cấu hình)
  2. Phân rã giá trị trả về để lấy trạng thái, các giá trị được tính toán và các trình xử lý
  3. Kết xuất markup của riêng bạn bằng cách sử dụng các giá trị trả về
  4. Kết nối các tương tác bằng cách gọi các trình xử lý được cung cấp

Hook quản lý tất cả sự phức tạp — tạo lịch, tính toán ngày, trạng thái mở/đóng, điều hướng bằng bàn phím, logic phạm vi — và bạn chỉ tập trung hoàn toàn vào phần trình bày.

Ví dụ: Bộ chọn ngày tùy chỉnh

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>
);
}

Các khái niệm chính

getDayProps

Hàm getDayProps là cốt lõi của việc kết xuất lịch. Đối với mỗi ô ngày, nó trả về một đối tượng DayProps với 18 cờ boolean/số mô tả trạng thái của ô:

const dp = getDayProps(day);
// Trạng thái lựa chọn
dp.isSelected; // Ngày được chọn hiện tại
dp.isToday; // Ngày hôm nay
// Trạng thái phạm vi (cho bộ chọn phạm vi)
dp.isInRange; // Giữa ngày bắt đầu và ngày kết thúc
dp.isRangeStart; // Ngày đầu tiên của phạm vi
dp.isRangeEnd; // Ngày cuối cùng của phạm vi
dp.isInHoverRange; // Trong phạm vi xem trước khi di chuột
dp.isHoverTarget; // Chính ngày được di chuột qua
dp.isRangeSingle; // Cả bắt đầu và kết thúc phạm vi (phạm vi một ngày)
// Các trình trợ giúp kết nối trực quan
dp.hasLeftConnection; // Được kết nối với ngày trước đó (cho nền phạm vi)
dp.hasRightConnection; // Được kết nối với ngày tiếp theo
dp.isConsecutiveRange; // Một phần của phạm vi nhiều ngày
// Trạng thái
dp.isDisabled; // Bị vô hiệu hóa bởi minDate/maxDate/isDateUnavailable
dp.isFocused; // Có tiêu điểm bàn phím
dp.isOutsideDay; // Thuộc về tháng liền kề (khi showOutsideDays được bật)
dp.isHighlighted; // Trong mảng highlightDates
// Dữ liệu
dp.date; // Đối tượng Date
dp.day; // Số ngày trong tháng
dp.dayOfWeek; // 0-6 (Chủ nhật-Thứ bảy)

Refs để xử lý nhấp chuột bên ngoài

Các hook trả về containerRefpopupRef. Gắn chúng vào các phần tử bao bọc và popup của bạn — hook sử dụng chúng để phát hiện các nhấp chuột bên ngoài và tự động đóng popup.

Điều hướng bằng bàn phím

Trình xử lý handleKeyDown hỗ trợ các phím mũi tên, Enter, Escape và Tab để điều hướng bằng bàn phím đầy đủ. Gắn nó vào vùng chứa popup.

Ngôn ngữ

Tất cả các chuỗi văn bản hiển thị cho người dùng đều đến từ đối tượng locale. Bạn có thể tùy chỉnh bất kỳ chuỗi nào bằng cách truyền một tùy chọn locale một phần:

const picker = useDatePicker({
value,
onChange: setValue,
locale: {
confirm: "OK",
cancel: "Trở lại",
placeholder: "Chọn một ngày...",
},
});

Các bước tiếp theo

  • Tham khảo Hook — Tùy chọn đầy đủ và giá trị trả về cho mỗi hook
  • Contexts — Sử dụng các provider cho các mẫu thành phần phức hợp
  • Tiện ích ngày — Các hàm trợ giúp để thao tác ngày tháng