Skip to content
4 changes: 2 additions & 2 deletions shiny/bookmark/_restore_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,11 +391,11 @@ def get_current_restore_context() -> RestoreContext | None:


@overload
def restore_input(resolved_id: ResolvedId, default: Any) -> Any: ...
def restore_input(resolved_id: ResolvedId, default: Optional[Any] = None) -> Any: ...
@overload
def restore_input(resolved_id: None, default: T) -> T: ...
@add_example()
def restore_input(resolved_id: ResolvedId | None, default: Any) -> Any:
def restore_input(resolved_id: ResolvedId | None, default: Optional[Any] = None) -> Any:
"""
Restore an input value

Expand Down
2 changes: 1 addition & 1 deletion shiny/playwright/controller/_navs.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ def _expect_content_text(


class _NavsetBase(UiWithContainer):
"""A Base mixin class for Nav controls"""
"""A Base class for Nav controls"""

def nav_panel(
self,
Expand Down
37 changes: 22 additions & 15 deletions shiny/ui/_accordion.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .._docstring import add_example
from .._namespaces import resolve_id_or_none
from .._utils import drop_none, private_random_id
from ..bookmark import restore_input
from ..session import require_active_session
from ..types import MISSING, MISSING_TYPE
from ._html_deps_shinyverse import components_dependencies
Expand Down Expand Up @@ -243,6 +244,21 @@ def accordion(
"All `accordion(*args)` must be of type `AccordionPanel` which can be created using `accordion_panel()`"
)

# Since multiple=False requires an id, we always include one,
# but only create a binding when it is provided
binding_class_value: TagAttrs | None = None
if id is None:
id = private_random_id("bslib_accordion")
binding_class_value = None
else:
binding_class_value = {"class": "bslib-accordion-input"}

accordion_id = resolve_id_or_none(id)
has_restored_input = not isinstance(
restore_input(accordion_id, MISSING), MISSING_TYPE
)
open = restore_input(accordion_id, open)

is_open: list[bool] = []
if open is None:
is_open = [False for _ in panels]
Expand All @@ -254,27 +270,18 @@ def accordion(
#
is_open = [panel._data_value in open for panel in panels]

# Open the first panel by default
if open is not False and len(is_open) > 0 and not any(is_open):
is_open[0] = True
if not has_restored_input:
# Open the first panel by default
if open is not False and len(is_open) > 0 and not any(is_open):
is_open[0] = True

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

# Since multiple=False requires an id, we always include one,
# but only create a binding when it is provided
binding_class_value: TagAttrs | None = None
if id is None:
id = private_random_id("bslib_accordion")
binding_class_value = None
else:
binding_class_value = {"class": "bslib-accordion-input"}

accordion_id = resolve_id_or_none(id)
for panel, open in zip(panels, is_open):
for panel, panel_is_open in zip(panels, is_open):
panel._accordion_id = accordion_id
panel._is_multiple = multiple
panel._is_open = open
panel._is_open = panel_is_open

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

Expand Down
31 changes: 14 additions & 17 deletions shiny/ui/_navs.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from .._docstring import add_example
from .._namespaces import resolve_id_or_none
from .._utils import private_random_int
from ..bookmark import restore_input
from ..types import DEPRECATED, MISSING, MISSING_TYPE, NavSetArg
from ._bootstrap import column, row
from ._card import CardItem, WrapperCallable, card, card_body, card_footer, card_header
Expand Down Expand Up @@ -393,9 +394,12 @@ def __init__(
header: TagChild = None,
footer: TagChild = None,
) -> None:
id_resolved = resolve_id_or_none(id)
selected = restore_input(id_resolved, selected)

self.args = args
self.ul_class = ul_class
self.id = id
self.id = id_resolved
self.selected = selected
self.header = header
self.footer = footer
Expand Down Expand Up @@ -464,11 +468,10 @@ def navset_tab(
-------
See :func:`~shiny.ui.nav_panel`
"""

return NavSet(
*args,
ul_class="nav nav-tabs",
id=resolve_id_or_none(id),
id=id,
selected=selected,
header=header,
footer=footer,
Expand Down Expand Up @@ -520,11 +523,10 @@ def navset_pill(
-------
See :func:`~shiny.ui.nav_panel`
"""

return NavSet(
*args,
ul_class="nav nav-pills",
id=resolve_id_or_none(id),
id=id,
selected=selected,
header=header,
footer=footer,
Expand Down Expand Up @@ -579,7 +581,7 @@ def navset_underline(
return NavSet(
*args,
ul_class="nav nav-underline",
id=resolve_id_or_none(id),
id=id,
selected=selected,
header=header,
footer=footer,
Expand Down Expand Up @@ -627,11 +629,10 @@ def navset_hidden(
* :func:`~shiny.ui.navset_card_underline`
* :func:`~shiny.ui.navset_pill_list`
"""

return NavSet(
*args,
ul_class="nav nav-hidden",
id=resolve_id_or_none(id),
id=id,
selected=selected,
header=header,
footer=footer,
Expand Down Expand Up @@ -747,11 +748,10 @@ def navset_card_tab(
-------
See :func:`~shiny.ui.nav_panel`
"""

return NavSetCard(
*args,
ul_class="nav nav-tabs card-header-tabs",
id=resolve_id_or_none(id),
id=id,
selected=selected,
title=title,
sidebar=sidebar,
Expand Down Expand Up @@ -813,11 +813,10 @@ def navset_card_pill(
-------
See :func:`~shiny.ui.nav_panel`
"""

return NavSetCard(
*args,
ul_class="nav nav-pills card-header-pills",
id=resolve_id_or_none(id),
id=id,
selected=selected,
title=title,
sidebar=sidebar,
Expand Down Expand Up @@ -882,7 +881,7 @@ def navset_card_underline(
return NavSetCard(
*args,
ul_class="nav nav-underline",
id=resolve_id_or_none(id),
id=id,
selected=selected,
title=title,
sidebar=sidebar,
Expand Down Expand Up @@ -977,11 +976,10 @@ def navset_pill_list(
-------
See :func:`~shiny.ui.nav_panel`
"""

return NavSetPillList(
*args,
ul_class="nav nav-pills nav-stacked",
id=resolve_id_or_none(id),
id=id,
selected=selected,
header=header,
footer=footer,
Expand Down Expand Up @@ -1571,11 +1569,10 @@ def navset_bar(
ul_class = "nav navbar-nav"
if navbar_opts.underline:
ul_class += " nav-underline"

return NavSetBar(
*new_args,
ul_class=ul_class,
id=resolve_id_or_none(id),
id=id,
selected=selected,
sidebar=sidebar,
fillable=fillable,
Expand Down
10 changes: 9 additions & 1 deletion shiny/ui/_sidebar.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from .._namespaces import resolve_id_or_none
from .._typing_extensions import TypedDict
from .._utils import private_random_id
from ..bookmark import restore_input
from ..module import ResolvedId
from ..session import require_active_session
from ..types import MISSING, MISSING_TYPE
Expand Down Expand Up @@ -545,13 +546,20 @@ def sidebar(

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

resolved_id = resolve_id_or_none(id)

if resolved_id:
restored_open: bool | None = restore_input(resolved_id)
if restored_open is not None:
open = "open" if restored_open else "closed"

return Sidebar(
children=children,
attrs=attrs,
width=width,
position=position,
open=open,
id=id,
id=resolved_id,
title=title,
fg=fg,
bg=bg,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from shiny.express import app_opts, expressify, input, module, render, session, ui

app_opts(bookmark_store="url")


@expressify
def my_accordion(**kwargs):
with ui.accordion(**kwargs):
for letter in "ABCDE":
with ui.accordion_panel(f"Section {letter}"):
f"Some narrative for section {letter}"


ui.h2("Accordion with bookmarking")

with ui.card():
ui.h3("Accordion non-module bookmarking")
my_accordion(multiple=False, id="acc_single")

@render.text
def accordion_global():
return f"input.accordion(): {input.acc_single()}"

# Module section in sidebar
@module
def accordion_module(input, output, session):
my_accordion(multiple=False, id="acc_mod")

@render.text
def accordion_module():
return f"input.acc_mod(): {input.acc_mod()}"

ui.h3("Accordion module bookmarking")
accordion_module("first")

ui.input_bookmark_button()


@session.bookmark.on_bookmarked
async def _(url: str):
await session.bookmark.update_query_string(url)
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from playwright.sync_api import Page

from shiny.playwright import controller
from shiny.pytest import create_app_fixture
from shiny.run import ShinyAppProc

app = create_app_fixture(["app-express.py"])


def test_accordion_bookmarking_demo(page: Page, app: ShinyAppProc) -> None:
page.goto(app.url)

# Test accordion bookmarking
acc_single = controller.Accordion(page, "acc_single")
acc_single.expect_open(["Section A"])
acc_single.set(["Section B"])

acc_mod = controller.Accordion(page, "first-acc_mod")
acc_mod.expect_open(["Section A"])
acc_mod.set(["Section C"])

# click bookmark button
bookmark_button = controller.InputBookmarkButton(page)
bookmark_button.click()

# reload page
page.reload()

acc_single.expect_open(["Section B"])
acc_mod.expect_open(["Section C"])

acc_single.set([])
acc_mod.set([])

bookmark_button.click()

# reload page
page.reload()

acc_single.expect_open([])
acc_mod.expect_open([])
Loading
Loading