Skip to content

Commit

Permalink
Add unit tests and fix some bugs
Browse files Browse the repository at this point in the history
  • Loading branch information
cpsievert committed Apr 26, 2022
1 parent 083573b commit bfdc2e9
Show file tree
Hide file tree
Showing 3 changed files with 205 additions and 20 deletions.
2 changes: 1 addition & 1 deletion shiny/examples/nav/app.py
Expand Up @@ -46,7 +46,7 @@ def nav_items(prefix: str) -> List[Union[Nav, NavMenu]]:
id="navbar_id",
footer=ui.div(
{"style": "width:80%;margin: 0 auto"},
ui.h4("navs_tab()"),
h4("navs_tab()"),
ui.navs_tab(*nav_items("navs_tab()")),
h4("navs_pill()"),
ui.navs_pill(*nav_items("navs_pill()")),
Expand Down
48 changes: 29 additions & 19 deletions shiny/ui/_navs.py
Expand Up @@ -39,40 +39,41 @@ class Nav(NamedTuple):
content: Optional[Tag]

def render(
self, selected: Optional[str], id: str, is_menu: bool = False
self, selected: Optional[str], id: Optional[str] = None, 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
if self.content is None:
return self

# At least currently, in the case where both nav and content are tags
# (i.e., nav()), the nav always has a child <a> tag...I'm not sure if
# there's a way to statically type this
a_tag = cast(Tag, x.nav.children[0])
nav = copy.deepcopy(self.nav)
a_tag = cast(Tag, 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")
nav.add_class("nav-item")

# Hyperlink the nav to the content
x.content.attrs["id"] = id
a_tag.attrs["href"] = f"#{id}"
content = copy.copy(self.content)
if id is not None:
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")
content.add_class("active")
a_tag.add_class("active")

x.nav.children[0] = a_tag
nav.children[0] = a_tag

return x.nav, x.content
return nav, content

def get_value(self) -> Optional[str]:
if self.content is None:
Expand Down Expand Up @@ -119,15 +120,16 @@ def nav(
~shiny.ui.navs_pill_card
~shiny.ui.navs_hidden
"""
if not value:
if value is None:
value = str(title)

# N.B. at this point, we don't have enough info to link the nav to the content
# or add relevant classes. That's done later by consumers (i.e. nav containers)
link = tags.a(
icon,
title,
data_bs_toggle="tab",
data_bs_toggle="tab", # Bootstrap 5
data_toggle="tab", # Needed for shiny.js' insert-tab handler
data_value=value,
role="tab",
)
Expand Down Expand Up @@ -222,7 +224,7 @@ def __init__(
self,
*args: Union[Nav, str],
title: TagChildArg,
value: Optional[str] = None,
value: str,
align: Literal["left", "right"] = "left",
) -> None:
self.nav_items: List[Nav] = [menu_string_as_nav(x) for x in args]
Expand Down Expand Up @@ -264,6 +266,10 @@ def render(self, selected: Optional[str], **kwargs: Any) -> Tuple[Tag, TagList]:
)

def get_value(self) -> Optional[str]:
for x in self.nav_items:
val = x.get_value()
if val:
return val
return None


Expand Down Expand Up @@ -328,6 +334,9 @@ def nav_menu(
-------
See :func:`~shiny.ui.nav`
"""
if value is None:
value = str(title)

return NavMenu(
*args,
title=TagList(icon, title),
Expand Down Expand Up @@ -748,16 +757,17 @@ def navs_bar(
nav = div(nav, id=collapse_id, class_="collapse navbar-collapse")

nav_container.append(nav)
nav_final = tags.nav({"class": "navbar"}, nav_container)
nav_final = tags.nav({"class": "navbar navbar-expand-md"}, nav_container)

if position != "static-top":
nav_final.add_class(position)

nav_final.add_class(f"navbar-{'dark' if inverse else 'light'}")

if bg:
nav_final.attrs["style"] = "background-color: " + bg

if inverse:
nav_final.add_class("navbar-dark")
else:
nav_final.add_class(f"bg-{'dark' if inverse else 'light'}")

return TagList(
nav_final,
Expand Down
175 changes: 175 additions & 0 deletions tests/test_navs.py
@@ -0,0 +1,175 @@
import pytest

import random
import textwrap
from typing import Callable, Any, Union

from shiny import ui
from shiny._utils import private_seed
from htmltools import Tag, TagList


# Fix the randomness of these functions to make the tests deterministic
def with_private_seed(
func: Callable[[], Union[Tag, TagList]], *args: Any, **kwargs: Any
):
with private_seed():
random.seed(0)
return func(*args, **kwargs)


def test_nav_markup():
a = ui.nav("a", "a")
b = ui.nav("b", "b")
c = ui.nav("c", "c")
menu = ui.nav_menu(
"Menu",
c,
"----",
"Plain text",
"----",
ui.nav_item("Other item"),
)

x = with_private_seed(ui.navs_tab, a, b, ui.nav_item("Some item"), menu)

assert x.render()["html"] == textwrap.dedent(
"""\
<ul class="nav nav-tabs" data-tabsetid="7311">
<li class="nav-item">
<a data-bs-toggle="tab" data-toggle="tab" data-value="a" role="tab" class="nav-link active" href="#tab-7311-0">a</a>
</li>
<li class="nav-item">
<a data-bs-toggle="tab" data-toggle="tab" data-value="b" role="tab" class="nav-link" href="#tab-7311-1">b</a>
</li>
<li>Some item</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle " data-bs-toggle="dropdown" data-value="Menu" href="#" role="button">Menu</a>
<ul class="dropdown-menu " data-tabsetid="7890">
<li>
<a data-bs-toggle="tab" data-toggle="tab" data-value="c" role="tab" class="dropdown-item" href="#tab-7890-0">c</a>
</li>
<li class="dropdown-divider"></li>
<li class="dropdown-header">Plain text</li>
<li class="dropdown-divider"></li>
<li>Other item</li>
</ul>
</li>
</ul>
<div class="tab-content" data-tabsetid="7311">
<div class="tab-pane active" role="tabpanel" data-value="a" id="tab-7311-0">a</div>
<div class="tab-pane" role="tabpanel" data-value="b" id="tab-7311-1">b</div>
<div class="tab-pane" role="tabpanel" data-value="c" id="tab-7890-0">c</div>
</div>"""
)

x = with_private_seed(
ui.navs_pill,
menu,
a,
id="navs_pill_id",
)

assert x.render()["html"] == textwrap.dedent(
"""\
<ul class="nav nav-pills shiny-tab-input" id="navs_pill_id" data-tabsetid="7311">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle active" data-bs-toggle="dropdown" data-value="Menu" href="#" role="button">Menu</a>
<ul class="dropdown-menu " data-tabsetid="7890">
<li>
<a data-bs-toggle="tab" data-toggle="tab" data-value="c" role="tab" class="dropdown-item active" href="#tab-7890-0">c</a>
</li>
<li class="dropdown-divider"></li>
<li class="dropdown-header">Plain text</li>
<li class="dropdown-divider"></li>
<li>Other item</li>
</ul>
</li>
<li class="nav-item">
<a data-bs-toggle="tab" data-toggle="tab" data-value="a" role="tab" class="nav-link" href="#tab-7311-1">a</a>
</li>
</ul>
<div class="tab-content" data-tabsetid="7311">
<div class="tab-pane active" role="tabpanel" data-value="c" id="tab-7890-0">c</div>
<div class="tab-pane" role="tabpanel" data-value="a" id="tab-7311-1">a</div>
</div>"""
)

x = with_private_seed(
ui.navs_pill_card,
a,
ui.nav_menu("Menu", c),
b,
selected="c",
)

assert x.render()["html"] == textwrap.dedent(
"""\
<div class="card">
<div class="card-header">
<ul class="nav nav-pills card-header-pills" data-tabsetid="7311">
<li class="nav-item">
<a data-bs-toggle="tab" data-toggle="tab" data-value="a" role="tab" class="nav-link" href="#tab-7311-0">a</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle active" data-bs-toggle="dropdown" data-value="Menu" href="#" role="button">Menu</a>
<ul class="dropdown-menu " data-tabsetid="7890">
<li>
<a data-bs-toggle="tab" data-toggle="tab" data-value="c" role="tab" class="dropdown-item active" href="#tab-7890-0">c</a>
</li>
</ul>
</li>
<li class="nav-item">
<a data-bs-toggle="tab" data-toggle="tab" data-value="b" role="tab" class="nav-link" href="#tab-7311-2">b</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content" data-tabsetid="7311">
<div class="tab-pane" role="tabpanel" data-value="a" id="tab-7311-0">a</div>
<div class="tab-pane active" role="tabpanel" data-value="c" id="tab-7890-0">c</div>
<div class="tab-pane" role="tabpanel" data-value="b" id="tab-7311-2">b</div>
</div>
</div>
</div>"""
)

x = with_private_seed(
ui.navs_bar, # type: ignore
ui.nav_menu("Menu", "Plain text", c),
title="Page title",
footer="Page footer",
header="Page header",
)

assert x.render()["html"] == textwrap.dedent(
"""\
<nav class="navbar navbar-expand-md navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">Page title</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-collapse-1663" aria-controls="navbar-collapse-1663" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div id="navbar-collapse-1663" class="collapse navbar-collapse">
<ul class="nav navbar-nav" data-tabsetid="7311">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle active" data-bs-toggle="dropdown" data-value="Menu" href="#" role="button">Menu</a>
<ul class="dropdown-menu " data-tabsetid="7890">
<li class="dropdown-header">Plain text</li>
<li>
<a data-bs-toggle="tab" data-toggle="tab" data-value="c" role="tab" class="dropdown-item active" href="#tab-7890-1">c</a>
</li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">Page header</div>
<div class="tab-content" data-tabsetid="7311">
<div class="tab-pane active" role="tabpanel" data-value="c" id="tab-7890-1">c</div>
</div>
<div class="row">Page footer</div>
</div>"""
)

0 comments on commit bfdc2e9

Please sign in to comment.