Skip to content

Commit 72d7754

Browse files
committed
Add bookmarking restore capability to input components
1 parent 064940a commit 72d7754

File tree

9 files changed

+107
-23
lines changed

9 files changed

+107
-23
lines changed

shiny/bookmark/_restore_state.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from contextlib import contextmanager
55
from contextvars import ContextVar, Token
66
from pathlib import Path
7-
from typing import TYPE_CHECKING, Any, Literal, Optional
7+
from typing import TYPE_CHECKING, Any, Literal, Optional, TypeVar, overload
88
from urllib.parse import parse_qs, parse_qsl
99

1010
from .._docstring import add_example
@@ -387,8 +387,13 @@ def get_current_restore_context() -> RestoreContext | None:
387387
return ctx
388388

389389

390+
T = TypeVar("T")
391+
@overload
392+
def restore_input(resolved_id: ResolvedId, default: Any) -> Any: ...
393+
@overload
394+
def restore_input(resolved_id: None, default: T) -> T: ...
390395
@add_example()
391-
def restore_input(resolved_id: ResolvedId, default: Any) -> Any:
396+
def restore_input(resolved_id: ResolvedId | None, default: Any) -> Any:
392397
"""
393398
Restore an input value
394399
@@ -402,6 +407,9 @@ def restore_input(resolved_id: ResolvedId, default: Any) -> Any:
402407
default
403408
A default value to use, if there's no value to restore.
404409
"""
410+
if resolved_id is None:
411+
return default
412+
405413
if not isinstance(resolved_id, ResolvedId):
406414
raise TypeError(
407415
"Expected `resolved_id` to be of type `ResolvedId` which is returned from `shiny.module.resolve_id(id)`."

shiny/playwright/controller/_input_buttons.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,47 @@ def expect_disabled(self, value: bool, *, timeout: Timeout = None):
6565
self.loc, "disabled", re.compile(".*") if value else None, timeout=timeout
6666
)
6767

68+
class InputBookmarkButton(
69+
WidthLocStlyeM,
70+
InputActionBase,
71+
):
72+
"""Controller for :func:`shiny.ui.input_bookmark_button`."""
73+
74+
def __init__(
75+
self,
76+
page: Page,
77+
id: str,
78+
) -> None:
79+
"""
80+
Initializes the input bookmark button.
81+
82+
Parameters
83+
----------
84+
page
85+
The page where the input bookmark button is located.
86+
id
87+
The id of the input bookmark button.
88+
"""
89+
super().__init__(
90+
page,
91+
id=id,
92+
loc=f"button#{id}.action-button.shiny-bound-input",
93+
)
94+
95+
def expect_disabled(self, value: bool, *, timeout: Timeout = None):
96+
"""
97+
Expect the input bookmark button to be disabled.
98+
99+
Parameters
100+
----------
101+
value
102+
The expected value of the `disabled` attribute.
103+
timeout
104+
The maximum time to wait for the expectation to be fulfilled. Defaults to `None`.
105+
"""
106+
_expect_attribute_to_have_value(
107+
self.loc, "disabled", re.compile(".*") if value else None, timeout=timeout
108+
)
68109

69110
class InputDarkMode(UiBase):
70111
"""Controller for :func:`shiny.ui.input_dark_mode`."""

shiny/ui/_input_check_radio.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ def input_checkbox_group(
221221
options = _generate_options(
222222
id=resolved_id,
223223
type="checkbox",
224-
choices=choices,
224+
choices=restore_input(resolved_id, choices),
225225
selected=selected,
226226
inline=inline,
227227
)
@@ -292,7 +292,7 @@ def input_radio_buttons(
292292
options = _generate_options(
293293
id=resolved_id,
294294
type="radio",
295-
choices=choices,
295+
choices=restore_input(resolved_id, choices),
296296
selected=restore_input(resolved_id, selected),
297297
inline=inline,
298298
)

shiny/ui/_input_dark_mode.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
from __future__ import annotations
22

3+
from shiny._namespaces import resolve_id_or_none
4+
from shiny.bookmark._restore_state import restore_input
5+
36
__all__ = ("input_dark_mode", "update_dark_mode")
47

58
from typing import Literal, Optional
69

710
from htmltools import Tag, TagAttrValue, css
811

912
from .._docstring import add_example, no_example
10-
from ..module import resolve_id
13+
14+
# from ..module import resolve_id
1115
from ..session import Session, require_active_session
1216
from ._web_component import web_component
1317

@@ -46,13 +50,11 @@ def input_dark_mode(
4650
----------
4751
* <https://getbootstrap.com/docs/5.3/customize/color-modes>
4852
"""
53+
resolved_id = resolve_id_or_none(id)
4954

5055
if mode is not None:
5156
mode = validate_dark_mode_option(mode)
5257

53-
if id is not None:
54-
id = resolve_id(id)
55-
5658
return web_component(
5759
"bslib-input-dark-mode",
5860
{
@@ -65,9 +67,9 @@ def input_dark_mode(
6567
},
6668
)
6769
},
68-
id=id,
70+
id=resolved_id,
6971
attribute="data-bs-theme",
70-
mode=mode,
72+
mode=restore_input(resolved_id, mode),
7173
**kwargs,
7274
)
7375

shiny/ui/_input_date.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from ..module import resolve_id
1313
from ._html_deps_external import datepicker_deps
1414
from ._utils import shiny_input_label
15+
from shiny.bookmark import restore_input
16+
from shiny.module import resolve_id
1517

1618

1719
@add_example()
@@ -111,11 +113,13 @@ def input_date(
111113
"""
112114

113115
resolved_id = resolve_id(id)
116+
default_value = value if value is not None else date.today()
117+
114118
return div(
115119
shiny_input_label(resolved_id, label),
116120
_date_input_tag(
117121
id=resolved_id,
118-
value=value,
122+
value=restore_input(resolved_id, default_value),
119123
min=min,
120124
max=max,
121125
format=format,
@@ -230,12 +234,15 @@ def input_date_range(
230234
"""
231235

232236
resolved_id = resolve_id(id)
237+
default_start = start if start is not None else date.today()
238+
default_end = end if end is not None else date.today()
239+
restored_date_range = restore_input(resolved_id, [default_start, default_end])
233240
return div(
234241
shiny_input_label(resolved_id, label),
235242
div(
236243
_date_input_tag(
237244
id=resolved_id,
238-
value=start,
245+
value=restored_date_range[0],
239246
min=min,
240247
max=max,
241248
format=format,
@@ -251,7 +258,7 @@ def input_date_range(
251258
),
252259
_date_input_tag(
253260
id=resolved_id,
254-
value=end,
261+
value=restored_date_range[1],
255262
min=min,
256263
max=max,
257264
format=format,

shiny/ui/_input_numeric.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from htmltools import Tag, TagChild, css, div, tags
66

7+
from shiny.bookmark._restore_state import restore_input
8+
79
from .._docstring import add_example
810
from ..module import resolve_id
911
from ._utils import shiny_input_label
@@ -69,7 +71,7 @@ def input_numeric(
6971
id=resolved_id,
7072
type="number",
7173
class_="shiny-input-number form-control",
72-
value=value,
74+
value=restore_input(resolved_id, value),
7375
min=min,
7476
max=max,
7577
step=step,

shiny/ui/_input_select.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"input_select",
99
"input_selectize",
1010
)
11-
11+
from shiny.bookmark._restore_state import restore_input
1212
import copy
1313
from json import dumps
1414
from typing import Any, Mapping, Optional, Union, cast
@@ -111,11 +111,12 @@ def input_selectize(
111111
* :func:`~shiny.ui.input_radio_buttons`
112112
* :func:`~shiny.ui.input_checkbox_group`
113113
"""
114+
resolved_id = resolve_id(id)
114115

115116
x = input_select(
116-
id,
117-
label,
118-
choices,
117+
id=resolved_id,
118+
label=label,
119+
choices=restore_input(resolved_id, choices),
119120
selected=selected,
120121
multiple=multiple,
121122
selectize=True,
@@ -196,7 +197,9 @@ def input_select(
196197

197198
remove_button = _resolve_remove_button(remove_button, multiple)
198199

199-
choices_ = _normalize_choices(choices)
200+
resolved_id = resolve_id(id)
201+
202+
choices_ = restore_input(resolved_id, choices)
200203
if selected is None and not multiple:
201204
selected = _find_first_option(choices_)
202205

@@ -207,8 +210,6 @@ def input_select(
207210

208211
choices_tags = _render_choices(choices_, selected)
209212

210-
resolved_id = resolve_id(id)
211-
212213
return div(
213214
shiny_input_label(resolved_id, label),
214215
div(

shiny/ui/_input_text.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from htmltools import Tag, TagChild, css, div, tags
66

7+
from shiny.bookmark._restore_state import restore_input
8+
79
from .._docstring import add_example
810
from ..module import resolve_id
911
from ._html_deps_py_shiny import autoresize_dependency
@@ -76,7 +78,7 @@ def input_text(
7678
id=resolved_id,
7779
type="text",
7880
class_="shiny-input-text form-control",
79-
value=value,
81+
value=restore_input(resolved_id, value),
8082
placeholder=placeholder,
8183
autocomplete=autocomplete,
8284
spellcheck=spellcheck,
@@ -180,7 +182,7 @@ def input_text_area(
180182

181183
resolved_id = resolve_id(id)
182184
area = tags.textarea(
183-
value,
185+
value=restore_input(resolved_id, value),
184186
id=resolved_id,
185187
class_=" ".join(classes),
186188
style=css(width=None if width else "100%", height=height, resize=resize),
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from playwright.sync_api import Page
2+
3+
from shiny.playwright import controller
4+
from shiny.pytest import create_app_fixture
5+
from shiny.run import ShinyAppProc
6+
7+
app = create_app_fixture(["app-express.py"])
8+
9+
10+
def test_bookmark_date_inputs(page: Page, app: ShinyAppProc) -> None:
11+
page.goto(app.url)
12+
13+
# Test basic date input
14+
date1 = controller.InputDate(page, "date1")
15+
date1.expect_label("Default date input:")
16+
date1.expect_value("2024-01-01")
17+
18+
text1 = controller.Text(page, "selected_date1")
19+
text1.expect_text("Selected date: 2024-01-01")
20+
21+
bookmark_button = controller.InputActionButton(page, "bookmark_button")

0 commit comments

Comments
 (0)