Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
## Fixed
- [#3629](https://github.com/plotly/dash/pull/3629) Fix date pickers not showing date when initially rendered in a hidden container.
- [#3627][(](https://github.com/plotly/dash/pull/3627)) Make dropdowns searchable wheen focused, without requiring to open them first
- [#3660][(](https://github.com/plotly/dash/pull/3660)) Allow same date to be selected for both start and end in DatePickerRange components



Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
CaretDownIcon,
Cross1Icon,
} from '@radix-ui/react-icons';
import {addDays, subDays} from 'date-fns';
import {addDays, subDays, differenceInCalendarDays} from 'date-fns';
import AutosizeInput from 'react-input-autosize';
import uuid from 'uniqid';

Expand Down Expand Up @@ -108,6 +108,7 @@ const DatePickerRange = ({
const startAutosizeRef = useRef<any>(null);
const endAutosizeRef = useRef<any>(null);
const calendarRef = useRef<CalendarHandle>(null);
const isNewRangeRef = useRef(false);
const hasPortal = with_portal || with_full_screen_portal;

// Capture CSS variables for portal mode
Expand Down Expand Up @@ -161,11 +162,17 @@ const DatePickerRange = ({
end_date: dateAsStr(internalEndDate),
});
} else if (!internalStartDate && !internalEndDate) {
// Both dates cleared - send undefined for both
// Both dates cleared - send both
setProps({
start_date: dateAsStr(internalStartDate),
end_date: dateAsStr(internalEndDate),
});
} else if (endChanged && !internalEndDate) {
// End date was cleared (user started a new range).
setProps({
start_date: dateAsStr(internalStartDate) ?? null,
end_date: null,
});
} else if (updatemode === 'singledate' && internalStartDate) {
// Only start changed - send just that one
setProps({start_date: dateAsStr(internalStartDate)});
Expand Down Expand Up @@ -311,6 +318,22 @@ const DatePickerRange = ({
setInternalStartDate(start);
setInternalEndDate(undefined);
} else {
// Skip the mouseUp from the same click that started this range
if (isNewRangeRef.current && isSameDay(start, end)) {
isNewRangeRef.current = false;
return;
}
isNewRangeRef.current = !!(start && !end);

if (start && end && minimum_nights) {
const numNights = Math.abs(
differenceInCalendarDays(end, start)
);
if (numNights < minimum_nights) {
return;
}
}

// Normalize dates: ensure start <= end
if (start && end && start > end) {
setInternalStartDate(end);
Expand All @@ -325,7 +348,12 @@ const DatePickerRange = ({
}
}
},
[internalStartDate, internalEndDate, stay_open_on_select]
[
internalStartDate,
internalEndDate,
stay_open_on_select,
minimum_nights,
]
);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,7 @@ const CalendarComponent = ({
// Complete the selection with an end date
if (selectionStart && !selectionEnd) {
// Incomplete selection exists (range picker mid-selection)
if (!isSameDay(selectionStart, date)) {
onSelectionChange(selectionStart, date);
}
onSelectionChange(selectionStart, date);
} else {
// Complete selection exists or a single date was chosen
onSelectionChange(date, date);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,11 @@ def update_output(start_date, end_date):
assert get_focused_text(dash_dcc.driver) == "12"

# Press Space to start a NEW range selection with Jan 12 as start_date
# This should clear end_date and set only start_date
# In singledate mode (default), end_date is cleared immediately
send_keys(dash_dcc.driver, Keys.SPACE)

# Verify new start date was selected (only start_date, no end_date)
dash_dcc.wait_for_text_to_equal("#output-dates", "2021-01-12 to 2021-01-20")
# Output updates: new start_date sent, old end_date cleared
dash_dcc.wait_for_text_to_equal("#output-dates", "Start: 2021-01-12")

# Navigate to new end date: Arrow Down + Arrow Right (Jan 12 -> 19 -> 20)
send_keys(dash_dcc.driver, Keys.ARROW_DOWN)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
ElementClickInterceptedException,
TimeoutException,
)
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys


Expand Down Expand Up @@ -377,6 +378,97 @@ def test_dtpr008_input_click_opens_but_keeps_focus(dash_dcc):
assert dash_dcc.get_logs() == []


def test_dtpr009_same_date_selection_minimum_nights_zero(dash_dcc):
"""Bug #3645: With minimum_nights=0, selecting the same date for start and end should work."""
app = Dash(__name__)
app.layout = html.Div(
[
dcc.DatePickerRange(
id="dpr",
min_date_allowed=datetime(2021, 1, 1),
max_date_allowed=datetime(2021, 1, 31),
initial_visible_month=datetime(2021, 1, 1),
minimum_nights=0,
display_format="MM/DD/YYYY",
),
html.Div(id="output"),
]
)

@app.callback(
Output("output", "children"),
Input("dpr", "start_date"),
Input("dpr", "end_date"),
)
def display_dates(start_date, end_date):
return f"Start: {start_date}, End: {end_date}"

dash_dcc.start_server(app)

# Select day 10 for both start and end (same date)
result = dash_dcc.select_date_range("dpr", day_range=(10, 10))
assert result == (
"01/10/2021",
"01/10/2021",
), f"Same date selection should work with minimum_nights=0, got {result}"

dash_dcc.wait_for_text_to_equal("#output", "Start: 2021-01-10, End: 2021-01-10")

assert dash_dcc.get_logs() == []


def test_dtpr010_new_start_date_clears_end_date(dash_dcc):
"""Bug #3645: When a new start date is selected after a range, end_date should be cleared."""
app = Dash(__name__)
app.layout = html.Div(
[
dcc.DatePickerRange(
id="dpr",
min_date_allowed=datetime(2021, 1, 1),
max_date_allowed=datetime(2021, 1, 31),
initial_visible_month=datetime(2021, 1, 1),
minimum_nights=0,
display_format="MM/DD/YYYY",
),
html.Div(id="output"),
]
)

@app.callback(
Output("output", "children"),
Input("dpr", "start_date"),
Input("dpr", "end_date"),
)
def display_dates(start_date, end_date):
return f"Start: {start_date}, End: {end_date}"

dash_dcc.start_server(app)

# First, select a range: Jan 2 to Jan 11
dash_dcc.select_date_range("dpr", day_range=(2, 11))
dash_dcc.wait_for_text_to_equal("#output", "Start: 2021-01-02, End: 2021-01-11")

# Now click just a new start date (Jan 4) without selecting an end date
date = dash_dcc.find_element("#dpr")
date.click()
dash_dcc._wait_until_day_is_clickable()
days = dash_dcc.find_elements(dash_dcc.date_picker_day_locator)
day_4 = [d for d in days if d.find_element(By.CSS_SELECTOR, "span").text == "4"][0]
day_4.click()

# The calendar should still be open (waiting for end date).
# The old end_date (Jan 11) should NOT be retained.
# Click outside to close the calendar.
time.sleep(0.3)
dash_dcc.find_element("body").click()
time.sleep(0.3)

# end_date must be cleared, not silently retained from previous selection
dash_dcc.wait_for_text_to_equal("#output", "Start: 2021-01-04, End: None")

assert dash_dcc.get_logs() == []


def test_dtpr030_external_date_range_update(dash_dcc):
"""Test that DatePickerRange accepts external date updates via callback without resetting."""
app = Dash(__name__)
Expand Down
Loading