@@ -17,17 +17,39 @@ interface DateRange {
1717}
1818
1919interface DateRangePickerProps {
20- onChange ?: ( dateRange : DateRange ) => void ;
21- initialDateRange ?: DateRange ;
20+ /**
21+ * Callback function triggered when date range changes
22+ * @param dateRange Object containing startDate and endDate
23+ */
24+ onChange : ( dateRange : DateRange ) => void ;
25+
26+ /**
27+ * Initial date range to display
28+ */
29+ value ?: DateRange ;
30+
31+ /**
32+ * Format for displaying dates (default: MM/DD/YYYY)
33+ */
2234 dateFormat ?: string ;
35+
36+ /**
37+ * Additional CSS class names
38+ */
2339 className ?: string ;
40+
41+ /**
42+ * Placeholder text when no dates are selected
43+ */
44+ placeholder ?: string ;
2445}
2546
2647export const DateRangePicker : FC < DateRangePickerProps > = ( {
2748 onChange,
28- initialDateRange ,
49+ value ,
2950 dateFormat = "MM/DD/YYYY" ,
3051 className = "" ,
52+ placeholder = "Select date range" ,
3153} ) => {
3254 const { t } = useTranslation ( ) ;
3355 const calendarRef = useRef < HTMLDivElement > ( null ) ;
@@ -50,26 +72,17 @@ export const DateRangePicker: FC<DateRangePickerProps> = ({
5072
5173 // Set default initial date range
5274 const getDefaultDateRange = ( ) : DateRange => {
53- const today = new Date ( ) ;
54- const thirtyDaysLater = new Date ( ) ;
55- thirtyDaysLater . setDate ( today . getDate ( ) + 30 ) ;
5675 return {
57- startDate : today ,
58- endDate : thirtyDaysLater ,
76+ startDate : null ,
77+ endDate : null ,
5978 } ;
6079 } ;
6180
6281 // Parse initial date range with fallbacks for null values
6382 const parsedInitialRange = ( ) : DateRange => {
64- if ( ! initialDateRange ) return getDefaultDateRange ( ) ;
65-
6683 return {
67- startDate : initialDateRange . startDate
68- ? parseDate ( initialDateRange . startDate )
69- : new Date ( ) ,
70- endDate : initialDateRange . endDate
71- ? parseDate ( initialDateRange . endDate )
72- : new Date ( new Date ( ) . setDate ( new Date ( ) . getDate ( ) + 30 ) ) ,
84+ startDate : value ?. startDate ? parseDate ( value . startDate ) : null ,
85+ endDate : value ?. endDate ? parseDate ( value . endDate ) : null ,
7386 } ;
7487 } ;
7588
@@ -78,55 +91,77 @@ export const DateRangePicker: FC<DateRangePickerProps> = ({
7891 const [ currentMonth , setCurrentMonth ] = useState < Date > ( ( ) => {
7992 // Safely get start date for current month
8093 const range = parsedInitialRange ( ) ;
81- return range . startDate instanceof Date
82- ? range . startDate
83- : parseDate ( range . startDate ) ;
94+ if ( range . startDate ) {
95+ return range . startDate instanceof Date
96+ ? range . startDate
97+ : parseDate ( range . startDate ) ;
98+ }
99+ return new Date ( ) ; // Default to current month if no range provided
84100 } ) ;
85101
86- // Update state when initialDateRange prop changes
102+ // Update state when value prop changes
87103 useEffect ( ( ) => {
88- if ( initialDateRange ) {
104+ if ( value ) {
89105 const newDateRange = {
90- startDate : initialDateRange . startDate
91- ? parseDate ( initialDateRange . startDate )
92- : new Date ( ) ,
93- endDate : initialDateRange . endDate
94- ? parseDate ( initialDateRange . endDate )
95- : new Date ( new Date ( ) . setDate ( new Date ( ) . getDate ( ) + 30 ) ) ,
106+ startDate : value ?. startDate ? parseDate ( value . startDate ) : null ,
107+ endDate : value ?. endDate ? parseDate ( value . endDate ) : null ,
96108 } ;
109+
97110 setDateRange ( newDateRange ) ;
98111
99- // Safely update current month
112+ // Safely update current month if startDate exists
100113 if ( newDateRange . startDate ) {
101114 setCurrentMonth (
102115 newDateRange . startDate instanceof Date
103- ? newDateRange . startDate
116+ ? new Date ( newDateRange . startDate )
104117 : parseDate ( newDateRange . startDate )
105118 ) ;
106119 }
120+ } else {
121+ // Reset to empty range if value is undefined/null
122+ setDateRange ( getDefaultDateRange ( ) ) ;
107123 }
108- } , [ initialDateRange ] ) ;
124+ } , [ value ] ) ;
109125
110126 // Format date according to specified format (default: MM/DD/YYYY)
111- const formatDate = ( date : Date | null ) : string => {
112- if ( ! date ) return "MM/DD/YYYY" ;
113-
114- // Default format (MM/DD/YYYY)
115- const month = String ( date . getMonth ( ) + 1 ) . padStart ( 2 , "0" ) ;
116- const day = String ( date . getDate ( ) ) . padStart ( 2 , "0" ) ;
117- const year = date . getFullYear ( ) ;
118- return `${ month } /${ day } /${ year } ` ;
119- } ;
120-
121- const getFormattedDate = ( date : Date | string | null ) : string => {
122- if ( ! date ) return "MM/DD/YYYY" ;
123- const parsedDate = date instanceof Date ? date : parseDate ( date ) ;
124- return formatDate ( parsedDate ) ;
127+ const formatDateValue = (
128+ date : Date | string | null ,
129+ format : string = dateFormat
130+ ) : string => {
131+ // If no date, return formatted placeholder
132+ if ( ! date ) {
133+ return format
134+ . replace ( / M + / g, "MM" )
135+ . replace ( / D + / g, "DD" )
136+ . replace ( / Y + / g, "YYYY" ) ;
137+ }
138+
139+ // Convert to Date object if string
140+ const dateObj = date instanceof Date ? date : parseDate ( date ) ;
141+
142+ // Format according to specified pattern
143+ const month = String ( dateObj . getMonth ( ) + 1 ) . padStart ( 2 , "0" ) ;
144+ const day = String ( dateObj . getDate ( ) ) . padStart ( 2 , "0" ) ;
145+ const year = dateObj . getFullYear ( ) ;
146+
147+ let formattedDate = format ;
148+ formattedDate = formattedDate . replace ( / M + / g, month ) ;
149+ formattedDate = formattedDate . replace ( / D + / g, day ) ;
150+ formattedDate = formattedDate . replace ( / Y + / g, year . toString ( ) ) ;
151+
152+ return formattedDate ;
125153 } ;
126-
154+
127155 const formatDateRange = ( ) : string => {
128- const start = getFormattedDate ( dateRange ?. startDate ?? null ) ;
129- const end = getFormattedDate ( dateRange ?. endDate ?? null ) ;
156+ // If neither date is selected, show placeholder
157+ if ( ! dateRange . startDate && ! dateRange . endDate ) {
158+ return t ( placeholder ) ;
159+ }
160+
161+ // Simplified - no need for conditional check as formatDateValue handles nulls
162+ const start = formatDateValue ( dateRange . startDate ) ;
163+ const end = formatDateValue ( dateRange . endDate ) ;
164+
130165 return `${ start } - ${ end } ` ;
131166 } ;
132167
@@ -265,13 +300,18 @@ export const DateRangePicker: FC<DateRangePickerProps> = ({
265300
266301 // Clone the date to avoid reference issues
267302 const selectedDate = new Date ( date ) ;
303+ selectedDate . setHours ( 0 , 0 , 0 , 0 ) ;
268304
269305 if ( ! dateRange . startDate || ( dateRange . startDate && dateRange . endDate ) ) {
270306 // Start new selection
271- setDateRange ( {
307+ const newRange = {
272308 startDate : selectedDate ,
273309 endDate : null ,
274- } ) ;
310+ } ;
311+
312+ setDateRange ( newRange ) ;
313+ // Always notify parent of changes, even for partial selections
314+ onChange ( newRange ) ;
275315 } else {
276316 // Complete the selection
277317 let newStartDate : Date ;
@@ -290,18 +330,14 @@ export const DateRangePicker: FC<DateRangePickerProps> = ({
290330 newEndDate = selectedDate ;
291331 }
292332
293- setDateRange ( {
333+ const newRange = {
294334 startDate : newStartDate ,
295335 endDate : newEndDate ,
296- } ) ;
336+ } ;
297337
298- // Trigger onChange after completing selection
299- if ( onChange ) {
300- onChange ( {
301- startDate : newStartDate ,
302- endDate : newEndDate ,
303- } ) ;
304- }
338+ setDateRange ( newRange ) ;
339+ // Notify parent component of the complete selection
340+ onChange ( newRange ) ;
305341 }
306342 } ;
307343
@@ -356,16 +392,6 @@ export const DateRangePicker: FC<DateRangePickerProps> = ({
356392 }
357393 } ;
358394
359- // Keep track of last valid date range
360- const [ lastValidDateRange , setLastValidDateRange ] = useState < DateRange > ( parsedInitialRange ( ) ) ;
361-
362- // Update lastValidDateRange whenever we have a complete valid selection
363- useEffect ( ( ) => {
364- if ( dateRange . startDate && dateRange . endDate ) {
365- setLastValidDateRange ( { ...dateRange } ) ;
366- }
367- } , [ dateRange ] ) ;
368-
369395 // Handle calendar toggle
370396 const toggleCalendar = ( ) : void => {
371397 setIsOpen ( ! isOpen ) ;
@@ -377,19 +403,17 @@ export const DateRangePicker: FC<DateRangePickerProps> = ({
377403 event . stopPropagation ( ) ;
378404 }
379405 setIsOpen ( false ) ;
380-
406+
381407 // Reset both dates to null when closing with the X button
382408 const emptyDateRange = {
383409 startDate : null ,
384- endDate : null
410+ endDate : null ,
385411 } ;
386-
412+
387413 setDateRange ( emptyDateRange ) ;
388-
414+
389415 // Notify parent component of the reset
390- if ( onChange ) {
391- onChange ( emptyDateRange ) ;
392- }
416+ onChange ( emptyDateRange ) ;
393417 } ;
394418
395419 // Format month and year for display
@@ -421,21 +445,18 @@ export const DateRangePicker: FC<DateRangePickerProps> = ({
421445 ! calendarRef . current . contains ( event . target as Node )
422446 ) {
423447 setIsOpen ( false ) ;
424-
448+
425449 // If selection is incomplete, complete it with current dates
426- // instead of resetting to lastValidDateRange
427450 if ( dateRange . startDate && ! dateRange . endDate ) {
428451 const completedRange = {
429452 startDate : dateRange . startDate ,
430- endDate : dateRange . startDate // Set end date same as start date
453+ endDate : dateRange . startDate , // Set end date same as start date
431454 } ;
432-
455+
433456 setDateRange ( completedRange ) ;
434-
457+
435458 // Notify parent component of the completed selection
436- if ( onChange ) {
437- onChange ( completedRange ) ;
438- }
459+ onChange ( completedRange ) ;
439460 }
440461 }
441462 } ;
@@ -472,25 +493,29 @@ export const DateRangePicker: FC<DateRangePickerProps> = ({
472493 aria-expanded = { isOpen }
473494 type = "button"
474495 >
475- < span data-testid = "date-range-text" > { formatDateRange ( ) } </ span >
496+ < span className = { `date-range-text ${ isOpen ? "open" : "" } ` } data-testid = "date-range-text" > { formatDateRange ( ) } </ span >
476497 < div className = "date-range-controls" >
477- < button
478- className = "date-range-close button-as-div"
479- data-testid = "date-range-close-btn"
480- aria-label = { t ( "Close calendar" ) }
481- type = "button"
482- onKeyDown = { ( e ) => handleNavKeyDown ( e , ( ) => handleCloseCalendar ( ) ) }
483- >
484- < CloseIcon color = "white" onClick = { ( e ) => handleCloseCalendar ( e ) } />
485- </ button >
498+ { isOpen && (
499+ < button
500+ className = "date-range-close button-as-div"
501+ data-testid = "date-range-close-btn"
502+ aria-label = { t ( "Close calendar" ) }
503+ type = "button"
504+ onClick = { ( e ) => handleCloseCalendar ( e ) }
505+ onKeyDown = { ( e ) =>
506+ handleNavKeyDown ( e , ( ) => handleCloseCalendar ( ) )
507+ }
508+ >
509+ < CloseIcon width = { 10 } height = { 10 } color = "white" />
510+ </ button >
511+ ) }
486512 < span
487- className = " date-range-toggle-icon"
513+ className = { ` date-range-toggle-icon ${ isOpen ? "open" : "" } ` }
488514 data-testid = "date-range-toggle-icon"
489515 aria-hidden = "true"
490516 >
491517 { isOpen ? (
492- < UpArrowIcon color = "white" />
493-
518+ < UpArrowIcon color = "white" />
494519 ) : (
495520 < DownArrowIcon color = "white" />
496521 ) }
@@ -622,4 +647,4 @@ export const DateRangePicker: FC<DateRangePickerProps> = ({
622647 ) }
623648 </ div >
624649 ) ;
625- } ;
650+ } ;
0 commit comments