Fix CalendarInput date clearing and validation

- Expose native input field with data-testid support for test control
- Support clearing dates with empty value (null/undefined)
- Add ISO date format parsing (YYYY-MM-DD) for string inputs
- Sync native input with Calendar selection automatically
- Update styling to show native input with calendar icon button
- Display elements (selected-departure-date, selected-return-date) now disappear when date is cleared
This commit is contained in:
gnezim
2026-04-06 08:42:16 +03:00
parent 1e3147929c
commit 947af08bb1
2 changed files with 176 additions and 32 deletions
@@ -1,30 +1,91 @@
.calendar-input { .calendar-input {
width: 100%; width: 100%;
&__wrapper {
position: relative;
display: flex;
align-items: center;
width: 100%;
gap: 0;
}
&__native-input {
flex: 1;
padding: 8px 12px;
border: 1px solid #e0e0e0;
border-right: none;
border-radius: 4px 0 0 4px;
font-size: 14px;
font-family: inherit;
transition: border-color 0.2s ease;
&:focus {
border-color: #1976d2;
outline: none;
box-shadow: inset 0 0 0 2px rgba(25, 118, 210, 0.1);
}
&:disabled {
background-color: #f5f5f5;
color: #999;
cursor: not-allowed;
}
&::placeholder {
color: #999;
}
}
&__calendar-wrapper {
display: flex;
align-items: center;
border: 1px solid #e0e0e0;
border-left: none;
border-radius: 0 4px 4px 0;
background: white;
padding-right: 4px;
&:has(:focus) {
border-color: #1976d2;
}
}
:global { :global {
.calendar-input__field { .calendar-input__calendar {
width: 100%; width: auto;
.p-calendar-trigger {
background: none;
border: none;
cursor: pointer;
padding: 4px 8px;
color: #1976d2;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s ease;
&:hover {
color: #1565c0;
}
&:focus {
outline: none;
}
.pi {
font-size: 16px;
}
}
.p-inputtext {
display: none;
}
} }
.p-calendar { .p-calendar {
width: 100%; width: 100%;
.p-inputtext {
width: 100%;
padding: 8px 12px;
border: 1px solid #e0e0e0;
border-radius: 4px;
font-size: 14px;
&:focus {
border-color: #1976d2;
outline: none;
}
}
.p-calendar-trigger {
padding: 8px 12px;
}
} }
} }
} }
@@ -1,4 +1,4 @@
import React from 'react' import React, { useRef, useEffect, useState } from 'react'
import { Calendar } from 'primereact/calendar' import { Calendar } from 'primereact/calendar'
import './calendar-input.scss' import './calendar-input.scss'
@@ -21,20 +21,103 @@ export const CalendarInput: React.FC<CalendarInputProps> = ({
disabled = false, disabled = false,
'data-testid': dataTestId, 'data-testid': dataTestId,
}) => { }) => {
const inputRef = useRef<HTMLInputElement>(null)
const calendarRef = useRef<any>(null)
const [internalValue, setInternalValue] = useState<string>('')
// Format Date to ISO string (YYYY-MM-DD)
const formatDateToISO = (date: Date | null): string => {
if (!date) return ''
const d = new Date(date)
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const year = d.getFullYear()
return `${year}-${month}-${day}`
}
// Parse ISO string to Date
const parseISOToDate = (dateStr: string): Date | null => {
if (!dateStr || dateStr.trim() === '') return null
const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})$/)
if (!match) return null
const [, year, month, day] = match
const date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day))
// Validate date is valid
if (isNaN(date.getTime())) return null
return date
}
// Update input display when value prop changes
useEffect(() => {
const formattedValue = formatDateToISO(value)
setInternalValue(formattedValue)
if (inputRef.current) {
inputRef.current.value = formattedValue
}
}, [value])
// Handle manual input change from user typing or clearing
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value
setInternalValue(inputValue)
// If input is cleared
if (inputValue === '' || inputValue.trim() === '') {
onChange(null)
return
}
// Try to parse the input
const parsedDate = parseISOToDate(inputValue)
if (parsedDate) {
onChange(parsedDate)
}
// If invalid format, we don't call onChange - let user fix it
}
// Handle calendar selection
const handleCalendarChange = (e: any) => {
const selectedDate = e.value as Date | null
onChange(selectedDate)
// Focus back on input after selection
setTimeout(() => {
inputRef.current?.focus()
}, 0)
}
return ( return (
<div className="calendar-input" data-testid={dataTestId || 'calendar-input'}> <div className="calendar-input" data-testid={dataTestId || 'calendar-input'}>
<Calendar <div className="calendar-input__wrapper">
value={value} <input
onChange={(e) => onChange(e.value as Date | null)} ref={inputRef}
dateFormat="dd.mm.yy" type="text"
minDate={minDate} className="calendar-input__native-input"
maxDate={maxDate} placeholder={placeholder}
placeholder={placeholder} value={internalValue}
disabled={disabled} onChange={handleInputChange}
showIcon disabled={disabled}
inline={false} data-testid={dataTestId}
className="calendar-input__field" />
/> <div className="calendar-input__calendar-wrapper">
<Calendar
ref={calendarRef}
value={value}
onChange={handleCalendarChange}
dateFormat="dd.mm.yy"
minDate={minDate}
maxDate={maxDate}
placeholder={placeholder}
disabled={disabled}
showIcon
inline={false}
className="calendar-input__calendar"
/>
</div>
</div>
</div> </div>
) )
} }