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:
- Gọi hook với các tùy chọn của bạn (giá trị, onChange, cấu hình)
- 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ý
- Kết xuất markup của riêng bạn bằng cách sử dụng các giá trị trả về
- 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ọndp.isSelected; // Ngày được chọn hiện tạidp.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úcdp.isRangeStart; // Ngày đầu tiên của phạm vidp.isRangeEnd; // Ngày cuối cùng của phạm vidp.isInHoverRange; // Trong phạm vi xem trước khi di chuộtdp.isHoverTarget; // Chính ngày được di chuột quadp.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 quandp.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 theodp.isConsecutiveRange; // Một phần của phạm vi nhiều ngày
// Trạng tháidp.isDisabled; // Bị vô hiệu hóa bởi minDate/maxDate/isDateUnavailabledp.isFocused; // Có tiêu điểm bàn phímdp.isOutsideDay; // Thuộc về tháng liền kề (khi showOutsideDays được bật)dp.isHighlighted; // Trong mảng highlightDates
// Dữ liệudp.date; // Đối tượng Datedp.day; // Số ngày trong thángdp.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ề containerRef và popupRef. 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