Skip to content

Commit 02acd6e

Browse files
authored
Merge branch 'dev' into fix-multi-select-dropdown-with-components-as-labels
2 parents bc2719e + 1c10c88 commit 02acd6e

File tree

6 files changed

+389
-268
lines changed

6 files changed

+389
-268
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
1010
## Fixed
1111
- [#3629](https://github.com/plotly/dash/pull/3629) Fix date pickers not showing date when initially rendered in a hidden container.
1212
- [#3627][(](https://github.com/plotly/dash/pull/3627)) Make dropdowns searchable wheen focused, without requiring to open them first
13+
- [#3660][(](https://github.com/plotly/dash/pull/3660)) Allow same date to be selected for both start and end in DatePickerRange components
1314

1415

1516

components/dash-core-components/src/fragments/DatePickerRange.tsx

Lines changed: 178 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
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';
1010
import AutosizeInput from 'react-input-autosize';
1111
import 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

Comments
 (0)