Skip to content
Merged
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* `playwright.controller.InputActionButton` gains a `expect_icon()` method. As a result, the already existing `expect_label()` no longer includes the icon. (#2020)

### Changes

* `express.ui.insert_accordion_panel()`'s function signature has changed to be more ergonomic. Now you can pass the `panel_title` and `panel_contents` directly instead of `ui.hold()`ing the `ui.accordion_panel()` context manager. (#2042)

### Improvements

* Improved the styling and readability of markdown tables rendered by `ui.Chat()` and `ui.MarkdownStream()`. (#1973)
Expand Down
16 changes: 12 additions & 4 deletions shiny/api-examples/insert_accordion_panel/app-express.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import random

from shiny import reactive, ui
from shiny.express import input
from shiny import reactive
from shiny.express import input, ui


def make_panel(letter):
Expand All @@ -11,10 +11,18 @@ def make_panel(letter):


ui.input_action_button("add_panel", "Add random panel", class_="mt-3 mb-3")
ui.accordion(*[make_panel(letter) for letter in "ABCDE"], id="acc", multiple=True)

with ui.accordion(id="acc", multiple=True):
for letter in "ABCDE":
with ui.accordion_panel(f"Section {letter}"):
f"Some narrative for section {letter}"


@reactive.effect
@reactive.event(input.add_panel)
def _():
ui.insert_accordion_panel("acc", make_panel(str(random.randint(0, 10000))))
ui.insert_accordion_panel(
"acc",
f"Section {random.randint(0, 10000)}",
f"Some narrative for section {random.randint(0, 10000)}",
)
5 changes: 4 additions & 1 deletion shiny/express/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@
input_text,
input_text_area,
panel_title,
insert_accordion_panel,
remove_accordion_panel,
update_accordion,
update_accordion_panel,
Expand Down Expand Up @@ -161,6 +160,10 @@
hold,
)

from ._insert import (
insert_accordion_panel,
)

__all__ = (
# Imports from htmltools
"TagList",
Expand Down
86 changes: 86 additions & 0 deletions shiny/express/ui/_insert.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""
Shims for `ui.insert_*()`, `ui.update_*()`, etc. functions that lead to a more ergonomic
Express API.

These functions tend to have one issue in common: if they were re-exported verbatim from
Core to Express, they would want to take RecallContextManager(s) as input, which leads
to a somewhat awkward API. That's because, you'd have to know to use something like
@ui.hold() pass the UI as a value without displaying it.
"""

from typing import Literal, Optional

from htmltools import TagAttrs, TagChild

from ..._docstring import add_example
from ...session import Session
from ...types import MISSING, MISSING_TYPE


@add_example()
def insert_accordion_panel(
id: str,
panel_title: str,
*panel_contents: TagChild | TagAttrs,
panel_value: str | MISSING_TYPE | None = MISSING,
panel_icon: TagChild = None,
target: Optional[str] = None,
position: Literal["after", "before"] = "after",
session: Optional[Session] = None,
) -> None:
"""
Insert an accordion panel into an existing accordion.

Parameters
----------
id
A string that matches an existing :func:`~shiny.ui.express.accordion`'s `id`.
panel_title
The title to appear in the panel header.
panel_contents
UI elements for the panel's body. Can also be a dict of tag attributes for the
body's HTML container.
panel_value
A character string that uniquely identifies this panel. If `MISSING`, the
`title` will be used.
panel_icon
A :class:`~htmltools.TagChild` which is positioned just before the `title`.
target
The `value` of an existing panel to insert next to.
position
Should `panel` be added before or after the target? When `target=None`,
`"after"` will append after the last panel and `"before"` will prepend before
the first panel.
session
A Shiny session object (the default should almost always be used).

References
----------
[Bootstrap Accordion](https://getbootstrap.com/docs/5.3/components/accordion/)

See Also
--------
* :func:`~shiny.ui.express.accordion`
* :func:`~shiny.ui.express.accordion_panel`
* :func:`~shiny.ui.express.update_accordion`
"""

from ...ui import AccordionPanel, accordion_panel, insert_accordion_panel

if isinstance(panel_title, AccordionPanel):
# If the user passed an AccordionPanel, we can just use it as is.
# This isn't recommended, but we support it for backwards compatibility
# with the old API.
panel = panel_title
else:
panel = accordion_panel(
panel_title, *panel_contents, value=panel_value, icon=panel_icon
)

insert_accordion_panel(
id=id,
panel=panel,
target=target,
position=position,
session=session,
)
10 changes: 5 additions & 5 deletions shiny/ui/_accordion.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,17 +315,17 @@ def accordion_panel(
Parameters
----------
title
A title to appear in the :func:`~shiny.ui.accordion_panel`'s header.
A title to appear in the panel's header.
*args
Contents to the accordion panel body. Or tag attributes that are supplied to the
returned :class:`~htmltools.Tag` object.
UI elements for the panel's body. Can also be a dict of tag attributes for the
body's HTML container.
value
A character string that uniquely identifies this panel. If `MISSING`, the
`title` will be used.
icon
A :class:`~htmltools.Tag` which is positioned just before the `title`.
A :class:`~htmltools.TagChild` which is positioned just before the `title`.
**kwargs
Tag attributes to the `accordion-body` div Tag.
Tag attributes for the body's HTML container.

Returns
-------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import datetime

from shiny import reactive
from shiny.express import input, ui

with ui.accordion(id="my_accordion"):
with ui.accordion_panel(title="About"):
"This is a simple Shiny app."

with ui.accordion_panel(title="Panel 1"):
"Some initial content for Panel 1."

with ui.accordion_panel(title="Panel 2", value="panel_2_val"):
"Some initial content for Panel 2."

ui.input_action_button("update_button", "Update Panel 2")
ui.input_action_button("add_panel_button", "Add New Panel")

panel_counter = reactive.value(3)


@reactive.effect
@reactive.event(input.update_button)
def _():
new_content = f"Content updated at: {datetime.datetime.now().strftime('%H:%M:%S')}"
ui.update_accordion_panel(
"my_accordion", "panel_2_val", new_content, title="Panel 2 (Updated)"
)


@reactive.effect
@reactive.event(input.add_panel_button)
def _():
current_count = panel_counter.get()
panel_counter.set(current_count + 1)
ui.insert_accordion_panel(
"my_accordion",
f"Panel {current_count}",
f"This is dynamically added panel {current_count}, created at {datetime.datetime.now().strftime('%H:%M:%S')}",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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_and_buttons(page: Page, app: ShinyAppProc) -> None:
page.goto(app.url)

accordion = controller.Accordion(page, "my_accordion")
update_button = controller.InputActionButton(page, "update_button")
add_panel_button = controller.InputActionButton(page, "add_panel_button")

accordion.expect_panels(["About", "Panel 1", "panel_2_val"])

about_panel = accordion.accordion_panel("About")
about_panel.expect_label("About")
about_panel.expect_body("This is a simple Shiny app.")

panel_1 = accordion.accordion_panel("Panel 1")
panel_1.expect_label("Panel 1")
panel_1.expect_body("Some initial content for Panel 1.")

panel_2 = accordion.accordion_panel("panel_2_val")
panel_2.expect_label("Panel 2")
panel_2.expect_body("Some initial content for Panel 2.")

update_button.expect_label("Update Panel 2")
update_button.click()

panel_2.expect_label("Panel 2 (Updated)")

add_panel_button.expect_label("Add New Panel")
add_panel_button.click()

accordion.expect_panels(["About", "Panel 1", "panel_2_val", "Panel 3"])

panel_3 = accordion.accordion_panel("Panel 3")
panel_3.expect_label("Panel 3")

add_panel_button.click()

accordion.expect_panels(["About", "Panel 1", "panel_2_val", "Panel 3", "Panel 4"])

panel_4 = accordion.accordion_panel("Panel 4")
panel_4.expect_label("Panel 4")
Loading