@@ -4,8 +4,10 @@ import {
44 CalendarIcon ,
55 CaretDownIcon ,
66 Cross1Icon ,
7+ ArrowLeftIcon ,
78 ArrowRightIcon ,
89} from '@radix-ui/react-icons' ;
10+ import AutosizeInput from 'react-input-autosize' ;
911import Calendar from '../utils/calendar/Calendar' ;
1012import { DatePickerRangeProps , CalendarDirection } from '../types' ;
1113import {
@@ -15,9 +17,9 @@ import {
1517 isDateDisabled ,
1618 isSameDay ,
1719} from '../utils/calendar/helpers' ;
18- import { DateSet } from '../utils/calendar/DateSet' ;
1920import '../components/css/datepickers.css' ;
2021import uuid from 'uniqid' ;
22+ import moment from 'moment' ;
2123
2224const DatePickerRange = ( {
2325 id,
@@ -28,6 +30,7 @@ const DatePickerRange = ({
2830 max_date_allowed,
2931 initial_visible_month = start_date ?? min_date_allowed ?? max_date_allowed ,
3032 disabled_days,
33+ minimum_nights,
3134 first_day_of_week,
3235 show_outside_days,
3336 clearable,
@@ -59,24 +62,45 @@ const DatePickerRange = ({
5962 const initialMonth = strAsDate ( initial_visible_month ) ;
6063 const minDate = strAsDate ( min_date_allowed ) ;
6164 const maxDate = strAsDate ( max_date_allowed ) ;
62- const disabledDates = useMemo (
63- ( ) => new DateSet ( disabled_days ) ,
64- [ disabled_days ]
65- ) ;
65+ const disabledDates = useMemo ( ( ) => {
66+ const baseDates =
67+ disabled_days
68+ ?. map ( d => strAsDate ( d ) )
69+ . filter ( ( d ) : d is Date => d !== undefined ) || [ ] ;
70+
71+ // Add minimum_nights constraint: disable dates within the minimum nights range
72+ if (
73+ internalStartDate &&
74+ minimum_nights &&
75+ minimum_nights > 0 &&
76+ ! internalEndDate
77+ ) {
78+ const minimumNightsDates : Date [ ] = [ ] ;
79+ for ( let i = 1 ; i < minimum_nights ; i ++ ) {
80+ minimumNightsDates . push (
81+ moment ( internalStartDate ) . add ( i , 'day' ) . toDate ( )
82+ ) ;
83+ minimumNightsDates . push (
84+ moment ( internalStartDate ) . subtract ( i , 'day' ) . toDate ( )
85+ ) ;
86+ }
87+ return [ ...baseDates , ...minimumNightsDates ] ;
88+ }
89+
90+ return baseDates ;
91+ } , [ disabled_days , internalStartDate , internalEndDate , minimum_nights ] ) ;
6692
6793 const [ isCalendarOpen , setIsCalendarOpen ] = useState ( false ) ;
68- const [ startInputValue , setStartInputValue ] = useState < string > (
69- ( internalStartDate && formatDate ( internalStartDate , display_format ) ) ??
70- ''
94+ const [ startInputValue , setStartInputValue ] = useState (
95+ formatDate ( internalStartDate , display_format )
7196 ) ;
72- const [ endInputValue , setEndInputValue ] = useState < string > (
73- ( internalEndDate && formatDate ( internalEndDate , display_format ) ) ?? ''
97+ const [ endInputValue , setEndInputValue ] = useState (
98+ formatDate ( internalEndDate , display_format )
7499 ) ;
75100
76101 const containerRef = useRef < HTMLDivElement > ( null ) ;
77- const startInputRef = useRef < HTMLInputElement > ( null ) ;
78- const endInputRef = useRef < HTMLInputElement > ( null ) ;
79- const calendarRef = useRef < HTMLDivElement > ( null ) ;
102+ const startInputRef = useRef < HTMLInputElement | null > ( null ) ;
103+ const endInputRef = useRef < HTMLInputElement | null > ( null ) ;
80104
81105 useEffect ( ( ) => {
82106 setInternalStartDate ( strAsDate ( start_date ) ) ;
@@ -119,7 +143,7 @@ const DatePickerRange = ({
119143 endInputRef . current ?. focus ( ) ;
120144 }
121145 }
122- } , [ isCalendarOpen ] ) ;
146+ } , [ isCalendarOpen , startInputValue ] ) ;
123147
124148 const sendStartInputAsDate = useCallback ( ( ) => {
125149 const parsed = strAsDate ( startInputValue , display_format ) ;
@@ -224,6 +248,42 @@ const DatePickerRange = ({
224248 classNames += ' ' + className ;
225249 }
226250
251+ const initialCalendarDate =
252+ initialMonth || internalStartDate || internalEndDate ;
253+
254+ const ArrowIcon =
255+ direction === CalendarDirection . LeftToRight
256+ ? ArrowRightIcon
257+ : ArrowLeftIcon ;
258+
259+ const handleSelectionChange = useCallback (
260+ ( start ?: Date , end ?: Date ) => {
261+ const isNewSelection =
262+ isSameDay ( start , end ) &&
263+ ( ( ! internalStartDate && ! internalEndDate ) ||
264+ ( internalStartDate && internalEndDate ) ) ;
265+
266+ if ( isNewSelection ) {
267+ setInternalStartDate ( start ) ;
268+ setInternalEndDate ( undefined ) ;
269+ } else {
270+ // Normalize dates: ensure start <= end
271+ if ( start && end && start > end ) {
272+ setInternalStartDate ( end ) ;
273+ setInternalEndDate ( start ) ;
274+ } else {
275+ setInternalStartDate ( start ) ;
276+ setInternalEndDate ( end ) ;
277+ }
278+
279+ if ( end && ! stay_open_on_select ) {
280+ setIsCalendarOpen ( false ) ;
281+ }
282+ }
283+ } ,
284+ [ internalStartDate , internalEndDate , stay_open_on_select ]
285+ ) ;
286+
227287 return (
228288 < div className = "dash-datepicker" ref = { containerRef } >
229289 < Popover . Root
@@ -241,30 +301,44 @@ const DatePickerRange = ({
241301 aria-disabled = { disabled }
242302 >
243303 < CalendarIcon className = "dash-datepicker-trigger-icon" />
244- < input
245- ref = { startInputRef }
304+ < AutosizeInput
305+ inputRef = { node => {
306+ startInputRef . current = node ;
307+ } }
246308 type = "text"
247309 id = { start_date_id || accessibleId }
248- className = "dash-datepicker-input dash-datepicker-start-date"
310+ inputClassName = "dash-datepicker-input dash-datepicker-start-date"
249311 value = { startInputValue }
250312 onChange = { e => setStartInputValue ( e . target . value ) }
251313 onKeyDown = { handleStartInputKeyDown }
252314 onBlur = { sendStartInputAsDate }
315+ onClick = { ( ) => {
316+ if ( ! isCalendarOpen && ! disabled ) {
317+ setIsCalendarOpen ( true ) ;
318+ }
319+ } }
253320 placeholder = { start_date_placeholder_text }
254321 disabled = { disabled }
255322 dir = { direction }
256323 aria-label = { start_date_placeholder_text }
257324 />
258- < ArrowRightIcon />
259- < input
260- ref = { endInputRef }
325+ < ArrowIcon />
326+ < AutosizeInput
327+ inputRef = { node => {
328+ endInputRef . current = node ;
329+ } }
261330 type = "text"
262331 id = { end_date_id || accessibleId + '-end-date' }
263- className = "dash-datepicker-input dash-datepicker-end-date"
332+ inputClassName = "dash-datepicker-input dash-datepicker-end-date"
264333 value = { endInputValue }
265334 onChange = { e => setEndInputValue ( e . target . value ) }
266335 onKeyDown = { handleEndInputKeyDown }
267336 onBlur = { sendEndInputAsDate }
337+ onClick = { ( ) => {
338+ if ( ! isCalendarOpen && ! disabled ) {
339+ setIsCalendarOpen ( true ) ;
340+ }
341+ } }
268342 placeholder = { end_date_placeholder_text }
269343 disabled = { disabled }
270344 dir = { direction }
@@ -290,47 +364,22 @@ const DatePickerRange = ({
290364 sideOffset = { 5 }
291365 onOpenAutoFocus = { e => e . preventDefault ( ) }
292366 >
293- < div ref = { calendarRef } >
294- < Calendar
295- initialVisibleDate = {
296- initialMonth ||
297- internalStartDate ||
298- internalEndDate
299- }
300- selectionStart = { internalStartDate }
301- selectionEnd = { internalEndDate }
302- minDateAllowed = { minDate }
303- maxDateAllowed = { maxDate }
304- disabledDates = { disabledDates }
305- firstDayOfWeek = { first_day_of_week }
306- showOutsideDays = { show_outside_days }
307- monthFormat = { month_format }
308- numberOfMonthsShown = { number_of_months_shown }
309- calendarOrientation = { calendar_orientation }
310- daySize = { day_size }
311- direction = { direction }
312- onSelectionChange = { ( start , end ) => {
313- const isNewSelection =
314- isSameDay ( start , end ) &&
315- ( ( ! internalStartDate &&
316- ! internalEndDate ) ||
317- ( internalStartDate &&
318- internalEndDate ) ) ;
319-
320- if ( isNewSelection ) {
321- setInternalStartDate ( start ) ;
322- setInternalEndDate ( undefined ) ;
323- } else {
324- setInternalStartDate ( start ) ;
325- setInternalEndDate ( end ) ;
326-
327- if ( end && ! stay_open_on_select ) {
328- setIsCalendarOpen ( false ) ;
329- }
330- }
331- } }
332- />
333- </ div >
367+ < Calendar
368+ initialVisibleDate = { initialCalendarDate }
369+ selectionStart = { internalStartDate }
370+ selectionEnd = { internalEndDate }
371+ minDateAllowed = { minDate }
372+ maxDateAllowed = { maxDate }
373+ disabledDates = { disabledDates }
374+ firstDayOfWeek = { first_day_of_week }
375+ showOutsideDays = { show_outside_days }
376+ monthFormat = { month_format }
377+ numberOfMonthsShown = { number_of_months_shown }
378+ calendarOrientation = { calendar_orientation }
379+ daySize = { day_size }
380+ direction = { direction }
381+ onSelectionChange = { handleSelectionChange }
382+ />
334383 </ Popover . Content >
335384 </ Popover . Portal >
336385 </ Popover . Root >
0 commit comments