Skip to content

Commit 2ceaa36

Browse files
fix: Address datepicker issues (#2057)
Co-authored-by: Carson <[email protected]>
1 parent ef6649b commit 2ceaa36

File tree

7 files changed

+375
-20
lines changed

7 files changed

+375
-20
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3131

3232
### Improvements
3333

34+
* `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)
35+
3436
* 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)
3537

3638
* Improved the styling and readability of markdown tables rendered by `ui.Chat()` and `ui.MarkdownStream()`. (#1973)

shiny/ui/_input_date.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def input_date(
4545
value
4646
The starting date. Either a :class:`~datetime.date` object, or a string in
4747
`yyyy-mm-dd` format. If None (the default), will use the current date in the
48-
client's time zone.
48+
client's time zone. If an empty string is passed, the date picker will be blank.
4949
min
5050
The minimum allowed date. Either a :class:`~datetime.date` object, or a string in
5151
yyyy-mm-dd format.
@@ -307,6 +307,9 @@ def _date_input_tag(
307307
def _as_date_attr(x: Optional[date | str]) -> Optional[str]:
308308
if x is None:
309309
return None
310-
if isinstance(x, date):
311-
return str(x)
312-
return str(date.fromisoformat(x))
310+
if isinstance(x, str):
311+
if len(x) == 0:
312+
return x
313+
x = date.fromisoformat(x)
314+
# Ensure we return a date (not datetime) string
315+
return x.strftime("%Y-%m-%d")

shiny/ui/_input_update.py

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -463,12 +463,11 @@ def update_date(
463463
label
464464
An input label.
465465
value
466-
The starting date. Either a `date()` object, or a string in yyyy-mm-dd format.
467-
If ``None`` (the default), will use the current date in the client's time zone.
466+
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.
468467
min
469-
The minimum allowed value.
468+
The minimum allowed value. Either a `date()` object, or a string in yyyy-mm-dd format. An empty string will clear the minimum constraint.
470469
max
471-
The maximum allowed value.
470+
The maximum allowed value. Either a `date()` object, or a string in yyyy-mm-dd format. An empty string will clear the maximum constraint.
472471
session
473472
A :class:`~shiny.Session` instance. If not provided, it is inferred via
474473
:func:`~shiny.session.get_current_session`.
@@ -483,13 +482,27 @@ def update_date(
483482
"""
484483

485484
session = require_active_session(session)
485+
486486
msg = {
487487
"label": session._process_ui(label) if label is not None else None,
488488
"value": _as_date_attr(value),
489489
"min": _as_date_attr(min),
490490
"max": _as_date_attr(max),
491491
}
492-
session.send_input_message(id, drop_none(msg))
492+
493+
msg = drop_none(msg)
494+
495+
# Handle the special case of "", which means the value should be cleared
496+
# (i.e., go from a specified date to no specified date).
497+
# This is equivalent to the NA case in R
498+
if value == "":
499+
msg["value"] = None
500+
if min == "":
501+
msg["min"] = None
502+
if max == "":
503+
msg["max"] = None
504+
505+
session.send_input_message(id, msg)
493506

494507

495508
@add_example()
@@ -514,17 +527,15 @@ def update_date_range(
514527
label
515528
An input label.
516529
start
517-
The initial start date. Either a :class:`~datetime.date` object, or a string in
518-
yyyy-mm-dd format. If ``None`` (the default), will use the current date in the
519-
client's time zone.
530+
The starting date. Either a `date()` object, or a string in yyyy-mm-dd format.
531+
If an empty string is provided, the value will be cleared.
520532
end
521-
The initial end date. Either a :class:`~datetime.date` object, or a string in
522-
yyyy-mm-dd format. If ``None`` (the default), will use the current date in the
523-
client's time zone.
533+
The ending date. Either a `date()` object, or a string in yyyy-mm-dd format.
534+
If an empty string is provided, the value will be cleared.
524535
min
525-
The minimum allowed value.
536+
The minimum allowed value. If an empty string is passed there will be no minimum date.
526537
max
527-
The maximum allowed value.
538+
The maximum allowed value. If an empty string is passed there will be no maximum date.
528539
session
529540
A :class:`~shiny.Session` instance. If not provided, it is inferred via
530541
:func:`~shiny.session.get_current_session`.
@@ -539,14 +550,41 @@ def update_date_range(
539550
"""
540551

541552
session = require_active_session(session)
542-
value = {"start": _as_date_attr(start), "end": _as_date_attr(end)}
553+
543554
msg = {
544555
"label": session._process_ui(label) if label is not None else None,
545-
"value": drop_none(value),
546556
"min": _as_date_attr(min),
547557
"max": _as_date_attr(max),
548558
}
549-
session.send_input_message(id, drop_none(msg))
559+
560+
msg = drop_none(msg)
561+
562+
# Handle the special case of "", which means the value should be cleared
563+
# (i.e., go from a specified date to no specified date).
564+
# This is equivalent to the NA case in R
565+
if min == "":
566+
msg["min"] = None
567+
if max == "":
568+
msg["max"] = None
569+
570+
value = {
571+
"start": _as_date_attr(start),
572+
"end": _as_date_attr(end),
573+
}
574+
575+
value = drop_none(value)
576+
577+
# Handle the special case of "", which means the value should be cleared
578+
# (i.e., go from a specified date to no specified date)
579+
# This is equivalent to the NA case in R
580+
if start == "":
581+
value["start"] = None
582+
if end == "":
583+
value["end"] = None
584+
585+
msg["value"] = value
586+
587+
session.send_input_message(id, msg)
550588

551589

552590
# -----------------------------------------------------------------------------
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from datetime import date
2+
3+
from dateutil.relativedelta import relativedelta
4+
5+
from shiny import App, Inputs, Outputs, Session, reactive, render, ui
6+
7+
min_date = date.fromisoformat("2011-11-04")
8+
max_date = min_date + relativedelta(days=10)
9+
10+
app_ui = ui.page_fluid(
11+
ui.input_date(
12+
"start_date_picker",
13+
"Date Type Input:",
14+
value=max_date,
15+
min=min_date,
16+
max=max_date,
17+
format="dd.mm.yyyy",
18+
language="en",
19+
),
20+
ui.output_text("start"),
21+
ui.input_date(
22+
"min_date_picker",
23+
"Date Type Input:",
24+
value=min_date,
25+
min=min_date,
26+
max=max_date,
27+
format="dd.mm.yyyy",
28+
language="en",
29+
),
30+
ui.output_text("min"),
31+
ui.input_date(
32+
"str_date_picker",
33+
"String Type Input:",
34+
value="2023-10-05",
35+
min="2023-10-01",
36+
max="2023-10-07",
37+
format="dd-mm-yyyy",
38+
language="en",
39+
),
40+
ui.output_text("str_format"),
41+
ui.input_date("none_date_picker", "None Type Input:"),
42+
ui.output_text("none_format"),
43+
ui.input_date(
44+
"empty_date_picker",
45+
"empty Type Input:",
46+
value="",
47+
min=None,
48+
max=None,
49+
format="dd-mm-yyyy",
50+
language="en",
51+
),
52+
ui.output_text("empty_format"),
53+
ui.input_action_button("update", "Update Dates"),
54+
)
55+
56+
57+
def server(input: Inputs, output: Outputs, session: Session):
58+
@render.text
59+
def start():
60+
return "Date Picker Value: " + str(input.start_date_picker())
61+
62+
@render.text
63+
def min():
64+
return "Date Picker Value: " + str(input.min_date_picker())
65+
66+
@render.text
67+
def str_format():
68+
return "Date Picker Value: " + str(input.str_date_picker())
69+
70+
@render.text
71+
def none_format():
72+
return "Date Picker Value: " + str(input.none_date_picker())
73+
74+
@render.text
75+
def empty_format():
76+
return "Date Picker Value: " + str(input.empty_date_picker())
77+
78+
@reactive.effect
79+
@reactive.event(input.update, ignore_none=True, ignore_init=True)
80+
def _():
81+
d = date.fromisoformat("2011-11-05")
82+
ui.update_date("start_date_picker", value=d)
83+
ui.update_date("min_date_picker", value="", min="")
84+
ui.update_date("str_date_picker", value="2023-10-03", max="2023-10-20")
85+
86+
87+
app = App(app_ui, server, debug=True)
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import datetime
2+
3+
from playwright.sync_api import Page
4+
5+
from shiny.playwright import controller
6+
from shiny.run import ShinyAppProc
7+
8+
9+
def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None:
10+
page.goto(local_app.url)
11+
12+
# Check the initial state of the date picker where value = max
13+
date1 = controller.InputDate(page, "start_date_picker")
14+
date1.expect_value("14.11.2011")
15+
16+
# Can't find a way to inspect the actual display date in the datepicker,
17+
# as even in the broken example, the correct attribute was still set.
18+
# To work around this, we will check the displayed value to ensure the
19+
# correct value is being passed to the server.
20+
date_output = controller.OutputText(page, "start")
21+
date_output.expect_value("Date Picker Value: 2011-11-14")
22+
23+
# Test the date picker with the min date = initial value (unchanged case)
24+
date2 = controller.InputDate(page, "min_date_picker")
25+
date2.expect_value("04.11.2011")
26+
27+
date_output2 = controller.OutputText(page, "min")
28+
date_output2.expect_value("Date Picker Value: 2011-11-04")
29+
30+
# Test the date picker with the date set as a string instead of as a date object
31+
date3 = controller.InputDate(page, "str_date_picker")
32+
date3.expect_value("05-10-2023")
33+
34+
date_output3 = controller.OutputText(page, "str_format")
35+
date_output3.expect_value("Date Picker Value: 2023-10-05")
36+
37+
# Test the date picker with a None value inputted
38+
# None defaults to today's date
39+
input = datetime.date.today().strftime("%Y-%m-%d")
40+
date4 = controller.InputDate(page, "none_date_picker")
41+
date4.expect_value(input)
42+
43+
date_output4 = controller.OutputText(page, "none_format")
44+
date_output4.expect_value("Date Picker Value: " + input)
45+
46+
# Test the default value when an empty string is passed is correctly blank
47+
date5 = controller.InputDate(page, "empty_date_picker")
48+
date5.expect_value("")
49+
50+
date_output5 = controller.OutputText(page, "empty_format")
51+
date_output5.expect_value("Date Picker Value: None")
52+
53+
controller.InputActionButton(page, "update").click()
54+
55+
date1.expect_value("05.11.2011")
56+
date_output.expect_value("Date Picker Value: 2011-11-05")
57+
58+
date2.expect_value("")
59+
date_output2.expect_value("Date Picker Value: None")
60+
# This is updating min and max dates, but the playwright locater cannot find it.
61+
# It appears that the disabled dates are correct.
62+
# Expect that means the calcuated values for disabled dates are updating correctly
63+
# but the attribute is not updated.
64+
65+
# date2.expect_min_date(None)
66+
67+
date3.expect_value("03-10-2023")
68+
date_output3.expect_value("Date Picker Value: 2023-10-03")

0 commit comments

Comments
 (0)