การสร้าง UI แบบกำหนดเอง

คู่มือนี้จะแนะนำรูปแบบการสร้าง UI ตัวเลือกวันที่ของคุณเองบน hook แบบ headless

รูปแบบ

hook แบบ headless ทุกตัวใช้รูปแบบเดียวกัน:

  1. เรียกใช้ hook พร้อมกับตัวเลือกของคุณ (value, onChange, configuration)
  2. แยกโครงสร้างค่าที่ส่งคืน เพื่อรับ state, ค่าที่คำนวณได้ และ handlers
  3. แสดงผล markup ของคุณเอง โดยใช้ค่าที่ส่งคืน
  4. เชื่อมต่อการโต้ตอบ โดยการเรียกใช้ handlers ที่ให้มา

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 ที่มีแฟล็กแบบ boolean/number 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 สำหรับการคลิกภายนอก

hook จะส่งคืน containerRef และ popupRef ให้แนบสิ่งเหล่านี้เข้ากับองค์ประกอบ wrapper และ popup ของคุณ — hook ใช้สิ่งเหล่านี้เพื่อตรวจจับการคลิกภายนอกและปิด popup โดยอัตโนมัติ

การนำทางด้วยคีย์บอร์ด

handler handleKeyDown รองรับปุ่มลูกศร, Enter, Escape และ Tab สำหรับการนำทางด้วยคีย์บอร์ดเต็มรูปแบบ ให้แนบเข้ากับคอนเทนเนอร์ popup

Locale

สตริงทั้งหมดที่ผู้ใช้เห็นมาจากอ็อบเจกต์ locale คุณสามารถปรับแต่งสตริงใดก็ได้โดยส่งตัวเลือก locale บางส่วน:

const picker = useDatePicker({
value,
onChange: setValue,
locale: {
confirm: "ตกลง",
cancel: "ย้อนกลับ",
placeholder: "เลือกวันที่...",
},
});

ขั้นตอนถัดไป

  • Hook Reference — ตัวเลือกและค่าที่ส่งคืนทั้งหมดสำหรับแต่ละ hook
  • Contexts — ใช้ providers สำหรับรูปแบบส่วนประกอบแบบผสม (compound component patterns)
  • Date Utilities — ฟังก์ชันช่วยเหลือสำหรับการจัดการวันที่