diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c9f43adb..a88fddcab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Bug fixes +* Using `update_slider` to update a slider's value to a `datetime` object or other non-numeric value would result in an error. (#649) + ### Other changes * Documentation updates. (#591) diff --git a/e2e/bugs/0648-update-slider-datetime-value/app.py b/e2e/bugs/0648-update-slider-datetime-value/app.py new file mode 100644 index 000000000..64c599323 --- /dev/null +++ b/e2e/bugs/0648-update-slider-datetime-value/app.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +import datetime +from typing import Any, Optional, Union + +import shiny.experimental as x +from shiny import App, Inputs, Outputs, Session, reactive, render, ui + +start_time = datetime.datetime(2023, 7, 1, 0, 0, 0, 0, tzinfo=datetime.timezone.utc) +end_time = start_time + datetime.timedelta(hours=1) + +from shiny import App, Inputs, Outputs, Session, module, reactive, render, ui + + +@module.ui +def slider_with_reset_ui( + label: str, + value: Union[ + datetime.datetime, + tuple[datetime.datetime, datetime.datetime], + list[datetime.datetime], + ], +) -> ui.TagChild: + return x.ui.card( + ui.input_slider( + "times", + "Times", + start_time, + end_time, + value, + timezone="UTC", + time_format="%H:%M:%S", + ), + ui.output_text_verbatim("txt"), + ui.div(ui.input_action_button("reset", label)), + ) + + +@module.server +def slider_with_reset_server( + input: Inputs, + output: Outputs, + session: Session, + *, + min: Optional[datetime.datetime] = None, + max: Optional[datetime.datetime] = None, + value: Any = None, +): + @output + @render.text + def txt(): + if isinstance(input.times(), (tuple, list)): + return " - ".join([str(x) for x in input.times()]) + else: + return input.times() + + @reactive.Effect + @reactive.event(input.reset) + def reset_time(): + ui.update_slider("times", min=min, max=max, value=value) + + +app_ui = ui.page_fluid( + x.ui.layout_column_wrap( + "400px", + slider_with_reset_ui("one", "Jump to end", start_time), + slider_with_reset_ui("two", "Select all", (start_time, start_time)), + slider_with_reset_ui("three", "Select all", [start_time, start_time]), + slider_with_reset_ui("four", "Extend min", [start_time, end_time]), + slider_with_reset_ui("five", "Extend max", [start_time, end_time]), + slider_with_reset_ui("six", "Extend min and max", [start_time, end_time]), + ), + class_="p-3", +) + + +def server(input: Inputs, output: Outputs, session: Session): + slider_with_reset_server("one", value=end_time) + slider_with_reset_server("two", value=(start_time, end_time)) + slider_with_reset_server("three", value=[start_time, end_time]) + slider_with_reset_server("four", min=start_time - datetime.timedelta(hours=1)) + slider_with_reset_server("five", max=end_time + datetime.timedelta(hours=1)) + slider_with_reset_server( + "six", + min=start_time - datetime.timedelta(hours=1), + max=end_time + datetime.timedelta(hours=1), + ) + + +app = App(app_ui, server) diff --git a/e2e/bugs/0648-update-slider-datetime-value/test_update_slider_datetime_value.py b/e2e/bugs/0648-update-slider-datetime-value/test_update_slider_datetime_value.py new file mode 100644 index 000000000..b569d0de7 --- /dev/null +++ b/e2e/bugs/0648-update-slider-datetime-value/test_update_slider_datetime_value.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +from typing import Optional + +from conftest import ShinyAppProc +from controls import InputActionButton, InputSlider, OutputTextVerbatim +from playwright.sync_api import Page, expect + + +def test_slider_app(page: Page, local_app: ShinyAppProc) -> None: + def check_case( + id: str, + *, + value: tuple[Optional[str], Optional[str]] = (None, None), + min: tuple[Optional[str], Optional[str]] = (None, None), + max: tuple[Optional[str], Optional[str]] = (None, None), + ): + slider_times = InputSlider(page, f"{id}-times") + btn_reset = InputActionButton(page, f"{id}-reset") + out_txt = OutputTextVerbatim(page, f"{id}-txt") + + if value[0] is not None: + out_txt.expect_value(value[0]) + if min[0] is not None: + expect(slider_times.loc_irs.locator(".irs-min")).to_have_text(min[0]) + if max[0] is not None: + expect(slider_times.loc_irs.locator(".irs-max")).to_have_text(max[0]) + + btn_reset.loc.click() + + if value[1] is not None: + out_txt.expect_value(value[1]) + if min[1] is not None: + expect(slider_times.loc_irs.locator(".irs-min")).to_have_text(min[1]) + if max[1] is not None: + expect(slider_times.loc_irs.locator(".irs-max")).to_have_text(max[1]) + + page.goto(local_app.url) + + start_time = "2023-07-01 00:00:00" + end_time = "2023-07-01 01:00:00" + + check_case("one", value=(start_time, end_time)) + check_case( + "two", + value=(f"{start_time} - {start_time}", f"{start_time} - {end_time}"), + ) + check_case( + "three", + value=(f"{start_time} - {start_time}", f"{start_time} - {end_time}"), + ) + check_case("four", min=("00:00:00", "23:00:00")) + check_case("five", max=("01:00:00", "02:00:00")) + check_case("six", min=("00:00:00", "23:00:00"), max=("01:00:00", "02:00:00")) diff --git a/shiny/ui/_input_update.py b/shiny/ui/_input_update.py index ca455a776..35a34ec52 100644 --- a/shiny/ui/_input_update.py +++ b/shiny/ui/_input_update.py @@ -775,8 +775,8 @@ def update_slider( session = require_active_session(session) # Get any non-None value to see if the `data-type` may need to change - val = value[0] if isinstance(value, tuple) else value - present_val = next((x for x in [val, min, max]), None) + val = value[0] if isinstance(value, (tuple, list)) else value + present_val = next((x for x in [val, min, max] if x is not None), None) data_type = None if present_val is None else _slider_type(present_val) if time_format is None and data_type and data_type[0:4] == "date": @@ -785,10 +785,16 @@ def update_slider( min_num = None if min is None else _as_numeric(min) max_num = None if max is None else _as_numeric(max) step_num = None if step is None else _as_numeric(step) + if isinstance(value, (tuple, list)): + value_num = [_as_numeric(x) for x in value] + elif value is not None: + value_num = _as_numeric(value) + else: + value_num = None msg = { "label": label, - "value": value, + "value": value_num, "min": min_num, "max": max_num, "step": step_num,