From d761a5d4c38e24dff09ca3dcc35d599d7ed8a50a Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 27 Apr 2022 16:47:17 -0500 Subject: [PATCH 01/56] Implement dynamic navs (i.e., nav_insert(), nav_remove(), nav_show(), nav_hide()) --- shiny/examples/nav_insert/app.py | 57 +++++++++ shiny/examples/nav_show/app.py | 48 ++++++++ shiny/ui/__init__.py | 10 ++ shiny/ui/_navs_dynamic.py | 199 +++++++++++++++++++++++++++++++ 4 files changed, 314 insertions(+) create mode 100644 shiny/examples/nav_insert/app.py create mode 100644 shiny/examples/nav_show/app.py create mode 100644 shiny/ui/_navs_dynamic.py diff --git a/shiny/examples/nav_insert/app.py b/shiny/examples/nav_insert/app.py new file mode 100644 index 000000000..638ac5402 --- /dev/null +++ b/shiny/examples/nav_insert/app.py @@ -0,0 +1,57 @@ +from shiny import * + +app_ui = ui.page_fluid( + ui.layout_sidebar( + ui.panel_sidebar( + ui.input_action_button("add", "Add 'Dynamic' tab"), + ui.input_action_button("removeFoo", "Remove 'Foo' tabs"), + ui.input_action_button("addFoo", "Add New 'Foo' tab"), + ), + ui.panel_main( + ui.navset_tab( + ui.nav("Hello", "This is the hello tab"), + ui.nav("Foo", "This is the Foo tab", value="Foo"), + ui.nav_menu( + "Static", + ui.nav("Static 1", "Static 1", value="s1"), + ui.nav("Static 2", "Static 2", value="s2"), + value="Menu", + ), + id="tabs", + ), + ), + ) +) + + +def server(input: Inputs, output: Outputs, session: Session): + @reactive.Effect() + @event(input.add) + def _(): + id = "Dynamic-" + str(input.add()) + ui.nav_insert( + "tabs", + ui.nav(id, id), + target="s2", + position="before", + ) + + @reactive.Effect() + @event(input.removeFoo) + def _(): + ui.nav_remove("tabs", target="Foo") + + @reactive.Effect() + @event(input.addFoo) + def _(): + n = str(input.addFoo()) + ui.nav_insert( + "tabs", + ui.nav("Foo-" + n, "This is the new Foo-" + n + " tab", value="Foo"), + target="Menu", + position="before", + select=True, + ) + + +app = App(app_ui, server) diff --git a/shiny/examples/nav_show/app.py b/shiny/examples/nav_show/app.py new file mode 100644 index 000000000..27037e86b --- /dev/null +++ b/shiny/examples/nav_show/app.py @@ -0,0 +1,48 @@ +from shiny import * + +app_ui = ui.page_navbar( + ui.nav( + "Home", + ui.input_action_button("hideTab", "Hide 'Foo' tab"), + ui.input_action_button("showTab", "Show 'Foo' tab"), + ui.input_action_button("hideMenu", "Hide 'More' nav_menu"), + ui.input_action_button("showMenu", "Show 'More' nav_menu"), + ), + ui.nav("Foo", "This is the foo tab"), + ui.nav("Bar", "This is the bar tab"), + ui.nav_menu( + "More", + ui.nav("Table", "Table page"), + ui.nav("About", "About page"), + "------", + "Even more!", + ui.nav("Email", "Email page"), + ), + title="Navbar page", + id="tabs", +) + + +def server(input: Inputs, output: Outputs, session: Session): + @reactive.Effect() + @event(input.hideTab) + def _(): + ui.nav_hide("tabs", target="Foo") + + @reactive.Effect() + @event(input.showTab) + def _(): + ui.nav_show("tabs", target="Foo") + + @reactive.Effect() + @event(input.hideMenu) + def _(): + ui.nav_hide("tabs", target="More") + + @reactive.Effect() + @event(input.showMenu) + def _(): + ui.nav_show("tabs", target="More") + + +app = App(app_ui, server) diff --git a/shiny/ui/__init__.py b/shiny/ui/__init__.py index 3464b9d3c..69defc8bb 100644 --- a/shiny/ui/__init__.py +++ b/shiny/ui/__init__.py @@ -66,6 +66,12 @@ navset_hidden, navset_bar, ) +from ._navs_dynamic import ( + nav_hide, + nav_insert, + nav_remove, + nav_show, +) from ._notification import notification_show, notification_remove from ._output import ( output_plot, @@ -179,6 +185,10 @@ "navset_pill_list", "navset_hidden", "navset_bar", + "nav_hide", + "nav_insert", + "nav_remove", + "nav_show", "notification_show", "notification_remove", "output_plot", diff --git a/shiny/ui/_navs_dynamic.py b/shiny/ui/_navs_dynamic.py new file mode 100644 index 000000000..eb19c3de9 --- /dev/null +++ b/shiny/ui/_navs_dynamic.py @@ -0,0 +1,199 @@ +__all__ = ( + "nav_insert", + "nav_remove", + "nav_hide", + "nav_show", +) + +import sys +from typing import Optional, Union + +if sys.version_info >= (3, 8): + from typing import Literal +else: + from typing_extensions import Literal + +from .._docstring import add_example +from .._utils import run_coro_sync +from ..session import Session, require_active_session +from ..types import NavSetArg +from ._input_update import update_navs +from ._navs import menu_string_as_nav + + +@add_example() +def nav_insert( + id: str, + nav: Union[NavSetArg, str], + target: Optional[str] = None, + position: Literal["after", "before"] = "after", + select: bool = False, + session: Optional[Session] = None, +) -> None: + """ + Insert a new nav item into a navigation container. + + Parameters + ---------- + id + The ``id`` of the relevant navigation container (i.e., ``navset_*()`` object). + nav + The navigation item to insert (typically a :func:`~shiny.ui.nav` or + :func:`~shiny.ui.nav_menu`). A :func:`~shiny.ui.nav_menu` isn't allowed when the + ``target`` references an :func:`~shiny.ui.nav_menu` (or an item within it). A + string is only allowed when the ``target`` references a + :func:`~shiny.ui.nav_menu`. + target + The ``value`` of an existing :func:`shiny.ui.nav` item, next to which tab will + be added. + position + The position of the new nav item relative to the target nav item. + select + Whether the nav item should be selected upon insertion. + session + A :class:`~shiny.Session` instance. If not provided, it is inferred via + :func:`~shiny.session.get_current_session`. + + See Also + -------- + ~nav_remove + ~nav_show + ~nav_hide + ~shiny.ui.nav + """ + + session = require_active_session(session) + + # N.B. this is only sensible if the target is a menu, but we don't know that, + # which could cause confusion of we decide to support top-level strings at some + # in the future. + if isinstance(nav, str): + nav = menu_string_as_nav(nav) + + # N.B. shiny.js' is smart enough to know how to add active classes and href/id attrs + li_tag, div_tag = nav.resolve( + selected=None, context=dict(tabsetid="tsid", index="id") + ) + + msg = { + "inputId": session.ns(id), + "liTag": session._process_ui(li_tag), + "divTag": session._process_ui(div_tag), + "menuName": None, + "target": target, + "position": position, + "select": select, + } + + def callback() -> None: + run_coro_sync(session._send_message({"shiny-insert-tab": msg})) + + session.on_flush(callback, once=True) + + +def nav_remove(id: str, target: str, session: Optional[Session] = None) -> None: + """ + Remove a nav item from a navigation container. + + Parameters + ---------- + id + The ``id`` of the relevant navigation container (i.e., ``navset_*()`` object). + target + The ``value`` of an existing :func:`shiny.ui.nav` item to remove. + session + A :class:`~shiny.Session` instance. If not provided, it is inferred via + :func:`~shiny.session.get_current_session`. + + See Also + -------- + ~nav_insert + ~nav_show + ~nav_hide + ~shiny.ui.nav + """ + + session = require_active_session(session) + + msg = {"inputId": session.ns(id), "target": target} + + def callback() -> None: + run_coro_sync(session._send_message({"shiny-remove-tab": msg})) + + session.on_flush(callback, once=True) + + +def nav_show( + id: str, target: str, select: bool = False, session: Optional[Session] = None +) -> None: + """ + Show a navigation item + + Parameters + ---------- + id + The ``id`` of the relevant navigation container (i.e., ``navset_*()`` object). + target + The ``value`` of an existing :func:`shiny.ui.nav` item to show. + select + Whether the nav item's content should also be shown. + session + A :class:`~shiny.Session` instance. If not provided, it is inferred via + :func:`~shiny.session.get_current_session`. + + Note + ---- + For ``nav_show()`` to be relevant/useful, a :func:`shiny.ui.nav` item must + have been hidden using :func:`~nav_hide`. + + See Also + -------- + ~nav_hide + ~nav_insert + ~nav_remove + ~shiny.ui.nav + """ + + session = require_active_session(session) + + if select: + update_navs(id, selected=target) + + msg = {"inputId": session.ns(id), "target": target, "type": "show"} + + def callback() -> None: + run_coro_sync(session._send_message({"shiny-change-tab-visibility": msg})) + + session.on_flush(callback, once=True) + + +def nav_hide(id: str, target: str, session: Optional[Session] = None) -> None: + """ + Hide a navigation item + + Parameters + ---------- + id + The ``id`` of the relevant navigation container (i.e., ``navset_*()`` object). + target + The ``value`` of an existing :func:`shiny.ui.nav` item to hide. + session + A :class:`~shiny.Session` instance. If not provided, it is inferred via + :func:`~shiny.session.get_current_session`. + + See Also + -------- + ~nav_show + ~nav_insert + ~nav_remove + ~shiny.ui.nav + """ + + session = require_active_session(session) + + msg = {"inputId": session.ns(id), "target": target, "type": "hide"} + + def callback() -> None: + run_coro_sync(session._send_message({"shiny-change-tab-visibility": msg})) + + session.on_flush(callback, once=True) From 904847850bca3c29af14bc576eb056c477add901 Mon Sep 17 00:00:00 2001 From: Joe Cheng Date: Mon, 22 May 2023 10:31:04 -0700 Subject: [PATCH 02/56] Use resolve_id instead of session.ns for id resolution in navs_dynamic --- shiny/session/_session.py | 2 +- shiny/ui/_navs_dynamic.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/shiny/session/_session.py b/shiny/session/_session.py index 5db4d7a18..f90640118 100644 --- a/shiny/session/_session.py +++ b/shiny/session/_session.py @@ -561,7 +561,7 @@ def send_input_message(self, id: str, message: dict[str, object]) -> None: message The message to send. """ - msg: dict[str, object] = {"id": id, "message": message} + msg: dict[str, object] = {"id": self.ns(id), "message": message} self._outbound_message_queues["input_messages"].append(msg) self._request_flush() diff --git a/shiny/ui/_navs_dynamic.py b/shiny/ui/_navs_dynamic.py index eb19c3de9..b6e7a5aad 100644 --- a/shiny/ui/_navs_dynamic.py +++ b/shiny/ui/_navs_dynamic.py @@ -14,6 +14,7 @@ from typing_extensions import Literal from .._docstring import add_example +from .._namespaces import resolve_id from .._utils import run_coro_sync from ..session import Session, require_active_session from ..types import NavSetArg @@ -76,7 +77,7 @@ def nav_insert( ) msg = { - "inputId": session.ns(id), + "inputId": resolve_id(id), "liTag": session._process_ui(li_tag), "divTag": session._process_ui(div_tag), "menuName": None, @@ -115,7 +116,7 @@ def nav_remove(id: str, target: str, session: Optional[Session] = None) -> None: session = require_active_session(session) - msg = {"inputId": session.ns(id), "target": target} + msg = {"inputId": resolve_id(id), "target": target} def callback() -> None: run_coro_sync(session._send_message({"shiny-remove-tab": msg})) @@ -156,10 +157,11 @@ def nav_show( session = require_active_session(session) + id = resolve_id(id) if select: update_navs(id, selected=target) - msg = {"inputId": session.ns(id), "target": target, "type": "show"} + msg = {"inputId": id, "target": target, "type": "show"} def callback() -> None: run_coro_sync(session._send_message({"shiny-change-tab-visibility": msg})) @@ -191,7 +193,7 @@ def nav_hide(id: str, target: str, session: Optional[Session] = None) -> None: session = require_active_session(session) - msg = {"inputId": session.ns(id), "target": target, "type": "hide"} + msg = {"inputId": resolve_id(id), "target": target, "type": "hide"} def callback() -> None: run_coro_sync(session._send_message({"shiny-change-tab-visibility": msg})) From acde8d82e71e0e45ffbb9bfae1814a06fa9e0ca6 Mon Sep 17 00:00:00 2001 From: Joe Cheng Date: Mon, 22 May 2023 10:48:38 -0700 Subject: [PATCH 03/56] Explain how to prepend/append --- shiny/ui/_navs_dynamic.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/shiny/ui/_navs_dynamic.py b/shiny/ui/_navs_dynamic.py index b6e7a5aad..e1c1fe348 100644 --- a/shiny/ui/_navs_dynamic.py +++ b/shiny/ui/_navs_dynamic.py @@ -46,9 +46,11 @@ def nav_insert( :func:`~shiny.ui.nav_menu`. target The ``value`` of an existing :func:`shiny.ui.nav` item, next to which tab will - be added. + be added. Can also be ``None``; see ``position``. position - The position of the new nav item relative to the target nav item. + The position of the new nav item relative to the target nav item. If + ``target=None``, then ``"before"`` means the new nav item should be inserted at + the head of the navlist, and ``"after"`` is the end. select Whether the nav item should be selected upon insertion. session From 2799518368afd431f89a2c23f91c1e9c5c461043 Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 16 Jul 2025 16:46:24 -0500 Subject: [PATCH 04/56] Modernize message sending --- shiny/ui/_navs_dynamic.py | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/shiny/ui/_navs_dynamic.py b/shiny/ui/_navs_dynamic.py index e1c1fe348..723a6e573 100644 --- a/shiny/ui/_navs_dynamic.py +++ b/shiny/ui/_navs_dynamic.py @@ -88,10 +88,7 @@ def nav_insert( "select": select, } - def callback() -> None: - run_coro_sync(session._send_message({"shiny-insert-tab": msg})) - - session.on_flush(callback, once=True) + session._send_message_sync({"custom": {"shiny-insert-tab": msg}}) def nav_remove(id: str, target: str, session: Optional[Session] = None) -> None: @@ -118,12 +115,12 @@ def nav_remove(id: str, target: str, session: Optional[Session] = None) -> None: session = require_active_session(session) - msg = {"inputId": resolve_id(id), "target": target} - - def callback() -> None: - run_coro_sync(session._send_message({"shiny-remove-tab": msg})) + msg = { + "inputId": resolve_id(id), + "target": target, + } - session.on_flush(callback, once=True) + session._send_message_sync({"custom": {"shiny-remove-tab": msg}}) def nav_show( @@ -163,12 +160,13 @@ def nav_show( if select: update_navs(id, selected=target) - msg = {"inputId": id, "target": target, "type": "show"} - - def callback() -> None: - run_coro_sync(session._send_message({"shiny-change-tab-visibility": msg})) + msg = { + "inputId": id, + "target": target, + "type": "show", + } - session.on_flush(callback, once=True) + session._send_message_sync({"custom": {"shiny-change-tab-visibility": msg}}) def nav_hide(id: str, target: str, session: Optional[Session] = None) -> None: @@ -195,9 +193,10 @@ def nav_hide(id: str, target: str, session: Optional[Session] = None) -> None: session = require_active_session(session) - msg = {"inputId": resolve_id(id), "target": target, "type": "hide"} - - def callback() -> None: - run_coro_sync(session._send_message({"shiny-change-tab-visibility": msg})) + msg = { + "inputId": resolve_id(id), + "target": target, + "type": "hide", + } - session.on_flush(callback, once=True) + session._send_message_sync({"custom": {"shiny-change-tab-visibility": msg}}) From 5d6cb934d77bdc2b3d63a0a72a67632079885118 Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 16 Jul 2025 17:04:59 -0500 Subject: [PATCH 05/56] Cleanup --- shiny/ui/_navs_dynamic.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shiny/ui/_navs_dynamic.py b/shiny/ui/_navs_dynamic.py index 723a6e573..7f273779e 100644 --- a/shiny/ui/_navs_dynamic.py +++ b/shiny/ui/_navs_dynamic.py @@ -15,7 +15,6 @@ from .._docstring import add_example from .._namespaces import resolve_id -from .._utils import run_coro_sync from ..session import Session, require_active_session from ..types import NavSetArg from ._input_update import update_navs @@ -91,6 +90,7 @@ def nav_insert( session._send_message_sync({"custom": {"shiny-insert-tab": msg}}) +@add_example() def nav_remove(id: str, target: str, session: Optional[Session] = None) -> None: """ Remove a nav item from a navigation container. @@ -123,6 +123,7 @@ def nav_remove(id: str, target: str, session: Optional[Session] = None) -> None: session._send_message_sync({"custom": {"shiny-remove-tab": msg}}) +@add_example() def nav_show( id: str, target: str, select: bool = False, session: Optional[Session] = None ) -> None: @@ -169,6 +170,7 @@ def nav_show( session._send_message_sync({"custom": {"shiny-change-tab-visibility": msg}}) +@add_example() def nav_hide(id: str, target: str, session: Optional[Session] = None) -> None: """ Hide a navigation item From 24765ad17d9c66b565b910aea1a96d8126646a3f Mon Sep 17 00:00:00 2001 From: E Nelson Date: Wed, 23 Jul 2025 12:05:59 -0400 Subject: [PATCH 06/56] Update shiny/ui/_navs_dynamic.py Co-authored-by: Carson Sievert --- shiny/ui/_navs_dynamic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shiny/ui/_navs_dynamic.py b/shiny/ui/_navs_dynamic.py index 7f273779e..4d32a1d23 100644 --- a/shiny/ui/_navs_dynamic.py +++ b/shiny/ui/_navs_dynamic.py @@ -24,7 +24,7 @@ @add_example() def nav_insert( id: str, - nav: Union[NavSetArg, str], + nav_panel: Union[NavSetArg, str], target: Optional[str] = None, position: Literal["after", "before"] = "after", select: bool = False, From b32d14b54343f18336dc384ca1ba2d556107b3cd Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Wed, 23 Jul 2025 12:06:43 -0400 Subject: [PATCH 07/56] First revisions --- shiny/examples/nav_insert/app.py | 54 +++++++++++++++----------------- shiny/ui/_navs_dynamic.py | 14 +++++---- 2 files changed, 33 insertions(+), 35 deletions(-) diff --git a/shiny/examples/nav_insert/app.py b/shiny/examples/nav_insert/app.py index 638ac5402..d53dd6379 100644 --- a/shiny/examples/nav_insert/app.py +++ b/shiny/examples/nav_insert/app.py @@ -1,53 +1,49 @@ -from shiny import * +from shiny import App, Inputs, Outputs, Session, reactive, ui -app_ui = ui.page_fluid( - ui.layout_sidebar( - ui.panel_sidebar( - ui.input_action_button("add", "Add 'Dynamic' tab"), - ui.input_action_button("removeFoo", "Remove 'Foo' tabs"), - ui.input_action_button("addFoo", "Add New 'Foo' tab"), +app_ui = ui.page_sidebar( + ui.sidebar( + ui.input_action_button("add", "Add 'Dynamic' tab"), + ui.input_action_button("removeFoo", "Remove 'Foo' tabs"), + ui.input_action_button("addFoo", "Add New 'Foo' tab"), + ), + ui.navset_tab( + ui.nav_panel("Hello", "This is the hello tab"), + ui.nav_panel("Foo", "This is the Foo tab", value="Foo"), + ui.nav_menu( + "Static", + ui.nav_panel("Static 1", "Static 1", value="s1"), + ui.nav_panel("Static 2", "Static 2", value="s2"), + value="Menu", ), - ui.panel_main( - ui.navset_tab( - ui.nav("Hello", "This is the hello tab"), - ui.nav("Foo", "This is the Foo tab", value="Foo"), - ui.nav_menu( - "Static", - ui.nav("Static 1", "Static 1", value="s1"), - ui.nav("Static 2", "Static 2", value="s2"), - value="Menu", - ), - id="tabs", - ), - ), - ) + id="tabs", + ), ) def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect() - @event(input.add) + @reactive.effect() + @reactive.event(input.add) def _(): id = "Dynamic-" + str(input.add()) ui.nav_insert( "tabs", - ui.nav(id, id), + ui.nav_panel(id, id), target="s2", position="before", ) - @reactive.Effect() - @event(input.removeFoo) + @reactive.effect() + @reactive.event(input.removeFoo) def _(): ui.nav_remove("tabs", target="Foo") - @reactive.Effect() - @event(input.addFoo) + @reactive.effect() + @reactive.event(input.addFoo) def _(): n = str(input.addFoo()) ui.nav_insert( "tabs", - ui.nav("Foo-" + n, "This is the new Foo-" + n + " tab", value="Foo"), + ui.nav_panel("Foo-" + n, "This is the new Foo-" + n + " tab", value="Foo"), target="Menu", position="before", select=True, diff --git a/shiny/ui/_navs_dynamic.py b/shiny/ui/_navs_dynamic.py index 7f273779e..b7fdc2c82 100644 --- a/shiny/ui/_navs_dynamic.py +++ b/shiny/ui/_navs_dynamic.py @@ -24,7 +24,7 @@ @add_example() def nav_insert( id: str, - nav: Union[NavSetArg, str], + nav_panel: Union[NavSetArg, str], target: Optional[str] = None, position: Literal["after", "before"] = "after", select: bool = False, @@ -37,8 +37,8 @@ def nav_insert( ---------- id The ``id`` of the relevant navigation container (i.e., ``navset_*()`` object). - nav - The navigation item to insert (typically a :func:`~shiny.ui.nav` or + nav_panel + The navigation item to insert (typically a :func:`~shiny.ui.nav_panel` or :func:`~shiny.ui.nav_menu`). A :func:`~shiny.ui.nav_menu` isn't allowed when the ``target`` references an :func:`~shiny.ui.nav_menu` (or an item within it). A string is only allowed when the ``target`` references a @@ -69,9 +69,11 @@ def nav_insert( # N.B. this is only sensible if the target is a menu, but we don't know that, # which could cause confusion of we decide to support top-level strings at some # in the future. - if isinstance(nav, str): - nav = menu_string_as_nav(nav) - + # print("IS INSTANCE: ", isinstance(nav_panel, str)) + # if isinstance(nav_panel, str): + # print("isnav") + nav = menu_string_as_nav(nav_panel) + print(nav) # N.B. shiny.js' is smart enough to know how to add active classes and href/id attrs li_tag, div_tag = nav.resolve( selected=None, context=dict(tabsetid="tsid", index="id") From 01c3aad6e24098d289d4ae84bb881ecce109587b Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Wed, 23 Jul 2025 21:30:36 -0400 Subject: [PATCH 08/56] Updating nav implementation and examples --- .../nav_insert/app-core.py} | 0 shiny/api-examples/nav_insert/app-express.py | 49 +++++++++++++++++ shiny/api-examples/nav_show/app-core.py | 52 +++++++++++++++++++ shiny/api-examples/nav_show/app-express.py | 47 +++++++++++++++++ shiny/examples/nav_show/app.py | 48 ----------------- shiny/ui/_navs_dynamic.py | 25 +++++---- 6 files changed, 160 insertions(+), 61 deletions(-) rename shiny/{examples/nav_insert/app.py => api-examples/nav_insert/app-core.py} (100%) create mode 100644 shiny/api-examples/nav_insert/app-express.py create mode 100644 shiny/api-examples/nav_show/app-core.py create mode 100644 shiny/api-examples/nav_show/app-express.py delete mode 100644 shiny/examples/nav_show/app.py diff --git a/shiny/examples/nav_insert/app.py b/shiny/api-examples/nav_insert/app-core.py similarity index 100% rename from shiny/examples/nav_insert/app.py rename to shiny/api-examples/nav_insert/app-core.py diff --git a/shiny/api-examples/nav_insert/app-express.py b/shiny/api-examples/nav_insert/app-express.py new file mode 100644 index 000000000..a3b7699c1 --- /dev/null +++ b/shiny/api-examples/nav_insert/app-express.py @@ -0,0 +1,49 @@ +from shiny import reactive +from shiny.express import input, ui + +with ui.layout_sidebar(): + with ui.sidebar(): + ui.input_action_button("add", "Add 'Dynamic' tab") + ui.input_action_button("removeFoo", "Remove 'Foo' tabs") + ui.input_action_button("addFoo", "Add New 'Foo' tab") + + @reactive.effect() + @reactive.event(input.removeFoo) + def _(): + ui.nav_remove("tabs", target="Foo") + + @reactive.effect() + @reactive.event(input.addFoo) + def _(): + n = str(input.addFoo()) + ui.nav_insert( + "tabs", + ui.nav_panel( + "Foo-" + n, "This is the new Foo-" + n + " tab", value="Foo" + ), + target="Menu", + position="before", + select=True, + ) + + with ui.navset_tab(id="tabs"): + with ui.nav_panel("Hello"): + "This is the hello tab" + with ui.nav_panel("Foo", value="Foo"): + "This is the Foo tab" + with ui.nav_menu("Static", value="Menu"): + with ui.nav_panel("Static 1", value="s1"): + "Static 1" + with ui.nav_panel("Static 2", value="s2"): + "Static 2", + + @reactive.effect() + @reactive.event(input.add) + def _(): + id = "Dynamic-" + str(input.add()) + ui.nav_insert( + "tabs", + ui.nav_panel(id, id), + target="s2", + position="before", + ) diff --git a/shiny/api-examples/nav_show/app-core.py b/shiny/api-examples/nav_show/app-core.py new file mode 100644 index 000000000..5fb0169ef --- /dev/null +++ b/shiny/api-examples/nav_show/app-core.py @@ -0,0 +1,52 @@ +from shiny import App, Inputs, Outputs, Session, reactive, ui + +app_ui = ui.page_sidebar( + ui.sidebar( + "Home", + ui.input_action_button("hideTab", "Hide 'Foo' tab"), + ui.input_action_button("showTab", "Show 'Foo' tab"), + ui.input_action_button("hideMenu", "Hide 'More' nav_menu"), + ui.input_action_button("showMenu", "Show 'More' nav_menu"), + ), + ui.navset_tab( + ui.nav_panel("Foo", "This is the foo tab"), + ui.nav_panel("Bar", "This is the bar tab"), + ui.nav_menu( + "More", + ui.nav_panel("Table", "Table page"), + ui.nav_panel("About", "About page"), + "------", + "Even more!", + ui.nav_panel("Email", "Email page"), + value="More", + ), + id="tabs", + ), + title="Navbar page", + id="sidebar", +) + + +def server(input: Inputs, output: Outputs, session: Session): + @reactive.effect() + @reactive.event(input.hideTab) + def _(): + ui.nav_hide("tabs", target="Foo") + + @reactive.effect() + @reactive.event(input.showTab) + def _(): + ui.nav_show("tabs", target="Foo") + + @reactive.effect() + @reactive.event(input.hideMenu) + def _(): + ui.nav_hide("tabs", target="More") + + @reactive.effect() + @reactive.event(input.showMenu) + def _(): + ui.nav_show("tabs", target="More") + + +app = App(app_ui, server) diff --git a/shiny/api-examples/nav_show/app-express.py b/shiny/api-examples/nav_show/app-express.py new file mode 100644 index 000000000..6837e5038 --- /dev/null +++ b/shiny/api-examples/nav_show/app-express.py @@ -0,0 +1,47 @@ +from shiny import reactive +from shiny.express import input, ui, expressify, render + +with ui.layout_sidebar(): + with ui.sidebar(): + ui.input_action_button("add", "Add 'Dynamic' tab") + ui.input_action_button("removeFoo", "Remove 'Foo' tabs") + ui.input_action_button("addFoo", "Add New 'Foo' tab") + + with ui.navset_tab(id="tabs"): + with ui.nav_panel("Hello"): + "This is the hello tab" + with ui.nav_panel("Foo", value="Foo"): + "This is the Foo tab" + with ui.nav_menu("Static", value="Menu"): + with ui.nav_panel("Static 1", value="s1"): + "Static 1" + with ui.nav_panel("Static 2", value="s2"): + "Static 2" + + @expressify() + @reactive.event(input.add) + def _(): + id = "Dynamic-" + str(input.add()) + ui.nav_insert( + "tabs", + ui.nav_panel(id, id), + target="s2", + position="before", + ) + + @reactive.effect() + @reactive.event(input.removeFoo) + def _(): + ui.nav_remove("tabs", target="Foo") + + @reactive.effect() + @reactive.event(input.addFoo) + def _(): + n = str(input.addFoo()) + ui.nav_insert( + "tabs", + ui.nav_panel("Foo-" + n, "This is the new Foo-" + n + " tab", value="Foo"), + target="Menu", + position="before", + select=True, + ) diff --git a/shiny/examples/nav_show/app.py b/shiny/examples/nav_show/app.py deleted file mode 100644 index 27037e86b..000000000 --- a/shiny/examples/nav_show/app.py +++ /dev/null @@ -1,48 +0,0 @@ -from shiny import * - -app_ui = ui.page_navbar( - ui.nav( - "Home", - ui.input_action_button("hideTab", "Hide 'Foo' tab"), - ui.input_action_button("showTab", "Show 'Foo' tab"), - ui.input_action_button("hideMenu", "Hide 'More' nav_menu"), - ui.input_action_button("showMenu", "Show 'More' nav_menu"), - ), - ui.nav("Foo", "This is the foo tab"), - ui.nav("Bar", "This is the bar tab"), - ui.nav_menu( - "More", - ui.nav("Table", "Table page"), - ui.nav("About", "About page"), - "------", - "Even more!", - ui.nav("Email", "Email page"), - ), - title="Navbar page", - id="tabs", -) - - -def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect() - @event(input.hideTab) - def _(): - ui.nav_hide("tabs", target="Foo") - - @reactive.Effect() - @event(input.showTab) - def _(): - ui.nav_show("tabs", target="Foo") - - @reactive.Effect() - @event(input.hideMenu) - def _(): - ui.nav_hide("tabs", target="More") - - @reactive.Effect() - @event(input.showMenu) - def _(): - ui.nav_show("tabs", target="More") - - -app = App(app_ui, server) diff --git a/shiny/ui/_navs_dynamic.py b/shiny/ui/_navs_dynamic.py index b7fdc2c82..fd279c9d8 100644 --- a/shiny/ui/_navs_dynamic.py +++ b/shiny/ui/_navs_dynamic.py @@ -61,7 +61,7 @@ def nav_insert( ~nav_remove ~nav_show ~nav_hide - ~shiny.ui.nav + ~shiny.ui.nav_panel """ session = require_active_session(session) @@ -69,11 +69,11 @@ def nav_insert( # N.B. this is only sensible if the target is a menu, but we don't know that, # which could cause confusion of we decide to support top-level strings at some # in the future. - # print("IS INSTANCE: ", isinstance(nav_panel, str)) - # if isinstance(nav_panel, str): - # print("isnav") - nav = menu_string_as_nav(nav_panel) - print(nav) + if isinstance(nav_panel, str): + nav = menu_string_as_nav(nav_panel) + else: + nav = nav_panel + # N.B. shiny.js' is smart enough to know how to add active classes and href/id attrs li_tag, div_tag = nav.resolve( selected=None, context=dict(tabsetid="tsid", index="id") @@ -89,7 +89,7 @@ def nav_insert( "select": select, } - session._send_message_sync({"custom": {"shiny-insert-tab": msg}}) + session._send_message_sync({"shiny-insert-tab": msg}) @add_example() @@ -102,7 +102,7 @@ def nav_remove(id: str, target: str, session: Optional[Session] = None) -> None: id The ``id`` of the relevant navigation container (i.e., ``navset_*()`` object). target - The ``value`` of an existing :func:`shiny.ui.nav` item to remove. + The ``value`` of an existing :func:`shiny.ui.nav_panel` item to remove. session A :class:`~shiny.Session` instance. If not provided, it is inferred via :func:`~shiny.session.get_current_session`. @@ -112,17 +112,16 @@ def nav_remove(id: str, target: str, session: Optional[Session] = None) -> None: ~nav_insert ~nav_show ~nav_hide - ~shiny.ui.nav + ~shiny.ui.nav_panel """ session = require_active_session(session) - msg = { "inputId": resolve_id(id), "target": target, } - session._send_message_sync({"custom": {"shiny-remove-tab": msg}}) + session._send_message_sync({"shiny-remove-tab": msg}) @add_example() @@ -169,7 +168,7 @@ def nav_show( "type": "show", } - session._send_message_sync({"custom": {"shiny-change-tab-visibility": msg}}) + session._send_message_sync({"shiny-change-tab-visibility": msg}) @add_example() @@ -203,4 +202,4 @@ def nav_hide(id: str, target: str, session: Optional[Session] = None) -> None: "type": "hide", } - session._send_message_sync({"custom": {"shiny-change-tab-visibility": msg}}) + session._send_message_sync({"shiny-change-tab-visibility": msg}) From bb5ffab97c517a59780403934f2755029fb939b3 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Thu, 24 Jul 2025 10:11:51 -0400 Subject: [PATCH 09/56] Express for nav_show works --- shiny/api-examples/nav_insert/app-express.py | 74 ++++++++++---------- shiny/api-examples/nav_show/app-express.py | 66 +++++++++-------- shiny/express/ui/__init__.py | 8 +++ 3 files changed, 76 insertions(+), 72 deletions(-) diff --git a/shiny/api-examples/nav_insert/app-express.py b/shiny/api-examples/nav_insert/app-express.py index a3b7699c1..1dd7d41e2 100644 --- a/shiny/api-examples/nav_insert/app-express.py +++ b/shiny/api-examples/nav_insert/app-express.py @@ -1,5 +1,5 @@ from shiny import reactive -from shiny.express import input, ui +from shiny.express import input, ui, expressify, render with ui.layout_sidebar(): with ui.sidebar(): @@ -7,43 +7,41 @@ ui.input_action_button("removeFoo", "Remove 'Foo' tabs") ui.input_action_button("addFoo", "Add New 'Foo' tab") - @reactive.effect() - @reactive.event(input.removeFoo) - def _(): - ui.nav_remove("tabs", target="Foo") + with ui.navset_tab(id="set"): + with ui.nav_panel("Hello"): + "This is the hello tab" + with ui.nav_panel("Foo", value="Foo"): + "This is the Foo tab" + with ui.nav_menu("Static", value="tabs"): + with ui.nav_panel("Static 1", value="s1"): + "Static 1" + with ui.nav_panel("Static 2", value="s2"): + "Static 2" - @reactive.effect() - @reactive.event(input.addFoo) - def _(): - n = str(input.addFoo()) - ui.nav_insert( - "tabs", - ui.nav_panel( - "Foo-" + n, "This is the new Foo-" + n + " tab", value="Foo" - ), - target="Menu", - position="before", - select=True, - ) + @reactive.effect() + @reactive.event(input.add) + def _(): + id = "Dynamic-" + str(input.add()) + ui.nav_insert( + "tabs", + ui.nav_panel(id, value=id), + target="s2", + position="before", + ) - with ui.navset_tab(id="tabs"): - with ui.nav_panel("Hello"): - "This is the hello tab" - with ui.nav_panel("Foo", value="Foo"): - "This is the Foo tab" - with ui.nav_menu("Static", value="Menu"): - with ui.nav_panel("Static 1", value="s1"): - "Static 1" - with ui.nav_panel("Static 2", value="s2"): - "Static 2", + @reactive.effect() + @reactive.event(input.removeFoo) + def _(): + ui.nav_remove("set", target="Foo") - @reactive.effect() - @reactive.event(input.add) - def _(): - id = "Dynamic-" + str(input.add()) - ui.nav_insert( - "tabs", - ui.nav_panel(id, id), - target="s2", - position="before", - ) + @reactive.effect() + @reactive.event(input.addFoo) + def _(): + n = str(input.addFoo()) + ui.nav_insert( + "tabs", + ui.nav_panel("Foo-" + n, "This is the new Foo-" + n + " tab", value="Foo"), + target="Menu", + position="before", + select=True, + ) diff --git a/shiny/api-examples/nav_show/app-express.py b/shiny/api-examples/nav_show/app-express.py index 6837e5038..af81ebe23 100644 --- a/shiny/api-examples/nav_show/app-express.py +++ b/shiny/api-examples/nav_show/app-express.py @@ -1,47 +1,45 @@ from shiny import reactive -from shiny.express import input, ui, expressify, render +from shiny.express import input, ui with ui.layout_sidebar(): - with ui.sidebar(): - ui.input_action_button("add", "Add 'Dynamic' tab") - ui.input_action_button("removeFoo", "Remove 'Foo' tabs") - ui.input_action_button("addFoo", "Add New 'Foo' tab") + with ui.sidebar(title="Navbar page", id="sidebar"): + "Home" + ui.input_action_button("hideTab", "Hide 'Foo' tab") + ui.input_action_button("showTab", "Show 'Foo' tab") + ui.input_action_button("hideMenu", "Hide 'More' nav_menu") + ui.input_action_button("showMenu", "Show 'More' nav_menu") with ui.navset_tab(id="tabs"): - with ui.nav_panel("Hello"): - "This is the hello tab" - with ui.nav_panel("Foo", value="Foo"): - "This is the Foo tab" - with ui.nav_menu("Static", value="Menu"): - with ui.nav_panel("Static 1", value="s1"): - "Static 1" - with ui.nav_panel("Static 2", value="s2"): - "Static 2" + with ui.nav_panel("Foo"): + "This is the foo tab" + with ui.nav_panel("Bar"): + "This is the bar tab" + with ui.nav_menu(title="More", value="More"): + with ui.nav_panel("Table"): + "Table page" + with ui.nav_panel("About"): + "About page" + "------" + "Even more!" + with ui.nav_panel("Email"): + "Email page" - @expressify() - @reactive.event(input.add) + @reactive.effect() + @reactive.event(input.hideTab) + def _(): + ui.nav_hide("tabs", target="Foo") + + @reactive.effect() + @reactive.event(input.showTab) def _(): - id = "Dynamic-" + str(input.add()) - ui.nav_insert( - "tabs", - ui.nav_panel(id, id), - target="s2", - position="before", - ) + ui.nav_show("tabs", target="Foo") @reactive.effect() - @reactive.event(input.removeFoo) + @reactive.event(input.hideMenu) def _(): - ui.nav_remove("tabs", target="Foo") + ui.nav_hide("tabs", target="More") @reactive.effect() - @reactive.event(input.addFoo) + @reactive.event(input.showMenu) def _(): - n = str(input.addFoo()) - ui.nav_insert( - "tabs", - ui.nav_panel("Foo-" + n, "This is the new Foo-" + n + " tab", value="Foo"), - target="Menu", - position="before", - select=True, - ) + ui.nav_show("tabs", target="More") diff --git a/shiny/express/ui/__init__.py b/shiny/express/ui/__init__.py index 62a43de79..4a9ba5bb5 100644 --- a/shiny/express/ui/__init__.py +++ b/shiny/express/ui/__init__.py @@ -109,6 +109,10 @@ notification_remove, nav_spacer, navbar_options, + nav_hide, + nav_insert, + nav_remove, + nav_show, Progress, Theme, value_box_theme, @@ -289,6 +293,10 @@ "navset_hidden", "navset_pill", "navset_pill_list", + "nav_hide", + "nav_insert", + "nav_remove", + "nav_show", "navset_tab", "navset_underline", "navbar_options", From 06f0d0e3105df68a9f1b5754b28af22f1c3217b4 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Thu, 24 Jul 2025 10:32:03 -0400 Subject: [PATCH 10/56] Fixing formatting --- shiny/api-examples/nav_insert/app-express.py | 2 +- shiny/session/_session.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/shiny/api-examples/nav_insert/app-express.py b/shiny/api-examples/nav_insert/app-express.py index 1dd7d41e2..7ac7560d8 100644 --- a/shiny/api-examples/nav_insert/app-express.py +++ b/shiny/api-examples/nav_insert/app-express.py @@ -1,5 +1,5 @@ from shiny import reactive -from shiny.express import input, ui, expressify, render +from shiny.express import expressify, input, render, ui with ui.layout_sidebar(): with ui.sidebar(): diff --git a/shiny/session/_session.py b/shiny/session/_session.py index 7f7257586..dd78a6819 100644 --- a/shiny/session/_session.py +++ b/shiny/session/_session.py @@ -50,8 +50,9 @@ from ..http_staticfiles import FileResponse from ..input_handler import input_handlers from ..module import ResolvedId -from ..reactive import Effect_, Value, effect, isolate +from ..reactive import Effect_, Value, effect from ..reactive import flush as reactive_flush +from ..reactive import isolate from ..reactive._core import lock from ..reactive._core import on_flushed as reactive_on_flushed from ..render.renderer import Renderer, RendererT From 94148790bfc83585c4d2a7bb411de36751258f28 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Thu, 24 Jul 2025 10:43:46 -0400 Subject: [PATCH 11/56] Update nav language to say nav_panel --- shiny/ui/_navs_dynamic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shiny/ui/_navs_dynamic.py b/shiny/ui/_navs_dynamic.py index fd279c9d8..fef31df58 100644 --- a/shiny/ui/_navs_dynamic.py +++ b/shiny/ui/_navs_dynamic.py @@ -153,7 +153,7 @@ def nav_show( ~nav_hide ~nav_insert ~nav_remove - ~shiny.ui.nav + ~shiny.ui.nav_panel """ session = require_active_session(session) @@ -191,7 +191,7 @@ def nav_hide(id: str, target: str, session: Optional[Session] = None) -> None: ~nav_show ~nav_insert ~nav_remove - ~shiny.ui.nav + ~shiny.ui.nav_panel """ session = require_active_session(session) From 019d619c2e8baa889ce2fabd2fd31c85d4ea4299 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Thu, 24 Jul 2025 11:20:13 -0400 Subject: [PATCH 12/56] Removing unused imports --- shiny/api-examples/nav_insert/app-express.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shiny/api-examples/nav_insert/app-express.py b/shiny/api-examples/nav_insert/app-express.py index 7ac7560d8..eea1c27d7 100644 --- a/shiny/api-examples/nav_insert/app-express.py +++ b/shiny/api-examples/nav_insert/app-express.py @@ -1,5 +1,5 @@ from shiny import reactive -from shiny.express import expressify, input, render, ui +from shiny.express import input, ui with ui.layout_sidebar(): with ui.sidebar(): From 6de5bdede4fc51b6f68d9718acbed05492e95be9 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Thu, 24 Jul 2025 13:29:25 -0400 Subject: [PATCH 13/56] Example app with dynamic tabs --- examples/dynamic-tabs/README.md | 3 + examples/dynamic-tabs/app.py | 77 ++++++++++++++++++++++++++ examples/dynamic-tabs/requirements.txt | 1 + 3 files changed, 81 insertions(+) create mode 100644 examples/dynamic-tabs/README.md create mode 100644 examples/dynamic-tabs/app.py create mode 100644 examples/dynamic-tabs/requirements.txt diff --git a/examples/dynamic-tabs/README.md b/examples/dynamic-tabs/README.md new file mode 100644 index 000000000..d64b5a6a4 --- /dev/null +++ b/examples/dynamic-tabs/README.md @@ -0,0 +1,3 @@ +## Example of inserting, deleting showing, an hiding tabs + + diff --git a/examples/dynamic-tabs/app.py b/examples/dynamic-tabs/app.py new file mode 100644 index 000000000..4418dcfb1 --- /dev/null +++ b/examples/dynamic-tabs/app.py @@ -0,0 +1,77 @@ +from shiny import App, Inputs, Outputs, Session, reactive, ui + +app_ui = ui.page_sidebar( + ui.sidebar( + ui.input_action_button("add", "Add 'Dynamic' tab"), + ui.input_action_button("removeFoo", "Remove 'Foo' tabs"), + ui.input_action_button("addFoo", "Add New 'Foo' tab"), + ui.input_action_button("hideTab", "Hide 'Foo' tab"), + ui.input_action_button("showTab", "Show 'Foo' tab"), + ui.input_action_button("hideMenu", "Hide 'Static' nav_menu"), + ui.input_action_button("showMenu", "Show 'Static' nav_menu"), + ), + ui.navset_tab( + ui.nav_panel("Hello", "This is the hello tab"), + ui.nav_panel("Foo", "This is the Foo tab", value="Foo"), + ui.nav_menu( + "Static", + ui.nav_panel("Static 1", "Static 1", value="s1"), + ui.nav_panel("Static 2", "Static 2", value="s2"), + value="Menu", + ), + id="tabs", + ), +) + + +def server(input: Inputs, output: Outputs, session: Session): + @reactive.effect() + @reactive.event(input.add) + def _(): + id = "Dynamic-" + str(input.add()) + ui.nav_insert( + "tabs", + ui.nav_panel(id, id), + target="s2", + position="before", + ) + + @reactive.effect() + @reactive.event(input.removeFoo) + def _(): + ui.nav_remove("tabs", target="Foo") + + @reactive.effect() + @reactive.event(input.addFoo) + def _(): + n = str(input.addFoo()) + ui.nav_insert( + "tabs", + ui.nav_panel("Foo-" + n, "This is the new Foo-" + n + " tab", value="Foo"), + target="Menu", + position="before", + select=True, + ) + + @reactive.effect() + @reactive.event(input.hideTab) + def _(): + ui.nav_hide("tabs", target="Foo") + + @reactive.effect() + @reactive.event(input.showTab) + def _(): + ui.nav_show("tabs", target="Foo") + + @reactive.effect() + @reactive.event(input.hideMenu) + def _(): + ui.nav_hide("tabs", target="Menu") + + @reactive.effect() + @reactive.event(input.showMenu) + def _(): + ui.nav_show("tabs", target="Menu") + + +app = App(app_ui, server) diff --git a/examples/dynamic-tabs/requirements.txt b/examples/dynamic-tabs/requirements.txt new file mode 100644 index 000000000..3e78250b6 --- /dev/null +++ b/examples/dynamic-tabs/requirements.txt @@ -0,0 +1 @@ +shiny From 78f89d48773bb93eb46f3744af3b39357a022926 Mon Sep 17 00:00:00 2001 From: Carson Date: Thu, 24 Jul 2025 16:47:49 -0500 Subject: [PATCH 14/56] Add more ergonomic nav_insert() --- shiny/express/ui/__init__.py | 5 ++- shiny/express/ui/_insert.py | 79 ++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 shiny/express/ui/_insert.py diff --git a/shiny/express/ui/__init__.py b/shiny/express/ui/__init__.py index 4a9ba5bb5..a8ecb3dc3 100644 --- a/shiny/express/ui/__init__.py +++ b/shiny/express/ui/__init__.py @@ -165,6 +165,10 @@ hold, ) +from ._insert import ( + nav_insert, +) + __all__ = ( # Imports from htmltools "TagList", @@ -294,7 +298,6 @@ "navset_pill", "navset_pill_list", "nav_hide", - "nav_insert", "nav_remove", "nav_show", "navset_tab", diff --git a/shiny/express/ui/_insert.py b/shiny/express/ui/_insert.py new file mode 100644 index 000000000..baea30479 --- /dev/null +++ b/shiny/express/ui/_insert.py @@ -0,0 +1,79 @@ +""" +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 TagChild + +from ..._docstring import add_example +from ...session import Session + + +@add_example() +def nav_insert( + id: str, + title: str, + *args: TagChild, + value: Optional[str] = None, + icon: TagChild = None, + target: Optional[str] = None, + position: Literal["after", "before"] = "after", + select: bool = False, + session: Optional[Session] = None, +) -> None: + """ + Create a nav item pointing to some internal content. + + Parameters + ---------- + id + The id of the navset container to insert the item into. + title + A title to display. Can be a character string or UI elements (i.e., tags). + *args + UI elements to display when the item is active. + value + The value of the item. Use this value to determine whether the item is active + (when an `id` is provided to the nav container) or to programmatically + select the item (e.g., :func:`~shiny.ui.update_navs`). You can also + provide the value to the `selected` argument of the navigation container + (e.g., :func:`~shiny.ui.navset_tab`). + icon + An icon to appear inline with the button/link. + target + The `value` of an existing :func:`shiny.ui.nav` item, next to which tab will + be added. Can also be `None`; see `position`. + position + The position of the new nav item relative to the target nav item. If + `target=None`, then `"before"` means the new nav item should be inserted at + the head of the navlist, and `"after"` is the end. + select + Whether the nav item should be selected upon insertion. + session + A :class:`~shiny.Session` instance. If not provided, it is inferred via + :func:`~shiny.session.get_current_session`. + """ + + from ...ui import nav_insert, nav_panel + + panel = nav_panel( + title, + *args, + value=value, + icon=icon, + ) + + nav_insert( + id=id, + nav_panel=panel, + target=target, + position=position, + select=select, + session=session, + ) From 7e57fcef15a6a8acbd61c5fec3b6474e126a06e1 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Fri, 25 Jul 2025 00:28:50 -0400 Subject: [PATCH 15/56] Finishing visibility tests --- examples/dynamic-tabs/README.md | 3 - examples/dynamic-tabs/requirements.txt | 1 - shiny/api-examples/nav_insert/app-express.py | 12 ++- shiny/ui/_navs_dynamic.py | 24 +++--- .../shiny/components/dynamic_navs}/app.py | 6 +- .../dynamic_navs/test_navs_dynamic.py | 77 +++++++++++++++++++ 6 files changed, 100 insertions(+), 23 deletions(-) delete mode 100644 examples/dynamic-tabs/README.md delete mode 100644 examples/dynamic-tabs/requirements.txt rename {examples/dynamic-tabs => tests/playwright/shiny/components/dynamic_navs}/app.py (90%) create mode 100644 tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py diff --git a/examples/dynamic-tabs/README.md b/examples/dynamic-tabs/README.md deleted file mode 100644 index d64b5a6a4..000000000 --- a/examples/dynamic-tabs/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## Example of inserting, deleting showing, an hiding tabs - - diff --git a/examples/dynamic-tabs/requirements.txt b/examples/dynamic-tabs/requirements.txt deleted file mode 100644 index 3e78250b6..000000000 --- a/examples/dynamic-tabs/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -shiny diff --git a/shiny/api-examples/nav_insert/app-express.py b/shiny/api-examples/nav_insert/app-express.py index eea1c27d7..ebc7bc0c7 100644 --- a/shiny/api-examples/nav_insert/app-express.py +++ b/shiny/api-examples/nav_insert/app-express.py @@ -8,7 +8,7 @@ ui.input_action_button("addFoo", "Add New 'Foo' tab") with ui.navset_tab(id="set"): - with ui.nav_panel("Hello"): + with ui.nav_panel("Hello", value="Hello"): "This is the hello tab" with ui.nav_panel("Foo", value="Foo"): "This is the Foo tab" @@ -22,9 +22,12 @@ @reactive.event(input.add) def _(): id = "Dynamic-" + str(input.add()) + with ui.hold() as new_panel: + with ui.nav_panel(id, value=id): + pass ui.nav_insert( "tabs", - ui.nav_panel(id, value=id), + new_panel, target="s2", position="before", ) @@ -38,9 +41,12 @@ def _(): @reactive.event(input.addFoo) def _(): n = str(input.addFoo()) + with ui.hold() as new_panel: + with ui.nav_panel("Foo-" + n, value="Foo"): + "This is the new Foo-" + n + " tab" ui.nav_insert( "tabs", - ui.nav_panel("Foo-" + n, "This is the new Foo-" + n + " tab", value="Foo"), + new_panel[0], target="Menu", position="before", select=True, diff --git a/shiny/ui/_navs_dynamic.py b/shiny/ui/_navs_dynamic.py index fef31df58..68ba9e0b4 100644 --- a/shiny/ui/_navs_dynamic.py +++ b/shiny/ui/_navs_dynamic.py @@ -36,20 +36,20 @@ def nav_insert( Parameters ---------- id - The ``id`` of the relevant navigation container (i.e., ``navset_*()`` object). + The `id` of the relevant navigation container (i.e., `navset_*()` object). nav_panel The navigation item to insert (typically a :func:`~shiny.ui.nav_panel` or :func:`~shiny.ui.nav_menu`). A :func:`~shiny.ui.nav_menu` isn't allowed when the - ``target`` references an :func:`~shiny.ui.nav_menu` (or an item within it). A - string is only allowed when the ``target`` references a + `target` references an :func:`~shiny.ui.nav_menu` (or an item within it). A + string is only allowed when the `target` references a :func:`~shiny.ui.nav_menu`. target - The ``value`` of an existing :func:`shiny.ui.nav` item, next to which tab will - be added. Can also be ``None``; see ``position``. + The `value` of an existing :func:`shiny.ui.nav` item, next to which tab will + be added. Can also be `None`; see `position`. position The position of the new nav item relative to the target nav item. If - ``target=None``, then ``"before"`` means the new nav item should be inserted at - the head of the navlist, and ``"after"`` is the end. + `target=None`, then `"before"` means the new nav item should be inserted at + the head of the navlist, and `"after"` is the end. select Whether the nav item should be selected upon insertion. session @@ -92,7 +92,6 @@ def nav_insert( session._send_message_sync({"shiny-insert-tab": msg}) -@add_example() def nav_remove(id: str, target: str, session: Optional[Session] = None) -> None: """ Remove a nav item from a navigation container. @@ -100,9 +99,9 @@ def nav_remove(id: str, target: str, session: Optional[Session] = None) -> None: Parameters ---------- id - The ``id`` of the relevant navigation container (i.e., ``navset_*()`` object). + The `id` of the relevant navigation container (i.e., `navset_*()` object). target - The ``value`` of an existing :func:`shiny.ui.nav_panel` item to remove. + The `value` of an existing :func:`shiny.ui.nav_panel` item to remove. session A :class:`~shiny.Session` instance. If not provided, it is inferred via :func:`~shiny.session.get_current_session`. @@ -171,7 +170,6 @@ def nav_show( session._send_message_sync({"shiny-change-tab-visibility": msg}) -@add_example() def nav_hide(id: str, target: str, session: Optional[Session] = None) -> None: """ Hide a navigation item @@ -179,9 +177,9 @@ def nav_hide(id: str, target: str, session: Optional[Session] = None) -> None: Parameters ---------- id - The ``id`` of the relevant navigation container (i.e., ``navset_*()`` object). + The `id` of the relevant navigation container (i.e., `navset_*()` object). target - The ``value`` of an existing :func:`shiny.ui.nav` item to hide. + The `value` of an existing :func:`shiny.ui.nav` item to hide. session A :class:`~shiny.Session` instance. If not provided, it is inferred via :func:`~shiny.session.get_current_session`. diff --git a/examples/dynamic-tabs/app.py b/tests/playwright/shiny/components/dynamic_navs/app.py similarity index 90% rename from examples/dynamic-tabs/app.py rename to tests/playwright/shiny/components/dynamic_navs/app.py index 4418dcfb1..177ff45a5 100644 --- a/examples/dynamic-tabs/app.py +++ b/tests/playwright/shiny/components/dynamic_navs/app.py @@ -11,8 +11,8 @@ ui.input_action_button("showMenu", "Show 'Static' nav_menu"), ), ui.navset_tab( - ui.nav_panel("Hello", "This is the hello tab"), - ui.nav_panel("Foo", "This is the Foo tab", value="Foo"), + ui.nav_panel("Hello", "This is the hello tab", value="Hello"), + ui.nav_panel("Foo", "Foo", value="Foo"), ui.nav_menu( "Static", ui.nav_panel("Static 1", "Static 1", value="s1"), @@ -47,7 +47,7 @@ def _(): n = str(input.addFoo()) ui.nav_insert( "tabs", - ui.nav_panel("Foo-" + n, "This is the new Foo-" + n + " tab", value="Foo"), + ui.nav_panel("Foo-" + n, "Foo-" + n, value="Foo"), target="Menu", position="before", select=True, diff --git a/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py b/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py new file mode 100644 index 000000000..8d85c85f6 --- /dev/null +++ b/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py @@ -0,0 +1,77 @@ +# import pytest +import pytest +from playwright.sync_api import Page, expect + +from shiny.playwright import controller +from shiny.run import ShinyAppProc + + +@pytest.mark.flaky(reruns=3, reruns_delay=2) +def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: + page.goto(local_app.url) + + # Page begins with 2 tabs: "Hello" and "Foo" and a nav menu with 2 static items. + navset_tab = controller.NavsetTab(page, "tabs") + + # Click add-foo to add a new Foo tab + addfoo = controller.InputActionButton(page, "addFoo") + addfoo.click() + controller.NavsetTab(page, "tabs").expect_nav_titles( + ["Hello", "Foo", "Foo-1", "Static 1", "Static 2"] + ) + + # Click hide-tab to hide the Foo tabs + hidetab = controller.InputActionButton(page, "hideTab") + hidetab.click() + + # Expect the Foo tabs to be hidden + expect( + page.get_by_role("presentation").filter(has_text="This is the Foo tab") + ).to_be_hidden() + expect(page.get_by_role("presentation").filter(has_text="Foo-")).to_be_hidden() + + # Click show-tab to show the Foo tabs again + showtab = controller.InputActionButton(page, "showTab") + showtab.click() + + # Expect the Foo tabs to be visible again + expect( + page.get_by_role("presentation").filter(has_text="This is the Foo tab") + ).not_to_be_hidden() + expect(page.get_by_role("presentation").filter(has_text="Foo-")).not_to_be_hidden() + + # Click remove-foo to remove the Foo tabs + removefoo = controller.InputActionButton(page, "removeFoo") + removefoo.click() + controller.NavsetTab(page, "tabs").expect_nav_titles( + ["Hello", "Static 1", "Static 2"] + ) + + # Click add to add a dynamic tab + add = controller.InputActionButton(page, "add") + add.click() + controller.NavsetTab(page, "tabs").expect_nav_titles( + ["Hello", "Static 1", "Dynamic-1", "Static 2"] + ) + + # Click add again to add another dynamic tab + add.click() + controller.NavsetTab(page, "tabs").expect_nav_titles( + ["Hello", "Static 1", "Dynamic-1", "Dynamic-2", "Static 2"] + ) + + # Click hide-menu to hide the static menu + hidemenu = controller.InputActionButton(page, "hideMenu") + hidemenu.click() + + # Expect the Menu to be hidden + expect(page.get_by_role("presentation").filter(has_text="Static")).to_be_hidden() + + # Click show-menu to show the static menu again + showmenu = controller.InputActionButton(page, "showMenu") + showmenu.click() + + # Expect the Menu to be visible again + expect( + page.get_by_role("presentation").filter(has_text="Static") + ).not_to_be_hidden() From 1fa58d5fd3e9c25928f8c36e5705367689b5ad55 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Fri, 25 Jul 2025 11:08:13 -0400 Subject: [PATCH 16/56] PLAYWRIGHT WORKS --- .../shiny/components/dynamic_navs/app.py | 8 ++-- .../dynamic_navs/test_navs_dynamic.py | 47 ++++++++++++------- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/tests/playwright/shiny/components/dynamic_navs/app.py b/tests/playwright/shiny/components/dynamic_navs/app.py index 177ff45a5..661044382 100644 --- a/tests/playwright/shiny/components/dynamic_navs/app.py +++ b/tests/playwright/shiny/components/dynamic_navs/app.py @@ -12,11 +12,11 @@ ), ui.navset_tab( ui.nav_panel("Hello", "This is the hello tab", value="Hello"), - ui.nav_panel("Foo", "Foo", value="Foo"), + ui.nav_panel("Foo", "This is the Foo tab", value="Foo"), ui.nav_menu( - "Static", - ui.nav_panel("Static 1", "Static 1", value="s1"), - ui.nav_panel("Static 2", "Static 2", value="s2"), + "Menu", + ui.nav_panel("Static1", "Static1", value="s1"), + ui.nav_panel("Static2", "Static2", value="s2"), value="Menu", ), id="tabs", diff --git a/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py b/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py index 8d85c85f6..68a784407 100644 --- a/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py +++ b/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py @@ -17,61 +17,76 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: addfoo = controller.InputActionButton(page, "addFoo") addfoo.click() controller.NavsetTab(page, "tabs").expect_nav_titles( - ["Hello", "Foo", "Foo-1", "Static 1", "Static 2"] + ["Hello", "Foo", "Foo-1", "Static1", "Static2"] ) + # Test Foo-1 tab is added + navpanel = controller.NavPanel(page, "tabs", "Foo").loc.filter(has_text="Foo-1") + expect(navpanel).to_have_text("Foo-1") # Click hide-tab to hide the Foo tabs hidetab = controller.InputActionButton(page, "hideTab") hidetab.click() # Expect the Foo tabs to be hidden - expect( - page.get_by_role("presentation").filter(has_text="This is the Foo tab") - ).to_be_hidden() - expect(page.get_by_role("presentation").filter(has_text="Foo-")).to_be_hidden() + navpanel = controller.NavPanel(page, "tabs", "Foo").loc.filter(has_text="Foo-1") + expect(navpanel).to_be_hidden() + navpanel = controller.NavPanel(page, "tabs", "Foo").loc.filter( + has_text="This is the Foo tab" + ) + expect(navpanel).to_be_hidden() # Click show-tab to show the Foo tabs again showtab = controller.InputActionButton(page, "showTab") showtab.click() # Expect the Foo tabs to be visible again - expect( - page.get_by_role("presentation").filter(has_text="This is the Foo tab") - ).not_to_be_hidden() - expect(page.get_by_role("presentation").filter(has_text="Foo-")).not_to_be_hidden() + navpanel2 = controller.NavPanel(page, "tabs", "Foo").loc.first + expect(navpanel2).to_be_visible(timeout=10000) + navpanel3 = controller.NavPanel(page, "tabs", "Foo").loc.last + expect(navpanel3).to_be_visible(timeout=10000) # Click remove-foo to remove the Foo tabs removefoo = controller.InputActionButton(page, "removeFoo") removefoo.click() controller.NavsetTab(page, "tabs").expect_nav_titles( - ["Hello", "Static 1", "Static 2"] + ["Hello", "Static1", "Static2"] ) # Click add to add a dynamic tab add = controller.InputActionButton(page, "add") add.click() controller.NavsetTab(page, "tabs").expect_nav_titles( - ["Hello", "Static 1", "Dynamic-1", "Static 2"] + ["Hello", "Static1", "Dynamic-1", "Static2"] ) # Click add again to add another dynamic tab add.click() controller.NavsetTab(page, "tabs").expect_nav_titles( - ["Hello", "Static 1", "Dynamic-1", "Dynamic-2", "Static 2"] + ["Hello", "Static1", "Dynamic-1", "Dynamic-2", "Static2"] ) + page.get_by_role("button", name="Menu", exact=True).click() + + navpanel3 = controller.NavPanel(page, "tabs", "s1").loc + expect(navpanel3).to_be_visible(timeout=20000) + # Click hide-menu to hide the static menu hidemenu = controller.InputActionButton(page, "hideMenu") hidemenu.click() # Expect the Menu to be hidden - expect(page.get_by_role("presentation").filter(has_text="Static")).to_be_hidden() + navpanel3 = controller.NavPanel(page, "tabs", "s1").loc + expect(navpanel3).to_be_hidden() # Click show-menu to show the static menu again showmenu = controller.InputActionButton(page, "showMenu") showmenu.click() # Expect the Menu to be visible again - expect( - page.get_by_role("presentation").filter(has_text="Static") - ).not_to_be_hidden() + expect(page.get_by_role("button", name="Menu", exact=True)).to_be_visible( + timeout=10000 + ) + # Click the Menu button to show the static menu + page.get_by_role("button", name="Menu", exact=True).click() + navpanel3 = controller.NavPanel(page, "tabs", "s1").loc + expect(navpanel3).to_be_visible(timeout=20000) From eff71c72e291e4f4594073562e49a5af4c2a9a99 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Fri, 25 Jul 2025 11:46:51 -0400 Subject: [PATCH 17/56] Removing timeout --- .../components/dynamic_navs/test_navs_dynamic.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py b/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py index 68a784407..cf17951a5 100644 --- a/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py +++ b/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py @@ -41,9 +41,9 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: # Expect the Foo tabs to be visible again navpanel2 = controller.NavPanel(page, "tabs", "Foo").loc.first - expect(navpanel2).to_be_visible(timeout=10000) + expect(navpanel2).to_be_visible() navpanel3 = controller.NavPanel(page, "tabs", "Foo").loc.last - expect(navpanel3).to_be_visible(timeout=10000) + expect(navpanel3).to_be_visible() # Click remove-foo to remove the Foo tabs removefoo = controller.InputActionButton(page, "removeFoo") @@ -68,7 +68,7 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: page.get_by_role("button", name="Menu", exact=True).click() navpanel3 = controller.NavPanel(page, "tabs", "s1").loc - expect(navpanel3).to_be_visible(timeout=20000) + expect(navpanel3).to_be_visible() # Click hide-menu to hide the static menu hidemenu = controller.InputActionButton(page, "hideMenu") @@ -83,10 +83,8 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: showmenu.click() # Expect the Menu to be visible again - expect(page.get_by_role("button", name="Menu", exact=True)).to_be_visible( - timeout=10000 - ) + expect(page.get_by_role("button", name="Menu", exact=True)).to_be_visible() # Click the Menu button to show the static menu page.get_by_role("button", name="Menu", exact=True).click() navpanel3 = controller.NavPanel(page, "tabs", "s1").loc - expect(navpanel3).to_be_visible(timeout=20000) + expect(navpanel3).to_be_visible() From 2cf6331fb59db21d31243d3eae7b25282b6c42d0 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Fri, 25 Jul 2025 11:55:34 -0400 Subject: [PATCH 18/56] Renaming --- shiny/api-examples/nav_insert/app-core.py | 6 +-- shiny/api-examples/nav_insert/app-express.py | 6 +-- shiny/api-examples/nav_show/app-core.py | 8 ++-- shiny/api-examples/nav_show/app-express.py | 8 ++-- shiny/express/ui/__init__.py | 16 ++++---- shiny/express/ui/_insert.py | 6 +-- shiny/ui/__init__.py | 16 ++++---- shiny/ui/_navs_dynamic.py | 40 +++++++++---------- .../shiny/components/dynamic_navs/app.py | 14 +++---- 9 files changed, 60 insertions(+), 60 deletions(-) diff --git a/shiny/api-examples/nav_insert/app-core.py b/shiny/api-examples/nav_insert/app-core.py index d53dd6379..95f129fd7 100644 --- a/shiny/api-examples/nav_insert/app-core.py +++ b/shiny/api-examples/nav_insert/app-core.py @@ -25,7 +25,7 @@ def server(input: Inputs, output: Outputs, session: Session): @reactive.event(input.add) def _(): id = "Dynamic-" + str(input.add()) - ui.nav_insert( + ui.insert_nav_panel( "tabs", ui.nav_panel(id, id), target="s2", @@ -35,13 +35,13 @@ def _(): @reactive.effect() @reactive.event(input.removeFoo) def _(): - ui.nav_remove("tabs", target="Foo") + ui.remove_nav_panel("tabs", target="Foo") @reactive.effect() @reactive.event(input.addFoo) def _(): n = str(input.addFoo()) - ui.nav_insert( + ui.insert_nav_panel( "tabs", ui.nav_panel("Foo-" + n, "This is the new Foo-" + n + " tab", value="Foo"), target="Menu", diff --git a/shiny/api-examples/nav_insert/app-express.py b/shiny/api-examples/nav_insert/app-express.py index ebc7bc0c7..3d3a8930b 100644 --- a/shiny/api-examples/nav_insert/app-express.py +++ b/shiny/api-examples/nav_insert/app-express.py @@ -25,7 +25,7 @@ def _(): with ui.hold() as new_panel: with ui.nav_panel(id, value=id): pass - ui.nav_insert( + ui.insert_nav_panel( "tabs", new_panel, target="s2", @@ -35,7 +35,7 @@ def _(): @reactive.effect() @reactive.event(input.removeFoo) def _(): - ui.nav_remove("set", target="Foo") + ui.remove_nav_panel("set", target="Foo") @reactive.effect() @reactive.event(input.addFoo) @@ -44,7 +44,7 @@ def _(): with ui.hold() as new_panel: with ui.nav_panel("Foo-" + n, value="Foo"): "This is the new Foo-" + n + " tab" - ui.nav_insert( + ui.insert_nav_panel( "tabs", new_panel[0], target="Menu", diff --git a/shiny/api-examples/nav_show/app-core.py b/shiny/api-examples/nav_show/app-core.py index 5fb0169ef..7a1ceefe1 100644 --- a/shiny/api-examples/nav_show/app-core.py +++ b/shiny/api-examples/nav_show/app-core.py @@ -31,22 +31,22 @@ def server(input: Inputs, output: Outputs, session: Session): @reactive.effect() @reactive.event(input.hideTab) def _(): - ui.nav_hide("tabs", target="Foo") + ui.hide_nav_panel("tabs", target="Foo") @reactive.effect() @reactive.event(input.showTab) def _(): - ui.nav_show("tabs", target="Foo") + ui.show_nav_panel("tabs", target="Foo") @reactive.effect() @reactive.event(input.hideMenu) def _(): - ui.nav_hide("tabs", target="More") + ui.hide_nav_panel("tabs", target="More") @reactive.effect() @reactive.event(input.showMenu) def _(): - ui.nav_show("tabs", target="More") + ui.show_nav_panel("tabs", target="More") app = App(app_ui, server) diff --git a/shiny/api-examples/nav_show/app-express.py b/shiny/api-examples/nav_show/app-express.py index af81ebe23..4441eb2c8 100644 --- a/shiny/api-examples/nav_show/app-express.py +++ b/shiny/api-examples/nav_show/app-express.py @@ -27,19 +27,19 @@ @reactive.effect() @reactive.event(input.hideTab) def _(): - ui.nav_hide("tabs", target="Foo") + ui.hide_nav_panel("tabs", target="Foo") @reactive.effect() @reactive.event(input.showTab) def _(): - ui.nav_show("tabs", target="Foo") + ui.show_nav_panel("tabs", target="Foo") @reactive.effect() @reactive.event(input.hideMenu) def _(): - ui.nav_hide("tabs", target="More") + ui.hide_nav_panel("tabs", target="More") @reactive.effect() @reactive.event(input.showMenu) def _(): - ui.nav_show("tabs", target="More") + ui.show_nav_panel("tabs", target="More") diff --git a/shiny/express/ui/__init__.py b/shiny/express/ui/__init__.py index a8ecb3dc3..c1253034b 100644 --- a/shiny/express/ui/__init__.py +++ b/shiny/express/ui/__init__.py @@ -109,10 +109,10 @@ notification_remove, nav_spacer, navbar_options, - nav_hide, - nav_insert, - nav_remove, - nav_show, + hide_nav_panel, + insert_nav_panel, + remove_nav_panel, + show_nav_panel, Progress, Theme, value_box_theme, @@ -166,7 +166,7 @@ ) from ._insert import ( - nav_insert, + insert_nav_panel, ) __all__ = ( @@ -297,9 +297,9 @@ "navset_hidden", "navset_pill", "navset_pill_list", - "nav_hide", - "nav_remove", - "nav_show", + "hide_nav_panel", + "remove_nav_panel", + "show_nav_panel", "navset_tab", "navset_underline", "navbar_options", diff --git a/shiny/express/ui/_insert.py b/shiny/express/ui/_insert.py index baea30479..1b78b32a8 100644 --- a/shiny/express/ui/_insert.py +++ b/shiny/express/ui/_insert.py @@ -16,7 +16,7 @@ @add_example() -def nav_insert( +def insert_nav_panel( id: str, title: str, *args: TagChild, @@ -60,7 +60,7 @@ def nav_insert( :func:`~shiny.session.get_current_session`. """ - from ...ui import nav_insert, nav_panel + from ...ui import insert_nav_panel, nav_panel panel = nav_panel( title, @@ -69,7 +69,7 @@ def nav_insert( icon=icon, ) - nav_insert( + insert_nav_panel( id=id, nav_panel=panel, target=target, diff --git a/shiny/ui/__init__.py b/shiny/ui/__init__.py index f4ddbbb79..0354f5c38 100644 --- a/shiny/ui/__init__.py +++ b/shiny/ui/__init__.py @@ -129,10 +129,10 @@ navset_underline, ) from ._navs_dynamic import ( - nav_hide, - nav_insert, - nav_remove, - nav_show, + hide_nav_panel, + insert_nav_panel, + remove_nav_panel, + show_nav_panel, ) from ._notification import notification_remove, notification_show from ._output import ( @@ -303,10 +303,10 @@ "navset_pill_list", "navset_hidden", "navset_bar", - "nav_hide", - "nav_insert", - "nav_remove", - "nav_show", + "hide_nav_panel", + "insert_nav_panel", + "remove_nav_panel", + "show_nav_panel", "navbar_options", # _notification "notification_show", diff --git a/shiny/ui/_navs_dynamic.py b/shiny/ui/_navs_dynamic.py index 68ba9e0b4..064256ac2 100644 --- a/shiny/ui/_navs_dynamic.py +++ b/shiny/ui/_navs_dynamic.py @@ -1,8 +1,8 @@ __all__ = ( - "nav_insert", - "nav_remove", - "nav_hide", - "nav_show", + "insert_nav_panel", + "remove_nav_panel", + "hide_nav_panel", + "show_nav_panel", ) import sys @@ -22,7 +22,7 @@ @add_example() -def nav_insert( +def insert_nav_panel( id: str, nav_panel: Union[NavSetArg, str], target: Optional[str] = None, @@ -58,8 +58,8 @@ def nav_insert( See Also -------- - ~nav_remove - ~nav_show + ~remove_nav_panel + ~show_nav_panel ~nav_hide ~shiny.ui.nav_panel """ @@ -92,7 +92,7 @@ def nav_insert( session._send_message_sync({"shiny-insert-tab": msg}) -def nav_remove(id: str, target: str, session: Optional[Session] = None) -> None: +def remove_nav_panel(id: str, target: str, session: Optional[Session] = None) -> None: """ Remove a nav item from a navigation container. @@ -108,8 +108,8 @@ def nav_remove(id: str, target: str, session: Optional[Session] = None) -> None: See Also -------- - ~nav_insert - ~nav_show + ~insert_nav_panel + ~show_nav_panel ~nav_hide ~shiny.ui.nav_panel """ @@ -124,7 +124,7 @@ def nav_remove(id: str, target: str, session: Optional[Session] = None) -> None: @add_example() -def nav_show( +def show_nav_panel( id: str, target: str, select: bool = False, session: Optional[Session] = None ) -> None: """ @@ -133,9 +133,9 @@ def nav_show( Parameters ---------- id - The ``id`` of the relevant navigation container (i.e., ``navset_*()`` object). + The `id` of the relevant navigation container (i.e., `navset_*()` object). target - The ``value`` of an existing :func:`shiny.ui.nav` item to show. + The `value` of an existing :func:`shiny.ui.nav` item to show. select Whether the nav item's content should also be shown. session @@ -144,14 +144,14 @@ def nav_show( Note ---- - For ``nav_show()`` to be relevant/useful, a :func:`shiny.ui.nav` item must + For `show_nav_panel()` to be relevant/useful, a :func:`shiny.ui.nav` item must have been hidden using :func:`~nav_hide`. See Also -------- ~nav_hide - ~nav_insert - ~nav_remove + ~insert_nav_panel + ~remove_nav_panel ~shiny.ui.nav_panel """ @@ -170,7 +170,7 @@ def nav_show( session._send_message_sync({"shiny-change-tab-visibility": msg}) -def nav_hide(id: str, target: str, session: Optional[Session] = None) -> None: +def hide_nav_panel(id: str, target: str, session: Optional[Session] = None) -> None: """ Hide a navigation item @@ -186,9 +186,9 @@ def nav_hide(id: str, target: str, session: Optional[Session] = None) -> None: See Also -------- - ~nav_show - ~nav_insert - ~nav_remove + ~show_nav_panel + ~insert_nav_panel + ~remove_nav_panel ~shiny.ui.nav_panel """ diff --git a/tests/playwright/shiny/components/dynamic_navs/app.py b/tests/playwright/shiny/components/dynamic_navs/app.py index 661044382..c2698dc96 100644 --- a/tests/playwright/shiny/components/dynamic_navs/app.py +++ b/tests/playwright/shiny/components/dynamic_navs/app.py @@ -29,7 +29,7 @@ def server(input: Inputs, output: Outputs, session: Session): @reactive.event(input.add) def _(): id = "Dynamic-" + str(input.add()) - ui.nav_insert( + ui.insert_nav_panel( "tabs", ui.nav_panel(id, id), target="s2", @@ -39,13 +39,13 @@ def _(): @reactive.effect() @reactive.event(input.removeFoo) def _(): - ui.nav_remove("tabs", target="Foo") + ui.remove_nav_panel("tabs", target="Foo") @reactive.effect() @reactive.event(input.addFoo) def _(): n = str(input.addFoo()) - ui.nav_insert( + ui.insert_nav_panel( "tabs", ui.nav_panel("Foo-" + n, "Foo-" + n, value="Foo"), target="Menu", @@ -56,22 +56,22 @@ def _(): @reactive.effect() @reactive.event(input.hideTab) def _(): - ui.nav_hide("tabs", target="Foo") + ui.hide_nav_panel("tabs", target="Foo") @reactive.effect() @reactive.event(input.showTab) def _(): - ui.nav_show("tabs", target="Foo") + ui.show_nav_panel("tabs", target="Foo") @reactive.effect() @reactive.event(input.hideMenu) def _(): - ui.nav_hide("tabs", target="Menu") + ui.hide_nav_panel("tabs", target="Menu") @reactive.effect() @reactive.event(input.showMenu) def _(): - ui.nav_show("tabs", target="Menu") + ui.show_nav_panel("tabs", target="Menu") app = App(app_ui, server) From 3cfa66bba9c49908ffe3d440e979a5da68b91b36 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Fri, 25 Jul 2025 12:15:31 -0400 Subject: [PATCH 19/56] Updated express nav_insert --- shiny/api-examples/nav_insert/app-express.py | 93 +++++++++----------- shiny/ui/_navs_dynamic.py | 8 +- 2 files changed, 47 insertions(+), 54 deletions(-) diff --git a/shiny/api-examples/nav_insert/app-express.py b/shiny/api-examples/nav_insert/app-express.py index 3d3a8930b..112bb55df 100644 --- a/shiny/api-examples/nav_insert/app-express.py +++ b/shiny/api-examples/nav_insert/app-express.py @@ -1,53 +1,46 @@ from shiny import reactive from shiny.express import input, ui -with ui.layout_sidebar(): - with ui.sidebar(): - ui.input_action_button("add", "Add 'Dynamic' tab") - ui.input_action_button("removeFoo", "Remove 'Foo' tabs") - ui.input_action_button("addFoo", "Add New 'Foo' tab") - - with ui.navset_tab(id="set"): - with ui.nav_panel("Hello", value="Hello"): - "This is the hello tab" - with ui.nav_panel("Foo", value="Foo"): - "This is the Foo tab" - with ui.nav_menu("Static", value="tabs"): - with ui.nav_panel("Static 1", value="s1"): - "Static 1" - with ui.nav_panel("Static 2", value="s2"): - "Static 2" - - @reactive.effect() - @reactive.event(input.add) - def _(): - id = "Dynamic-" + str(input.add()) - with ui.hold() as new_panel: - with ui.nav_panel(id, value=id): - pass - ui.insert_nav_panel( - "tabs", - new_panel, - target="s2", - position="before", - ) - - @reactive.effect() - @reactive.event(input.removeFoo) - def _(): - ui.remove_nav_panel("set", target="Foo") - - @reactive.effect() - @reactive.event(input.addFoo) - def _(): - n = str(input.addFoo()) - with ui.hold() as new_panel: - with ui.nav_panel("Foo-" + n, value="Foo"): - "This is the new Foo-" + n + " tab" - ui.insert_nav_panel( - "tabs", - new_panel[0], - target="Menu", - position="before", - select=True, - ) +with ui.sidebar(): + ui.input_action_button("add", "Add 'Dynamic' tab") + ui.input_action_button("removeFoo", "Remove 'Foo' tabs") + ui.input_action_button("addFoo", "Add New 'Foo' tab") + +with ui.navset_tab(id="tabs"): + with ui.nav_panel("Hello", value="Hello"): + "This is the hello tab" + with ui.nav_panel("Foo", value="Foo"): + "This is the Foo tab" + with ui.nav_menu("Static", value="Menu"): + with ui.nav_panel("Static 1", value="s1"): + "Static 1" + with ui.nav_panel("Static 2", value="s2"): + "Static 2" + + +@reactive.effect() +@reactive.event(input.add) +def _(): + id = "Dynamic-" + str(input.add()) + ui.insert_nav_panel("tabs", title=id, value=id, target="s2", position="before") + + +@reactive.effect() +@reactive.event(input.removeFoo) +def _(): + ui.remove_nav_panel("tabs", target="Foo") + + +@reactive.effect() +@reactive.event(input.addFoo) +def _(): + n = str(input.addFoo()) + ui.insert_nav_panel( + "tabs", + "Foo-" + n, + "This is the new Foo-" + n + " tab", + value="Foo", + target="Menu", + position="before", + select=True, + ) diff --git a/shiny/ui/_navs_dynamic.py b/shiny/ui/_navs_dynamic.py index 064256ac2..fa22da248 100644 --- a/shiny/ui/_navs_dynamic.py +++ b/shiny/ui/_navs_dynamic.py @@ -60,7 +60,7 @@ def insert_nav_panel( -------- ~remove_nav_panel ~show_nav_panel - ~nav_hide + ~hide_nav_panel ~shiny.ui.nav_panel """ @@ -110,7 +110,7 @@ def remove_nav_panel(id: str, target: str, session: Optional[Session] = None) -> -------- ~insert_nav_panel ~show_nav_panel - ~nav_hide + ~hide_nav_panel ~shiny.ui.nav_panel """ @@ -145,11 +145,11 @@ def show_nav_panel( Note ---- For `show_nav_panel()` to be relevant/useful, a :func:`shiny.ui.nav` item must - have been hidden using :func:`~nav_hide`. + have been hidden using :func:`~hide_nav_panel`. See Also -------- - ~nav_hide + ~hide_nav_panel ~insert_nav_panel ~remove_nav_panel ~shiny.ui.nav_panel From b350733ac4d66ec56a3239c089522f63dac752d8 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Fri, 25 Jul 2025 12:17:24 -0400 Subject: [PATCH 20/56] Rename folders for api-examples to show up right in docs --- shiny/api-examples/{nav_insert => insert_nav_panel}/app-core.py | 0 .../api-examples/{nav_insert => insert_nav_panel}/app-express.py | 0 shiny/api-examples/{nav_show => show_nav_panel}/app-core.py | 0 shiny/api-examples/{nav_show => show_nav_panel}/app-express.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename shiny/api-examples/{nav_insert => insert_nav_panel}/app-core.py (100%) rename shiny/api-examples/{nav_insert => insert_nav_panel}/app-express.py (100%) rename shiny/api-examples/{nav_show => show_nav_panel}/app-core.py (100%) rename shiny/api-examples/{nav_show => show_nav_panel}/app-express.py (100%) diff --git a/shiny/api-examples/nav_insert/app-core.py b/shiny/api-examples/insert_nav_panel/app-core.py similarity index 100% rename from shiny/api-examples/nav_insert/app-core.py rename to shiny/api-examples/insert_nav_panel/app-core.py diff --git a/shiny/api-examples/nav_insert/app-express.py b/shiny/api-examples/insert_nav_panel/app-express.py similarity index 100% rename from shiny/api-examples/nav_insert/app-express.py rename to shiny/api-examples/insert_nav_panel/app-express.py diff --git a/shiny/api-examples/nav_show/app-core.py b/shiny/api-examples/show_nav_panel/app-core.py similarity index 100% rename from shiny/api-examples/nav_show/app-core.py rename to shiny/api-examples/show_nav_panel/app-core.py diff --git a/shiny/api-examples/nav_show/app-express.py b/shiny/api-examples/show_nav_panel/app-express.py similarity index 100% rename from shiny/api-examples/nav_show/app-express.py rename to shiny/api-examples/show_nav_panel/app-express.py From f74e9388570167a7fef9d9dd255a73a0e402a3d9 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Fri, 25 Jul 2025 16:26:06 -0400 Subject: [PATCH 21/56] Initial untested module express example --- .../shiny/components/express_navs/app.py | 28 ++++++ .../shiny/components/express_navs/modules.py | 70 +++++++++++++++ .../express_navs/test_express_navs.py | 90 +++++++++++++++++++ 3 files changed, 188 insertions(+) create mode 100644 tests/playwright/shiny/components/express_navs/app.py create mode 100644 tests/playwright/shiny/components/express_navs/modules.py create mode 100644 tests/playwright/shiny/components/express_navs/test_express_navs.py diff --git a/tests/playwright/shiny/components/express_navs/app.py b/tests/playwright/shiny/components/express_navs/app.py new file mode 100644 index 000000000..4241c3266 --- /dev/null +++ b/tests/playwright/shiny/components/express_navs/app.py @@ -0,0 +1,28 @@ +from shiny import reactive +from shiny.express import render +from .modules import custom_sidebar, add_to_counter_content + +tabs_added = reactive.value(0) + + +def increment_tabs_counter(): + tabs_added.set(tabs_added() + 1) + + +custom_sidebar( + "nav", + _on_click=increment_tabs_counter, + label="Dynamic Sidebar", +) + +add_to_counter_content( + "buttonAdder", + _on_click=increment_tabs_counter, + starting_value=0, + label="Add to Counter", +) + + +@render.code +def out(): + return f"Tabs added: {tabs_added()}" diff --git a/tests/playwright/shiny/components/express_navs/modules.py b/tests/playwright/shiny/components/express_navs/modules.py new file mode 100644 index 000000000..4abfd9006 --- /dev/null +++ b/tests/playwright/shiny/components/express_navs/modules.py @@ -0,0 +1,70 @@ +from shiny import reactive +from shiny.express import ui, module + + +@module +def add_to_counter_content( + input, output, session, _on_click, starting_value, label="Add to Counter" +): + count = reactive.value(starting_value) + ui.input_action_button("add", label) + + @reactive.effect + @reactive.event(input.add) + def increment_button(): + _on_click() + count.set(count() + 1) + + +@module +def custom_sidebar( + input, output, session, _on_click, starting_value=0, label="Dynamic Sidebar" +): + count = reactive.value(starting_value) + + @reactive.effect + @reactive.event(input.addFoo) + def increment_button(): + _on_click() + count.set(count() + 1) + + with ui.sidebar(): + ui.input_action_button("add", "Add 'Dynamic' tab") + ui.input_action_button("removeFoo", "Remove 'Foo' tabs") + ui.input_action_button("addFoo", "Add New 'Foo' tab") + + @reactive.effect() + @reactive.event(input.removeFoo) + def _(): + ui.remove_nav_panel("tabs", target="Foo") + + @reactive.effect() + @reactive.event(input.addFoo) + def _(): + n = str(input.addFoo()) + ui.insert_nav_panel( + "tabs", + "Foo-" + n, + "This is the new Foo-" + n + " tab", + value="Foo", + target="Menu", + position="before", + select=True, + ) + + with ui.navset_tab(id="tabs"): + with ui.nav_panel("Hello", value="Hello"): + "This is the hello tab" + with ui.nav_panel("Foo", value="Foo"): + "This is the Foo tab" + with ui.nav_menu("Static", value="Menu"): + with ui.nav_panel("Static 1", value="s1"): + "Static 1" + with ui.nav_panel("Static 2", value="s2"): + "Static 2" + + @reactive.effect() + @reactive.event(input.add) + def _(): + id = "Dynamic-" + str(input.add()) + ui.insert_nav_panel("tabs", title=id, value=id, target="s2", position="before") diff --git a/tests/playwright/shiny/components/express_navs/test_express_navs.py b/tests/playwright/shiny/components/express_navs/test_express_navs.py new file mode 100644 index 000000000..cf17951a5 --- /dev/null +++ b/tests/playwright/shiny/components/express_navs/test_express_navs.py @@ -0,0 +1,90 @@ +# import pytest +import pytest +from playwright.sync_api import Page, expect + +from shiny.playwright import controller +from shiny.run import ShinyAppProc + + +@pytest.mark.flaky(reruns=3, reruns_delay=2) +def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: + page.goto(local_app.url) + + # Page begins with 2 tabs: "Hello" and "Foo" and a nav menu with 2 static items. + navset_tab = controller.NavsetTab(page, "tabs") + + # Click add-foo to add a new Foo tab + addfoo = controller.InputActionButton(page, "addFoo") + addfoo.click() + controller.NavsetTab(page, "tabs").expect_nav_titles( + ["Hello", "Foo", "Foo-1", "Static1", "Static2"] + ) + # Test Foo-1 tab is added + navpanel = controller.NavPanel(page, "tabs", "Foo").loc.filter(has_text="Foo-1") + expect(navpanel).to_have_text("Foo-1") + + # Click hide-tab to hide the Foo tabs + hidetab = controller.InputActionButton(page, "hideTab") + hidetab.click() + + # Expect the Foo tabs to be hidden + navpanel = controller.NavPanel(page, "tabs", "Foo").loc.filter(has_text="Foo-1") + expect(navpanel).to_be_hidden() + navpanel = controller.NavPanel(page, "tabs", "Foo").loc.filter( + has_text="This is the Foo tab" + ) + expect(navpanel).to_be_hidden() + + # Click show-tab to show the Foo tabs again + showtab = controller.InputActionButton(page, "showTab") + showtab.click() + + # Expect the Foo tabs to be visible again + navpanel2 = controller.NavPanel(page, "tabs", "Foo").loc.first + expect(navpanel2).to_be_visible() + navpanel3 = controller.NavPanel(page, "tabs", "Foo").loc.last + expect(navpanel3).to_be_visible() + + # Click remove-foo to remove the Foo tabs + removefoo = controller.InputActionButton(page, "removeFoo") + removefoo.click() + controller.NavsetTab(page, "tabs").expect_nav_titles( + ["Hello", "Static1", "Static2"] + ) + + # Click add to add a dynamic tab + add = controller.InputActionButton(page, "add") + add.click() + controller.NavsetTab(page, "tabs").expect_nav_titles( + ["Hello", "Static1", "Dynamic-1", "Static2"] + ) + + # Click add again to add another dynamic tab + add.click() + controller.NavsetTab(page, "tabs").expect_nav_titles( + ["Hello", "Static1", "Dynamic-1", "Dynamic-2", "Static2"] + ) + + page.get_by_role("button", name="Menu", exact=True).click() + + navpanel3 = controller.NavPanel(page, "tabs", "s1").loc + expect(navpanel3).to_be_visible() + + # Click hide-menu to hide the static menu + hidemenu = controller.InputActionButton(page, "hideMenu") + hidemenu.click() + + # Expect the Menu to be hidden + navpanel3 = controller.NavPanel(page, "tabs", "s1").loc + expect(navpanel3).to_be_hidden() + + # Click show-menu to show the static menu again + showmenu = controller.InputActionButton(page, "showMenu") + showmenu.click() + + # Expect the Menu to be visible again + expect(page.get_by_role("button", name="Menu", exact=True)).to_be_visible() + # Click the Menu button to show the static menu + page.get_by_role("button", name="Menu", exact=True).click() + navpanel3 = controller.NavPanel(page, "tabs", "s1").loc + expect(navpanel3).to_be_visible() From 48180918f8ef4b8f67cf5cf8b865407e2bd4a36b Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Fri, 25 Jul 2025 17:00:13 -0400 Subject: [PATCH 22/56] Test navs --- .../shiny/components/express_navs/app.py | 1 + .../express_navs/test_express_navs.py | 90 ------------------- 2 files changed, 1 insertion(+), 90 deletions(-) delete mode 100644 tests/playwright/shiny/components/express_navs/test_express_navs.py diff --git a/tests/playwright/shiny/components/express_navs/app.py b/tests/playwright/shiny/components/express_navs/app.py index 4241c3266..cc5d4d693 100644 --- a/tests/playwright/shiny/components/express_navs/app.py +++ b/tests/playwright/shiny/components/express_navs/app.py @@ -15,6 +15,7 @@ def increment_tabs_counter(): label="Dynamic Sidebar", ) + add_to_counter_content( "buttonAdder", _on_click=increment_tabs_counter, diff --git a/tests/playwright/shiny/components/express_navs/test_express_navs.py b/tests/playwright/shiny/components/express_navs/test_express_navs.py deleted file mode 100644 index cf17951a5..000000000 --- a/tests/playwright/shiny/components/express_navs/test_express_navs.py +++ /dev/null @@ -1,90 +0,0 @@ -# import pytest -import pytest -from playwright.sync_api import Page, expect - -from shiny.playwright import controller -from shiny.run import ShinyAppProc - - -@pytest.mark.flaky(reruns=3, reruns_delay=2) -def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: - page.goto(local_app.url) - - # Page begins with 2 tabs: "Hello" and "Foo" and a nav menu with 2 static items. - navset_tab = controller.NavsetTab(page, "tabs") - - # Click add-foo to add a new Foo tab - addfoo = controller.InputActionButton(page, "addFoo") - addfoo.click() - controller.NavsetTab(page, "tabs").expect_nav_titles( - ["Hello", "Foo", "Foo-1", "Static1", "Static2"] - ) - # Test Foo-1 tab is added - navpanel = controller.NavPanel(page, "tabs", "Foo").loc.filter(has_text="Foo-1") - expect(navpanel).to_have_text("Foo-1") - - # Click hide-tab to hide the Foo tabs - hidetab = controller.InputActionButton(page, "hideTab") - hidetab.click() - - # Expect the Foo tabs to be hidden - navpanel = controller.NavPanel(page, "tabs", "Foo").loc.filter(has_text="Foo-1") - expect(navpanel).to_be_hidden() - navpanel = controller.NavPanel(page, "tabs", "Foo").loc.filter( - has_text="This is the Foo tab" - ) - expect(navpanel).to_be_hidden() - - # Click show-tab to show the Foo tabs again - showtab = controller.InputActionButton(page, "showTab") - showtab.click() - - # Expect the Foo tabs to be visible again - navpanel2 = controller.NavPanel(page, "tabs", "Foo").loc.first - expect(navpanel2).to_be_visible() - navpanel3 = controller.NavPanel(page, "tabs", "Foo").loc.last - expect(navpanel3).to_be_visible() - - # Click remove-foo to remove the Foo tabs - removefoo = controller.InputActionButton(page, "removeFoo") - removefoo.click() - controller.NavsetTab(page, "tabs").expect_nav_titles( - ["Hello", "Static1", "Static2"] - ) - - # Click add to add a dynamic tab - add = controller.InputActionButton(page, "add") - add.click() - controller.NavsetTab(page, "tabs").expect_nav_titles( - ["Hello", "Static1", "Dynamic-1", "Static2"] - ) - - # Click add again to add another dynamic tab - add.click() - controller.NavsetTab(page, "tabs").expect_nav_titles( - ["Hello", "Static1", "Dynamic-1", "Dynamic-2", "Static2"] - ) - - page.get_by_role("button", name="Menu", exact=True).click() - - navpanel3 = controller.NavPanel(page, "tabs", "s1").loc - expect(navpanel3).to_be_visible() - - # Click hide-menu to hide the static menu - hidemenu = controller.InputActionButton(page, "hideMenu") - hidemenu.click() - - # Expect the Menu to be hidden - navpanel3 = controller.NavPanel(page, "tabs", "s1").loc - expect(navpanel3).to_be_hidden() - - # Click show-menu to show the static menu again - showmenu = controller.InputActionButton(page, "showMenu") - showmenu.click() - - # Expect the Menu to be visible again - expect(page.get_by_role("button", name="Menu", exact=True)).to_be_visible() - # Click the Menu button to show the static menu - page.get_by_role("button", name="Menu", exact=True).click() - navpanel3 = controller.NavPanel(page, "tabs", "s1").loc - expect(navpanel3).to_be_visible() From 53e0bfe40a50c3c9b436f559552429f009d65124 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Tue, 29 Jul 2025 09:53:24 -0400 Subject: [PATCH 23/56] Updating express test --- .../api-examples/insert_nav_panel/app-core.py | 24 +++++++ .../insert_nav_panel/app-express.py | 9 +++ .../shiny/components/express_navs/app.py | 66 +++++++++-------- .../shiny/components/express_navs/modules.py | 70 ------------------- .../express_navs/test_express_navs.py | 42 +++++++++++ 5 files changed, 113 insertions(+), 98 deletions(-) delete mode 100644 tests/playwright/shiny/components/express_navs/modules.py create mode 100644 tests/playwright/shiny/components/express_navs/test_express_navs.py diff --git a/shiny/api-examples/insert_nav_panel/app-core.py b/shiny/api-examples/insert_nav_panel/app-core.py index 95f129fd7..45b9c0c46 100644 --- a/shiny/api-examples/insert_nav_panel/app-core.py +++ b/shiny/api-examples/insert_nav_panel/app-core.py @@ -5,6 +5,7 @@ ui.input_action_button("add", "Add 'Dynamic' tab"), ui.input_action_button("removeFoo", "Remove 'Foo' tabs"), ui.input_action_button("addFoo", "Add New 'Foo' tab"), + ui.input_action_button("addTextPanel", "Add Text Panel"), ), ui.navset_tab( ui.nav_panel("Hello", "This is the hello tab"), @@ -32,6 +33,17 @@ def _(): position="before", ) + @reactive.effect() + @reactive.event(input.addTextPanel) + def _(): + id = "Text-" + str(input.addTextPanel()) + ui.insert_nav_panel( + "tabs", + id, + target="s2", + position="before", + ) + @reactive.effect() @reactive.event(input.removeFoo) def _(): @@ -49,5 +61,17 @@ def _(): select=True, ) + @reactive.effect() + @reactive.event(input.addTextPanel) + def _(): + n = str(input.addFoo()) + ui.insert_nav_panel( + "tabs", + "Placeholder Text Panel", + target="Menu", + position="before", + select=True, + ) + app = App(app_ui, server) diff --git a/shiny/api-examples/insert_nav_panel/app-express.py b/shiny/api-examples/insert_nav_panel/app-express.py index 112bb55df..6e461af9c 100644 --- a/shiny/api-examples/insert_nav_panel/app-express.py +++ b/shiny/api-examples/insert_nav_panel/app-express.py @@ -5,6 +5,8 @@ ui.input_action_button("add", "Add 'Dynamic' tab") ui.input_action_button("removeFoo", "Remove 'Foo' tabs") ui.input_action_button("addFoo", "Add New 'Foo' tab") + ui.input_action_button("addTextPanel", "Add Text Panel"), + with ui.navset_tab(id="tabs"): with ui.nav_panel("Hello", value="Hello"): @@ -31,6 +33,13 @@ def _(): ui.remove_nav_panel("tabs", target="Foo") +@reactive.effect() +@reactive.event(input.add) +def _(): + id = "Dynamic-" + str(input.add()) + ui.insert_nav_panel("tabs", title=id, value=id, target="s2", position="before") + + @reactive.effect() @reactive.event(input.addFoo) def _(): diff --git a/tests/playwright/shiny/components/express_navs/app.py b/tests/playwright/shiny/components/express_navs/app.py index cc5d4d693..d1c08bd74 100644 --- a/tests/playwright/shiny/components/express_navs/app.py +++ b/tests/playwright/shiny/components/express_navs/app.py @@ -1,29 +1,39 @@ from shiny import reactive -from shiny.express import render -from .modules import custom_sidebar, add_to_counter_content - -tabs_added = reactive.value(0) - - -def increment_tabs_counter(): - tabs_added.set(tabs_added() + 1) - - -custom_sidebar( - "nav", - _on_click=increment_tabs_counter, - label="Dynamic Sidebar", -) - - -add_to_counter_content( - "buttonAdder", - _on_click=increment_tabs_counter, - starting_value=0, - label="Add to Counter", -) - - -@render.code -def out(): - return f"Tabs added: {tabs_added()}" +from shiny.express import ui, module + + +@module +def my_nav(input, output, session): + with ui.navset_card_tab(id="navset"): + with ui.nav_panel("Panel 1"): + "This is the first panel" + ui.input_action_button("hideTab", "Hide panel 2") + ui.input_action_button("showTab", "Show panel 2") + ui.input_action_button("deleteTabs", "Delete panel 2") + + @reactive.effect + def _(): + ui.insert_nav_panel( + "navset", + "Panel 2", + "This is the second panel", + ) + + @reactive.effect() + @reactive.event(input.showTab) + def _(): + ui.show_nav_panel("navset", target="Panel 2") + + @reactive.effect() + @reactive.event(input.hideTab) + def _(): + ui.hide_nav_panel("navset", target="Panel 2") + + @reactive.effect() + @reactive.event(input.deleteTabs) + def _(): + ui.remove_nav_panel("navset", "Panel 2") + + +my_nav("foo") +my_nav("bar") diff --git a/tests/playwright/shiny/components/express_navs/modules.py b/tests/playwright/shiny/components/express_navs/modules.py deleted file mode 100644 index 4abfd9006..000000000 --- a/tests/playwright/shiny/components/express_navs/modules.py +++ /dev/null @@ -1,70 +0,0 @@ -from shiny import reactive -from shiny.express import ui, module - - -@module -def add_to_counter_content( - input, output, session, _on_click, starting_value, label="Add to Counter" -): - count = reactive.value(starting_value) - ui.input_action_button("add", label) - - @reactive.effect - @reactive.event(input.add) - def increment_button(): - _on_click() - count.set(count() + 1) - - -@module -def custom_sidebar( - input, output, session, _on_click, starting_value=0, label="Dynamic Sidebar" -): - count = reactive.value(starting_value) - - @reactive.effect - @reactive.event(input.addFoo) - def increment_button(): - _on_click() - count.set(count() + 1) - - with ui.sidebar(): - ui.input_action_button("add", "Add 'Dynamic' tab") - ui.input_action_button("removeFoo", "Remove 'Foo' tabs") - ui.input_action_button("addFoo", "Add New 'Foo' tab") - - @reactive.effect() - @reactive.event(input.removeFoo) - def _(): - ui.remove_nav_panel("tabs", target="Foo") - - @reactive.effect() - @reactive.event(input.addFoo) - def _(): - n = str(input.addFoo()) - ui.insert_nav_panel( - "tabs", - "Foo-" + n, - "This is the new Foo-" + n + " tab", - value="Foo", - target="Menu", - position="before", - select=True, - ) - - with ui.navset_tab(id="tabs"): - with ui.nav_panel("Hello", value="Hello"): - "This is the hello tab" - with ui.nav_panel("Foo", value="Foo"): - "This is the Foo tab" - with ui.nav_menu("Static", value="Menu"): - with ui.nav_panel("Static 1", value="s1"): - "Static 1" - with ui.nav_panel("Static 2", value="s2"): - "Static 2" - - @reactive.effect() - @reactive.event(input.add) - def _(): - id = "Dynamic-" + str(input.add()) - ui.insert_nav_panel("tabs", title=id, value=id, target="s2", position="before") diff --git a/tests/playwright/shiny/components/express_navs/test_express_navs.py b/tests/playwright/shiny/components/express_navs/test_express_navs.py new file mode 100644 index 000000000..44e49346f --- /dev/null +++ b/tests/playwright/shiny/components/express_navs/test_express_navs.py @@ -0,0 +1,42 @@ +import pytest +from playwright.sync_api import Page, expect + +from shiny.playwright import controller +from shiny.run import ShinyAppProc + + +@pytest.mark.flaky(reruns=3, reruns_delay=2) +def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: + page.goto(local_app.url) + + # Page begins with 2 tabs: "Hello" and "Foo" and a nav menu with 2 static items. + controller.NavsetTab(page, "foo-navset").expect_nav_titles(["Panel 1", "Panel 2"]) + controller.NavsetTab(page, "bar-navset").expect_nav_titles(["Panel 1", "Panel 2"]) + + # Click hide-tab to hide the Foo tabs + hidetab = controller.InputActionButton(page, "foo-hideTab") + hidetab.click() + + # Expect the Foo tabs to be hidden + navpanel = controller.NavPanel(page, "foo-navset", "Panel 2").loc + expect(navpanel).to_be_hidden() + + # Expect the bar tabs to not be affected + navpanel2 = controller.NavPanel(page, "bar-navset", "Panel 2").loc + expect(navpanel2).to_be_visible() + + # Click show-tab to show the Foo tabs again + showtab = controller.InputActionButton(page, "foo-showTab") + showtab.click() + + # Expect the Foo tabs to be visible again + navpanel2 = controller.NavPanel(page, "foo-navset", "Panel 2").loc + expect(navpanel2).to_be_visible() + navpanel3 = controller.NavPanel(page, "bar-navset", "Panel 2").loc + expect(navpanel3).to_be_visible() + + # Click the remove button to remove the panel 2 in bar + removeTab = controller.InputActionButton(page, "bar-deleteTabs") + removeTab.click() + controller.NavsetTab(page, "bar-navset").expect_nav_titles(["Panel 1"]) + controller.NavsetTab(page, "foo-navset").expect_nav_titles(["Panel 1", "Panel 2"]) From 79b0f67552433799afca9b0cc0b4b0aeddb9a7a7 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Tue, 29 Jul 2025 11:06:07 -0400 Subject: [PATCH 24/56] Updating to update_nav_panel --- .../app-core.py | 8 +-- .../app-express.py | 8 +-- shiny/express/ui/__init__.py | 6 +- shiny/ui/__init__.py | 6 +- shiny/ui/_navs_dynamic.py | 68 ++++--------------- .../shiny/components/dynamic_navs/app.py | 8 +-- .../shiny/components/express_navs/app.py | 4 +- 7 files changed, 32 insertions(+), 76 deletions(-) rename shiny/api-examples/{show_nav_panel => update_nav_panel}/app-core.py (82%) rename shiny/api-examples/{show_nav_panel => update_nav_panel}/app-express.py (81%) diff --git a/shiny/api-examples/show_nav_panel/app-core.py b/shiny/api-examples/update_nav_panel/app-core.py similarity index 82% rename from shiny/api-examples/show_nav_panel/app-core.py rename to shiny/api-examples/update_nav_panel/app-core.py index 7a1ceefe1..73795a5a4 100644 --- a/shiny/api-examples/show_nav_panel/app-core.py +++ b/shiny/api-examples/update_nav_panel/app-core.py @@ -31,22 +31,22 @@ def server(input: Inputs, output: Outputs, session: Session): @reactive.effect() @reactive.event(input.hideTab) def _(): - ui.hide_nav_panel("tabs", target="Foo") + ui.update_nav_panel("tabs", target="Foo", method="hide") @reactive.effect() @reactive.event(input.showTab) def _(): - ui.show_nav_panel("tabs", target="Foo") + ui.update_nav_panel("tabs", target="Foo", method="show") @reactive.effect() @reactive.event(input.hideMenu) def _(): - ui.hide_nav_panel("tabs", target="More") + ui.update_nav_panel("tabs", target="More", method="hide") @reactive.effect() @reactive.event(input.showMenu) def _(): - ui.show_nav_panel("tabs", target="More") + ui.update_nav_panel("tabs", target="More", method="show") app = App(app_ui, server) diff --git a/shiny/api-examples/show_nav_panel/app-express.py b/shiny/api-examples/update_nav_panel/app-express.py similarity index 81% rename from shiny/api-examples/show_nav_panel/app-express.py rename to shiny/api-examples/update_nav_panel/app-express.py index 4441eb2c8..6e05db52d 100644 --- a/shiny/api-examples/show_nav_panel/app-express.py +++ b/shiny/api-examples/update_nav_panel/app-express.py @@ -27,19 +27,19 @@ @reactive.effect() @reactive.event(input.hideTab) def _(): - ui.hide_nav_panel("tabs", target="Foo") + ui.update_nav_panel("tabs", target="Foo", method="hide") @reactive.effect() @reactive.event(input.showTab) def _(): - ui.show_nav_panel("tabs", target="Foo") + ui.update_nav_panel("tabs", target="Foo", method="show") @reactive.effect() @reactive.event(input.hideMenu) def _(): - ui.hide_nav_panel("tabs", target="More") + ui.update_nav_panel("tabs", target="More", method="hide") @reactive.effect() @reactive.event(input.showMenu) def _(): - ui.show_nav_panel("tabs", target="More") + ui.update_nav_panel("tabs", target="More", method="show") diff --git a/shiny/express/ui/__init__.py b/shiny/express/ui/__init__.py index c1253034b..e41090575 100644 --- a/shiny/express/ui/__init__.py +++ b/shiny/express/ui/__init__.py @@ -109,10 +109,9 @@ notification_remove, nav_spacer, navbar_options, - hide_nav_panel, insert_nav_panel, remove_nav_panel, - show_nav_panel, + update_nav_panel, Progress, Theme, value_box_theme, @@ -297,9 +296,8 @@ "navset_hidden", "navset_pill", "navset_pill_list", - "hide_nav_panel", + "update_nav_panel", "remove_nav_panel", - "show_nav_panel", "navset_tab", "navset_underline", "navbar_options", diff --git a/shiny/ui/__init__.py b/shiny/ui/__init__.py index 0354f5c38..e1af2ff1b 100644 --- a/shiny/ui/__init__.py +++ b/shiny/ui/__init__.py @@ -129,10 +129,9 @@ navset_underline, ) from ._navs_dynamic import ( - hide_nav_panel, + update_nav_panel, insert_nav_panel, remove_nav_panel, - show_nav_panel, ) from ._notification import notification_remove, notification_show from ._output import ( @@ -303,10 +302,9 @@ "navset_pill_list", "navset_hidden", "navset_bar", - "hide_nav_panel", "insert_nav_panel", "remove_nav_panel", - "show_nav_panel", + "update_nav_panel", "navbar_options", # _notification "notification_show", diff --git a/shiny/ui/_navs_dynamic.py b/shiny/ui/_navs_dynamic.py index fa22da248..fe168b926 100644 --- a/shiny/ui/_navs_dynamic.py +++ b/shiny/ui/_navs_dynamic.py @@ -1,9 +1,4 @@ -__all__ = ( - "insert_nav_panel", - "remove_nav_panel", - "hide_nav_panel", - "show_nav_panel", -) +__all__ = ("insert_nav_panel", "remove_nav_panel", "update_nav_panel") import sys from typing import Optional, Union @@ -59,8 +54,7 @@ def insert_nav_panel( See Also -------- ~remove_nav_panel - ~show_nav_panel - ~hide_nav_panel + ~update_nav_panel ~shiny.ui.nav_panel """ @@ -109,8 +103,7 @@ def remove_nav_panel(id: str, target: str, session: Optional[Session] = None) -> See Also -------- ~insert_nav_panel - ~show_nav_panel - ~hide_nav_panel + ~update_nav_panel ~shiny.ui.nav_panel """ @@ -124,11 +117,14 @@ def remove_nav_panel(id: str, target: str, session: Optional[Session] = None) -> @add_example() -def show_nav_panel( - id: str, target: str, select: bool = False, session: Optional[Session] = None +def update_nav_panel( + id: str, + target: str, + method: Literal["show", "hide"], + session: Optional[Session] = None, ) -> None: """ - Show a navigation item + Show/hide a navigation item Parameters ---------- @@ -136,68 +132,32 @@ def show_nav_panel( The `id` of the relevant navigation container (i.e., `navset_*()` object). target The `value` of an existing :func:`shiny.ui.nav` item to show. - select - Whether the nav item's content should also be shown. + method + The action to perform on the nav_panel (`"show"` or `"hide"`). session A :class:`~shiny.Session` instance. If not provided, it is inferred via :func:`~shiny.session.get_current_session`. Note ---- - For `show_nav_panel()` to be relevant/useful, a :func:`shiny.ui.nav` item must - have been hidden using :func:`~hide_nav_panel`. + On reveal, the `nav_panel` will not be the active tab. To change the active tab, use `~update_navs()` See Also -------- - ~hide_nav_panel ~insert_nav_panel ~remove_nav_panel ~shiny.ui.nav_panel + ~update_navs """ session = require_active_session(session) id = resolve_id(id) - if select: - update_navs(id, selected=target) msg = { "inputId": id, "target": target, - "type": "show", - } - - session._send_message_sync({"shiny-change-tab-visibility": msg}) - - -def hide_nav_panel(id: str, target: str, session: Optional[Session] = None) -> None: - """ - Hide a navigation item - - Parameters - ---------- - id - The `id` of the relevant navigation container (i.e., `navset_*()` object). - target - The `value` of an existing :func:`shiny.ui.nav` item to hide. - session - A :class:`~shiny.Session` instance. If not provided, it is inferred via - :func:`~shiny.session.get_current_session`. - - See Also - -------- - ~show_nav_panel - ~insert_nav_panel - ~remove_nav_panel - ~shiny.ui.nav_panel - """ - - session = require_active_session(session) - - msg = { - "inputId": resolve_id(id), - "target": target, - "type": "hide", + "type": method, } session._send_message_sync({"shiny-change-tab-visibility": msg}) diff --git a/tests/playwright/shiny/components/dynamic_navs/app.py b/tests/playwright/shiny/components/dynamic_navs/app.py index c2698dc96..29cf2679c 100644 --- a/tests/playwright/shiny/components/dynamic_navs/app.py +++ b/tests/playwright/shiny/components/dynamic_navs/app.py @@ -56,22 +56,22 @@ def _(): @reactive.effect() @reactive.event(input.hideTab) def _(): - ui.hide_nav_panel("tabs", target="Foo") + ui.update_nav_panel("tabs", target="Foo", method="hide") @reactive.effect() @reactive.event(input.showTab) def _(): - ui.show_nav_panel("tabs", target="Foo") + ui.update_nav_panel("tabs", target="Foo", method="show") @reactive.effect() @reactive.event(input.hideMenu) def _(): - ui.hide_nav_panel("tabs", target="Menu") + ui.update_nav_panel("tabs", target="Menu", method="hide") @reactive.effect() @reactive.event(input.showMenu) def _(): - ui.show_nav_panel("tabs", target="Menu") + ui.update_nav_panel("tabs", target="Menu", method="show") app = App(app_ui, server) diff --git a/tests/playwright/shiny/components/express_navs/app.py b/tests/playwright/shiny/components/express_navs/app.py index d1c08bd74..438450c80 100644 --- a/tests/playwright/shiny/components/express_navs/app.py +++ b/tests/playwright/shiny/components/express_navs/app.py @@ -22,12 +22,12 @@ def _(): @reactive.effect() @reactive.event(input.showTab) def _(): - ui.show_nav_panel("navset", target="Panel 2") + ui.update_nav_panel("navset", target="Panel 2", method="show") @reactive.effect() @reactive.event(input.hideTab) def _(): - ui.hide_nav_panel("navset", target="Panel 2") + ui.update_nav_panel("navset", target="Panel 2", method="hide") @reactive.effect() @reactive.event(input.deleteTabs) From 1a2e7694bad40518b4bf246a0c4cb09a9965aa29 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Tue, 29 Jul 2025 11:17:29 -0400 Subject: [PATCH 25/56] Clarified test comments --- .../components/dynamic_navs/test_navs_dynamic.py | 6 ++++-- .../components/express_navs/test_express_navs.py | 14 ++++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py b/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py index cf17951a5..6dddebeb7 100644 --- a/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py +++ b/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py @@ -19,7 +19,7 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: controller.NavsetTab(page, "tabs").expect_nav_titles( ["Hello", "Foo", "Foo-1", "Static1", "Static2"] ) - # Test Foo-1 tab is added + # Check that Foo-1 tab is added navpanel = controller.NavPanel(page, "tabs", "Foo").loc.filter(has_text="Foo-1") expect(navpanel).to_have_text("Foo-1") @@ -67,6 +67,7 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: page.get_by_role("button", name="Menu", exact=True).click() + # Expect static tabs to be visible navpanel3 = controller.NavPanel(page, "tabs", "s1").loc expect(navpanel3).to_be_visible() @@ -84,7 +85,8 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: # Expect the Menu to be visible again expect(page.get_by_role("button", name="Menu", exact=True)).to_be_visible() - # Click the Menu button to show the static menu + + # Click the Menu button to show the static menu and expect the panels to be visible again page.get_by_role("button", name="Menu", exact=True).click() navpanel3 = controller.NavPanel(page, "tabs", "s1").loc expect(navpanel3).to_be_visible() diff --git a/tests/playwright/shiny/components/express_navs/test_express_navs.py b/tests/playwright/shiny/components/express_navs/test_express_navs.py index 44e49346f..f5a94be52 100644 --- a/tests/playwright/shiny/components/express_navs/test_express_navs.py +++ b/tests/playwright/shiny/components/express_navs/test_express_navs.py @@ -9,27 +9,27 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: page.goto(local_app.url) - # Page begins with 2 tabs: "Hello" and "Foo" and a nav menu with 2 static items. + # Page begins with 2 tabs: "Panel 1" and "Panel 2" controller.NavsetTab(page, "foo-navset").expect_nav_titles(["Panel 1", "Panel 2"]) controller.NavsetTab(page, "bar-navset").expect_nav_titles(["Panel 1", "Panel 2"]) - # Click hide-tab to hide the Foo tabs + # Click hide-tab to hide Panel 2 in the foo navset hidetab = controller.InputActionButton(page, "foo-hideTab") hidetab.click() - # Expect the Foo tabs to be hidden + # Expect the Foo's Panel 2 to be hidden navpanel = controller.NavPanel(page, "foo-navset", "Panel 2").loc expect(navpanel).to_be_hidden() - # Expect the bar tabs to not be affected + # Expect the bar Panel 2 tab to not be affected navpanel2 = controller.NavPanel(page, "bar-navset", "Panel 2").loc expect(navpanel2).to_be_visible() - # Click show-tab to show the Foo tabs again + # Click show-tab to show the foo Panel 2 tab again showtab = controller.InputActionButton(page, "foo-showTab") showtab.click() - # Expect the Foo tabs to be visible again + # Expect the Foo Panel 2 tab to be visible again as well as the bar Panel 2 navpanel2 = controller.NavPanel(page, "foo-navset", "Panel 2").loc expect(navpanel2).to_be_visible() navpanel3 = controller.NavPanel(page, "bar-navset", "Panel 2").loc @@ -38,5 +38,7 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: # Click the remove button to remove the panel 2 in bar removeTab = controller.InputActionButton(page, "bar-deleteTabs") removeTab.click() + + # Check that bar's Panel 2 is gone, but foo's Panel 2 is unaffected controller.NavsetTab(page, "bar-navset").expect_nav_titles(["Panel 1"]) controller.NavsetTab(page, "foo-navset").expect_nav_titles(["Panel 1", "Panel 2"]) From 41575519757c19494f5157c908d7e05215efda10 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Tue, 29 Jul 2025 11:30:04 -0400 Subject: [PATCH 26/56] Added string tests --- .../shiny/components/dynamic_navs/app.py | 16 ++++++++++++++++ .../components/dynamic_navs/test_navs_dynamic.py | 6 ++++++ 2 files changed, 22 insertions(+) diff --git a/tests/playwright/shiny/components/dynamic_navs/app.py b/tests/playwright/shiny/components/dynamic_navs/app.py index 29cf2679c..213945c6f 100644 --- a/tests/playwright/shiny/components/dynamic_navs/app.py +++ b/tests/playwright/shiny/components/dynamic_navs/app.py @@ -25,6 +25,22 @@ def server(input: Inputs, output: Outputs, session: Session): + + @reactive.effect + def _(): + ui.insert_nav_panel( + "tabs", + "Stringy Panel", + target="Foo", + position="before", + ) + ui.insert_nav_panel( + "tabs", + "Stringier Panel", + target="s2", + position="before", + ) + @reactive.effect() @reactive.event(input.add) def _(): diff --git a/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py b/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py index 6dddebeb7..bc5cc4e70 100644 --- a/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py +++ b/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py @@ -10,6 +10,12 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: page.goto(local_app.url) + # String insertion as panels worked correctly + expect(page.get_by_text("Stringy Panel")).to_be_visible() + page.get_by_role("button", name="Menu", exact=True).click() + expect(page.get_by_text("Stringier Panel")).to_be_visible() + page.get_by_role("button", name="Menu", exact=True).click() + # Page begins with 2 tabs: "Hello" and "Foo" and a nav menu with 2 static items. navset_tab = controller.NavsetTab(page, "tabs") From 3200c31c5bdec99f154a8a12e57a3e1c08f5c353 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Tue, 29 Jul 2025 11:58:59 -0400 Subject: [PATCH 27/56] Isort --- shiny/express/ui/__init__.py | 2 +- shiny/ui/_navs_dynamic.py | 1 - .../shiny/components/dynamic_navs/test_navs_dynamic.py | 5 +++-- tests/playwright/shiny/components/express_navs/app.py | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/shiny/express/ui/__init__.py b/shiny/express/ui/__init__.py index e41090575..4c3d61967 100644 --- a/shiny/express/ui/__init__.py +++ b/shiny/express/ui/__init__.py @@ -109,7 +109,6 @@ notification_remove, nav_spacer, navbar_options, - insert_nav_panel, remove_nav_panel, update_nav_panel, Progress, @@ -297,6 +296,7 @@ "navset_pill", "navset_pill_list", "update_nav_panel", + "insert_nav_panel", "remove_nav_panel", "navset_tab", "navset_underline", diff --git a/shiny/ui/_navs_dynamic.py b/shiny/ui/_navs_dynamic.py index fe168b926..d893bdbed 100644 --- a/shiny/ui/_navs_dynamic.py +++ b/shiny/ui/_navs_dynamic.py @@ -12,7 +12,6 @@ from .._namespaces import resolve_id from ..session import Session, require_active_session from ..types import NavSetArg -from ._input_update import update_navs from ._navs import menu_string_as_nav diff --git a/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py b/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py index bc5cc4e70..2964c2c76 100644 --- a/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py +++ b/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py @@ -17,8 +17,9 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: page.get_by_role("button", name="Menu", exact=True).click() # Page begins with 2 tabs: "Hello" and "Foo" and a nav menu with 2 static items. - navset_tab = controller.NavsetTab(page, "tabs") - + controller.NavsetTab(page, "tabs").expect_nav_titles( + ["Hello", "Foo", "Static1", "Static2"] + ) # Click add-foo to add a new Foo tab addfoo = controller.InputActionButton(page, "addFoo") addfoo.click() diff --git a/tests/playwright/shiny/components/express_navs/app.py b/tests/playwright/shiny/components/express_navs/app.py index 438450c80..8dbf52ab7 100644 --- a/tests/playwright/shiny/components/express_navs/app.py +++ b/tests/playwright/shiny/components/express_navs/app.py @@ -1,9 +1,9 @@ -from shiny import reactive -from shiny.express import ui, module +from shiny import Inputs, Outputs, Session, reactive +from shiny.express import module, ui @module -def my_nav(input, output, session): +def my_nav(input: Inputs, output: Outputs, session: Session): with ui.navset_card_tab(id="navset"): with ui.nav_panel("Panel 1"): "This is the first panel" From 57e0540952393804243a030f39fccd29bb39a9f4 Mon Sep 17 00:00:00 2001 From: E Nelson Date: Tue, 29 Jul 2025 12:12:47 -0400 Subject: [PATCH 28/56] Update shiny/api-examples/insert_nav_panel/app-core.py Co-authored-by: Carson Sievert --- shiny/api-examples/insert_nav_panel/app-core.py | 1 - 1 file changed, 1 deletion(-) diff --git a/shiny/api-examples/insert_nav_panel/app-core.py b/shiny/api-examples/insert_nav_panel/app-core.py index 45b9c0c46..4f61a8312 100644 --- a/shiny/api-examples/insert_nav_panel/app-core.py +++ b/shiny/api-examples/insert_nav_panel/app-core.py @@ -64,7 +64,6 @@ def _(): @reactive.effect() @reactive.event(input.addTextPanel) def _(): - n = str(input.addFoo()) ui.insert_nav_panel( "tabs", "Placeholder Text Panel", From 387902951a8afc1e21c65c291832338bee30171c Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Tue, 29 Jul 2025 13:59:46 -0400 Subject: [PATCH 29/56] Updating textpanels --- .../api-examples/insert_nav_panel/app-core.py | 45 +++++++++--------- .../insert_nav_panel/app-express.py | 46 +++++++++++++++---- 2 files changed, 60 insertions(+), 31 deletions(-) diff --git a/shiny/api-examples/insert_nav_panel/app-core.py b/shiny/api-examples/insert_nav_panel/app-core.py index 4f61a8312..59514de54 100644 --- a/shiny/api-examples/insert_nav_panel/app-core.py +++ b/shiny/api-examples/insert_nav_panel/app-core.py @@ -3,9 +3,10 @@ app_ui = ui.page_sidebar( ui.sidebar( ui.input_action_button("add", "Add 'Dynamic' tab"), - ui.input_action_button("removeFoo", "Remove 'Foo' tabs"), - ui.input_action_button("addFoo", "Add New 'Foo' tab"), - ui.input_action_button("addTextPanel", "Add Text Panel"), + ui.input_action_button("remove_foo", "Remove 'Foo' tabs"), + ui.input_action_button("add_foo", "Add New 'Foo' tab"), + # Add text panels adds both a text panel in the main navset as well as one in the menu dropdown + ui.input_action_button("add_text_panel", "Add Text Panels"), ), ui.navset_tab( ui.nav_panel("Hello", "This is the hello tab"), @@ -22,7 +23,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.effect() + @reactive.effect @reactive.event(input.add) def _(): id = "Dynamic-" + str(input.add()) @@ -34,25 +35,14 @@ def _(): ) @reactive.effect() - @reactive.event(input.addTextPanel) - def _(): - id = "Text-" + str(input.addTextPanel()) - ui.insert_nav_panel( - "tabs", - id, - target="s2", - position="before", - ) - - @reactive.effect() - @reactive.event(input.removeFoo) + @reactive.event(input.remove_foo) def _(): ui.remove_nav_panel("tabs", target="Foo") - @reactive.effect() - @reactive.event(input.addFoo) + @reactive.effect + @reactive.event(input.add_foo) def _(): - n = str(input.addFoo()) + n = str(input.add_foo()) ui.insert_nav_panel( "tabs", ui.nav_panel("Foo-" + n, "This is the new Foo-" + n + " tab", value="Foo"), @@ -61,8 +51,10 @@ def _(): select=True, ) - @reactive.effect() - @reactive.event(input.addTextPanel) + # Button push for add_text_panel adds two panels + + @reactive.effect + @reactive.event(input.add_text_panel) def _(): ui.insert_nav_panel( "tabs", @@ -72,5 +64,16 @@ def _(): select=True, ) + @reactive.effect + @reactive.event(input.add_text_panel) + def _(): + id = "Text-" + str(input.add_text_panel()) + ui.insert_nav_panel( + "tabs", + id, + target="s2", + position="before", + ) + app = App(app_ui, server) diff --git a/shiny/api-examples/insert_nav_panel/app-express.py b/shiny/api-examples/insert_nav_panel/app-express.py index 6e461af9c..4c260b811 100644 --- a/shiny/api-examples/insert_nav_panel/app-express.py +++ b/shiny/api-examples/insert_nav_panel/app-express.py @@ -3,9 +3,10 @@ with ui.sidebar(): ui.input_action_button("add", "Add 'Dynamic' tab") - ui.input_action_button("removeFoo", "Remove 'Foo' tabs") - ui.input_action_button("addFoo", "Add New 'Foo' tab") - ui.input_action_button("addTextPanel", "Add Text Panel"), + ui.input_action_button("remove_foo", "Remove 'Foo' tabs") + ui.input_action_button("add_foo", "Add New 'Foo' tab") + # Add text panels adds both a text panel in the main navset as well as one in the menu dropdown + ui.input_action_button("add_text_panel", "Add Text Panels") with ui.navset_tab(id="tabs"): @@ -20,30 +21,30 @@ "Static 2" -@reactive.effect() +@reactive.effect @reactive.event(input.add) def _(): id = "Dynamic-" + str(input.add()) ui.insert_nav_panel("tabs", title=id, value=id, target="s2", position="before") -@reactive.effect() -@reactive.event(input.removeFoo) +@reactive.effect +@reactive.event(input.remove_foo) def _(): ui.remove_nav_panel("tabs", target="Foo") -@reactive.effect() +@reactive.effect @reactive.event(input.add) def _(): id = "Dynamic-" + str(input.add()) ui.insert_nav_panel("tabs", title=id, value=id, target="s2", position="before") -@reactive.effect() -@reactive.event(input.addFoo) +@reactive.effect +@reactive.event(input.add_foo) def _(): - n = str(input.addFoo()) + n = str(input.add_foo()) ui.insert_nav_panel( "tabs", "Foo-" + n, @@ -53,3 +54,28 @@ def _(): position="before", select=True, ) + + +# Button push for add_text_panel adds two panels +@reactive.effect +@reactive.event(input.add_text_panel) +def _(): + id = "Text-" + str(input.add_text_panel()) + ui.insert_nav_panel( + "tabs", + id, + target="s2", + position="before", + ) + + +@reactive.effect +@reactive.event(input.add_text_panel) +def _(): + ui.insert_nav_panel( + "tabs", + "Placeholder Text Panel", + target="Menu", + position="before", + select=True, + ) From 238032bf944c2a7e50f9d1bf7006338be8fae2fb Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Tue, 29 Jul 2025 14:00:11 -0400 Subject: [PATCH 30/56] Update reactive effect --- shiny/api-examples/insert_nav_panel/app-core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shiny/api-examples/insert_nav_panel/app-core.py b/shiny/api-examples/insert_nav_panel/app-core.py index 59514de54..16fee0c9b 100644 --- a/shiny/api-examples/insert_nav_panel/app-core.py +++ b/shiny/api-examples/insert_nav_panel/app-core.py @@ -34,7 +34,7 @@ def _(): position="before", ) - @reactive.effect() + @reactive.effect @reactive.event(input.remove_foo) def _(): ui.remove_nav_panel("tabs", target="Foo") From 036d38b80da0236eca1758f464f2713b25900533 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Tue, 29 Jul 2025 14:01:42 -0400 Subject: [PATCH 31/56] Updating reactive effects --- shiny/api-examples/update_nav_panel/app-core.py | 8 ++++---- shiny/api-examples/update_nav_panel/app-express.py | 8 ++++---- .../shiny/components/dynamic_navs/app.py | 14 +++++++------- .../shiny/components/express_navs/app.py | 6 +++--- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/shiny/api-examples/update_nav_panel/app-core.py b/shiny/api-examples/update_nav_panel/app-core.py index 73795a5a4..ff6fb87df 100644 --- a/shiny/api-examples/update_nav_panel/app-core.py +++ b/shiny/api-examples/update_nav_panel/app-core.py @@ -28,22 +28,22 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.effect() + @reactive.effect @reactive.event(input.hideTab) def _(): ui.update_nav_panel("tabs", target="Foo", method="hide") - @reactive.effect() + @reactive.effect @reactive.event(input.showTab) def _(): ui.update_nav_panel("tabs", target="Foo", method="show") - @reactive.effect() + @reactive.effect @reactive.event(input.hideMenu) def _(): ui.update_nav_panel("tabs", target="More", method="hide") - @reactive.effect() + @reactive.effect @reactive.event(input.showMenu) def _(): ui.update_nav_panel("tabs", target="More", method="show") diff --git a/shiny/api-examples/update_nav_panel/app-express.py b/shiny/api-examples/update_nav_panel/app-express.py index 6e05db52d..bd3a96f7c 100644 --- a/shiny/api-examples/update_nav_panel/app-express.py +++ b/shiny/api-examples/update_nav_panel/app-express.py @@ -24,22 +24,22 @@ with ui.nav_panel("Email"): "Email page" - @reactive.effect() + @reactive.effect @reactive.event(input.hideTab) def _(): ui.update_nav_panel("tabs", target="Foo", method="hide") - @reactive.effect() + @reactive.effect @reactive.event(input.showTab) def _(): ui.update_nav_panel("tabs", target="Foo", method="show") - @reactive.effect() + @reactive.effect @reactive.event(input.hideMenu) def _(): ui.update_nav_panel("tabs", target="More", method="hide") - @reactive.effect() + @reactive.effect @reactive.event(input.showMenu) def _(): ui.update_nav_panel("tabs", target="More", method="show") diff --git a/tests/playwright/shiny/components/dynamic_navs/app.py b/tests/playwright/shiny/components/dynamic_navs/app.py index 213945c6f..cc8677d84 100644 --- a/tests/playwright/shiny/components/dynamic_navs/app.py +++ b/tests/playwright/shiny/components/dynamic_navs/app.py @@ -41,7 +41,7 @@ def _(): position="before", ) - @reactive.effect() + @reactive.effect @reactive.event(input.add) def _(): id = "Dynamic-" + str(input.add()) @@ -52,12 +52,12 @@ def _(): position="before", ) - @reactive.effect() + @reactive.effect @reactive.event(input.removeFoo) def _(): ui.remove_nav_panel("tabs", target="Foo") - @reactive.effect() + @reactive.effect @reactive.event(input.addFoo) def _(): n = str(input.addFoo()) @@ -69,22 +69,22 @@ def _(): select=True, ) - @reactive.effect() + @reactive.effect @reactive.event(input.hideTab) def _(): ui.update_nav_panel("tabs", target="Foo", method="hide") - @reactive.effect() + @reactive.effect @reactive.event(input.showTab) def _(): ui.update_nav_panel("tabs", target="Foo", method="show") - @reactive.effect() + @reactive.effect @reactive.event(input.hideMenu) def _(): ui.update_nav_panel("tabs", target="Menu", method="hide") - @reactive.effect() + @reactive.effect @reactive.event(input.showMenu) def _(): ui.update_nav_panel("tabs", target="Menu", method="show") diff --git a/tests/playwright/shiny/components/express_navs/app.py b/tests/playwright/shiny/components/express_navs/app.py index 8dbf52ab7..ec5994c58 100644 --- a/tests/playwright/shiny/components/express_navs/app.py +++ b/tests/playwright/shiny/components/express_navs/app.py @@ -19,17 +19,17 @@ def _(): "This is the second panel", ) - @reactive.effect() + @reactive.effect @reactive.event(input.showTab) def _(): ui.update_nav_panel("navset", target="Panel 2", method="show") - @reactive.effect() + @reactive.effect @reactive.event(input.hideTab) def _(): ui.update_nav_panel("navset", target="Panel 2", method="hide") - @reactive.effect() + @reactive.effect @reactive.event(input.deleteTabs) def _(): ui.remove_nav_panel("navset", "Panel 2") From 7f0a796d3b08cdc022c277f45a572c820aed7599 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Tue, 29 Jul 2025 14:11:55 -0400 Subject: [PATCH 32/56] updating description --- shiny/express/ui/_insert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shiny/express/ui/_insert.py b/shiny/express/ui/_insert.py index 1b78b32a8..b8eb3c6ea 100644 --- a/shiny/express/ui/_insert.py +++ b/shiny/express/ui/_insert.py @@ -28,7 +28,7 @@ def insert_nav_panel( session: Optional[Session] = None, ) -> None: """ - Create a nav item pointing to some internal content. + Create a new nav panel in an existing navset. Parameters ---------- From 206252b221fc76ad6493f74c415a70ecac03e8db Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Tue, 29 Jul 2025 14:16:48 -0400 Subject: [PATCH 33/56] Updating comments --- shiny/express/ui/_insert.py | 18 +++++++++--------- shiny/ui/_navs_dynamic.py | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/shiny/express/ui/_insert.py b/shiny/express/ui/_insert.py index b8eb3c6ea..6d9481649 100644 --- a/shiny/express/ui/_insert.py +++ b/shiny/express/ui/_insert.py @@ -33,28 +33,28 @@ def insert_nav_panel( Parameters ---------- id - The id of the navset container to insert the item into. + The id of the navset container to insert into. title - A title to display. Can be a character string or UI elements (i.e., tags). + A title for the inserted nav panel. Can be a character string or UI elements (i.e., tags). *args - UI elements to display when the item is active. + UI elements for the inserted nav panel. value - The value of the item. Use this value to determine whether the item is active + The value of the nav panel. Use this value to determine whether the item is active (when an `id` is provided to the nav container) or to programmatically select the item (e.g., :func:`~shiny.ui.update_navs`). You can also provide the value to the `selected` argument of the navigation container (e.g., :func:`~shiny.ui.navset_tab`). icon - An icon to appear inline with the button/link. + An icon to appear inline with the title. target - The `value` of an existing :func:`shiny.ui.nav` item, next to which tab will + The `value` of an existing :func:`shiny.ui.nav_panel`, next to which tab will be added. Can also be `None`; see `position`. position - The position of the new nav item relative to the target nav item. If + The position of the new nav item relative to the target nav panel. If `target=None`, then `"before"` means the new nav item should be inserted at - the head of the navlist, and `"after"` is the end. + the head of the navset, and `"after"` is the end. select - Whether the nav item should be selected upon insertion. + Whether the nav panel should be selected upon insertion. session A :class:`~shiny.Session` instance. If not provided, it is inferred via :func:`~shiny.session.get_current_session`. diff --git a/shiny/ui/_navs_dynamic.py b/shiny/ui/_navs_dynamic.py index d893bdbed..c5aff66cd 100644 --- a/shiny/ui/_navs_dynamic.py +++ b/shiny/ui/_navs_dynamic.py @@ -38,7 +38,7 @@ def insert_nav_panel( string is only allowed when the `target` references a :func:`~shiny.ui.nav_menu`. target - The `value` of an existing :func:`shiny.ui.nav` item, next to which tab will + The `value` of an existing :func:`shiny.ui.nav_panel`, next to which tab will be added. Can also be `None`; see `position`. position The position of the new nav item relative to the target nav item. If From eba940b8fe8fd172f39b373863d834d33e391e97 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Tue, 29 Jul 2025 14:17:51 -0400 Subject: [PATCH 34/56] Update text --- shiny/express/ui/_insert.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shiny/express/ui/_insert.py b/shiny/express/ui/_insert.py index 6d9481649..7741ca8c8 100644 --- a/shiny/express/ui/_insert.py +++ b/shiny/express/ui/_insert.py @@ -50,9 +50,9 @@ def insert_nav_panel( The `value` of an existing :func:`shiny.ui.nav_panel`, next to which tab will be added. Can also be `None`; see `position`. position - The position of the new nav item relative to the target nav panel. If - `target=None`, then `"before"` means the new nav item should be inserted at - the head of the navset, and `"after"` is the end. + The position of the new nav panel relative to the target. If + `target=None`, then `"before"` means the new panel should be inserted at + the head of the navlist, and `"after"` is the end. select Whether the nav panel should be selected upon insertion. session From c0366a12e3a6bd8ef397ee4fdce8d7497fb9acb5 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Tue, 29 Jul 2025 14:18:26 -0400 Subject: [PATCH 35/56] Update text --- shiny/express/ui/_insert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shiny/express/ui/_insert.py b/shiny/express/ui/_insert.py index 7741ca8c8..17d03c26f 100644 --- a/shiny/express/ui/_insert.py +++ b/shiny/express/ui/_insert.py @@ -39,7 +39,7 @@ def insert_nav_panel( *args UI elements for the inserted nav panel. value - The value of the nav panel. Use this value to determine whether the item is active + The value of the panel. Use this value to determine whether the panel is active (when an `id` is provided to the nav container) or to programmatically select the item (e.g., :func:`~shiny.ui.update_navs`). You can also provide the value to the `selected` argument of the navigation container From 2455c3139aa8aaa6ae3aa7a91c1532ab3ded8f48 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Tue, 29 Jul 2025 14:37:14 -0400 Subject: [PATCH 36/56] Add example with update_navs --- shiny/ui/_navs_dynamic.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/shiny/ui/_navs_dynamic.py b/shiny/ui/_navs_dynamic.py index c5aff66cd..839328a6d 100644 --- a/shiny/ui/_navs_dynamic.py +++ b/shiny/ui/_navs_dynamic.py @@ -140,6 +140,14 @@ def update_nav_panel( Note ---- On reveal, the `nav_panel` will not be the active tab. To change the active tab, use `~update_navs()` + For example: + ``` + @reactive.effect + @reactive.event(input.showTab) + def _(): + ui.update_nav_panel("tabset_id", target="Foo", method="show") + ui.update_navs("tabset_id", selected="Foo") + ``` See Also -------- From 8d4f20133836f80917fc31a0a237128408003749 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Tue, 29 Jul 2025 14:37:48 -0400 Subject: [PATCH 37/56] Update name --- shiny/ui/_navs_dynamic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shiny/ui/_navs_dynamic.py b/shiny/ui/_navs_dynamic.py index 839328a6d..4f4190f57 100644 --- a/shiny/ui/_navs_dynamic.py +++ b/shiny/ui/_navs_dynamic.py @@ -143,7 +143,7 @@ def update_nav_panel( For example: ``` @reactive.effect - @reactive.event(input.showTab) + @reactive.event(input.show_tab) def _(): ui.update_nav_panel("tabset_id", target="Foo", method="show") ui.update_navs("tabset_id", selected="Foo") From 2086f4a4d5d27d68b0dd83a72ebe128bd2fa777e Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Tue, 29 Jul 2025 14:48:03 -0400 Subject: [PATCH 38/56] Updating all camelcase before .loc edits --- .../shiny/components/dynamic_navs/app.py | 26 +++++++++---------- .../dynamic_navs/test_navs_dynamic.py | 13 +++++----- .../shiny/components/express_navs/app.py | 12 ++++----- .../express_navs/test_express_navs.py | 7 +++-- 4 files changed, 28 insertions(+), 30 deletions(-) diff --git a/tests/playwright/shiny/components/dynamic_navs/app.py b/tests/playwright/shiny/components/dynamic_navs/app.py index cc8677d84..0dc7d4989 100644 --- a/tests/playwright/shiny/components/dynamic_navs/app.py +++ b/tests/playwright/shiny/components/dynamic_navs/app.py @@ -3,12 +3,12 @@ app_ui = ui.page_sidebar( ui.sidebar( ui.input_action_button("add", "Add 'Dynamic' tab"), - ui.input_action_button("removeFoo", "Remove 'Foo' tabs"), - ui.input_action_button("addFoo", "Add New 'Foo' tab"), - ui.input_action_button("hideTab", "Hide 'Foo' tab"), - ui.input_action_button("showTab", "Show 'Foo' tab"), - ui.input_action_button("hideMenu", "Hide 'Static' nav_menu"), - ui.input_action_button("showMenu", "Show 'Static' nav_menu"), + ui.input_action_button("remove_foo", "Remove 'Foo' tabs"), + ui.input_action_button("add_foo", "Add New 'Foo' tab"), + ui.input_action_button("hide_tab", "Hide 'Foo' tab"), + ui.input_action_button("show_tab", "Show 'Foo' tab"), + ui.input_action_button("hide_menu", "Hide 'Static' nav_menu"), + ui.input_action_button("show_menu", "Show 'Static' nav_menu"), ), ui.navset_tab( ui.nav_panel("Hello", "This is the hello tab", value="Hello"), @@ -53,14 +53,14 @@ def _(): ) @reactive.effect - @reactive.event(input.removeFoo) + @reactive.event(input.remove_foo) def _(): ui.remove_nav_panel("tabs", target="Foo") @reactive.effect - @reactive.event(input.addFoo) + @reactive.event(input.add_foo) def _(): - n = str(input.addFoo()) + n = str(input.add_foo()) ui.insert_nav_panel( "tabs", ui.nav_panel("Foo-" + n, "Foo-" + n, value="Foo"), @@ -70,22 +70,22 @@ def _(): ) @reactive.effect - @reactive.event(input.hideTab) + @reactive.event(input.hide_tab) def _(): ui.update_nav_panel("tabs", target="Foo", method="hide") @reactive.effect - @reactive.event(input.showTab) + @reactive.event(input.show_tab) def _(): ui.update_nav_panel("tabs", target="Foo", method="show") @reactive.effect - @reactive.event(input.hideMenu) + @reactive.event(input.hide_menu) def _(): ui.update_nav_panel("tabs", target="Menu", method="hide") @reactive.effect - @reactive.event(input.showMenu) + @reactive.event(input.show_menu) def _(): ui.update_nav_panel("tabs", target="Menu", method="show") diff --git a/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py b/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py index 2964c2c76..12433b2a4 100644 --- a/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py +++ b/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py @@ -6,7 +6,6 @@ from shiny.run import ShinyAppProc -@pytest.mark.flaky(reruns=3, reruns_delay=2) def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: page.goto(local_app.url) @@ -21,7 +20,7 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: ["Hello", "Foo", "Static1", "Static2"] ) # Click add-foo to add a new Foo tab - addfoo = controller.InputActionButton(page, "addFoo") + addfoo = controller.InputActionButton(page, "add_foo") addfoo.click() controller.NavsetTab(page, "tabs").expect_nav_titles( ["Hello", "Foo", "Foo-1", "Static1", "Static2"] @@ -31,7 +30,7 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: expect(navpanel).to_have_text("Foo-1") # Click hide-tab to hide the Foo tabs - hidetab = controller.InputActionButton(page, "hideTab") + hidetab = controller.InputActionButton(page, "hide_tab") hidetab.click() # Expect the Foo tabs to be hidden @@ -43,7 +42,7 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: expect(navpanel).to_be_hidden() # Click show-tab to show the Foo tabs again - showtab = controller.InputActionButton(page, "showTab") + showtab = controller.InputActionButton(page, "show_tab") showtab.click() # Expect the Foo tabs to be visible again @@ -53,7 +52,7 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: expect(navpanel3).to_be_visible() # Click remove-foo to remove the Foo tabs - removefoo = controller.InputActionButton(page, "removeFoo") + removefoo = controller.InputActionButton(page, "remove_foo") removefoo.click() controller.NavsetTab(page, "tabs").expect_nav_titles( ["Hello", "Static1", "Static2"] @@ -79,7 +78,7 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: expect(navpanel3).to_be_visible() # Click hide-menu to hide the static menu - hidemenu = controller.InputActionButton(page, "hideMenu") + hidemenu = controller.InputActionButton(page, "hide_menu") hidemenu.click() # Expect the Menu to be hidden @@ -87,7 +86,7 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: expect(navpanel3).to_be_hidden() # Click show-menu to show the static menu again - showmenu = controller.InputActionButton(page, "showMenu") + showmenu = controller.InputActionButton(page, "show_menu") showmenu.click() # Expect the Menu to be visible again diff --git a/tests/playwright/shiny/components/express_navs/app.py b/tests/playwright/shiny/components/express_navs/app.py index ec5994c58..93d65edaa 100644 --- a/tests/playwright/shiny/components/express_navs/app.py +++ b/tests/playwright/shiny/components/express_navs/app.py @@ -7,9 +7,9 @@ def my_nav(input: Inputs, output: Outputs, session: Session): with ui.navset_card_tab(id="navset"): with ui.nav_panel("Panel 1"): "This is the first panel" - ui.input_action_button("hideTab", "Hide panel 2") - ui.input_action_button("showTab", "Show panel 2") - ui.input_action_button("deleteTabs", "Delete panel 2") + ui.input_action_button("hide_tab", "Hide panel 2") + ui.input_action_button("show_tab", "Show panel 2") + ui.input_action_button("delete_tabs", "Delete panel 2") @reactive.effect def _(): @@ -20,17 +20,17 @@ def _(): ) @reactive.effect - @reactive.event(input.showTab) + @reactive.event(input.show_tab) def _(): ui.update_nav_panel("navset", target="Panel 2", method="show") @reactive.effect - @reactive.event(input.hideTab) + @reactive.event(input.hide_tab) def _(): ui.update_nav_panel("navset", target="Panel 2", method="hide") @reactive.effect - @reactive.event(input.deleteTabs) + @reactive.event(input.delete_tabs) def _(): ui.remove_nav_panel("navset", "Panel 2") diff --git a/tests/playwright/shiny/components/express_navs/test_express_navs.py b/tests/playwright/shiny/components/express_navs/test_express_navs.py index f5a94be52..3981ca35b 100644 --- a/tests/playwright/shiny/components/express_navs/test_express_navs.py +++ b/tests/playwright/shiny/components/express_navs/test_express_navs.py @@ -5,7 +5,6 @@ from shiny.run import ShinyAppProc -@pytest.mark.flaky(reruns=3, reruns_delay=2) def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: page.goto(local_app.url) @@ -14,7 +13,7 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: controller.NavsetTab(page, "bar-navset").expect_nav_titles(["Panel 1", "Panel 2"]) # Click hide-tab to hide Panel 2 in the foo navset - hidetab = controller.InputActionButton(page, "foo-hideTab") + hidetab = controller.InputActionButton(page, "foo-hide_tab") hidetab.click() # Expect the Foo's Panel 2 to be hidden @@ -26,7 +25,7 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: expect(navpanel2).to_be_visible() # Click show-tab to show the foo Panel 2 tab again - showtab = controller.InputActionButton(page, "foo-showTab") + showtab = controller.InputActionButton(page, "foo-show_tab") showtab.click() # Expect the Foo Panel 2 tab to be visible again as well as the bar Panel 2 @@ -36,7 +35,7 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: expect(navpanel3).to_be_visible() # Click the remove button to remove the panel 2 in bar - removeTab = controller.InputActionButton(page, "bar-deleteTabs") + removeTab = controller.InputActionButton(page, "bar-delete_tabs") removeTab.click() # Check that bar's Panel 2 is gone, but foo's Panel 2 is unaffected From 5d2b3911def946359dd05176c60d243cb6b01bfc Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Tue, 29 Jul 2025 14:52:24 -0400 Subject: [PATCH 39/56] Updating spacing --- .../shiny/components/express_navs/app.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/playwright/shiny/components/express_navs/app.py b/tests/playwright/shiny/components/express_navs/app.py index 93d65edaa..3927da4ab 100644 --- a/tests/playwright/shiny/components/express_navs/app.py +++ b/tests/playwright/shiny/components/express_navs/app.py @@ -19,20 +19,20 @@ def _(): "This is the second panel", ) - @reactive.effect - @reactive.event(input.show_tab) - def _(): - ui.update_nav_panel("navset", target="Panel 2", method="show") - - @reactive.effect - @reactive.event(input.hide_tab) - def _(): - ui.update_nav_panel("navset", target="Panel 2", method="hide") - - @reactive.effect - @reactive.event(input.delete_tabs) - def _(): - ui.remove_nav_panel("navset", "Panel 2") + @reactive.effect + @reactive.event(input.show_tab) + def _(): + ui.update_nav_panel("navset", target="Panel 2", method="show") + + @reactive.effect + @reactive.event(input.hide_tab) + def _(): + ui.update_nav_panel("navset", target="Panel 2", method="hide") + + @reactive.effect + @reactive.event(input.delete_tabs) + def _(): + ui.remove_nav_panel("navset", "Panel 2") my_nav("foo") From ea8518e11da9cdb097fcae4c19cda6ed9e3d957b Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Tue, 29 Jul 2025 14:56:34 -0400 Subject: [PATCH 40/56] Aligning --- shiny/api-examples/insert_nav_panel/app-core.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/shiny/api-examples/insert_nav_panel/app-core.py b/shiny/api-examples/insert_nav_panel/app-core.py index 16fee0c9b..fc4118eda 100644 --- a/shiny/api-examples/insert_nav_panel/app-core.py +++ b/shiny/api-examples/insert_nav_panel/app-core.py @@ -56,23 +56,23 @@ def _(): @reactive.effect @reactive.event(input.add_text_panel) def _(): + id = "Text-" + str(input.add_text_panel()) ui.insert_nav_panel( "tabs", - "Placeholder Text Panel", - target="Menu", + id, + target="s2", position="before", - select=True, ) @reactive.effect @reactive.event(input.add_text_panel) def _(): - id = "Text-" + str(input.add_text_panel()) ui.insert_nav_panel( "tabs", - id, - target="s2", + "Placeholder Text Panel", + target="Menu", position="before", + select=True, ) From 41b2e5ebcb98ed47060962132646c12ebe257718 Mon Sep 17 00:00:00 2001 From: Carson Date: Tue, 29 Jul 2025 14:43:53 -0500 Subject: [PATCH 41/56] Add a note about menu divider/header --- shiny/express/ui/_insert.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/shiny/express/ui/_insert.py b/shiny/express/ui/_insert.py index 17d03c26f..91546a453 100644 --- a/shiny/express/ui/_insert.py +++ b/shiny/express/ui/_insert.py @@ -18,7 +18,7 @@ @add_example() def insert_nav_panel( id: str, - title: str, + title: TagChild, *args: TagChild, value: Optional[str] = None, icon: TagChild = None, @@ -58,6 +58,13 @@ def insert_nav_panel( session A :class:`~shiny.Session` instance. If not provided, it is inferred via :func:`~shiny.session.get_current_session`. + + Note + ---- + Unlike :func:`~shiny.ui.insert_nav_panel`, this function does not support inserting + of a heading/divider into an existing :func:`~shiny.ui.nav_menu`. To do so, use + :func:`~shiny.ui.insert_nav_panel` instead of this Express variant (i.e., + `shiny.ui.insert_nav_panel("id", "Header")`). """ from ...ui import insert_nav_panel, nav_panel From 559ab2a7b8948033b1591ad0e3ce8f81f949214f Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Tue, 29 Jul 2025 15:53:33 -0400 Subject: [PATCH 42/56] Updating to remove top level text render --- shiny/api-examples/insert_nav_panel/app-core.py | 14 -------------- shiny/api-examples/insert_nav_panel/app-express.py | 13 ------------- .../components/dynamic_navs/test_navs_dynamic.py | 1 - .../components/express_navs/test_express_navs.py | 1 - 4 files changed, 29 deletions(-) diff --git a/shiny/api-examples/insert_nav_panel/app-core.py b/shiny/api-examples/insert_nav_panel/app-core.py index fc4118eda..eb53fd502 100644 --- a/shiny/api-examples/insert_nav_panel/app-core.py +++ b/shiny/api-examples/insert_nav_panel/app-core.py @@ -5,7 +5,6 @@ ui.input_action_button("add", "Add 'Dynamic' tab"), ui.input_action_button("remove_foo", "Remove 'Foo' tabs"), ui.input_action_button("add_foo", "Add New 'Foo' tab"), - # Add text panels adds both a text panel in the main navset as well as one in the menu dropdown ui.input_action_button("add_text_panel", "Add Text Panels"), ), ui.navset_tab( @@ -51,8 +50,6 @@ def _(): select=True, ) - # Button push for add_text_panel adds two panels - @reactive.effect @reactive.event(input.add_text_panel) def _(): @@ -64,16 +61,5 @@ def _(): position="before", ) - @reactive.effect - @reactive.event(input.add_text_panel) - def _(): - ui.insert_nav_panel( - "tabs", - "Placeholder Text Panel", - target="Menu", - position="before", - select=True, - ) - app = App(app_ui, server) diff --git a/shiny/api-examples/insert_nav_panel/app-express.py b/shiny/api-examples/insert_nav_panel/app-express.py index 4c260b811..28368a66c 100644 --- a/shiny/api-examples/insert_nav_panel/app-express.py +++ b/shiny/api-examples/insert_nav_panel/app-express.py @@ -5,7 +5,6 @@ ui.input_action_button("add", "Add 'Dynamic' tab") ui.input_action_button("remove_foo", "Remove 'Foo' tabs") ui.input_action_button("add_foo", "Add New 'Foo' tab") - # Add text panels adds both a text panel in the main navset as well as one in the menu dropdown ui.input_action_button("add_text_panel", "Add Text Panels") @@ -67,15 +66,3 @@ def _(): target="s2", position="before", ) - - -@reactive.effect -@reactive.event(input.add_text_panel) -def _(): - ui.insert_nav_panel( - "tabs", - "Placeholder Text Panel", - target="Menu", - position="before", - select=True, - ) diff --git a/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py b/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py index 12433b2a4..c47fb96d3 100644 --- a/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py +++ b/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py @@ -1,5 +1,4 @@ # import pytest -import pytest from playwright.sync_api import Page, expect from shiny.playwright import controller diff --git a/tests/playwright/shiny/components/express_navs/test_express_navs.py b/tests/playwright/shiny/components/express_navs/test_express_navs.py index 3981ca35b..49d28a0ed 100644 --- a/tests/playwright/shiny/components/express_navs/test_express_navs.py +++ b/tests/playwright/shiny/components/express_navs/test_express_navs.py @@ -1,4 +1,3 @@ -import pytest from playwright.sync_api import Page, expect from shiny.playwright import controller From b20faac6a038ed614f2951b4a22c02a2cb454e74 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Tue, 29 Jul 2025 15:54:22 -0400 Subject: [PATCH 43/56] Updating to remove top level --- shiny/api-examples/insert_nav_panel/app-express.py | 1 - 1 file changed, 1 deletion(-) diff --git a/shiny/api-examples/insert_nav_panel/app-express.py b/shiny/api-examples/insert_nav_panel/app-express.py index 28368a66c..cafd9cb54 100644 --- a/shiny/api-examples/insert_nav_panel/app-express.py +++ b/shiny/api-examples/insert_nav_panel/app-express.py @@ -55,7 +55,6 @@ def _(): ) -# Button push for add_text_panel adds two panels @reactive.effect @reactive.event(input.add_text_panel) def _(): From 748d81b61b721d942d63b3d65591325ba7e35a3d Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Wed, 30 Jul 2025 09:46:43 -0400 Subject: [PATCH 44/56] Updated tests' --- shiny/api-examples/insert_nav_panel/app-express.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/shiny/api-examples/insert_nav_panel/app-express.py b/shiny/api-examples/insert_nav_panel/app-express.py index cafd9cb54..9f485b264 100644 --- a/shiny/api-examples/insert_nav_panel/app-express.py +++ b/shiny/api-examples/insert_nav_panel/app-express.py @@ -33,13 +33,6 @@ def _(): ui.remove_nav_panel("tabs", target="Foo") -@reactive.effect -@reactive.event(input.add) -def _(): - id = "Dynamic-" + str(input.add()) - ui.insert_nav_panel("tabs", title=id, value=id, target="s2", position="before") - - @reactive.effect @reactive.event(input.add_foo) def _(): From 0c9448c1b983f1d47889474cf83346c78d2698f1 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Wed, 30 Jul 2025 13:29:39 -0400 Subject: [PATCH 45/56] Updating tests --- .../api-examples/insert_nav_panel/app-core.py | 12 --------- .../insert_nav_panel/app-express.py | 13 ---------- .../api-examples/update_nav_panel/app-core.py | 4 +-- .../update_nav_panel/app-express.py | 4 +-- .../shiny/components/dynamic_navs/app.py | 2 +- .../dynamic_navs/test_navs_dynamic.py | 26 +++++++++---------- 6 files changed, 17 insertions(+), 44 deletions(-) diff --git a/shiny/api-examples/insert_nav_panel/app-core.py b/shiny/api-examples/insert_nav_panel/app-core.py index eb53fd502..df3b503cc 100644 --- a/shiny/api-examples/insert_nav_panel/app-core.py +++ b/shiny/api-examples/insert_nav_panel/app-core.py @@ -5,7 +5,6 @@ ui.input_action_button("add", "Add 'Dynamic' tab"), ui.input_action_button("remove_foo", "Remove 'Foo' tabs"), ui.input_action_button("add_foo", "Add New 'Foo' tab"), - ui.input_action_button("add_text_panel", "Add Text Panels"), ), ui.navset_tab( ui.nav_panel("Hello", "This is the hello tab"), @@ -50,16 +49,5 @@ def _(): select=True, ) - @reactive.effect - @reactive.event(input.add_text_panel) - def _(): - id = "Text-" + str(input.add_text_panel()) - ui.insert_nav_panel( - "tabs", - id, - target="s2", - position="before", - ) - app = App(app_ui, server) diff --git a/shiny/api-examples/insert_nav_panel/app-express.py b/shiny/api-examples/insert_nav_panel/app-express.py index 9f485b264..f97a64338 100644 --- a/shiny/api-examples/insert_nav_panel/app-express.py +++ b/shiny/api-examples/insert_nav_panel/app-express.py @@ -5,7 +5,6 @@ ui.input_action_button("add", "Add 'Dynamic' tab") ui.input_action_button("remove_foo", "Remove 'Foo' tabs") ui.input_action_button("add_foo", "Add New 'Foo' tab") - ui.input_action_button("add_text_panel", "Add Text Panels") with ui.navset_tab(id="tabs"): @@ -46,15 +45,3 @@ def _(): position="before", select=True, ) - - -@reactive.effect -@reactive.event(input.add_text_panel) -def _(): - id = "Text-" + str(input.add_text_panel()) - ui.insert_nav_panel( - "tabs", - id, - target="s2", - position="before", - ) diff --git a/shiny/api-examples/update_nav_panel/app-core.py b/shiny/api-examples/update_nav_panel/app-core.py index ff6fb87df..1d2d8987d 100644 --- a/shiny/api-examples/update_nav_panel/app-core.py +++ b/shiny/api-examples/update_nav_panel/app-core.py @@ -9,8 +9,8 @@ ui.input_action_button("showMenu", "Show 'More' nav_menu"), ), ui.navset_tab( - ui.nav_panel("Foo", "This is the foo tab"), - ui.nav_panel("Bar", "This is the bar tab"), + ui.nav_panel("Foo", "This is the foo tab", value="Foo"), + ui.nav_panel("Bar", "This is the bar tab", value="Bar"), ui.nav_menu( "More", ui.nav_panel("Table", "Table page"), diff --git a/shiny/api-examples/update_nav_panel/app-express.py b/shiny/api-examples/update_nav_panel/app-express.py index bd3a96f7c..841a2bd4e 100644 --- a/shiny/api-examples/update_nav_panel/app-express.py +++ b/shiny/api-examples/update_nav_panel/app-express.py @@ -10,9 +10,9 @@ ui.input_action_button("showMenu", "Show 'More' nav_menu") with ui.navset_tab(id="tabs"): - with ui.nav_panel("Foo"): + with ui.nav_panel("Foo", value="Foo"): "This is the foo tab" - with ui.nav_panel("Bar"): + with ui.nav_panel("Bar", value="Bar"): "This is the bar tab" with ui.nav_menu(title="More", value="More"): with ui.nav_panel("Table"): diff --git a/tests/playwright/shiny/components/dynamic_navs/app.py b/tests/playwright/shiny/components/dynamic_navs/app.py index 0dc7d4989..3f22874e0 100644 --- a/tests/playwright/shiny/components/dynamic_navs/app.py +++ b/tests/playwright/shiny/components/dynamic_navs/app.py @@ -63,7 +63,7 @@ def _(): n = str(input.add_foo()) ui.insert_nav_panel( "tabs", - ui.nav_panel("Foo-" + n, "Foo-" + n, value="Foo"), + ui.nav_panel("Foo-" + n, "Foo-" + n, value="Foo-" + n), target="Menu", position="before", select=True, diff --git a/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py b/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py index c47fb96d3..0e0823143 100644 --- a/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py +++ b/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py @@ -18,6 +18,7 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: controller.NavsetTab(page, "tabs").expect_nav_titles( ["Hello", "Foo", "Static1", "Static2"] ) + # Click add-foo to add a new Foo tab addfoo = controller.InputActionButton(page, "add_foo") addfoo.click() @@ -25,49 +26,46 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: ["Hello", "Foo", "Foo-1", "Static1", "Static2"] ) # Check that Foo-1 tab is added - navpanel = controller.NavPanel(page, "tabs", "Foo").loc.filter(has_text="Foo-1") - expect(navpanel).to_have_text("Foo-1") + expect(controller.NavPanel(page, "tabs", "Foo-1").loc).to_be_visible() # Click hide-tab to hide the Foo tabs hidetab = controller.InputActionButton(page, "hide_tab") hidetab.click() - # Expect the Foo tabs to be hidden - navpanel = controller.NavPanel(page, "tabs", "Foo").loc.filter(has_text="Foo-1") - expect(navpanel).to_be_hidden() - navpanel = controller.NavPanel(page, "tabs", "Foo").loc.filter( - has_text="This is the Foo tab" - ) + # Expect the Foo tab to be hidden + navpanel = controller.NavPanel(page, "tabs", "Foo").loc expect(navpanel).to_be_hidden() + navpanel = controller.NavPanel(page, "tabs", "Foo-1").loc + expect(navpanel).to_be_visible() # Click show-tab to show the Foo tabs again showtab = controller.InputActionButton(page, "show_tab") showtab.click() # Expect the Foo tabs to be visible again - navpanel2 = controller.NavPanel(page, "tabs", "Foo").loc.first + navpanel2 = controller.NavPanel(page, "tabs", "Foo").loc expect(navpanel2).to_be_visible() - navpanel3 = controller.NavPanel(page, "tabs", "Foo").loc.last + navpanel3 = controller.NavPanel(page, "tabs", "Foo-1").loc expect(navpanel3).to_be_visible() - # Click remove-foo to remove the Foo tabs + # Click remove-foo to remove the Foo tab removefoo = controller.InputActionButton(page, "remove_foo") removefoo.click() controller.NavsetTab(page, "tabs").expect_nav_titles( - ["Hello", "Static1", "Static2"] + ["Hello", "Foo-1", "Static1", "Static2"] ) # Click add to add a dynamic tab add = controller.InputActionButton(page, "add") add.click() controller.NavsetTab(page, "tabs").expect_nav_titles( - ["Hello", "Static1", "Dynamic-1", "Static2"] + ["Hello", "Foo-1", "Static1", "Dynamic-1", "Static2"] ) # Click add again to add another dynamic tab add.click() controller.NavsetTab(page, "tabs").expect_nav_titles( - ["Hello", "Static1", "Dynamic-1", "Dynamic-2", "Static2"] + ["Hello", "Foo-1", "Static1", "Dynamic-1", "Dynamic-2", "Static2"] ) page.get_by_role("button", name="Menu", exact=True).click() From 1fff229d721807a49fca72a14fd72572ab487d6b Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Wed, 30 Jul 2025 13:46:41 -0400 Subject: [PATCH 46/56] Updating examples --- .../api-examples/insert_nav_panel/app-core.py | 37 ++++++++---------- .../insert_nav_panel/app-express.py | 39 ++++++++----------- 2 files changed, 34 insertions(+), 42 deletions(-) diff --git a/shiny/api-examples/insert_nav_panel/app-core.py b/shiny/api-examples/insert_nav_panel/app-core.py index df3b503cc..b014daeef 100644 --- a/shiny/api-examples/insert_nav_panel/app-core.py +++ b/shiny/api-examples/insert_nav_panel/app-core.py @@ -3,11 +3,10 @@ app_ui = ui.page_sidebar( ui.sidebar( ui.input_action_button("add", "Add 'Dynamic' tab"), - ui.input_action_button("remove_foo", "Remove 'Foo' tabs"), - ui.input_action_button("add_foo", "Add New 'Foo' tab"), + ui.input_action_button("update_foo", "Add/Remove 'Foo' tab"), ), ui.navset_tab( - ui.nav_panel("Hello", "This is the hello tab"), + ui.nav_panel("Hello", "This is the hello tab", value="Hello"), ui.nav_panel("Foo", "This is the Foo tab", value="Foo"), ui.nav_menu( "Static", @@ -21,32 +20,30 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.effect - @reactive.event(input.add) - def _(): - id = "Dynamic-" + str(input.add()) - ui.insert_nav_panel( - "tabs", - ui.nav_panel(id, id), - target="s2", - position="before", - ) @reactive.effect - @reactive.event(input.remove_foo) + @reactive.event(input.update_foo) def _(): - ui.remove_nav_panel("tabs", target="Foo") + if input.update_foo() % 2 == 0: + ui.insert_nav_panel( + "tabs", + ui.nav_panel("Foo", "Foo is back now", value="Foo"), + target="Menu", + position="before", + select=True, + ) + else: + ui.remove_nav_panel("tabs", target="Foo") @reactive.effect - @reactive.event(input.add_foo) + @reactive.event(input.add) def _(): - n = str(input.add_foo()) + id = "Dynamic-" + str(input.add()) ui.insert_nav_panel( "tabs", - ui.nav_panel("Foo-" + n, "This is the new Foo-" + n + " tab", value="Foo"), - target="Menu", + ui.nav_panel(id, id, value=id), + target="s2", position="before", - select=True, ) diff --git a/shiny/api-examples/insert_nav_panel/app-express.py b/shiny/api-examples/insert_nav_panel/app-express.py index f97a64338..f2cf54eaf 100644 --- a/shiny/api-examples/insert_nav_panel/app-express.py +++ b/shiny/api-examples/insert_nav_panel/app-express.py @@ -3,8 +3,7 @@ with ui.sidebar(): ui.input_action_button("add", "Add 'Dynamic' tab") - ui.input_action_button("remove_foo", "Remove 'Foo' tabs") - ui.input_action_button("add_foo", "Add New 'Foo' tab") + ui.input_action_button("update_foo", "Add/Remove 'Foo' tab"), with ui.navset_tab(id="tabs"): @@ -20,28 +19,24 @@ @reactive.effect -@reactive.event(input.add) -def _(): - id = "Dynamic-" + str(input.add()) - ui.insert_nav_panel("tabs", title=id, value=id, target="s2", position="before") - - -@reactive.effect -@reactive.event(input.remove_foo) +@reactive.event(input.update_foo) def _(): - ui.remove_nav_panel("tabs", target="Foo") + if input.update_foo() % 2 == 0: + ui.insert_nav_panel( + "tabs", + "Foo", + "Foo is back now", + value="Foo", + target="Menu", + position="before", + select=True, + ) + else: + ui.remove_nav_panel("tabs", target="Foo") @reactive.effect -@reactive.event(input.add_foo) +@reactive.event(input.add) def _(): - n = str(input.add_foo()) - ui.insert_nav_panel( - "tabs", - "Foo-" + n, - "This is the new Foo-" + n + " tab", - value="Foo", - target="Menu", - position="before", - select=True, - ) + id = "Dynamic-" + str(input.add()) + ui.insert_nav_panel("tabs", title=id, value=id, target="s2", position="before") From c29710b0006cb7edfc61a883b31cdf24708359df Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Wed, 30 Jul 2025 13:50:37 -0400 Subject: [PATCH 47/56] Adding changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1324948a..2eade7d3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### New features +* Added `ui.insert_nav_panel()`, `ui.remove_nav_panel()`, and `ui.update_nav_panel` to support dynamic navigation. (#90) + * `ui.sidebar()` is now interactively resizable. (#2020) * `ui.update_*()` functions now accept `ui.TagChild` (i.e., HTML) as input to the `label` and `icon` arguments. (#2020) From 5a89e2c7864efa04d5470d70f20bbfcd7011f384 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Wed, 30 Jul 2025 13:52:35 -0400 Subject: [PATCH 48/56] Minor correction --- shiny/api-examples/insert_nav_panel/app-express.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shiny/api-examples/insert_nav_panel/app-express.py b/shiny/api-examples/insert_nav_panel/app-express.py index f2cf54eaf..41163f3ac 100644 --- a/shiny/api-examples/insert_nav_panel/app-express.py +++ b/shiny/api-examples/insert_nav_panel/app-express.py @@ -3,7 +3,7 @@ with ui.sidebar(): ui.input_action_button("add", "Add 'Dynamic' tab") - ui.input_action_button("update_foo", "Add/Remove 'Foo' tab"), + ui.input_action_button("update_foo", "Add/Remove 'Foo' tab") with ui.navset_tab(id="tabs"): From 78b54601cbc65faf69d99465e4a81d5788b9d4fa Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Wed, 30 Jul 2025 14:15:30 -0400 Subject: [PATCH 49/56] Adding notifications --- shiny/api-examples/insert_nav_panel/app-core.py | 2 ++ shiny/api-examples/insert_nav_panel/app-express.py | 1 + tests/playwright/shiny/components/dynamic_navs/app.py | 6 ------ .../shiny/components/dynamic_navs/test_navs_dynamic.py | 1 - 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/shiny/api-examples/insert_nav_panel/app-core.py b/shiny/api-examples/insert_nav_panel/app-core.py index b014daeef..cf4bdb383 100644 --- a/shiny/api-examples/insert_nav_panel/app-core.py +++ b/shiny/api-examples/insert_nav_panel/app-core.py @@ -46,5 +46,7 @@ def _(): position="before", ) + ui.notification_show(f"Added tab to menu: {id}") + app = App(app_ui, server) diff --git a/shiny/api-examples/insert_nav_panel/app-express.py b/shiny/api-examples/insert_nav_panel/app-express.py index 41163f3ac..56e6df7f4 100644 --- a/shiny/api-examples/insert_nav_panel/app-express.py +++ b/shiny/api-examples/insert_nav_panel/app-express.py @@ -40,3 +40,4 @@ def _(): def _(): id = "Dynamic-" + str(input.add()) ui.insert_nav_panel("tabs", title=id, value=id, target="s2", position="before") + ui.notification_show(f"Added tab to menu: {id}") diff --git a/tests/playwright/shiny/components/dynamic_navs/app.py b/tests/playwright/shiny/components/dynamic_navs/app.py index 3f22874e0..6fa4f2e34 100644 --- a/tests/playwright/shiny/components/dynamic_navs/app.py +++ b/tests/playwright/shiny/components/dynamic_navs/app.py @@ -28,12 +28,6 @@ def server(input: Inputs, output: Outputs, session: Session): @reactive.effect def _(): - ui.insert_nav_panel( - "tabs", - "Stringy Panel", - target="Foo", - position="before", - ) ui.insert_nav_panel( "tabs", "Stringier Panel", diff --git a/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py b/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py index 0e0823143..31a223042 100644 --- a/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py +++ b/tests/playwright/shiny/components/dynamic_navs/test_navs_dynamic.py @@ -9,7 +9,6 @@ def test_dynamic_navs(page: Page, local_app: ShinyAppProc) -> None: page.goto(local_app.url) # String insertion as panels worked correctly - expect(page.get_by_text("Stringy Panel")).to_be_visible() page.get_by_role("button", name="Menu", exact=True).click() expect(page.get_by_text("Stringier Panel")).to_be_visible() page.get_by_role("button", name="Menu", exact=True).click() From 1fced93295357a15477e9bc526b0c2c1ad3a1559 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Wed, 30 Jul 2025 14:23:15 -0400 Subject: [PATCH 50/56] Minor changes --- shiny/ui/_navs_dynamic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shiny/ui/_navs_dynamic.py b/shiny/ui/_navs_dynamic.py index 4f4190f57..f7322acd9 100644 --- a/shiny/ui/_navs_dynamic.py +++ b/shiny/ui/_navs_dynamic.py @@ -130,7 +130,7 @@ def update_nav_panel( id The `id` of the relevant navigation container (i.e., `navset_*()` object). target - The `value` of an existing :func:`shiny.ui.nav` item to show. + The `value` of an existing :func:`shiny.ui.nav_panel` item to show. method The action to perform on the nav_panel (`"show"` or `"hide"`). session @@ -139,7 +139,7 @@ def update_nav_panel( Note ---- - On reveal, the `nav_panel` will not be the active tab. To change the active tab, use `~update_navs()` + On reveal, the `nav_panel` will not be the active tab. To change the active tab, use :func:`~shiny.ui.update_navs()` For example: ``` @reactive.effect From 7ec91915239139f123b36e2899acd52563547025 Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 30 Jul 2025 13:58:20 -0500 Subject: [PATCH 51/56] Add to API reference --- docs/_quartodoc-core.yml | 3 +++ docs/_quartodoc-express.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/docs/_quartodoc-core.yml b/docs/_quartodoc-core.yml index 57b6cf86f..cb8cff5c5 100644 --- a/docs/_quartodoc-core.yml +++ b/docs/_quartodoc-core.yml @@ -84,6 +84,9 @@ quartodoc: - ui.navset_pill_list - ui.navset_hidden - ui.navbar_options + - ui.insert_nav_panel + - ui.remove_nav_panel + - ui.update_nav_panel - title: UI panels desc: Visually group together a section of UI components. contents: diff --git a/docs/_quartodoc-express.yml b/docs/_quartodoc-express.yml index 28d976969..a140abb5d 100644 --- a/docs/_quartodoc-express.yml +++ b/docs/_quartodoc-express.yml @@ -78,6 +78,9 @@ quartodoc: - express.ui.navset_pill_list - express.ui.navset_hidden - express.ui.navbar_options + - express.ui.insert_nav_panel + - express.ui.remove_nav_panel + - express.ui.update_nav_panel - title: Chat interface desc: Build a chatbot interface contents: From 49438c1666fa2853fec88d0bbd787122cc9a4107 Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 30 Jul 2025 14:01:43 -0500 Subject: [PATCH 52/56] Make no example --- shiny/ui/_navs_dynamic.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/shiny/ui/_navs_dynamic.py b/shiny/ui/_navs_dynamic.py index f7322acd9..729634c47 100644 --- a/shiny/ui/_navs_dynamic.py +++ b/shiny/ui/_navs_dynamic.py @@ -8,7 +8,7 @@ else: from typing_extensions import Literal -from .._docstring import add_example +from .._docstring import add_example, no_example from .._namespaces import resolve_id from ..session import Session, require_active_session from ..types import NavSetArg @@ -85,6 +85,7 @@ def insert_nav_panel( session._send_message_sync({"shiny-insert-tab": msg}) +@no_example() def remove_nav_panel(id: str, target: str, session: Optional[Session] = None) -> None: """ Remove a nav item from a navigation container. @@ -99,6 +100,10 @@ def remove_nav_panel(id: str, target: str, session: Optional[Session] = None) -> A :class:`~shiny.Session` instance. If not provided, it is inferred via :func:`~shiny.session.get_current_session`. + Note + ---- + See :func:`~shiny.ui.insert_nav_panel` for an example. + See Also -------- ~insert_nav_panel From f2aad2ef59b530641786d56739f6ddeabb75b360 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Wed, 30 Jul 2025 16:24:46 -0400 Subject: [PATCH 53/56] adding python string --- shiny/ui/_navs_dynamic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shiny/ui/_navs_dynamic.py b/shiny/ui/_navs_dynamic.py index f7322acd9..9f2a46129 100644 --- a/shiny/ui/_navs_dynamic.py +++ b/shiny/ui/_navs_dynamic.py @@ -141,7 +141,7 @@ def update_nav_panel( ---- On reveal, the `nav_panel` will not be the active tab. To change the active tab, use :func:`~shiny.ui.update_navs()` For example: - ``` + ```python @reactive.effect @reactive.event(input.show_tab) def _(): From d5c199e5c6dc1ef5a68ef5f78483324e11381a71 Mon Sep 17 00:00:00 2001 From: Liz Nelson Date: Wed, 30 Jul 2025 16:40:54 -0400 Subject: [PATCH 54/56] Update docstrings --- shiny/ui/_navs_dynamic.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/shiny/ui/_navs_dynamic.py b/shiny/ui/_navs_dynamic.py index cf196bdd5..8eb7733ea 100644 --- a/shiny/ui/_navs_dynamic.py +++ b/shiny/ui/_navs_dynamic.py @@ -52,8 +52,8 @@ def insert_nav_panel( See Also -------- - ~remove_nav_panel - ~update_nav_panel + ~shiny.ui.remove_nav_panel + ~shiny.ui.update_nav_panel ~shiny.ui.nav_panel """ @@ -106,8 +106,8 @@ def remove_nav_panel(id: str, target: str, session: Optional[Session] = None) -> See Also -------- - ~insert_nav_panel - ~update_nav_panel + ~shiny.ui.insert_nav_panel + ~shiny.ui.update_nav_panel ~shiny.ui.nav_panel """ @@ -156,10 +156,10 @@ def _(): See Also -------- - ~insert_nav_panel - ~remove_nav_panel + ~shiny.ui.insert_nav_panel + ~shiny.ui.remove_nav_panel ~shiny.ui.nav_panel - ~update_navs + ~shiny.ui.update_navs """ session = require_active_session(session) From f28420e0b6fd223fce28db443cc44fbd382191a4 Mon Sep 17 00:00:00 2001 From: E Nelson Date: Thu, 31 Jul 2025 13:15:15 -0400 Subject: [PATCH 55/56] Update CHANGELOG.md Co-authored-by: Carson Sievert --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c03eb8b2..f828fc612 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### New features -* Added `ui.insert_nav_panel()`, `ui.remove_nav_panel()`, and `ui.update_nav_panel` to support dynamic navigation. (#90) +* Added `ui.insert_nav_panel()`, `ui.remove_nav_panel()`, and `ui.update_nav_panel()` to support dynamic navigation. (#90) * Added support for python 3.13. (#1711) From 8f0fb76387d5317807b23121c17f8a6dcb8bc732 Mon Sep 17 00:00:00 2001 From: E Nelson Date: Thu, 31 Jul 2025 13:15:31 -0400 Subject: [PATCH 56/56] Update shiny/ui/_navs_dynamic.py Co-authored-by: Carson Sievert --- shiny/ui/_navs_dynamic.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/shiny/ui/_navs_dynamic.py b/shiny/ui/_navs_dynamic.py index 8eb7733ea..ddd98cc15 100644 --- a/shiny/ui/_navs_dynamic.py +++ b/shiny/ui/_navs_dynamic.py @@ -164,10 +164,8 @@ def _(): session = require_active_session(session) - id = resolve_id(id) - msg = { - "inputId": id, + "inputId": resolve_id(id), "target": target, "type": method, }