diff --git a/shiny/ui/_navs.py b/shiny/ui/_navs.py
index 3160179e6..62a5bf42d 100644
--- a/shiny/ui/_navs.py
+++ b/shiny/ui/_navs.py
@@ -16,7 +16,7 @@
import copy
import re
import sys
-from typing import Optional, Tuple, List, Union, NamedTuple, cast
+from typing import Optional, Tuple, List, Union, NamedTuple, cast, Any
if sys.version_info >= (3, 8):
from typing import Literal
@@ -38,6 +38,48 @@ class Nav(NamedTuple):
# nav_item()/nav_spacer() have None as their content
content: Optional[Tag]
+ def render(
+ self, selected: Optional[str], id: str, is_menu: bool = False
+ ) -> Tuple[Tag, Optional[Tag]]:
+ """
+ Add appropriate tag attributes to nav/content tags when linking to internal content.
+ """
+
+ x = copy.copy(self)
+
+ # Nothing to do for nav_item()/nav_spacer()
+ if x.content is None:
+ return x.nav, None
+
+ # At least currently, in the case where both nav and content are tags
+ # (i.e., nav()), the nav always has a child tag...I'm not sure if
+ # there's a way to statically type this
+ a_tag = cast(Tag, x.nav.children[0])
+ if is_menu:
+ a_tag.add_class("dropdown-item")
+ else:
+ a_tag.add_class("nav-link")
+ x.nav.add_class("nav-item")
+
+ # Hyperlink the nav to the content
+ x.content.attrs["id"] = id
+ a_tag.attrs["href"] = f"#{id}"
+
+ # Mark the nav/content as active if it should be
+ if isinstance(selected, str) and selected == self.get_value():
+ x.content.add_class("active")
+ a_tag.add_class("active")
+
+ x.nav.children[0] = a_tag
+
+ return x.nav, x.content
+
+ def get_value(self) -> Optional[str]:
+ if self.content is None:
+ return None
+ a_tag = cast(Tag, self.nav.children[0])
+ return a_tag.attrs.get("data-value", None)
+
@add_example()
def nav(
@@ -188,6 +230,42 @@ def __init__(
self.value = value
self.align = align
+ def render(self, selected: Optional[str], **kwargs: Any) -> Tuple[Tag, TagList]:
+ nav, content = render_tabset(
+ *self.nav_items,
+ ul_class=f"dropdown-menu {'dropdown-menu-right' if self.align == 'right' else ''}",
+ id=None,
+ selected=selected,
+ is_menu=True,
+ )
+
+ active = False
+ for tab in content.children:
+ if isinstance(tab, Tag) and tab.has_class("active"):
+ active = True
+ break
+
+ return (
+ tags.li(
+ tags.a(
+ self.title,
+ class_=f"nav-link dropdown-toggle {'active' if active else ''}",
+ data_bs_toggle="dropdown",
+ # N.B. this value is only relevant for locating the insertion/removal
+ # of items inside the nav container
+ data_value=self.value,
+ href="#",
+ role="button",
+ ),
+ nav,
+ class_="nav-item dropdown",
+ ),
+ content.children,
+ )
+
+ def get_value(self) -> Optional[str]:
+ return None
+
def menu_string_as_nav(x: Union[str, Nav]) -> Nav:
if not isinstance(x, str):
@@ -708,105 +786,21 @@ def render_tabset(
if id is not None:
ul_class += " shiny-tab-input"
+ # If the user hasn't provided a selected value, use the first one
if selected is None:
- selected = find_first_nav_value(*items)
+ for x in items:
+ selected = x.get_value()
+ if selected is not None:
+ break
- result = {
- "ul_tag": tags.ul(
- bootstrap_deps(), class_=ul_class, id=id, data_tabsetid=tabsetid
- ),
- "div_tag": div(class_="tab-content", data_tabsetid=tabsetid),
- }
+ ul_tag = tags.ul(bootstrap_deps(), class_=ul_class, id=id, data_tabsetid=tabsetid)
+ div_tag = div(class_="tab-content", data_tabsetid=tabsetid)
for i, x in enumerate(items):
- if isinstance(x, NavMenu):
- nav, contents = render_dropdown(x, selected)
- elif isinstance(x, Nav):
- nav, contents = render_nav(x, f"tab-{tabsetid}-{i}", selected, is_menu)
- else:
- raise ValueError(f"Unknown nav type: {type(x)}")
- result["ul_tag"].append(nav)
- result["div_tag"].append(contents)
-
- return result["ul_tag"], result["div_tag"]
-
-
-def render_nav(
- x: Nav, id: str, selected: Optional[str], is_menu: bool = False
-) -> Tuple[Tag, Optional[Tag]]:
- """
- Add appropriate tag attributes to nav/content tags when linking to internal content.
- """
-
- x = copy.copy(x)
-
- # Nothing to do for nav_item()/nav_spacer()
- if x.content is None:
- return x.nav, None
-
- # At least currently, in the case where both nav and content are tags
- # (i.e., nav()), the nav always has a child tag...I'm not sure if
- # there's a way to statically type this
- a_tag = cast(Tag, x.nav.children[0])
- if is_menu:
- a_tag.add_class("dropdown-item")
- else:
- a_tag.add_class("nav-link")
- x.nav.add_class("nav-item")
-
- # Hyperlink the nav to the content
- x.content.attrs["id"] = id
- a_tag.attrs["href"] = f"#{id}"
-
- # Mark the nav/content as active if it should be
- if isinstance(selected, str) and selected == a_tag.attrs.get("data-value", None):
- x.content.add_class("active")
- a_tag.add_class("active")
-
- x.nav.children[0] = a_tag
-
- return x.nav, x.content
-
-
-def render_dropdown(x: NavMenu, selected: Optional[str]) -> Tuple[Tag, TagList]:
- nav, content = render_tabset(
- *x.nav_items,
- ul_class=f"dropdown-menu {'dropdown-menu-right' if x.align == 'right' else ''}",
- id=None,
- selected=selected,
- is_menu=True,
- )
-
- active = False
- for tab in content.children:
- if isinstance(tab, Tag) and tab.has_class("active"):
- active = True
- break
-
- return (
- tags.li(
- tags.a(
- x.title,
- class_=f"nav-link dropdown-toggle {'active' if active else ''}",
- data_bs_toggle="dropdown",
- data_value=x.value,
- href="#",
- role="button",
- ),
- nav,
- class_="nav-item dropdown",
- ),
- content.children,
- )
-
+ nav, contents = x.render(selected, id=f"tab-{tabsetid}-{i}", is_menu=is_menu)
+ ul_tag.append(nav)
+ div_tag.append(contents)
-def find_first_nav_value(*args: Union[Nav, NavMenu]) -> Optional[str]:
- for x in args:
- if isinstance(x, Nav) and x.content is not None:
- a_tag = cast(Tag, x.nav.children[0])
- return a_tag.attrs.get("data-value", None)
- if isinstance(x, NavMenu):
- return find_first_nav_value(*x.nav_items)
- return None
+ return ul_tag, div_tag
def card(