Display DD/MM/YYYY and 12-hour AM/PM in DatePicker
Input variant now shows user-friendly format (DD/MM/YYYY for date, DD/MM/YYYY h:mm AM/PM for datetime) instead of raw ISO strings. Internal display state syncs bidirectionally with ISO value prop using a ref flag to avoid overwriting during active typing. Popup time selectors changed from 24-hour to 12-hour with AM/PM dropdown. Button variant datetime display also updated to AM/PM. Backend contract unchanged — onChange still emits ISO strings. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4dc3c856b0
commit
59a4f67b42
@ -21,12 +21,63 @@ function formatDate(y: number, m: number, d: number): string {
|
|||||||
return `${y}-${pad(m + 1)}-${pad(d)}`;
|
return `${y}-${pad(m + 1)}-${pad(d)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function to12Hour(h24: number): { hour: number; ampm: 'AM' | 'PM' } {
|
||||||
|
if (h24 === 0) return { hour: 12, ampm: 'AM' };
|
||||||
|
if (h24 < 12) return { hour: h24, ampm: 'AM' };
|
||||||
|
if (h24 === 12) return { hour: 12, ampm: 'PM' };
|
||||||
|
return { hour: h24 - 12, ampm: 'PM' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function to24Hour(h12: number, ampm: string): number {
|
||||||
|
const isPM = ampm.toUpperCase() === 'PM';
|
||||||
|
if (h12 === 12) return isPM ? 12 : 0;
|
||||||
|
return isPM ? h12 + 12 : h12;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ISO string → user-friendly display: DD/MM/YYYY or DD/MM/YYYY h:mm AM/PM */
|
||||||
|
function isoToDisplay(iso: string, mode: 'date' | 'datetime'): string {
|
||||||
|
if (!iso) return '';
|
||||||
|
const parts = iso.split('T');
|
||||||
|
const dp = parts[0]?.split('-');
|
||||||
|
if (!dp || dp.length !== 3) return '';
|
||||||
|
const dateStr = `${dp[2]}/${dp[1]}/${dp[0]}`;
|
||||||
|
if (mode === 'date') return dateStr;
|
||||||
|
const tp = parts[1]?.split(':');
|
||||||
|
if (!tp || tp.length < 2) return dateStr;
|
||||||
|
const h = parseInt(tp[0], 10);
|
||||||
|
if (isNaN(h)) return dateStr;
|
||||||
|
const { hour: h12, ampm } = to12Hour(h);
|
||||||
|
return `${dateStr} ${h12}:${tp[1].slice(0, 2)} ${ampm}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** User-friendly display → ISO string, or null if incomplete/invalid */
|
||||||
|
function displayToIso(display: string, mode: 'date' | 'datetime'): string | null {
|
||||||
|
if (!display) return null;
|
||||||
|
if (mode === 'date') {
|
||||||
|
const m = display.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
|
||||||
|
if (!m) return null;
|
||||||
|
const d = parseInt(m[1], 10), mo = parseInt(m[2], 10), y = parseInt(m[3], 10);
|
||||||
|
if (mo < 1 || mo > 12 || d < 1 || d > getDaysInMonth(y, mo - 1)) return null;
|
||||||
|
return `${m[3]}-${m[2]}-${m[1]}`;
|
||||||
|
}
|
||||||
|
const m = display.match(/^(\d{2})\/(\d{2})\/(\d{4})\s+(\d{1,2}):(\d{2})\s*(AM|PM)$/i);
|
||||||
|
if (!m) return null;
|
||||||
|
const d = parseInt(m[1], 10), mo = parseInt(m[2], 10), y = parseInt(m[3], 10);
|
||||||
|
if (mo < 1 || mo > 12 || d < 1 || d > getDaysInMonth(y, mo - 1)) return null;
|
||||||
|
let h = parseInt(m[4], 10);
|
||||||
|
const min = parseInt(m[5], 10);
|
||||||
|
if (h < 1 || h > 12 || min < 0 || min > 59) return null;
|
||||||
|
h = to24Hour(h, m[6]);
|
||||||
|
return `${m[3]}-${m[2]}-${m[1]}T${pad(h)}:${pad(min)}`;
|
||||||
|
}
|
||||||
|
|
||||||
const MONTH_NAMES = [
|
const MONTH_NAMES = [
|
||||||
'January', 'February', 'March', 'April', 'May', 'June',
|
'January', 'February', 'March', 'April', 'May', 'June',
|
||||||
'July', 'August', 'September', 'October', 'November', 'December',
|
'July', 'August', 'September', 'October', 'November', 'December',
|
||||||
];
|
];
|
||||||
|
|
||||||
const DAY_HEADERS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
const DAY_HEADERS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
||||||
|
const HOUR_OPTIONS = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
||||||
|
|
||||||
// ── Props ──
|
// ── Props ──
|
||||||
|
|
||||||
@ -76,7 +127,7 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
const [startYear, endYear] = yearRange ?? [1900, currentYear + 20];
|
const [startYear, endYear] = yearRange ?? [1900, currentYear + 20];
|
||||||
|
|
||||||
// Parse current value
|
// Parse ISO value into parts
|
||||||
const parseDateValue = () => {
|
const parseDateValue = () => {
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
const parts = value.split('T');
|
const parts = value.split('T');
|
||||||
@ -101,20 +152,24 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|||||||
const [hour, setHour] = React.useState(parsed?.hour ?? 0);
|
const [hour, setHour] = React.useState(parsed?.hour ?? 0);
|
||||||
const [minute, setMinute] = React.useState(parsed?.minute ?? 0);
|
const [minute, setMinute] = React.useState(parsed?.minute ?? 0);
|
||||||
|
|
||||||
|
// Input variant: user-friendly display string (DD/MM/YYYY or DD/MM/YYYY h:mm AM/PM)
|
||||||
|
const [displayValue, setDisplayValue] = React.useState(() =>
|
||||||
|
variant === 'input' ? isoToDisplay(value, mode) : ''
|
||||||
|
);
|
||||||
|
const isInternalChange = React.useRef(false);
|
||||||
|
|
||||||
// Refs
|
// Refs
|
||||||
const triggerRef = React.useRef<HTMLButtonElement>(null); // button variant
|
const triggerRef = React.useRef<HTMLButtonElement>(null);
|
||||||
const wrapperRef = React.useRef<HTMLDivElement>(null); // input variant
|
const wrapperRef = React.useRef<HTMLDivElement>(null);
|
||||||
const inputElRef = React.useRef<HTMLInputElement>(null); // input variant
|
const inputElRef = React.useRef<HTMLInputElement>(null);
|
||||||
const popupRef = React.useRef<HTMLDivElement>(null);
|
const popupRef = React.useRef<HTMLDivElement>(null);
|
||||||
const blurTimeoutRef = React.useRef<ReturnType<typeof setTimeout>>();
|
const blurTimeoutRef = React.useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
const [pos, setPos] = React.useState<{ top: number; left: number }>({ top: 0, left: 0 });
|
const [pos, setPos] = React.useState<{ top: number; left: number }>({ top: 0, left: 0 });
|
||||||
|
|
||||||
// Merge forwarded ref with internal ref (button variant only)
|
|
||||||
React.useImperativeHandle(ref, () => triggerRef.current!);
|
React.useImperativeHandle(ref, () => triggerRef.current!);
|
||||||
|
|
||||||
// Sync internal state when value changes (only when popup is closed to avoid
|
// Sync popup view state when value changes (only when popup is closed)
|
||||||
// jumping the calendar view while the user is navigating months or typing)
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (open) return;
|
if (open) return;
|
||||||
const p = parseDateValue();
|
const p = parseDateValue();
|
||||||
@ -127,7 +182,19 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [value, open]);
|
}, [value, open]);
|
||||||
|
|
||||||
// Position popup relative to trigger (button) or wrapper (input)
|
// Sync display string from ISO value (input variant).
|
||||||
|
// Skips when the change originated from the text input to avoid
|
||||||
|
// overwriting the user's in-progress typing.
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (variant !== 'input') return;
|
||||||
|
if (isInternalChange.current) {
|
||||||
|
isInternalChange.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setDisplayValue(value ? isoToDisplay(value, mode) : '');
|
||||||
|
}, [value, mode, variant]);
|
||||||
|
|
||||||
|
// Position popup
|
||||||
const updatePosition = React.useCallback(() => {
|
const updatePosition = React.useCallback(() => {
|
||||||
const el = variant === 'input' ? wrapperRef.current : triggerRef.current;
|
const el = variant === 'input' ? wrapperRef.current : triggerRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
@ -153,9 +220,6 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|||||||
};
|
};
|
||||||
}, [open, updatePosition]);
|
}, [open, updatePosition]);
|
||||||
|
|
||||||
// Close popup. refocusTrigger=true returns focus to the trigger/input
|
|
||||||
// (used for day selection, Escape, Done). false lets focus go wherever the
|
|
||||||
// user clicked (used for outside-click dismiss).
|
|
||||||
const closePopup = React.useCallback(
|
const closePopup = React.useCallback(
|
||||||
(refocusTrigger = true) => {
|
(refocusTrigger = true) => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@ -194,9 +258,7 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|||||||
return () => document.removeEventListener('keydown', handler);
|
return () => document.removeEventListener('keydown', handler);
|
||||||
}, [open, closePopup]);
|
}, [open, closePopup]);
|
||||||
|
|
||||||
// Input variant: smart blur — only fires onBlur when focus truly leaves the
|
// Input variant: smart blur — only fires when focus truly leaves the component
|
||||||
// component group (input + icon + popup). Uses a short timeout to let focus
|
|
||||||
// settle on the new target before checking.
|
|
||||||
const handleInputBlur = React.useCallback(() => {
|
const handleInputBlur = React.useCallback(() => {
|
||||||
if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);
|
if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);
|
||||||
blurTimeoutRef.current = setTimeout(() => {
|
blurTimeoutRef.current = setTimeout(() => {
|
||||||
@ -209,7 +271,6 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|||||||
}, 10);
|
}, 10);
|
||||||
}, [onBlur]);
|
}, [onBlur]);
|
||||||
|
|
||||||
// Cleanup blur timeout on unmount
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);
|
if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);
|
||||||
@ -218,7 +279,6 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|||||||
|
|
||||||
const openPopup = () => {
|
const openPopup = () => {
|
||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
// Re-sync view to current value when opening
|
|
||||||
const p = parseDateValue();
|
const p = parseDateValue();
|
||||||
if (p) {
|
if (p) {
|
||||||
setViewYear(p.year);
|
setViewYear(p.year);
|
||||||
@ -234,7 +294,7 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|||||||
else openPopup();
|
else openPopup();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Min/Max date boundaries
|
// Min/Max boundaries
|
||||||
const minDate = min ? new Date(min + 'T00:00:00') : null;
|
const minDate = min ? new Date(min + 'T00:00:00') : null;
|
||||||
const maxDate = max ? new Date(max + 'T00:00:00') : null;
|
const maxDate = max ? new Date(max + 'T00:00:00') : null;
|
||||||
|
|
||||||
@ -245,7 +305,6 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Select a day
|
|
||||||
const selectDay = (day: number) => {
|
const selectDay = (day: number) => {
|
||||||
if (isDayDisabled(viewYear, viewMonth, day)) return;
|
if (isDayDisabled(viewYear, viewMonth, day)) return;
|
||||||
const dateStr = formatDate(viewYear, viewMonth, day);
|
const dateStr = formatDate(viewYear, viewMonth, day);
|
||||||
@ -266,45 +325,40 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Navigation
|
|
||||||
const prevMonth = () => {
|
const prevMonth = () => {
|
||||||
if (viewMonth === 0) {
|
if (viewMonth === 0) { setViewMonth(11); setViewYear((y) => y - 1); }
|
||||||
setViewMonth(11);
|
else setViewMonth((m) => m - 1);
|
||||||
setViewYear((y) => y - 1);
|
|
||||||
} else {
|
|
||||||
setViewMonth((m) => m - 1);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextMonth = () => {
|
const nextMonth = () => {
|
||||||
if (viewMonth === 11) {
|
if (viewMonth === 11) { setViewMonth(0); setViewYear((y) => y + 1); }
|
||||||
setViewMonth(0);
|
else setViewMonth((m) => m + 1);
|
||||||
setViewYear((y) => y + 1);
|
|
||||||
} else {
|
|
||||||
setViewMonth((m) => m + 1);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build calendar grid
|
// Calendar grid
|
||||||
const daysInMonth = getDaysInMonth(viewYear, viewMonth);
|
const daysInMonth = getDaysInMonth(viewYear, viewMonth);
|
||||||
const firstDay = getFirstDayOfWeek(viewYear, viewMonth);
|
const firstDay = getFirstDayOfWeek(viewYear, viewMonth);
|
||||||
const cells: (number | null)[] = [];
|
const cells: (number | null)[] = [];
|
||||||
for (let i = 0; i < firstDay; i++) cells.push(null);
|
for (let i = 0; i < firstDay; i++) cells.push(null);
|
||||||
for (let d = 1; d <= daysInMonth; d++) cells.push(d);
|
for (let d = 1; d <= daysInMonth; d++) cells.push(d);
|
||||||
|
|
||||||
const isToday = (d: number) =>
|
const isTodayDay = (d: number) =>
|
||||||
d === today.getDate() && viewMonth === today.getMonth() && viewYear === today.getFullYear();
|
d === today.getDate() && viewMonth === today.getMonth() && viewYear === today.getFullYear();
|
||||||
|
|
||||||
const isSelected = (d: number) =>
|
const isSelected = (d: number) =>
|
||||||
parsed !== null && d === parsed.day && viewMonth === parsed.month && viewYear === parsed.year;
|
parsed !== null && d === parsed.day && viewMonth === parsed.month && viewYear === parsed.year;
|
||||||
|
|
||||||
// Display text (button variant only)
|
// 12-hour display values for time selectors
|
||||||
|
const { hour: h12, ampm: currentAmpm } = to12Hour(hour);
|
||||||
|
|
||||||
|
// Button variant display text
|
||||||
const displayText = (() => {
|
const displayText = (() => {
|
||||||
if (!parsed) return '';
|
if (!parsed) return '';
|
||||||
const monthName = MONTH_NAMES[parsed.month];
|
const monthName = MONTH_NAMES[parsed.month];
|
||||||
const base = `${monthName} ${parsed.day}, ${parsed.year}`;
|
const base = `${monthName} ${parsed.day}, ${parsed.year}`;
|
||||||
if (mode === 'datetime') {
|
if (mode === 'datetime') {
|
||||||
return `${base} ${pad(parsed.hour)}:${pad(parsed.minute)}`;
|
const { hour: dh, ampm: da } = to12Hour(parsed.hour);
|
||||||
|
return `${base} ${dh}:${pad(parsed.minute)} ${da}`;
|
||||||
}
|
}
|
||||||
return base;
|
return base;
|
||||||
})();
|
})();
|
||||||
@ -322,16 +376,11 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|||||||
style={{ position: 'fixed', top: pos.top, left: pos.left, zIndex: 60 }}
|
style={{ position: 'fixed', top: pos.top, left: pos.left, zIndex: 60 }}
|
||||||
className="w-[280px] rounded-lg border border-input bg-card shadow-lg animate-fade-in"
|
className="w-[280px] rounded-lg border border-input bg-card shadow-lg animate-fade-in"
|
||||||
>
|
>
|
||||||
{/* Month/Year Nav */}
|
{/* Month/Year nav */}
|
||||||
<div className="flex items-center justify-between px-3 pt-3 pb-2">
|
<div className="flex items-center justify-between px-3 pt-3 pb-2">
|
||||||
<button
|
<button type="button" onClick={prevMonth} className="p-1 rounded-md hover:bg-accent/10 transition-colors">
|
||||||
type="button"
|
|
||||||
onClick={prevMonth}
|
|
||||||
className="p-1 rounded-md hover:bg-accent/10 transition-colors"
|
|
||||||
>
|
|
||||||
<ChevronLeft className="h-4 w-4" />
|
<ChevronLeft className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<select
|
<select
|
||||||
value={viewMonth}
|
value={viewMonth}
|
||||||
@ -339,9 +388,7 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|||||||
className="appearance-none bg-transparent text-sm font-medium cursor-pointer hover:text-accent focus:outline-none pr-1"
|
className="appearance-none bg-transparent text-sm font-medium cursor-pointer hover:text-accent focus:outline-none pr-1"
|
||||||
>
|
>
|
||||||
{MONTH_NAMES.map((n, i) => (
|
{MONTH_NAMES.map((n, i) => (
|
||||||
<option key={i} value={i} className="bg-card text-foreground">
|
<option key={i} value={i} className="bg-card text-foreground">{n}</option>
|
||||||
{n}
|
|
||||||
</option>
|
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<select
|
<select
|
||||||
@ -350,18 +397,11 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|||||||
className="appearance-none bg-transparent text-sm font-medium cursor-pointer hover:text-accent focus:outline-none"
|
className="appearance-none bg-transparent text-sm font-medium cursor-pointer hover:text-accent focus:outline-none"
|
||||||
>
|
>
|
||||||
{yearOptions.map((y) => (
|
{yearOptions.map((y) => (
|
||||||
<option key={y} value={y} className="bg-card text-foreground">
|
<option key={y} value={y} className="bg-card text-foreground">{y}</option>
|
||||||
{y}
|
|
||||||
</option>
|
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" onClick={nextMonth} className="p-1 rounded-md hover:bg-accent/10 transition-colors">
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={nextMonth}
|
|
||||||
className="p-1 rounded-md hover:bg-accent/10 transition-colors"
|
|
||||||
>
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
<ChevronRight className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -369,12 +409,7 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|||||||
{/* Day headers */}
|
{/* Day headers */}
|
||||||
<div className="grid grid-cols-7 px-3 pb-1">
|
<div className="grid grid-cols-7 px-3 pb-1">
|
||||||
{DAY_HEADERS.map((d) => (
|
{DAY_HEADERS.map((d) => (
|
||||||
<div
|
<div key={d} className="text-center text-[11px] font-medium text-muted-foreground py-1">{d}</div>
|
||||||
key={d}
|
|
||||||
className="text-center text-[11px] font-medium text-muted-foreground py-1"
|
|
||||||
>
|
|
||||||
{d}
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -393,7 +428,7 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|||||||
'h-8 w-full rounded-md text-sm transition-colors',
|
'h-8 w-full rounded-md text-sm transition-colors',
|
||||||
'hover:bg-accent/10 focus:outline-none focus-visible:ring-1 focus-visible:ring-ring',
|
'hover:bg-accent/10 focus:outline-none focus-visible:ring-1 focus-visible:ring-ring',
|
||||||
isSelected(day) && 'bg-accent text-accent-foreground font-medium',
|
isSelected(day) && 'bg-accent text-accent-foreground font-medium',
|
||||||
isToday(day) && !isSelected(day) && 'border border-accent/50 text-accent',
|
isTodayDay(day) && !isSelected(day) && 'border border-accent/50 text-accent',
|
||||||
isDayDisabled(viewYear, viewMonth, day) &&
|
isDayDisabled(viewYear, viewMonth, day) &&
|
||||||
'opacity-30 cursor-not-allowed hover:bg-transparent'
|
'opacity-30 cursor-not-allowed hover:bg-transparent'
|
||||||
)}
|
)}
|
||||||
@ -404,33 +439,37 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Time selectors (datetime mode only) */}
|
{/* Time selectors — 12-hour with AM/PM */}
|
||||||
{mode === 'datetime' && (
|
{mode === 'datetime' && (
|
||||||
<div className="flex items-center gap-2 px-3 pb-3 border-t border-border pt-2">
|
<div className="flex items-center gap-1.5 px-3 pb-3 border-t border-border pt-2">
|
||||||
<Clock className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
<Clock className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
||||||
<select
|
<select
|
||||||
value={hour}
|
value={h12}
|
||||||
onChange={(e) => handleTimeChange(parseInt(e.target.value, 10), minute)}
|
onChange={(e) => handleTimeChange(to24Hour(parseInt(e.target.value, 10), currentAmpm), minute)}
|
||||||
className="flex-1 appearance-none bg-secondary rounded-md px-2 py-1 text-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-ring cursor-pointer"
|
className="w-14 appearance-none bg-secondary rounded-md px-2 py-1 text-sm text-center focus:outline-none focus-visible:ring-1 focus-visible:ring-ring cursor-pointer"
|
||||||
>
|
>
|
||||||
{Array.from({ length: 24 }, (_, i) => (
|
{HOUR_OPTIONS.map((h) => (
|
||||||
<option key={i} value={i} className="bg-card text-foreground">
|
<option key={h} value={h} className="bg-card text-foreground">{h}</option>
|
||||||
{pad(i)}
|
|
||||||
</option>
|
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<span className="text-muted-foreground font-medium">:</span>
|
<span className="text-muted-foreground font-medium">:</span>
|
||||||
<select
|
<select
|
||||||
value={minute}
|
value={minute}
|
||||||
onChange={(e) => handleTimeChange(hour, parseInt(e.target.value, 10))}
|
onChange={(e) => handleTimeChange(hour, parseInt(e.target.value, 10))}
|
||||||
className="flex-1 appearance-none bg-secondary rounded-md px-2 py-1 text-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-ring cursor-pointer"
|
className="w-14 appearance-none bg-secondary rounded-md px-2 py-1 text-sm text-center focus:outline-none focus-visible:ring-1 focus-visible:ring-ring cursor-pointer"
|
||||||
>
|
>
|
||||||
{Array.from({ length: 60 }, (_, i) => (
|
{Array.from({ length: 60 }, (_, i) => (
|
||||||
<option key={i} value={i} className="bg-card text-foreground">
|
<option key={i} value={i} className="bg-card text-foreground">{pad(i)}</option>
|
||||||
{pad(i)}
|
|
||||||
</option>
|
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
<select
|
||||||
|
value={currentAmpm}
|
||||||
|
onChange={(e) => handleTimeChange(to24Hour(h12, e.target.value), minute)}
|
||||||
|
className="w-16 appearance-none bg-secondary rounded-md px-2 py-1 text-sm text-center focus:outline-none focus-visible:ring-1 focus-visible:ring-ring cursor-pointer"
|
||||||
|
>
|
||||||
|
<option value="AM" className="bg-card text-foreground">AM</option>
|
||||||
|
<option value="PM" className="bg-card text-foreground">PM</option>
|
||||||
|
</select>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => closePopup(true)}
|
onClick={() => closePopup(true)}
|
||||||
@ -456,8 +495,21 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|||||||
id={id}
|
id={id}
|
||||||
name={name}
|
name={name}
|
||||||
autoComplete={autoComplete}
|
autoComplete={autoComplete}
|
||||||
value={value}
|
value={displayValue}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => {
|
||||||
|
const typed = e.target.value;
|
||||||
|
setDisplayValue(typed);
|
||||||
|
if (typed === '') {
|
||||||
|
isInternalChange.current = true;
|
||||||
|
onChange('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const iso = displayToIso(typed, mode);
|
||||||
|
if (iso) {
|
||||||
|
isInternalChange.current = true;
|
||||||
|
onChange(iso);
|
||||||
|
}
|
||||||
|
}}
|
||||||
onBlur={handleInputBlur}
|
onBlur={handleInputBlur}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (open && e.key === 'Enter') {
|
if (open && e.key === 'Enter') {
|
||||||
@ -468,7 +520,7 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|||||||
}}
|
}}
|
||||||
required={required}
|
required={required}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
placeholder={placeholder || (mode === 'datetime' ? 'YYYY-MM-DDThh:mm' : 'YYYY-MM-DD')}
|
placeholder={placeholder || (mode === 'datetime' ? 'DD/MM/YYYY h:mm AM/PM' : 'DD/MM/YYYY')}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 pr-9 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 pr-9 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
className
|
className
|
||||||
@ -494,14 +546,7 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|||||||
// ── Button variant: non-editable trigger (registration DOB) ──
|
// ── Button variant: non-editable trigger (registration DOB) ──
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Hidden input for native form validation + autofill */}
|
<input type="hidden" name={name} autoComplete={autoComplete} value={value} required={required} />
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name={name}
|
|
||||||
autoComplete={autoComplete}
|
|
||||||
value={value}
|
|
||||||
required={required}
|
|
||||||
/>
|
|
||||||
{required && (
|
{required && (
|
||||||
<input
|
<input
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
@ -514,7 +559,6 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Trigger button */}
|
|
||||||
<button
|
<button
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
type="button"
|
type="button"
|
||||||
@ -526,7 +570,6 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
togglePopup();
|
togglePopup();
|
||||||
}
|
}
|
||||||
// Don't forward Enter to parent when popup is open
|
|
||||||
if (open) return;
|
if (open) return;
|
||||||
onKeyDown?.(e);
|
onKeyDown?.(e);
|
||||||
}}
|
}}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user