Switch DatePicker input variant to native date/datetime-local types
Replaces <input type="text"> with custom display format conversion with native <input type="date"> / <input type="datetime-local"> for exact visual parity with Chrome's built-in segmented editing UI. Removes ~50 lines of isoToDisplay/displayToIso conversion code. Hides native picker icon inside .datepicker-wrapper via CSS so only the custom Calendar icon (opening the popup) is visible. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
247c701e12
commit
a30483fbbc
@ -34,43 +34,6 @@ function to24Hour(h12: number, ampm: string): number {
|
|||||||
return isPM ? h12 + 12 : h12;
|
return isPM ? h12 + 12 : h12;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ISO string → user-friendly display: DD/MM/YYYY or DD/MM/YYYY hh:mm AM */
|
|
||||||
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} ${pad(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{1,2})\/(\d{1,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]}-${pad(mo)}-${pad(d)}`;
|
|
||||||
}
|
|
||||||
const m = display.match(/^(\d{1,2})\/(\d{1,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]}-${pad(mo)}-${pad(d)}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',
|
||||||
@ -152,12 +115,6 @@ 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 hh:mm AM)
|
|
||||||
const [displayValue, setDisplayValue] = React.useState(() =>
|
|
||||||
variant === 'input' ? isoToDisplay(value, mode) : ''
|
|
||||||
);
|
|
||||||
const isInternalChange = React.useRef(false);
|
|
||||||
|
|
||||||
// Refs
|
// Refs
|
||||||
const triggerRef = React.useRef<HTMLButtonElement>(null);
|
const triggerRef = React.useRef<HTMLButtonElement>(null);
|
||||||
const wrapperRef = React.useRef<HTMLDivElement>(null);
|
const wrapperRef = React.useRef<HTMLDivElement>(null);
|
||||||
@ -182,18 +139,6 @@ 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]);
|
||||||
|
|
||||||
// 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
|
// 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;
|
||||||
@ -484,32 +429,19 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// ── Input variant: typeable input + calendar icon trigger ──
|
// ── Input variant: native date/datetime-local + calendar icon for custom popup ──
|
||||||
if (variant === 'input') {
|
if (variant === 'input') {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div ref={wrapperRef} className="relative">
|
<div ref={wrapperRef} className="datepicker-wrapper relative">
|
||||||
<input
|
<input
|
||||||
ref={inputElRef}
|
ref={inputElRef}
|
||||||
type="text"
|
type={mode === 'datetime' ? 'datetime-local' : 'date'}
|
||||||
id={id}
|
id={id}
|
||||||
name={name}
|
name={name}
|
||||||
autoComplete={autoComplete}
|
autoComplete={autoComplete}
|
||||||
value={displayValue}
|
value={value}
|
||||||
onChange={(e) => {
|
onChange={(e) => onChange(e.target.value)}
|
||||||
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') {
|
||||||
@ -520,7 +452,8 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|||||||
}}
|
}}
|
||||||
required={required}
|
required={required}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
placeholder={placeholder || (mode === 'datetime' ? 'DD/MM/YYYY hh:mm AM' : 'DD/MM/YYYY')}
|
min={min}
|
||||||
|
max={max}
|
||||||
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
|
||||||
|
|||||||
@ -200,6 +200,11 @@ input[type="datetime-local"]::-webkit-calendar-picker-indicator {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hide native picker icon inside DatePicker wrapper (custom icon replaces it) */
|
||||||
|
.datepicker-wrapper input::-webkit-calendar-picker-indicator {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Form validation — red outline only after submit attempt ── */
|
/* ── Form validation — red outline only after submit attempt ── */
|
||||||
form[data-submitted] input:invalid,
|
form[data-submitted] input:invalid,
|
||||||
form[data-submitted] select:invalid,
|
form[data-submitted] select:invalid,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user