diff --git a/src/panel_material_ui/widgets/DateTimePicker.jsx b/src/panel_material_ui/widgets/DateTimePicker.jsx index da271d4a..79ec0f62 100644 --- a/src/panel_material_ui/widgets/DateTimePicker.jsx +++ b/src/panel_material_ui/widgets/DateTimePicker.jsx @@ -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(); @@ -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()) { diff --git a/tests/ui/widgets/test_date_picker.py b/tests/ui/widgets/test_date_picker.py index 855082e5..54e141d7 100644 --- a/tests/ui/widgets/test_date_picker.py +++ b/tests/ui/widgets/test_date_picker.py @@ -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)