Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement/improve nav() API by implementing in Python instead of React/JSX #136

Merged
merged 15 commits into from
Apr 28, 2022
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add unit tests and fix some bugs
cpsievert committed Apr 26, 2022
commit a094fb360582192d467ce814bf040b896af3edff
2 changes: 1 addition & 1 deletion shiny/examples/nav/app.py
Original file line number Diff line number Diff line change
@@ -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()")),
48 changes: 29 additions & 19 deletions shiny/ui/_navs.py
Original file line number Diff line number Diff line change
@@ -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:
@@ -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",
)
@@ -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]
@@ -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


@@ -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),
@@ -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,
175 changes: 175 additions & 0 deletions tests/test_navs.py
Original file line number Diff line number Diff line change
@@ -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>"""
)