Skip to content

Commit 9d56dd8

Browse files
authored
test(bookmarking): Add bookmarking restore capability to input components (#1920)
1 parent f7b7fb4 commit 9d56dd8

File tree

117 files changed

+1425
-40
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

117 files changed

+1425
-40
lines changed

shiny/bookmark/_restore_state.py

Lines changed: 12 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,15 @@ def get_current_restore_context() -> RestoreContext | None:
387387
return ctx
388388

389389

390+
T = TypeVar("T")
391+
392+
393+
@overload
394+
def restore_input(resolved_id: ResolvedId, default: Any) -> Any: ...
395+
@overload
396+
def restore_input(resolved_id: None, default: T) -> T: ...
390397
@add_example()
391-
def restore_input(resolved_id: ResolvedId, default: Any) -> Any:
398+
def restore_input(resolved_id: ResolvedId | None, default: Any) -> Any:
392399
"""
393400
Restore an input value
394401
@@ -402,6 +409,9 @@ def restore_input(resolved_id: ResolvedId, default: Any) -> Any:
402409
default
403410
A default value to use, if there's no value to restore.
404411
"""
412+
if resolved_id is None:
413+
return default
414+
405415
if not isinstance(resolved_id, ResolvedId):
406416
raise TypeError(
407417
"Expected `resolved_id` to be of type `ResolvedId` which is returned from `shiny.module.resolve_id(id)`."

shiny/playwright/controller/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from ._input_buttons import (
22
InputActionButton,
33
InputActionLink,
4+
InputBookmarkButton,
45
InputDarkMode,
56
InputFile,
67
InputTaskButton,
@@ -77,6 +78,7 @@
7778
__all__ = [
7879
"InputActionButton",
7980
"InputActionLink",
81+
"InputBookmarkButton",
8082
"InputCheckbox",
8183
"InputCheckboxGroup",
8284
"InputDarkMode",

shiny/playwright/controller/_input_buttons.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def __init__(
4747
super().__init__(
4848
page,
4949
id=id,
50-
loc=f"button#{id}.action-button.shiny-bound-input",
50+
loc=f'button[id="{id}"].action-button.shiny-bound-input',
5151
)
5252

5353
def expect_disabled(self, value: bool, *, timeout: Timeout = None):
@@ -66,6 +66,45 @@ def expect_disabled(self, value: bool, *, timeout: Timeout = None):
6666
)
6767

6868

69+
class InputBookmarkButton(
70+
InputActionButton,
71+
):
72+
"""Controller for :func:`shiny.ui.input_bookmark_button`."""
73+
74+
def __init__(
75+
self,
76+
page: Page,
77+
id: str = "._bookmark_",
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. Defaults to "._bookmark_".
88+
"""
89+
super().__init__(
90+
page,
91+
id=id,
92+
)
93+
94+
def expect_disabled(self, value: bool, *, timeout: Timeout = None):
95+
"""
96+
Expect the input bookmark button to be disabled.
97+
98+
Parameters
99+
----------
100+
value
101+
The expected value of the `disabled` attribute.
102+
timeout
103+
The maximum time to wait for the expectation to be fulfilled. Defaults to `None`.
104+
"""
105+
super().expect_disabled(value, timeout=timeout)
106+
107+
69108
class InputDarkMode(UiBase):
70109
"""Controller for :func:`shiny.ui.input_dark_mode`."""
71110

shiny/playwright/controller/_input_fields.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,20 @@ def expect_daysofweekdisabled(
647647
timeout=timeout,
648648
)
649649

650+
def set(self, value: str, *, timeout: Timeout = None) -> None:
651+
"""
652+
Sets the text value
653+
654+
Parameters
655+
----------
656+
value
657+
The text to set.
658+
timeout
659+
The maximum time to wait for the text to be set. Defaults to `None`.
660+
"""
661+
set_text(self.loc, value, timeout=timeout)
662+
self.loc.press("Enter", timeout=timeout)
663+
650664

651665
class InputDate(_DateBase):
652666
def __init__(self, page: Page, id: str) -> None:

shiny/ui/_input_check_radio.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,13 @@ def input_checkbox(
6666
* :func:`~shiny.ui.input_checkbox_group`
6767
* :func:`~shiny.ui.input_radio_buttons`
6868
"""
69-
69+
resolved_id = resolve_id(id)
70+
value = restore_input(resolved_id, value)
7071
return div(
7172
div(
7273
tags.label(
7374
tags.input(
74-
id=resolve_id(id),
75+
id=resolved_id,
7576
type="checkbox",
7677
checked="checked" if value else None,
7778
class_="shiny-input-checkbox",
@@ -142,11 +143,13 @@ def _bslib_input_checkbox(
142143
*,
143144
width: Optional[str] = None,
144145
) -> Tag:
146+
resolved_id = resolve_id(id)
147+
value = restore_input(resolved_id, value)
145148
return div(
146149
div(
147150
{"class": "form-check"},
148151
tags.input(
149-
id=resolve_id(id),
152+
id=resolved_id,
150153
class_="form-check-input",
151154
type="checkbox",
152155
role="switch",
@@ -157,7 +160,7 @@ def _bslib_input_checkbox(
157160
# Must be wrapped in `span` for update_switch(label=) method to work
158161
tags.span(label),
159162
class_="form-check-label",
160-
for_=resolve_id(id),
163+
for_=resolved_id,
161164
),
162165
class_=class_,
163166
),
@@ -218,11 +221,12 @@ def input_checkbox_group(
218221

219222
resolved_id = resolve_id(id)
220223
input_label = shiny_input_label(resolved_id, label)
224+
221225
options = _generate_options(
222226
id=resolved_id,
223227
type="checkbox",
224228
choices=choices,
225-
selected=selected,
229+
selected=restore_input(resolved_id, selected),
226230
inline=inline,
227231
)
228232
return div(

shiny/ui/_input_dark_mode.py

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

3-
__all__ = ("input_dark_mode", "update_dark_mode")
4-
53
from typing import Literal, Optional
64

75
from htmltools import Tag, TagAttrValue, css
86

97
from .._docstring import add_example, no_example
10-
from ..module import resolve_id
8+
from .._namespaces import resolve_id_or_none
9+
from ..bookmark import restore_input
1110
from ..session import Session, require_active_session
1211
from ._web_component import web_component
1312

13+
__all__ = ("input_dark_mode", "update_dark_mode")
14+
1415
BootstrapColorMode = Literal["light", "dark"]
1516

1617

@@ -46,13 +47,11 @@ def input_dark_mode(
4647
----------
4748
* <https://getbootstrap.com/docs/5.3/customize/color-modes>
4849
"""
50+
resolved_id = resolve_id_or_none(id)
4951

5052
if mode is not None:
5153
mode = validate_dark_mode_option(mode)
5254

53-
if id is not None:
54-
id = resolve_id(id)
55-
5655
return web_component(
5756
"bslib-input-dark-mode",
5857
{
@@ -65,9 +64,9 @@ def input_dark_mode(
6564
},
6665
)
6766
},
68-
id=id,
67+
id=resolved_id,
6968
attribute="data-bs-theme",
70-
mode=mode,
69+
mode=restore_input(resolved_id, mode),
7170
**kwargs,
7271
)
7372

shiny/ui/_input_date.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
from __future__ import annotations
22

3-
__all__ = ("input_date", "input_date_range")
4-
53
import json
64
from datetime import date
75
from typing import Optional
86

97
from htmltools import Tag, TagAttrValue, TagChild, css, div, span, tags
108

119
from .._docstring import add_example
10+
from ..bookmark import restore_input
1211
from ..module import resolve_id
1312
from ._html_deps_external import datepicker_deps
1413
from ._utils import shiny_input_label
1514

15+
__all__ = ("input_date", "input_date_range")
16+
1617

1718
@add_example()
1819
def input_date(
@@ -111,11 +112,13 @@ def input_date(
111112
"""
112113

113114
resolved_id = resolve_id(id)
115+
default_value = value if value is not None else date.today()
116+
114117
return div(
115118
shiny_input_label(resolved_id, label),
116119
_date_input_tag(
117120
id=resolved_id,
118-
value=value,
121+
value=restore_input(resolved_id, default_value),
119122
min=min,
120123
max=max,
121124
format=format,
@@ -230,12 +233,15 @@ def input_date_range(
230233
"""
231234

232235
resolved_id = resolve_id(id)
236+
default_start = start if start is not None else date.today()
237+
default_end = end if end is not None else date.today()
238+
restored_date_range = restore_input(resolved_id, [default_start, default_end])
233239
return div(
234240
shiny_input_label(resolved_id, label),
235241
div(
236242
_date_input_tag(
237243
id=resolved_id,
238-
value=start,
244+
value=restored_date_range[0],
239245
min=min,
240246
max=max,
241247
format=format,
@@ -251,7 +257,7 @@ def input_date_range(
251257
),
252258
_date_input_tag(
253259
id=resolved_id,
254-
value=end,
260+
value=restored_date_range[1],
255261
min=min,
256262
max=max,
257263
format=format,

shiny/ui/_input_numeric.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from htmltools import Tag, TagChild, css, div, tags
66

77
from .._docstring import add_example
8+
from ..bookmark import restore_input
89
from ..module import resolve_id
910
from ._utils import shiny_input_label
1011

@@ -69,7 +70,7 @@ def input_numeric(
6970
id=resolved_id,
7071
type="number",
7172
class_="shiny-input-number form-control",
72-
value=value,
73+
value=restore_input(resolved_id, value),
7374
min=min,
7475
max=max,
7576
step=step,

shiny/ui/_input_select.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
"input_select",
99
"input_selectize",
1010
)
11-
1211
import copy
1312
from json import dumps
1413
from typing import Any, Mapping, Optional, Union, cast
1514

1615
from htmltools import Tag, TagChild, TagList, css, div, tags
1716

1817
from .._docstring import add_example
18+
from ..bookmark import restore_input
1919
from ..module import resolve_id
2020
from ._html_deps_external import selectize_deps
2121
from ._utils import JSEval, extract_js_keys, shiny_input_label
@@ -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,11 @@ def input_select(
196197

197198
remove_button = _resolve_remove_button(remove_button, multiple)
198199

200+
resolved_id = resolve_id(id)
201+
199202
choices_ = _normalize_choices(choices)
203+
204+
selected = restore_input(resolved_id, selected)
200205
if selected is None and not multiple:
201206
selected = _find_first_option(choices_)
202207

@@ -207,8 +212,6 @@ def input_select(
207212

208213
choices_tags = _render_choices(choices_, selected)
209214

210-
resolved_id = resolve_id(id)
211-
212215
return div(
213216
shiny_input_label(resolved_id, label),
214217
div(

shiny/ui/_input_slider.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
from ..bookmark import restore_input
4+
35
__all__ = (
46
"input_slider",
57
"SliderValueArg",
@@ -140,9 +142,10 @@ def input_slider(
140142
* :func:`~shiny.ui.update_slider`
141143
"""
142144

145+
resolved_id = resolve_id(id)
146+
value = restore_input(resolved_id, value)
143147
# Thanks to generic typing, max, value, etc. should be of the same type
144148
data_type = _slider_type(min)
145-
146149
# Make sure min, max, value, and step are all numeric
147150
# (converts dates/datetimes to milliseconds since epoch...this is the value JS wants)
148151
min_num = _as_numeric(min)
@@ -201,7 +204,6 @@ def input_slider(
201204
# ionRangeSlider wants attr = 'true'/'false'
202205
props = {k: str(v).lower() if isinstance(v, bool) else v for k, v in props.items()}
203206

204-
resolved_id = resolve_id(id)
205207
slider_tag = div(
206208
shiny_input_label(resolved_id, label),
207209
tags.input(**props),

0 commit comments

Comments
 (0)