66 CaretDownIcon ,
77 Cross1Icon ,
88} from '@radix-ui/react-icons' ;
9- import { addDays , subDays } from 'date-fns' ;
9+ import { addDays , subDays , differenceInCalendarDays } from 'date-fns' ;
1010import AutosizeInput from 'react-input-autosize' ;
1111import uuid from 'uniqid' ;
1212
@@ -108,6 +108,7 @@ const DatePickerRange = ({
108108 const startAutosizeRef = useRef < any > ( null ) ;
109109 const endAutosizeRef = useRef < any > ( null ) ;
110110 const calendarRef = useRef < CalendarHandle > ( null ) ;
111+ const isNewRangeRef = useRef ( false ) ;
111112 const hasPortal = with_portal || with_full_screen_portal ;
112113
113114 // Capture CSS variables for portal mode
@@ -161,11 +162,17 @@ const DatePickerRange = ({
161162 end_date : dateAsStr ( internalEndDate ) ,
162163 } ) ;
163164 } else if ( ! internalStartDate && ! internalEndDate ) {
164- // Both dates cleared - send undefined for both
165+ // Both dates cleared - send both
165166 setProps ( {
166167 start_date : dateAsStr ( internalStartDate ) ,
167168 end_date : dateAsStr ( internalEndDate ) ,
168169 } ) ;
170+ } else if ( endChanged && ! internalEndDate ) {
171+ // End date was cleared (user started a new range).
172+ setProps ( {
173+ start_date : dateAsStr ( internalStartDate ) ?? null ,
174+ end_date : null ,
175+ } ) ;
169176 } else if ( updatemode === 'singledate' && internalStartDate ) {
170177 // Only start changed - send just that one
171178 setProps ( { start_date : dateAsStr ( internalStartDate ) } ) ;
@@ -311,6 +318,22 @@ const DatePickerRange = ({
311318 setInternalStartDate ( start ) ;
312319 setInternalEndDate ( undefined ) ;
313320 } else {
321+ // Skip the mouseUp from the same click that started this range
322+ if ( isNewRangeRef . current && isSameDay ( start , end ) ) {
323+ isNewRangeRef . current = false ;
324+ return ;
325+ }
326+ isNewRangeRef . current = ! ! ( start && ! end ) ;
327+
328+ if ( start && end && minimum_nights ) {
329+ const numNights = Math . abs (
330+ differenceInCalendarDays ( end , start )
331+ ) ;
332+ if ( numNights < minimum_nights ) {
333+ return ;
334+ }
335+ }
336+
314337 // Normalize dates: ensure start <= end
315338 if ( start && end && start > end ) {
316339 setInternalStartDate ( end ) ;
@@ -325,161 +348,167 @@ const DatePickerRange = ({
325348 }
326349 }
327350 } ,
328- [ internalStartDate , internalEndDate , stay_open_on_select ]
351+ [
352+ internalStartDate ,
353+ internalEndDate ,
354+ stay_open_on_select ,
355+ minimum_nights ,
356+ ]
329357 ) ;
330358
331359 return (
332- < ResizeDetector
333- onResize = { handleResize }
334- targets = { [ containerRef ] }
335- >
336- < div className = "dash-datepicker" ref = { containerRef } >
337- < Popover . Root
338- open = { ! disabled && isCalendarOpen }
339- onOpenChange = { disabled ? undefined : setIsCalendarOpen }
340- >
341- < Popover . Trigger asChild disabled = { disabled } >
342- < div
343- id = { accessibleId + '-wrapper' }
344- className = { classNames }
345- style = { style }
346- aria-labelledby = { `${ accessibleId } ${ accessibleId } -end-date ${ start_date_id } ${ end_date_id } ` }
347- aria-haspopup = "dialog"
348- aria-expanded = { isCalendarOpen }
349- aria-disabled = { disabled }
350- onClick = { e => {
351- e . preventDefault ( ) ;
352- if ( ! isCalendarOpen && ! disabled ) {
353- setIsCalendarOpen ( true ) ;
354- }
355- } }
356- >
357- < AutosizeInput
358- ref = { startAutosizeRef }
359- inputRef = { node => {
360- startInputRef . current = node ;
361- } }
362- type = "text"
363- id = { start_date_id || accessibleId }
364- inputClassName = "dash-datepicker-input dash-datepicker-start-date"
365- value = { startInputValue }
366- onChange = { e => setStartInputValue ( e . target ?. value ) }
367- onKeyDown = { handleStartInputKeyDown }
368- onFocus = { ( ) => {
369- if ( isCalendarOpen ) {
370- sendStartInputAsDate ( ) ;
360+ < ResizeDetector onResize = { handleResize } targets = { [ containerRef ] } >
361+ < div className = "dash-datepicker" ref = { containerRef } >
362+ < Popover . Root
363+ open = { ! disabled && isCalendarOpen }
364+ onOpenChange = { disabled ? undefined : setIsCalendarOpen }
365+ >
366+ < Popover . Trigger asChild disabled = { disabled } >
367+ < div
368+ id = { accessibleId + '-wrapper' }
369+ className = { classNames }
370+ style = { style }
371+ aria-labelledby = { `${ accessibleId } ${ accessibleId } -end-date ${ start_date_id } ${ end_date_id } ` }
372+ aria-haspopup = "dialog"
373+ aria-expanded = { isCalendarOpen }
374+ aria-disabled = { disabled }
375+ onClick = { e => {
376+ e . preventDefault ( ) ;
377+ if ( ! isCalendarOpen && ! disabled ) {
378+ setIsCalendarOpen ( true ) ;
371379 }
372380 } }
373- placeholder = { start_date_placeholder_text }
374- disabled = { disabled }
375- dir = { direction }
376- aria-label = { start_date_placeholder_text }
377- />
378- < ArrowIcon className = "dash-datepicker-range-arrow" />
379- < AutosizeInput
380- ref = { endAutosizeRef }
381- inputRef = { node => {
382- endInputRef . current = node ;
383- } }
384- type = "text"
385- id = { end_date_id || accessibleId + '-end-date' }
386- inputClassName = "dash-datepicker-input dash-datepicker-end-date"
387- value = { endInputValue }
388- onChange = { e => setEndInputValue ( e . target ?. value ) }
389- onKeyDown = { handleEndInputKeyDown }
390- onFocus = { ( ) => {
391- if ( isCalendarOpen ) {
392- sendEndInputAsDate ( ) ;
381+ >
382+ < AutosizeInput
383+ ref = { startAutosizeRef }
384+ inputRef = { node => {
385+ startInputRef . current = node ;
386+ } }
387+ type = "text"
388+ id = { start_date_id || accessibleId }
389+ inputClassName = "dash-datepicker-input dash-datepicker-start-date"
390+ value = { startInputValue }
391+ onChange = { e =>
392+ setStartInputValue ( e . target ?. value )
393393 }
394- } }
395- placeholder = { end_date_placeholder_text }
396- disabled = { disabled }
397- dir = { direction }
398- aria-label = { end_date_placeholder_text }
399- />
400- { clearable && ! disabled && (
401- < a
402- className = "dash-datepicker-clear"
403- onClick = { clearSelection }
404- aria-label = "Clear Dates"
405- >
406- < Cross1Icon />
407- </ a >
408- ) }
409- < CaretDownIcon className = "dash-datepicker-caret-icon" />
410- </ div >
411- </ Popover . Trigger >
412-
413- < Popover . Portal
414- container = { hasPortal ? undefined : containerRef . current }
415- >
416- < Popover . Content
417- className = { `dash-datepicker-content${
418- hasPortal ? ' dash-datepicker-portal' : ''
419- } ${
420- with_full_screen_portal
421- ? ' dash-datepicker-fullscreen'
422- : ''
423- } `}
424- style = { portalStyle }
425- align = { hasPortal ? 'center' : 'start' }
426- sideOffset = { hasPortal ? 0 : 5 }
427- avoidCollisions = { ! hasPortal }
428- onInteractOutside = {
429- with_full_screen_portal
430- ? e => e . preventDefault ( )
431- : undefined
432- }
433- onOpenAutoFocus = { e => e . preventDefault ( ) }
434- onCloseAutoFocus = { e => {
435- e . preventDefault ( ) ;
436- // Only focus if focus is not already on one of the inputs
437- const inputs : ( Element | null ) [ ] = [
438- startInputRef . current ,
439- endInputRef . current ,
440- ] ;
441- if ( inputs . includes ( document . activeElement ) ) {
442- return ;
394+ onKeyDown = { handleStartInputKeyDown }
395+ onFocus = { ( ) => {
396+ if ( isCalendarOpen ) {
397+ sendStartInputAsDate ( ) ;
398+ }
399+ } }
400+ placeholder = { start_date_placeholder_text }
401+ disabled = { disabled }
402+ dir = { direction }
403+ aria-label = { start_date_placeholder_text }
404+ />
405+ < ArrowIcon className = "dash-datepicker-range-arrow" />
406+ < AutosizeInput
407+ ref = { endAutosizeRef }
408+ inputRef = { node => {
409+ endInputRef . current = node ;
410+ } }
411+ type = "text"
412+ id = { end_date_id || accessibleId + '-end-date' }
413+ inputClassName = "dash-datepicker-input dash-datepicker-end-date"
414+ value = { endInputValue }
415+ onChange = { e =>
416+ setEndInputValue ( e . target ?. value )
417+ }
418+ onKeyDown = { handleEndInputKeyDown }
419+ onFocus = { ( ) => {
420+ if ( isCalendarOpen ) {
421+ sendEndInputAsDate ( ) ;
422+ }
423+ } }
424+ placeholder = { end_date_placeholder_text }
425+ disabled = { disabled }
426+ dir = { direction }
427+ aria-label = { end_date_placeholder_text }
428+ />
429+ { clearable && ! disabled && (
430+ < a
431+ className = "dash-datepicker-clear"
432+ onClick = { clearSelection }
433+ aria-label = "Clear Dates"
434+ >
435+ < Cross1Icon />
436+ </ a >
437+ ) }
438+ < CaretDownIcon className = "dash-datepicker-caret-icon" />
439+ </ div >
440+ </ Popover . Trigger >
441+
442+ < Popover . Portal
443+ container = { hasPortal ? undefined : containerRef . current }
444+ >
445+ < Popover . Content
446+ className = { `dash-datepicker-content${
447+ hasPortal ? ' dash-datepicker-portal' : ''
448+ } ${
449+ with_full_screen_portal
450+ ? ' dash-datepicker-fullscreen'
451+ : ''
452+ } `}
453+ style = { portalStyle }
454+ align = { hasPortal ? 'center' : 'start' }
455+ sideOffset = { hasPortal ? 0 : 5 }
456+ avoidCollisions = { ! hasPortal }
457+ onInteractOutside = {
458+ with_full_screen_portal
459+ ? e => e . preventDefault ( )
460+ : undefined
443461 }
462+ onOpenAutoFocus = { e => e . preventDefault ( ) }
463+ onCloseAutoFocus = { e => {
464+ e . preventDefault ( ) ;
465+ // Only focus if focus is not already on one of the inputs
466+ const inputs : ( Element | null ) [ ] = [
467+ startInputRef . current ,
468+ endInputRef . current ,
469+ ] ;
470+ if ( inputs . includes ( document . activeElement ) ) {
471+ return ;
472+ }
444473
445- // Keeps focus on the component when the calendar closes
446- if ( ! startInputValue ) {
447- startInputRef . current ?. focus ( ) ;
448- } else {
449- endInputRef . current ?. focus ( ) ;
450- }
451- } }
452- >
453- { with_full_screen_portal && (
454- < button
455- className = "dash-datepicker-close-button"
456- onClick = { ( ) => setIsCalendarOpen ( false ) }
457- aria-label = "Close calendar"
458- >
459- < Cross1Icon />
460- </ button >
461- ) }
462- < Calendar
463- ref = { calendarRef }
464- initialVisibleDate = { initialCalendarDate }
465- selectionStart = { internalStartDate }
466- selectionEnd = { internalEndDate }
467- minDateAllowed = { minDate }
468- maxDateAllowed = { maxDate }
469- disabledDates = { disabledDates }
470- firstDayOfWeek = { first_day_of_week }
471- showOutsideDays = { show_outside_days }
472- monthFormat = { month_format }
473- numberOfMonthsShown = { number_of_months_shown }
474- calendarOrientation = { calendar_orientation }
475- daySize = { day_size }
476- direction = { direction }
477- onSelectionChange = { handleSelectionChange }
478- />
479- </ Popover . Content >
480- </ Popover . Portal >
481- </ Popover . Root >
482- </ div >
474+ // Keeps focus on the component when the calendar closes
475+ if ( ! startInputValue ) {
476+ startInputRef . current ?. focus ( ) ;
477+ } else {
478+ endInputRef . current ?. focus ( ) ;
479+ }
480+ } }
481+ >
482+ { with_full_screen_portal && (
483+ < button
484+ className = "dash-datepicker-close-button"
485+ onClick = { ( ) => setIsCalendarOpen ( false ) }
486+ aria-label = "Close calendar"
487+ >
488+ < Cross1Icon />
489+ </ button >
490+ ) }
491+ < Calendar
492+ ref = { calendarRef }
493+ initialVisibleDate = { initialCalendarDate }
494+ selectionStart = { internalStartDate }
495+ selectionEnd = { internalEndDate }
496+ minDateAllowed = { minDate }
497+ maxDateAllowed = { maxDate }
498+ disabledDates = { disabledDates }
499+ firstDayOfWeek = { first_day_of_week }
500+ showOutsideDays = { show_outside_days }
501+ monthFormat = { month_format }
502+ numberOfMonthsShown = { number_of_months_shown }
503+ calendarOrientation = { calendar_orientation }
504+ daySize = { day_size }
505+ direction = { direction }
506+ onSelectionChange = { handleSelectionChange }
507+ />
508+ </ Popover . Content >
509+ </ Popover . Portal >
510+ </ Popover . Root >
511+ </ div >
483512 </ ResizeDetector >
484513 ) ;
485514} ;
0 commit comments