Skip to content
Open
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
19 changes: 17 additions & 2 deletions src/panel_material_ui/widgets/DateTimePicker.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,23 @@ export function render({model, view, el}) {

// Safely update the model value
function updateModelValue(date) {
if (!date || !date.isValid()) {
if (!date) {
if (clearable) {
lastCommittedRef.current = null;
setValue(null);
setInternalValue(null);
model.value = null;
}
return;
}

if (!date.isValid()) {
// Partial/incomplete edit (e.g. only some date sections filled in) -
// do not clear the committed value, just restore the display.
setInternalValue(value)
return;
}

const formattedDate = formatDateForPython(date);
// Skip update if this is the same value we just committed
const currentTimestamp = date.valueOf();
Expand All @@ -151,7 +159,14 @@ export function render({model, view, el}) {

// Handle changes from the date picker UI
const handleChange = (newValue) => {
setInternalValue(newValue)
// Ignore transient invalid states produced while a section is being
// typed (e.g. only the year digit typed so far) so the other, already
// filled-in sections aren't wiped from the display mid-edit. A fully
// empty field is reported as `null`, not an invalid date, so that case
// is still tracked to support clearing.
if (!newValue || newValue.isValid()) {
setInternalValue(newValue)
}

// For direct calendar selection, update immediately
if (isCalendarOpen && newValue && newValue.isValid()) {
Expand Down
37 changes: 37 additions & 0 deletions tests/ui/widgets/test_date_picker.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,40 @@ def test_datepicker_clearable_manual_delete_propagates_none(page):
input_el.press("Tab")

wait_until(lambda: widget.value is None, page)


def test_datepicker_clearable_partial_edit_does_not_clear(page):
# Regression test: editing a single date section (e.g. typing into the
# year) while other sections are still unset must not wipe the whole
# value, even when clearable=True.
widget = DatePicker(value=dt.date(2026, 8, 13), clearable=True)
serve_component(page, widget)

input_el = page.locator(".MuiPickersInputBase-input")
wait_until(lambda: "2026-08-13" in input_el.input_value(), page)

year_section = page.locator("span[aria-label='Year']")
year_section.click()
page.keyboard.press("2")

assert widget.value == dt.date(2026, 8, 13)
assert "08-13" in input_el.input_value()

page.keyboard.press("Tab")

assert widget.value == dt.date(2026, 8, 13)


def test_datepicker_edit_single_section_commits(page):
widget = DatePicker(value=dt.date(2026, 8, 13), clearable=True)
serve_component(page, widget)

input_el = page.locator(".MuiPickersInputBase-input")
wait_until(lambda: "2026-08-13" in input_el.input_value(), page)

year_section = page.locator("span[aria-label='Year']")
year_section.click()
page.keyboard.type("2030")
input_el.press("Tab")

wait_until(lambda: widget.value == dt.date(2030, 8, 13), page)
Loading