Skip to content

Commit

Permalink
Use a Protocol for typing navigation items inside consumers
Browse files Browse the repository at this point in the history
This way others can create their own custom nav item classes
  • Loading branch information
cpsievert committed Apr 26, 2022
1 parent a094fb3 commit 0bec791
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 26 deletions.
6 changes: 3 additions & 3 deletions shiny/examples/nav/app.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from typing import List, Union
from typing import List

from shiny import *
from shiny.types import NavsArg
from shiny.ui import h4
from shiny.ui._navs import Nav, NavMenu # TODO: export this via shiny.types
from fontawesome import icon_svg as icon


def nav_items(prefix: str) -> List[Union[Nav, NavMenu]]:
def nav_items(prefix: str) -> List[NavsArg]:
return [
ui.nav("a", prefix + ": tab a content"),
ui.nav("b", prefix + ": tab b content"),
Expand Down
43 changes: 42 additions & 1 deletion shiny/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
)

import sys
from typing import Union, Optional
from typing import Union, Optional, Tuple

# Even though TypedDict is available in Python 3.8, because it's used with NotRequired,
# they should both come from the same typing module.
Expand All @@ -23,6 +23,13 @@
else:
from typing_extensions import NotRequired, TypedDict

if sys.version_info >= (3, 8):
from typing import Protocol
else:
from typing_extensions import Protocol

from htmltools import TagChildArg

from ._docstring import add_example

# Sentinel value - indicates a missing value in a function call.
Expand Down Expand Up @@ -138,3 +145,37 @@ class SilentCancelOutputException(Exception):

class ActionButtonValue(int):
pass


class NavsArg(Protocol):
"""
An value suitable for passing to a navigation container (e.g.,
:func:`~shiny.ui.navs_tab`).
"""

def resolve(
self, selected: Optional[str], id: Optional[str] = None, is_menu: bool = False
) -> Tuple[TagChildArg, TagChildArg]:
"""
Resolve information provided by the navigation container.
Parameters
----------
selected
The value of the navigation item to be shown on page load.
id
The id of the navigation item.
is_menu
Whether the navigation item is contained within a menu.
"""
...

def get_value(self) -> Optional[str]:
"""
Get the value of this navigation item (if any).
This value is only used to determine what navigation item should be shown
by default when none is specified (i.e., the first navigation item that
returns a value is used to determine the container's ``selected`` value).
"""
...
38 changes: 19 additions & 19 deletions shiny/ui/_navs.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import copy
import re
import sys
from typing import Optional, Tuple, List, Union, NamedTuple, cast, Any
from typing import Optional, Tuple, Union, NamedTuple, cast

if sys.version_info >= (3, 8):
from typing import Literal
Expand All @@ -28,6 +28,7 @@
from ._bootstrap import row, column
from .._docstring import add_example
from ._html_dependencies import bootstrap_deps
from ..types import NavsArg
from .._utils import private_random_int

# -----------------------------------------------------------------------------
Expand All @@ -38,12 +39,9 @@ class Nav(NamedTuple):
# nav_item()/nav_spacer() have None as their content
content: Optional[Tag]

def render(
def resolve(
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.
"""
) -> Tuple[TagChildArg, TagChildArg]:

# Nothing to do for nav_item()/nav_spacer()
if self.content is None:
Expand Down Expand Up @@ -222,17 +220,19 @@ def nav_spacer() -> Nav:
class NavMenu:
def __init__(
self,
*args: Union[Nav, str],
*args: Union[NavsArg, str],
title: TagChildArg,
value: str,
align: Literal["left", "right"] = "left",
) -> None:
self.nav_items: List[Nav] = [menu_string_as_nav(x) for x in args]
self.nav_items = [menu_string_as_nav(x) for x in args]
self.title = title
self.value = value
self.align = align

def render(self, selected: Optional[str], **kwargs: Any) -> Tuple[Tag, TagList]:
def resolve(
self, selected: Optional[str], id: Optional[str] = None, is_menu: bool = False
) -> Tuple[TagChildArg, TagChildArg]:
nav, content = render_tabset(
*self.nav_items,
ul_class=f"dropdown-menu {'dropdown-menu-right' if self.align == 'right' else ''}",
Expand Down Expand Up @@ -273,7 +273,7 @@ def get_value(self) -> Optional[str]:
return None


def menu_string_as_nav(x: Union[str, Nav]) -> Nav:
def menu_string_as_nav(x: Union[str, NavsArg]) -> NavsArg:
if not isinstance(x, str):
return x

Expand Down Expand Up @@ -349,7 +349,7 @@ def nav_menu(
# Navigation containers
# -----------------------------------------------------------------------------
def navs_tab(
*args: Union[Nav, NavMenu],
*args: NavsArg,
id: Optional[str] = None,
selected: Optional[str] = None,
header: TagChildArg = None,
Expand Down Expand Up @@ -399,7 +399,7 @@ def navs_tab(


def navs_pill(
*args: Union[Nav, NavMenu],
*args: NavsArg,
id: Optional[str] = None,
selected: Optional[str] = None,
header: TagChildArg = None,
Expand Down Expand Up @@ -449,7 +449,7 @@ def navs_pill(

@add_example()
def navs_hidden(
*args: Union[Nav, NavMenu],
*args: NavsArg,
id: Optional[str] = None,
selected: Optional[str] = None,
header: TagChildArg = None,
Expand Down Expand Up @@ -495,7 +495,7 @@ def navs_hidden(


def navs_tab_card(
*args: Union[Nav, NavMenu],
*args: NavsArg,
id: Optional[str] = None,
selected: Optional[str] = None,
header: TagChildArg = None,
Expand Down Expand Up @@ -544,7 +544,7 @@ def navs_tab_card(


def navs_pill_card(
*args: Union[Nav, NavMenu],
*args: NavsArg,
id: Optional[str] = None,
selected: Optional[str] = None,
header: TagChildArg = None,
Expand Down Expand Up @@ -600,7 +600,7 @@ def navs_pill_card(


def navs_pill_list(
*args: Union[Nav, NavMenu],
*args: NavsArg,
id: Optional[str] = None,
selected: Optional[str] = None,
header: TagChildArg = None,
Expand Down Expand Up @@ -662,7 +662,7 @@ def navs_pill_list(


def navs_bar(
*args: Union[Nav, NavMenu],
*args: NavsArg,
title: TagChildArg,
id: Optional[str] = None,
selected: Optional[str] = None,
Expand Down Expand Up @@ -784,7 +784,7 @@ def navs_bar(
# Utilities for rendering navs
# -----------------------------------------------------------------------------\
def render_tabset(
*items: Union[Nav, NavMenu],
*items: NavsArg,
ul_class: str,
id: Optional[str],
selected: Optional[str],
Expand All @@ -806,7 +806,7 @@ def render_tabset(
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):
nav, contents = x.render(selected, id=f"tab-{tabsetid}-{i}", is_menu=is_menu)
nav, contents = x.resolve(selected, id=f"tab-{tabsetid}-{i}", is_menu=is_menu)
ul_tag.append(nav)
div_tag.append(contents)

Expand Down
5 changes: 2 additions & 3 deletions shiny/ui/_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,12 @@
from .._docstring import add_example
from ._html_dependencies import bootstrap_deps
from ._navs import navs_bar
from ..types import MISSING, MISSING_TYPE
from ._navs import Nav, NavMenu
from ..types import MISSING, MISSING_TYPE, NavsArg
from ._utils import get_window_title


def page_navbar(
*args: Union[Nav, NavMenu],
*args: NavsArg,
title: Optional[Union[str, Tag, TagList]] = None,
id: Optional[str] = None,
selected: Optional[str] = None,
Expand Down

0 comments on commit 0bec791

Please sign in to comment.