From d84b171a91d7d5fe16acd0a6db539ca327080b87 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Tue, 12 Aug 2025 10:17:27 -0400 Subject: [PATCH 01/21] Updating string formatting --- shiny/ui/_input_date.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shiny/ui/_input_date.py b/shiny/ui/_input_date.py index 1399bdbad..39668cbb4 100644 --- a/shiny/ui/_input_date.py +++ b/shiny/ui/_input_date.py @@ -112,15 +112,15 @@ def input_date( """ resolved_id = resolve_id(id) - default_value = value if value is not None else date.today() + default_value = value.strftime("%Y-%m-%d") if value is not None else date.today() return div( shiny_input_label(resolved_id, label), _date_input_tag( id=resolved_id, value=restore_input(resolved_id, default_value), - min=min, - max=max, + min=min.strftime("%Y-%m-%d") if min else None, + max=max.strftime("%Y-%m-%d") if max else None, format=format, startview=startview, weekstart=weekstart, From c123c20f35487963f85279674a3ccd682de618e0 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Tue, 12 Aug 2025 12:37:32 -0400 Subject: [PATCH 02/21] Updated types --- shiny/ui/_input_date.py | 17 +++++-- .../shiny/components/datepicker/app.py | 49 +++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 tests/playwright/shiny/components/datepicker/app.py diff --git a/shiny/ui/_input_date.py b/shiny/ui/_input_date.py index 39668cbb4..3131aa4ef 100644 --- a/shiny/ui/_input_date.py +++ b/shiny/ui/_input_date.py @@ -113,14 +113,25 @@ def input_date( resolved_id = resolve_id(id) - default_value = value.strftime("%Y-%m-%d") if value is not None else date.today() + def process_date(d: Optional[date | str]) -> Optional[str]: + if d is None: + return None + if isinstance(d, date): + return d.strftime("%Y-%m-%d") + if isinstance(d, str): + return d + + min_dt = process_date(min) + max_dt = process_date(max) + default_value = process_date(value) + return div( shiny_input_label(resolved_id, label), _date_input_tag( id=resolved_id, value=restore_input(resolved_id, default_value), - min=min.strftime("%Y-%m-%d") if min else None, - max=max.strftime("%Y-%m-%d") if max else None, + min=min_dt, + max=max_dt, format=format, startview=startview, weekstart=weekstart, diff --git a/tests/playwright/shiny/components/datepicker/app.py b/tests/playwright/shiny/components/datepicker/app.py new file mode 100644 index 000000000..0891f052e --- /dev/null +++ b/tests/playwright/shiny/components/datepicker/app.py @@ -0,0 +1,49 @@ +from datetime import datetime + +from dateutil.relativedelta import relativedelta + +from shiny import App, Inputs, Outputs, Session, ui + +min_date = datetime.now() +max_date = min_date + relativedelta(days=10) + +# Our input requires strings to be in the format "YYYY-MM-DD" +str_min = min_date.strftime("%Y-%m-%d") +str_max = max_date.strftime("%Y-%m-%d") + +ui = ui.page_fluid( + ui.input_date( + "start_date_picker", + "Date Type Input:", + value=max_date, + min=min_date, + max=max_date, + format="dd.mm.yyyy", + language="en", + ), + ui.input_date( + "str_date_picker", + "String Type Input:", + value=str_max, + min=str_min, + max=str_max, + format="dd-mm-yyyy", + language="en", + ), + ui.input_date( + "none_date_picker", + "None Type Input:", + value=None, + min=None, + max=None, + format="dd-mm-yyyy", + language="en", + ), +) + + +def server(input: Inputs, output: Outputs, session: Session): + pass + + +app = App(ui, server, debug=True) From 6a0ddbeecccf863ce18b957b9409fed50db9ad14 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Tue, 12 Aug 2025 13:00:27 -0400 Subject: [PATCH 03/21] Adding empty str as blank option --- shiny/ui/_input_date.py | 2 ++ .../shiny/components/datepicker/app.py | 31 +++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/shiny/ui/_input_date.py b/shiny/ui/_input_date.py index 3131aa4ef..f505cbc74 100644 --- a/shiny/ui/_input_date.py +++ b/shiny/ui/_input_date.py @@ -323,4 +323,6 @@ def _as_date_attr(x: Optional[date | str]) -> Optional[str]: return None if isinstance(x, date): return str(x) + if isinstance(x, str): + return x return str(date.fromisoformat(x)) diff --git a/tests/playwright/shiny/components/datepicker/app.py b/tests/playwright/shiny/components/datepicker/app.py index 0891f052e..4c96dbae1 100644 --- a/tests/playwright/shiny/components/datepicker/app.py +++ b/tests/playwright/shiny/components/datepicker/app.py @@ -2,7 +2,7 @@ from dateutil.relativedelta import relativedelta -from shiny import App, Inputs, Outputs, Session, ui +from shiny import App, Inputs, Outputs, Session, render, ui min_date = datetime.now() max_date = min_date + relativedelta(days=10) @@ -21,6 +21,7 @@ format="dd.mm.yyyy", language="en", ), + ui.output_text("start"), ui.input_date( "str_date_picker", "String Type Input:", @@ -30,6 +31,7 @@ format="dd-mm-yyyy", language="en", ), + ui.output_text("str_format"), ui.input_date( "none_date_picker", "None Type Input:", @@ -39,11 +41,36 @@ format="dd-mm-yyyy", language="en", ), + ui.output_text("none_format"), + ui.input_date( + "empty_date_picker", + "empty Type Input:", + value="", + min=None, + max=None, + format="dd-mm-yyyy", + language="en", + ), + ui.output_text("empty_format"), ) def server(input: Inputs, output: Outputs, session: Session): - pass + @render.text + def start(): + return "Start Date Picker Value: " + str(input.start_date_picker()) + + @render.text + def str_format(): + return "String Date Picker Value: " + str(input.str_date_picker()) + + @render.text + def none_format(): + return "None Date Picker Value: " + str(input.none_date_picker()) + + @render.text + def empty_format(): + return "Empty Date Picker Value: " + str(input.empty_date_picker()) app = App(ui, server, debug=True) From 2799fbfa22973c2b626903d654879300d2aba97e Mon Sep 17 00:00:00 2001 From: Carson Date: Tue, 12 Aug 2025 14:35:38 -0500 Subject: [PATCH 04/21] Simplify --- shiny/ui/_input_date.py | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/shiny/ui/_input_date.py b/shiny/ui/_input_date.py index f505cbc74..97f1151d2 100644 --- a/shiny/ui/_input_date.py +++ b/shiny/ui/_input_date.py @@ -112,26 +112,15 @@ def input_date( """ resolved_id = resolve_id(id) - - def process_date(d: Optional[date | str]) -> Optional[str]: - if d is None: - return None - if isinstance(d, date): - return d.strftime("%Y-%m-%d") - if isinstance(d, str): - return d - - min_dt = process_date(min) - max_dt = process_date(max) - default_value = process_date(value) + default_value = value if value is not None else date.today() return div( shiny_input_label(resolved_id, label), _date_input_tag( id=resolved_id, value=restore_input(resolved_id, default_value), - min=min_dt, - max=max_dt, + min=min, + max=max, format=format, startview=startview, weekstart=weekstart, @@ -321,8 +310,8 @@ def _date_input_tag( def _as_date_attr(x: Optional[date | str]) -> Optional[str]: if x is None: return None - if isinstance(x, date): - return str(x) - if isinstance(x, str): + if isinstance(x, str) and x == "": return x + if isinstance(x, date): + return x.strftime("%Y-%m-%d") return str(date.fromisoformat(x)) From 953aa4a6adaf8e09a720fda152a8304a97a219b2 Mon Sep 17 00:00:00 2001 From: Carson Date: Tue, 12 Aug 2025 14:51:46 -0500 Subject: [PATCH 05/21] Simplify again --- shiny/ui/_input_date.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/shiny/ui/_input_date.py b/shiny/ui/_input_date.py index 97f1151d2..7da5c9c90 100644 --- a/shiny/ui/_input_date.py +++ b/shiny/ui/_input_date.py @@ -310,8 +310,9 @@ def _date_input_tag( def _as_date_attr(x: Optional[date | str]) -> Optional[str]: if x is None: return None - if isinstance(x, str) and x == "": - return x - if isinstance(x, date): - return x.strftime("%Y-%m-%d") - return str(date.fromisoformat(x)) + if isinstance(x, str): + if x == "": + return x + x = date.fromisoformat(x) + + return x.isoformat() From c3cd6a71adae3073efa3b506efc720295b93f886 Mon Sep 17 00:00:00 2001 From: Carson Date: Tue, 12 Aug 2025 15:06:53 -0500 Subject: [PATCH 06/21] Faster check --- shiny/ui/_input_date.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shiny/ui/_input_date.py b/shiny/ui/_input_date.py index 7da5c9c90..5122b8f5c 100644 --- a/shiny/ui/_input_date.py +++ b/shiny/ui/_input_date.py @@ -311,7 +311,7 @@ def _as_date_attr(x: Optional[date | str]) -> Optional[str]: if x is None: return None if isinstance(x, str): - if x == "": + if len(x) == 0: return x x = date.fromisoformat(x) From ccb08f4c559ef475a1e3da80a2d0e6b14e361aeb Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Wed, 13 Aug 2025 09:22:39 -0400 Subject: [PATCH 07/21] Another datepicker test --- .../shiny/components/datepicker/app.py | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/tests/playwright/shiny/components/datepicker/app.py b/tests/playwright/shiny/components/datepicker/app.py index 4c96dbae1..c1cd24cbf 100644 --- a/tests/playwright/shiny/components/datepicker/app.py +++ b/tests/playwright/shiny/components/datepicker/app.py @@ -22,12 +22,22 @@ language="en", ), ui.output_text("start"), + ui.input_date( + "min_date_picker", + "Date Type Input:", + value=min_date, + min=min_date, + max=max_date, + format="dd.mm.yyyy", + language="en", + ), + ui.output_text("min"), ui.input_date( "str_date_picker", "String Type Input:", - value=str_max, - min=str_min, - max=str_max, + value="2023-10-01", + min="2000-01-01", + max="2023-10-01", format="dd-mm-yyyy", language="en", ), @@ -60,6 +70,10 @@ def server(input: Inputs, output: Outputs, session: Session): def start(): return "Start Date Picker Value: " + str(input.start_date_picker()) + @render.text + def min(): + return "Min Date Picker Value: " + str(input.min_date_picker()) + @render.text def str_format(): return "String Date Picker Value: " + str(input.str_date_picker()) From 36a70d0bc1f198105e754c65c8c560356bb9d24f Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Wed, 13 Aug 2025 11:44:05 -0400 Subject: [PATCH 08/21] Pre-test updates --- shiny/ui/_input_date.py | 4 ++-- tests/playwright/shiny/components/datepicker/app.py | 2 +- .../shiny/components/datepicker/test_datepicker.py | 12 ++++++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 tests/playwright/shiny/components/datepicker/test_datepicker.py diff --git a/shiny/ui/_input_date.py b/shiny/ui/_input_date.py index 5122b8f5c..df771d029 100644 --- a/shiny/ui/_input_date.py +++ b/shiny/ui/_input_date.py @@ -314,5 +314,5 @@ def _as_date_attr(x: Optional[date | str]) -> Optional[str]: if len(x) == 0: return x x = date.fromisoformat(x) - - return x.isoformat() + # Using strftime here to ensure we just just a date, regardless of if H:M:S are included + return x.strftime("%Y-%m-%d") diff --git a/tests/playwright/shiny/components/datepicker/app.py b/tests/playwright/shiny/components/datepicker/app.py index c1cd24cbf..a27fdbafd 100644 --- a/tests/playwright/shiny/components/datepicker/app.py +++ b/tests/playwright/shiny/components/datepicker/app.py @@ -4,7 +4,7 @@ from shiny import App, Inputs, Outputs, Session, render, ui -min_date = datetime.now() +min_date = datetime.fromisoformat("2011-11-04T00:05:23") max_date = min_date + relativedelta(days=10) # Our input requires strings to be in the format "YYYY-MM-DD" diff --git a/tests/playwright/shiny/components/datepicker/test_datepicker.py b/tests/playwright/shiny/components/datepicker/test_datepicker.py new file mode 100644 index 000000000..d86287dfd --- /dev/null +++ b/tests/playwright/shiny/components/datepicker/test_datepicker.py @@ -0,0 +1,12 @@ +from playwright.sync_api import Page, expect + +from shiny.playwright import controller +from shiny.run import ShinyAppProc + + +def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: + page.goto(local_app.url) + + # Check the initial state of the date picker where value = max + date1 = controller.InputDate(page, "start_date_picker") + date1.expect_value("14.11.2011") From 5be9d9890a3bf25a0691b31bf8d05f3163508f20 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Wed, 13 Aug 2025 12:52:25 -0400 Subject: [PATCH 09/21] Tests for datepicker --- .../shiny/components/datepicker/app.py | 12 +++--- .../components/datepicker/test_datepicker.py | 38 +++++++++++++++++++ 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/tests/playwright/shiny/components/datepicker/app.py b/tests/playwright/shiny/components/datepicker/app.py index a27fdbafd..3c63fe01c 100644 --- a/tests/playwright/shiny/components/datepicker/app.py +++ b/tests/playwright/shiny/components/datepicker/app.py @@ -48,7 +48,7 @@ value=None, min=None, max=None, - format="dd-mm-yyyy", + format="yyyy-mm-dd", language="en", ), ui.output_text("none_format"), @@ -68,23 +68,23 @@ def server(input: Inputs, output: Outputs, session: Session): @render.text def start(): - return "Start Date Picker Value: " + str(input.start_date_picker()) + return "Date Picker Value: " + str(input.start_date_picker()) @render.text def min(): - return "Min Date Picker Value: " + str(input.min_date_picker()) + return "Date Picker Value: " + str(input.min_date_picker()) @render.text def str_format(): - return "String Date Picker Value: " + str(input.str_date_picker()) + return "Date Picker Value: " + str(input.str_date_picker()) @render.text def none_format(): - return "None Date Picker Value: " + str(input.none_date_picker()) + return "Date Picker Value: " + str(input.none_date_picker()) @render.text def empty_format(): - return "Empty Date Picker Value: " + str(input.empty_date_picker()) + return "Date Picker Value: " + str(input.empty_date_picker()) app = App(ui, server, debug=True) diff --git a/tests/playwright/shiny/components/datepicker/test_datepicker.py b/tests/playwright/shiny/components/datepicker/test_datepicker.py index d86287dfd..eb83a88de 100644 --- a/tests/playwright/shiny/components/datepicker/test_datepicker.py +++ b/tests/playwright/shiny/components/datepicker/test_datepicker.py @@ -2,6 +2,7 @@ from shiny.playwright import controller from shiny.run import ShinyAppProc +import datetime def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: @@ -10,3 +11,40 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: # Check the initial state of the date picker where value = max date1 = controller.InputDate(page, "start_date_picker") date1.expect_value("14.11.2011") + + # Can't find a way to inspect the actual display date in the datepicker, + # as even in the broken example, the correct attribute was still set. + # To work around this, we will check the displayed value to ensure the + # correct value is being passed to the server. + date_output = controller.OutputText(page, "start") + date_output.expect_value("Date Picker Value: 2011-11-14") + + # Test the date picker with the min date = initial value (unchanged case) + date2 = controller.InputDate(page, "min_date_picker") + date2.expect_value("04.11.2011") + + date_output2 = controller.OutputText(page, "min") + date_output2.expect_value("Date Picker Value: 2011-11-04") + + # Test the date picker with the date set as a string instead of as a date object + date3 = controller.InputDate(page, "str_date_picker") + date3.expect_value("01-10-2023") + + date_output3 = controller.OutputText(page, "str_format") + date_output3.expect_value("Date Picker Value: 2023-10-01") + + # Test the date picker with a None value inputted + # None defaults to today's date + input = datetime.date.today().strftime("%Y-%m-%d") + date4 = controller.InputDate(page, "none_date_picker") + date4.expect_value(input) + + date_output4 = controller.OutputText(page, "none_format") + date_output4.expect_value("Date Picker Value: " + input) + + # Test the default value when an empty string is passed is correctly blank + date5 = controller.InputDate(page, "empty_date_picker") + date5.expect_value("") + + date_output5 = controller.OutputText(page, "empty_format") + date_output5.expect_value("Date Picker Value: None") From e46d60033a19ce9d07ad184ecb1f182f1519b72b Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Wed, 13 Aug 2025 15:33:16 -0400 Subject: [PATCH 10/21] Updated tests and doc strings --- shiny/ui/_input_date.py | 2 +- shiny/ui/_input_update.py | 4 +-- .../shiny/components/datepicker/app.py | 31 ++++++++++--------- .../components/datepicker/test_datepicker.py | 13 ++++++-- 4 files changed, 31 insertions(+), 19 deletions(-) diff --git a/shiny/ui/_input_date.py b/shiny/ui/_input_date.py index df771d029..0e0929e4f 100644 --- a/shiny/ui/_input_date.py +++ b/shiny/ui/_input_date.py @@ -45,7 +45,7 @@ def input_date( value The starting date. Either a :class:`~datetime.date` object, or a string in `yyyy-mm-dd` format. If None (the default), will use the current date in the - client's time zone. + client's time zone. If an empty string is passed, the date picker will be blank. min The minimum allowed date. Either a :class:`~datetime.date` object, or a string in yyyy-mm-dd format. diff --git a/shiny/ui/_input_update.py b/shiny/ui/_input_update.py index 7395e764b..1f2a4829c 100644 --- a/shiny/ui/_input_update.py +++ b/shiny/ui/_input_update.py @@ -464,7 +464,6 @@ def update_date( An input label. value The starting date. Either a `date()` object, or a string in yyyy-mm-dd format. - If ``None`` (the default), will use the current date in the client's time zone. min The minimum allowed value. max @@ -475,7 +474,7 @@ def update_date( Note ---- - {note} + You cannot update the value of a date input to `None` or an empty string. See Also -------- @@ -483,6 +482,7 @@ def update_date( """ session = require_active_session(session) + msg = { "label": session._process_ui(label) if label is not None else None, "value": _as_date_attr(value), diff --git a/tests/playwright/shiny/components/datepicker/app.py b/tests/playwright/shiny/components/datepicker/app.py index 3c63fe01c..89218f731 100644 --- a/tests/playwright/shiny/components/datepicker/app.py +++ b/tests/playwright/shiny/components/datepicker/app.py @@ -1,17 +1,17 @@ -from datetime import datetime +from datetime import date from dateutil.relativedelta import relativedelta -from shiny import App, Inputs, Outputs, Session, render, ui +from shiny import App, Inputs, Outputs, Session, reactive, render, ui -min_date = datetime.fromisoformat("2011-11-04T00:05:23") +min_date = date.fromisoformat("2011-11-04") max_date = min_date + relativedelta(days=10) # Our input requires strings to be in the format "YYYY-MM-DD" str_min = min_date.strftime("%Y-%m-%d") str_max = max_date.strftime("%Y-%m-%d") -ui = ui.page_fluid( +app_ui = ui.page_fluid( ui.input_date( "start_date_picker", "Date Type Input:", @@ -42,15 +42,7 @@ language="en", ), ui.output_text("str_format"), - ui.input_date( - "none_date_picker", - "None Type Input:", - value=None, - min=None, - max=None, - format="yyyy-mm-dd", - language="en", - ), + ui.input_date("none_date_picker", "None Type Input:"), ui.output_text("none_format"), ui.input_date( "empty_date_picker", @@ -62,6 +54,7 @@ language="en", ), ui.output_text("empty_format"), + ui.input_action_button("update", "Update Dates"), ) @@ -86,5 +79,15 @@ def none_format(): def empty_format(): return "Date Picker Value: " + str(input.empty_date_picker()) + @reactive.effect + @reactive.event(input.update, ignore_none=True, ignore_init=True) + def _(): + d = date.fromisoformat("2011-11-05") + ui.update_date("start_date_picker", value=d) + ui.update_date("str_date_picker", value="2020-01-01") + + # Note: You cannot update the value of a date input to None (it will be dropped). + # Note: You cannot update the value of a date input to an empty string. This is a Bootstrap Datepicker limitation. + -app = App(ui, server, debug=True) +app = App(app_ui, server, debug=True) diff --git a/tests/playwright/shiny/components/datepicker/test_datepicker.py b/tests/playwright/shiny/components/datepicker/test_datepicker.py index eb83a88de..29a89b151 100644 --- a/tests/playwright/shiny/components/datepicker/test_datepicker.py +++ b/tests/playwright/shiny/components/datepicker/test_datepicker.py @@ -1,8 +1,9 @@ -from playwright.sync_api import Page, expect +import datetime + +from playwright.sync_api import Page from shiny.playwright import controller from shiny.run import ShinyAppProc -import datetime def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: @@ -48,3 +49,11 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: date_output5 = controller.OutputText(page, "empty_format") date_output5.expect_value("Date Picker Value: None") + + controller.InputActionButton(page, "update").click() + + date1.expect_value("05.11.2011") + date_output.expect_value("Date Picker Value: 2011-11-05") + + date3.expect_value("01-01-2020") + date_output3.expect_value("Date Picker Value: 2020-01-01") From a899b94f8f47afe3899122514540c346d01dc8e7 Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 13 Aug 2025 16:06:17 -0500 Subject: [PATCH 11/21] Support clearing of values programmatically --- shiny/ui/_input_update.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/shiny/ui/_input_update.py b/shiny/ui/_input_update.py index 1f2a4829c..9d28509d5 100644 --- a/shiny/ui/_input_update.py +++ b/shiny/ui/_input_update.py @@ -474,7 +474,8 @@ def update_date( Note ---- - You cannot update the value of a date input to `None` or an empty string. + A special value of `""` (an empty string) can be used to clear the value, min, and + max. See Also -------- @@ -489,7 +490,19 @@ def update_date( "min": _as_date_attr(min), "max": _as_date_attr(max), } - session.send_input_message(id, drop_none(msg)) + + msg = drop_none(msg) + + # Handle the special case of "", which means the value should be cleared + # (i.e., go from a specified date to no specified date) + if value == "": + msg["value"] = None + if min == "": + msg["min"] = None + if max == "": + msg["max"] = None + + session.send_input_message(id, msg) @add_example() From 42f33cd819beeb6aa9c17e65c14437807c09a253 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Thu, 14 Aug 2025 09:54:01 -0400 Subject: [PATCH 12/21] Adding blank value test --- tests/playwright/shiny/components/datepicker/app.py | 4 +--- .../playwright/shiny/components/datepicker/test_datepicker.py | 3 +++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/playwright/shiny/components/datepicker/app.py b/tests/playwright/shiny/components/datepicker/app.py index 89218f731..80c569de0 100644 --- a/tests/playwright/shiny/components/datepicker/app.py +++ b/tests/playwright/shiny/components/datepicker/app.py @@ -84,10 +84,8 @@ def empty_format(): def _(): d = date.fromisoformat("2011-11-05") ui.update_date("start_date_picker", value=d) + ui.update_date("min_date_picker", value="") ui.update_date("str_date_picker", value="2020-01-01") - # Note: You cannot update the value of a date input to None (it will be dropped). - # Note: You cannot update the value of a date input to an empty string. This is a Bootstrap Datepicker limitation. - app = App(app_ui, server, debug=True) diff --git a/tests/playwright/shiny/components/datepicker/test_datepicker.py b/tests/playwright/shiny/components/datepicker/test_datepicker.py index 29a89b151..927553b12 100644 --- a/tests/playwright/shiny/components/datepicker/test_datepicker.py +++ b/tests/playwright/shiny/components/datepicker/test_datepicker.py @@ -55,5 +55,8 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: date1.expect_value("05.11.2011") date_output.expect_value("Date Picker Value: 2011-11-05") + date2.expect_value("") + date_output2.expect_value("Date Picker Value: None") + date3.expect_value("01-01-2020") date_output3.expect_value("Date Picker Value: 2020-01-01") From 33b5ef8305977ac02a4dabb46b7ee89173c7b9e2 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Thu, 14 Aug 2025 09:56:09 -0400 Subject: [PATCH 13/21] updates --- tests/playwright/shiny/components/datepicker/app.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/playwright/shiny/components/datepicker/app.py b/tests/playwright/shiny/components/datepicker/app.py index 80c569de0..3a0fac182 100644 --- a/tests/playwright/shiny/components/datepicker/app.py +++ b/tests/playwright/shiny/components/datepicker/app.py @@ -7,10 +7,6 @@ min_date = date.fromisoformat("2011-11-04") max_date = min_date + relativedelta(days=10) -# Our input requires strings to be in the format "YYYY-MM-DD" -str_min = min_date.strftime("%Y-%m-%d") -str_max = max_date.strftime("%Y-%m-%d") - app_ui = ui.page_fluid( ui.input_date( "start_date_picker", From 2317f6299cded74f491e3f79fe178dc49a9808b4 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Thu, 14 Aug 2025 11:05:52 -0400 Subject: [PATCH 14/21] Adding datepicker blanks example --- .../shiny/components/date_range/app.py | 86 +++++++++++++++++++ .../components/date_range/test_date_range.py | 35 ++++++++ 2 files changed, 121 insertions(+) create mode 100644 tests/playwright/shiny/components/date_range/app.py create mode 100644 tests/playwright/shiny/components/date_range/test_date_range.py diff --git a/tests/playwright/shiny/components/date_range/app.py b/tests/playwright/shiny/components/date_range/app.py new file mode 100644 index 000000000..43de9e5a9 --- /dev/null +++ b/tests/playwright/shiny/components/date_range/app.py @@ -0,0 +1,86 @@ +from datetime import date + +from dateutil.relativedelta import relativedelta + +from shiny import App, Inputs, Outputs, Session, reactive, render, ui + +min_date = date.fromisoformat("2011-11-04") +max_date = min_date + relativedelta(days=10) + +app_ui = ui.page_fluid( + ui.input_date_range( + "standard_date_picker", + "Normal Input:", + start=min_date, + end=max_date, + min=min_date, + max=max_date, + format="yyyy-mm-dd", + language="en", + ), + ui.output_text("standard"), + ui.br(), + ui.br(), + ui.input_date_range( + "none_date_picker", + "None Input:", + start=None, + end=None, + min=None, + max=None, + format="yyyy-mm-dd", + language="en", + ), + ui.output_text("none"), + ui.br(), + ui.br(), + ui.input_date_range( + "empty_date_picker", + "Empty Input:", + start="", + end="", + min=None, + max=None, + format="yyyy-mm-dd", + language="en", + ), + ui.output_text("empty"), +) + + +def server(input: Inputs, output: Outputs, session: Session): + @render.text + def standard(): + return ( + "Date Picker Value: " + + str(input.standard_date_picker()[0]) + + " to " + + str(input.standard_date_picker()[1]) + ) + + @render.text + def none(): + return ( + "Date Picker Value: " + + str(input.none_date_picker()[0]) + + " to " + + str(input.none_date_picker()[1]) + ) + + @render.text + def empty(): + return ( + "Date Picker Value: " + + str(input.empty_date_picker()[0]) + + " to " + + str(input.empty_date_picker()[1]) + ) + + @reactive.effect + @reactive.event(input.update, ignore_none=True, ignore_init=True) + def _(): + d = date.fromisoformat("2011-11-05") + ui.update_date("standard_date_picker", value=d) + + +app = App(app_ui, server, debug=True) diff --git a/tests/playwright/shiny/components/date_range/test_date_range.py b/tests/playwright/shiny/components/date_range/test_date_range.py new file mode 100644 index 000000000..d0a37e07d --- /dev/null +++ b/tests/playwright/shiny/components/date_range/test_date_range.py @@ -0,0 +1,35 @@ +import datetime + +from playwright.sync_api import Page + +from shiny.playwright import controller +from shiny.run import ShinyAppProc + + +def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: + page.goto(local_app.url) + + # Check the normal date range input + date1 = controller.InputDateRange(page, "standard_date_picker") + date1.expect_value(("2011-11-04", "2011-11-14")) + date1.expect_max_date("2011-11-14") + date1.expect_min_date("2011-11-04") + date_output = controller.OutputText(page, "standard") + date_output.expect_value("Date Picker Value: 2011-11-04 to 2011-11-14") + + # Check the None date range input + date2 = controller.InputDateRange(page, "none_date_picker") + input = datetime.date.today().strftime("%Y-%m-%d") + date2.expect_value((input, input)) + date2.expect_max_date(None) + date2.expect_min_date(None) + date_output_none = controller.OutputText(page, "none") + date_output_none.expect_value("Date Picker Value: " + input + " to " + input) + + # Check the empty date range input + date3 = controller.InputDateRange(page, "empty_date_picker") + date3.expect_value(("", "")) + date3.expect_max_date(None) + date3.expect_min_date(None) + date_output_empty = controller.OutputText(page, "empty") + date_output_empty.expect_value("Date Picker Value: None to None") From 96593b6766b365acbcec2dad196def44858ce7e9 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Thu, 14 Aug 2025 12:47:51 -0400 Subject: [PATCH 15/21] Adding date-range --- shiny/ui/_input_update.py | 29 +++++++++++++------ .../shiny/components/date_range/app.py | 19 +++++++++++- .../components/date_range/test_date_range.py | 19 ++++++++++++ 3 files changed, 57 insertions(+), 10 deletions(-) diff --git a/shiny/ui/_input_update.py b/shiny/ui/_input_update.py index 9d28509d5..c7f261cc6 100644 --- a/shiny/ui/_input_update.py +++ b/shiny/ui/_input_update.py @@ -527,17 +527,15 @@ def update_date_range( label An input label. start - The initial start date. Either a :class:`~datetime.date` object, or a string in - yyyy-mm-dd format. If ``None`` (the default), will use the current date in the - client's time zone. + The starting date. Either a `date()` object, or a string in yyyy-mm-dd format. + If an empty string is provided, the value will be cleared. end - The initial end date. Either a :class:`~datetime.date` object, or a string in - yyyy-mm-dd format. If ``None`` (the default), will use the current date in the - client's time zone. + The ending date. Either a `date()` object, or a string in yyyy-mm-dd format. + If an empty string is provided, the value will be cleared. min - The minimum allowed value. + The minimum allowed value. If an empty string is passed there will be no minimum date. max - The maximum allowed value. + The maximum allowed value. If an empty string is passed there will be no maximum date. session A :class:`~shiny.Session` instance. If not provided, it is inferred via :func:`~shiny.session.get_current_session`. @@ -559,7 +557,20 @@ def update_date_range( "min": _as_date_attr(min), "max": _as_date_attr(max), } - session.send_input_message(id, drop_none(msg)) + msg = drop_none(msg) + + # Handle the special case of "", which means the value should be cleared + # (i.e., go from a specified date to no specified date) + if start == "": + msg["value"]["start"] = None + if end == "": + msg["value"]["end"] = None + if min == "": + msg["min"] = None + if max == "": + msg["max"] = None + + session.send_input_message(id, msg) # ----------------------------------------------------------------------------- diff --git a/tests/playwright/shiny/components/date_range/app.py b/tests/playwright/shiny/components/date_range/app.py index 43de9e5a9..d387c4915 100644 --- a/tests/playwright/shiny/components/date_range/app.py +++ b/tests/playwright/shiny/components/date_range/app.py @@ -45,6 +45,7 @@ language="en", ), ui.output_text("empty"), + ui.input_action_button("update", "Update Dates"), ) @@ -80,7 +81,23 @@ def empty(): @reactive.event(input.update, ignore_none=True, ignore_init=True) def _(): d = date.fromisoformat("2011-11-05") - ui.update_date("standard_date_picker", value=d) + + # Take the existing date rang input and blank it out + ui.update_date_range( + "standard_date_picker", + start="", + end="", + min="", + max="", + ) + # Take the None date range input and set it + ui.update_date_range( + "none_date_picker", + start=d - relativedelta(days=3), + end=d + relativedelta(days=10), + min=d - relativedelta(days=4), + max=d + relativedelta(days=11), + ) app = App(app_ui, server, debug=True) diff --git a/tests/playwright/shiny/components/date_range/test_date_range.py b/tests/playwright/shiny/components/date_range/test_date_range.py index d0a37e07d..a1cf6a2d9 100644 --- a/tests/playwright/shiny/components/date_range/test_date_range.py +++ b/tests/playwright/shiny/components/date_range/test_date_range.py @@ -33,3 +33,22 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: date3.expect_min_date(None) date_output_empty = controller.OutputText(page, "empty") date_output_empty.expect_value("Date Picker Value: None to None") + + controller.InputActionButton(page, "update").click() + + # This is updating min and max dates, but the playwright locater cannot find it. + # It appears that the disabled dates are correct. + # Expect that means the calcuated values for disabled dates are updating correctly + # but the attribute is not updated. + date2.expect_value(("2011-11-02", "2011-11-15")) + # date2.expect_max_date("2011-11-16") + # date2.expect_min_date("2011-11-01") + date_output = controller.OutputText(page, "none") + date_output.expect_value("Date Picker Value: 2011-11-02 to 2011-11-15") + + # Check that the updated standard date range input is blanked + date1.expect_value(("", "")) + # date1.expect_max_date(None) + # date1.expect_min_date(None) + date_output = controller.OutputText(page, "standard") + date_output.expect_value("Date Picker Value: None to None") From e397dfa3e00a368f7584478f47d88c6e55e927eb Mon Sep 17 00:00:00 2001 From: Carson Date: Thu, 14 Aug 2025 12:54:40 -0500 Subject: [PATCH 16/21] Workaround for type checking issue --- shiny/ui/_input_update.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/shiny/ui/_input_update.py b/shiny/ui/_input_update.py index c7f261cc6..975fae6e7 100644 --- a/shiny/ui/_input_update.py +++ b/shiny/ui/_input_update.py @@ -494,7 +494,8 @@ def update_date( msg = drop_none(msg) # Handle the special case of "", which means the value should be cleared - # (i.e., go from a specified date to no specified date) + # (i.e., go from a specified date to no specified date). + # This is equivalent to the NA case in R if value == "": msg["value"] = None if min == "": @@ -550,26 +551,40 @@ def update_date_range( """ session = require_active_session(session) - value = {"start": _as_date_attr(start), "end": _as_date_attr(end)} + msg = { "label": session._process_ui(label) if label is not None else None, - "value": drop_none(value), "min": _as_date_attr(min), "max": _as_date_attr(max), } + msg = drop_none(msg) # Handle the special case of "", which means the value should be cleared - # (i.e., go from a specified date to no specified date) - if start == "": - msg["value"]["start"] = None - if end == "": - msg["value"]["end"] = None + # (i.e., go from a specified date to no specified date). + # This is equivalent to the NA case in R if min == "": msg["min"] = None if max == "": msg["max"] = None + value = { + "start": _as_date_attr(start), + "end": _as_date_attr(end), + } + + value = drop_none(value) + + # Handle the special case of "", which means the value should be cleared + # (i.e., go from a specified date to no specified date) + # This is equivalent to the NA case in R + if start == "": + value["start"] = None + if end == "": + value["end"] = None + + msg["value"] = value + session.send_input_message(id, msg) From d6cd8bcca2edb0a492b1b59eb1ff8d7c4775f608 Mon Sep 17 00:00:00 2001 From: E Nelson Date: Fri, 15 Aug 2025 10:22:55 -0400 Subject: [PATCH 17/21] Update shiny/ui/_input_date.py Co-authored-by: Carson Sievert --- shiny/ui/_input_date.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shiny/ui/_input_date.py b/shiny/ui/_input_date.py index 0e0929e4f..8a2ac4e06 100644 --- a/shiny/ui/_input_date.py +++ b/shiny/ui/_input_date.py @@ -314,5 +314,5 @@ def _as_date_attr(x: Optional[date | str]) -> Optional[str]: if len(x) == 0: return x x = date.fromisoformat(x) - # Using strftime here to ensure we just just a date, regardless of if H:M:S are included + # Ensure we return a date (not datetime) string return x.strftime("%Y-%m-%d") From 0085a3528244dafad880b58b8bcb16519f70876b Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Fri, 15 Aug 2025 10:30:49 -0400 Subject: [PATCH 18/21] Updates --- shiny/ui/_input_update.py | 9 ++++----- .../playwright/shiny/components/datepicker/app.py | 10 +++++----- .../shiny/components/datepicker/test_datepicker.py | 14 ++++++++++---- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/shiny/ui/_input_update.py b/shiny/ui/_input_update.py index 975fae6e7..2aa93cc35 100644 --- a/shiny/ui/_input_update.py +++ b/shiny/ui/_input_update.py @@ -463,19 +463,18 @@ def update_date( label An input label. value - The starting date. Either a `date()` object, or a string in yyyy-mm-dd format. + The starting date. Either a `date()` object, or a string in yyyy-mm-dd format. If an empty string is provided, the value will be cleared. min - The minimum allowed value. + The minimum allowed value. Either a `date()` object, or a string in yyyy-mm-dd format. An empty string will clear the minimum constraint. max - The maximum allowed value. + The maximum allowed value. Either a `date()` object, or a string in yyyy-mm-dd format. An empty string will clear the maximum constraint. session A :class:`~shiny.Session` instance. If not provided, it is inferred via :func:`~shiny.session.get_current_session`. Note ---- - A special value of `""` (an empty string) can be used to clear the value, min, and - max. + {note} See Also -------- diff --git a/tests/playwright/shiny/components/datepicker/app.py b/tests/playwright/shiny/components/datepicker/app.py index 3a0fac182..31fc29ddc 100644 --- a/tests/playwright/shiny/components/datepicker/app.py +++ b/tests/playwright/shiny/components/datepicker/app.py @@ -31,9 +31,9 @@ ui.input_date( "str_date_picker", "String Type Input:", - value="2023-10-01", - min="2000-01-01", - max="2023-10-01", + value="2023-10-05", + min="2023-10-01", + max="2023-10-07", format="dd-mm-yyyy", language="en", ), @@ -80,8 +80,8 @@ def empty_format(): def _(): d = date.fromisoformat("2011-11-05") ui.update_date("start_date_picker", value=d) - ui.update_date("min_date_picker", value="") - ui.update_date("str_date_picker", value="2020-01-01") + ui.update_date("min_date_picker", value="", min="") + ui.update_date("str_date_picker", value="2023-10-03", max="2023-10-20") app = App(app_ui, server, debug=True) diff --git a/tests/playwright/shiny/components/datepicker/test_datepicker.py b/tests/playwright/shiny/components/datepicker/test_datepicker.py index 927553b12..3660401c8 100644 --- a/tests/playwright/shiny/components/datepicker/test_datepicker.py +++ b/tests/playwright/shiny/components/datepicker/test_datepicker.py @@ -29,10 +29,10 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: # Test the date picker with the date set as a string instead of as a date object date3 = controller.InputDate(page, "str_date_picker") - date3.expect_value("01-10-2023") + date3.expect_value("05-10-2023") date_output3 = controller.OutputText(page, "str_format") - date_output3.expect_value("Date Picker Value: 2023-10-01") + date_output3.expect_value("Date Picker Value: 2023-10-05") # Test the date picker with a None value inputted # None defaults to today's date @@ -57,6 +57,12 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: date2.expect_value("") date_output2.expect_value("Date Picker Value: None") + # This is updating min and max dates, but the playwright locater cannot find it. + # It appears that the disabled dates are correct. + # Expect that means the calcuated values for disabled dates are updating correctly + # but the attribute is not updated. - date3.expect_value("01-01-2020") - date_output3.expect_value("Date Picker Value: 2020-01-01") + # date2.expect_min_date(None) + + date3.expect_value("03-10-2023") + date_output3.expect_value("Date Picker Value: 2023-10-03") From 2ddf4ca6c12f9cee89b9e134c6d9761e93bfcfe0 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Fri, 15 Aug 2025 10:33:46 -0400 Subject: [PATCH 19/21] moving folder --- .../{components/datepicker => inputs/input_datepicker}/app.py | 0 .../datepicker => inputs/input_datepicker}/test_datepicker.py | 0 .../{components/date_range => inputs/input_daterange}/app.py | 0 .../date_range => inputs/input_daterange}/test_date_range.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename tests/playwright/shiny/{components/datepicker => inputs/input_datepicker}/app.py (100%) rename tests/playwright/shiny/{components/datepicker => inputs/input_datepicker}/test_datepicker.py (100%) rename tests/playwright/shiny/{components/date_range => inputs/input_daterange}/app.py (100%) rename tests/playwright/shiny/{components/date_range => inputs/input_daterange}/test_date_range.py (100%) diff --git a/tests/playwright/shiny/components/datepicker/app.py b/tests/playwright/shiny/inputs/input_datepicker/app.py similarity index 100% rename from tests/playwright/shiny/components/datepicker/app.py rename to tests/playwright/shiny/inputs/input_datepicker/app.py diff --git a/tests/playwright/shiny/components/datepicker/test_datepicker.py b/tests/playwright/shiny/inputs/input_datepicker/test_datepicker.py similarity index 100% rename from tests/playwright/shiny/components/datepicker/test_datepicker.py rename to tests/playwright/shiny/inputs/input_datepicker/test_datepicker.py diff --git a/tests/playwright/shiny/components/date_range/app.py b/tests/playwright/shiny/inputs/input_daterange/app.py similarity index 100% rename from tests/playwright/shiny/components/date_range/app.py rename to tests/playwright/shiny/inputs/input_daterange/app.py diff --git a/tests/playwright/shiny/components/date_range/test_date_range.py b/tests/playwright/shiny/inputs/input_daterange/test_date_range.py similarity index 100% rename from tests/playwright/shiny/components/date_range/test_date_range.py rename to tests/playwright/shiny/inputs/input_daterange/test_date_range.py From 9e3227807eebcf7dcd19ca2a4d248fbd96336b63 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Fri, 15 Aug 2025 12:04:17 -0400 Subject: [PATCH 20/21] Updating Changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94b18d8f0..da7e06b4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Improvements +* Added support for blank values in `input_date()`, `input_date_range()`, `update_date()`, and `update_date_range()` for values, mins, and maxes to allow for providing blank input boxes and removing previously set values. (#1713) (#1689) + * Restricted the allowable types of the `choices` parameter of `input_select()`, `input_selectize()`, `update_select()`, and `update_selectize()` to actual set of allowable types (previously, the type was suggesting HTML-like values were supported). (#2048) * Improved the styling and readability of markdown tables rendered by `ui.Chat()` and `ui.MarkdownStream()`. (#1973) From acdd53574b497f82d26d4578e41395353a6e6998 Mon Sep 17 00:00:00 2001 From: E Nelson Date: Fri, 15 Aug 2025 12:41:23 -0400 Subject: [PATCH 21/21] Update CHANGELOG.md Co-authored-by: Carson Sievert --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da7e06b4c..2cc3a9740 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Improvements -* Added support for blank values in `input_date()`, `input_date_range()`, `update_date()`, and `update_date_range()` for values, mins, and maxes to allow for providing blank input boxes and removing previously set values. (#1713) (#1689) +* `input_date()`, `input_date_range()`, `update_date()`, and `update_date_range()` now supports `""` for values, mins, and maxes. In this case, no date will be specified on the client. (#1713) (#1689) * Restricted the allowable types of the `choices` parameter of `input_select()`, `input_selectize()`, `update_select()`, and `update_selectize()` to actual set of allowable types (previously, the type was suggesting HTML-like values were supported). (#2048)