Skip to content

Commit 1e4df73

Browse files
test(bookmarking): Add bookmarking capabilities for sidebar and accordion (#1938)
Co-authored-by: Barret Schloerke <barret@posit.co>
1 parent 29a1c66 commit 1e4df73

File tree

13 files changed

+466
-36
lines changed

13 files changed

+466
-36
lines changed

shiny/bookmark/_restore_state.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -391,11 +391,11 @@ def get_current_restore_context() -> RestoreContext | None:
391391

392392

393393
@overload
394-
def restore_input(resolved_id: ResolvedId, default: Any) -> Any: ...
394+
def restore_input(resolved_id: ResolvedId, default: Optional[Any] = None) -> Any: ...
395395
@overload
396396
def restore_input(resolved_id: None, default: T) -> T: ...
397397
@add_example()
398-
def restore_input(resolved_id: ResolvedId | None, default: Any) -> Any:
398+
def restore_input(resolved_id: ResolvedId | None, default: Optional[Any] = None) -> Any:
399399
"""
400400
Restore an input value
401401

shiny/playwright/controller/_navs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ def _expect_content_text(
187187

188188

189189
class _NavsetBase(UiWithContainer):
190-
"""A Base mixin class for Nav controls"""
190+
"""A Base class for Nav controls"""
191191

192192
def nav_panel(
193193
self,

shiny/ui/_accordion.py

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from .._docstring import add_example
88
from .._namespaces import resolve_id_or_none
99
from .._utils import drop_none, private_random_id
10+
from ..bookmark import restore_input
1011
from ..session import require_active_session
1112
from ..types import MISSING, MISSING_TYPE
1213
from ._html_deps_shinyverse import components_dependencies
@@ -243,6 +244,21 @@ def accordion(
243244
"All `accordion(*args)` must be of type `AccordionPanel` which can be created using `accordion_panel()`"
244245
)
245246

247+
# Since multiple=False requires an id, we always include one,
248+
# but only create a binding when it is provided
249+
binding_class_value: TagAttrs | None = None
250+
if id is None:
251+
id = private_random_id("bslib_accordion")
252+
binding_class_value = None
253+
else:
254+
binding_class_value = {"class": "bslib-accordion-input"}
255+
256+
accordion_id = resolve_id_or_none(id)
257+
has_restored_input = not isinstance(
258+
restore_input(accordion_id, MISSING), MISSING_TYPE
259+
)
260+
open = restore_input(accordion_id, open)
261+
246262
is_open: list[bool] = []
247263
if open is None:
248264
is_open = [False for _ in panels]
@@ -254,27 +270,18 @@ def accordion(
254270
#
255271
is_open = [panel._data_value in open for panel in panels]
256272

257-
# Open the first panel by default
258-
if open is not False and len(is_open) > 0 and not any(is_open):
259-
is_open[0] = True
273+
if not has_restored_input:
274+
# Open the first panel by default
275+
if open is not False and len(is_open) > 0 and not any(is_open):
276+
is_open[0] = True
260277

261278
if (not multiple) and sum(is_open) > 1:
262279
raise ValueError("Can't select more than one panel when `multiple = False`")
263280

264-
# Since multiple=False requires an id, we always include one,
265-
# but only create a binding when it is provided
266-
binding_class_value: TagAttrs | None = None
267-
if id is None:
268-
id = private_random_id("bslib_accordion")
269-
binding_class_value = None
270-
else:
271-
binding_class_value = {"class": "bslib-accordion-input"}
272-
273-
accordion_id = resolve_id_or_none(id)
274-
for panel, open in zip(panels, is_open):
281+
for panel, panel_is_open in zip(panels, is_open):
275282
panel._accordion_id = accordion_id
276283
panel._is_multiple = multiple
277-
panel._is_open = open
284+
panel._is_open = panel_is_open
278285

279286
panel_tags = [panel.resolve() for panel in panels]
280287

shiny/ui/_navs.py

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from .._docstring import add_example
2323
from .._namespaces import resolve_id_or_none
2424
from .._utils import private_random_int
25+
from ..bookmark import restore_input
2526
from ..types import DEPRECATED, MISSING, MISSING_TYPE, NavSetArg
2627
from ._bootstrap import column, row
2728
from ._card import CardItem, WrapperCallable, card, card_body, card_footer, card_header
@@ -393,9 +394,12 @@ def __init__(
393394
header: TagChild = None,
394395
footer: TagChild = None,
395396
) -> None:
397+
id_resolved = resolve_id_or_none(id)
398+
selected = restore_input(id_resolved, selected)
399+
396400
self.args = args
397401
self.ul_class = ul_class
398-
self.id = id
402+
self.id = id_resolved
399403
self.selected = selected
400404
self.header = header
401405
self.footer = footer
@@ -464,11 +468,10 @@ def navset_tab(
464468
-------
465469
See :func:`~shiny.ui.nav_panel`
466470
"""
467-
468471
return NavSet(
469472
*args,
470473
ul_class="nav nav-tabs",
471-
id=resolve_id_or_none(id),
474+
id=id,
472475
selected=selected,
473476
header=header,
474477
footer=footer,
@@ -520,11 +523,10 @@ def navset_pill(
520523
-------
521524
See :func:`~shiny.ui.nav_panel`
522525
"""
523-
524526
return NavSet(
525527
*args,
526528
ul_class="nav nav-pills",
527-
id=resolve_id_or_none(id),
529+
id=id,
528530
selected=selected,
529531
header=header,
530532
footer=footer,
@@ -579,7 +581,7 @@ def navset_underline(
579581
return NavSet(
580582
*args,
581583
ul_class="nav nav-underline",
582-
id=resolve_id_or_none(id),
584+
id=id,
583585
selected=selected,
584586
header=header,
585587
footer=footer,
@@ -627,11 +629,10 @@ def navset_hidden(
627629
* :func:`~shiny.ui.navset_card_underline`
628630
* :func:`~shiny.ui.navset_pill_list`
629631
"""
630-
631632
return NavSet(
632633
*args,
633634
ul_class="nav nav-hidden",
634-
id=resolve_id_or_none(id),
635+
id=id,
635636
selected=selected,
636637
header=header,
637638
footer=footer,
@@ -747,11 +748,10 @@ def navset_card_tab(
747748
-------
748749
See :func:`~shiny.ui.nav_panel`
749750
"""
750-
751751
return NavSetCard(
752752
*args,
753753
ul_class="nav nav-tabs card-header-tabs",
754-
id=resolve_id_or_none(id),
754+
id=id,
755755
selected=selected,
756756
title=title,
757757
sidebar=sidebar,
@@ -813,11 +813,10 @@ def navset_card_pill(
813813
-------
814814
See :func:`~shiny.ui.nav_panel`
815815
"""
816-
817816
return NavSetCard(
818817
*args,
819818
ul_class="nav nav-pills card-header-pills",
820-
id=resolve_id_or_none(id),
819+
id=id,
821820
selected=selected,
822821
title=title,
823822
sidebar=sidebar,
@@ -882,7 +881,7 @@ def navset_card_underline(
882881
return NavSetCard(
883882
*args,
884883
ul_class="nav nav-underline",
885-
id=resolve_id_or_none(id),
884+
id=id,
886885
selected=selected,
887886
title=title,
888887
sidebar=sidebar,
@@ -977,11 +976,10 @@ def navset_pill_list(
977976
-------
978977
See :func:`~shiny.ui.nav_panel`
979978
"""
980-
981979
return NavSetPillList(
982980
*args,
983981
ul_class="nav nav-pills nav-stacked",
984-
id=resolve_id_or_none(id),
982+
id=id,
985983
selected=selected,
986984
header=header,
987985
footer=footer,
@@ -1571,11 +1569,10 @@ def navset_bar(
15711569
ul_class = "nav navbar-nav"
15721570
if navbar_opts.underline:
15731571
ul_class += " nav-underline"
1574-
15751572
return NavSetBar(
15761573
*new_args,
15771574
ul_class=ul_class,
1578-
id=resolve_id_or_none(id),
1575+
id=id,
15791576
selected=selected,
15801577
sidebar=sidebar,
15811578
fillable=fillable,

shiny/ui/_sidebar.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from .._namespaces import resolve_id_or_none
2121
from .._typing_extensions import TypedDict
2222
from .._utils import private_random_id
23+
from ..bookmark import restore_input
2324
from ..module import ResolvedId
2425
from ..session import require_active_session
2526
from ..types import MISSING, MISSING_TYPE
@@ -545,13 +546,20 @@ def sidebar(
545546

546547
attrs, children = consolidate_attrs(*args, **kwargs)
547548

549+
resolved_id = resolve_id_or_none(id)
550+
551+
if resolved_id:
552+
restored_open: bool | None = restore_input(resolved_id)
553+
if restored_open is not None:
554+
open = "open" if restored_open else "closed"
555+
548556
return Sidebar(
549557
children=children,
550558
attrs=attrs,
551559
width=width,
552560
position=position,
553561
open=open,
554-
id=id,
562+
id=resolved_id,
555563
title=title,
556564
fg=fg,
557565
bg=bg,
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from shiny.express import app_opts, expressify, input, module, render, session, ui
2+
3+
app_opts(bookmark_store="url")
4+
5+
6+
@expressify
7+
def my_accordion(**kwargs):
8+
with ui.accordion(**kwargs):
9+
for letter in "ABCDE":
10+
with ui.accordion_panel(f"Section {letter}"):
11+
f"Some narrative for section {letter}"
12+
13+
14+
ui.h2("Accordion with bookmarking")
15+
16+
with ui.card():
17+
ui.h3("Accordion non-module bookmarking")
18+
my_accordion(multiple=False, id="acc_single")
19+
20+
@render.text
21+
def accordion_global():
22+
return f"input.accordion(): {input.acc_single()}"
23+
24+
# Module section in sidebar
25+
@module
26+
def accordion_module(input, output, session):
27+
my_accordion(multiple=False, id="acc_mod")
28+
29+
@render.text
30+
def accordion_module():
31+
return f"input.acc_mod(): {input.acc_mod()}"
32+
33+
ui.h3("Accordion module bookmarking")
34+
accordion_module("first")
35+
36+
ui.input_bookmark_button()
37+
38+
39+
@session.bookmark.on_bookmarked
40+
async def _(url: str):
41+
await session.bookmark.update_query_string(url)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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_accordion_bookmarking_demo(page: Page, app: ShinyAppProc) -> None:
11+
page.goto(app.url)
12+
13+
# Test accordion bookmarking
14+
acc_single = controller.Accordion(page, "acc_single")
15+
acc_single.expect_open(["Section A"])
16+
acc_single.set(["Section B"])
17+
18+
acc_mod = controller.Accordion(page, "first-acc_mod")
19+
acc_mod.expect_open(["Section A"])
20+
acc_mod.set(["Section C"])
21+
22+
# click bookmark button
23+
bookmark_button = controller.InputBookmarkButton(page)
24+
bookmark_button.click()
25+
26+
# reload page
27+
page.reload()
28+
29+
acc_single.expect_open(["Section B"])
30+
acc_mod.expect_open(["Section C"])
31+
32+
acc_single.set([])
33+
acc_mod.set([])
34+
35+
bookmark_button.click()
36+
37+
# reload page
38+
page.reload()
39+
40+
acc_single.expect_open([])
41+
acc_mod.expect_open([])

0 commit comments

Comments
 (0)