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:
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user