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
Show file tree
Hide file tree
Changes from all commits
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
12 changes: 6 additions & 6 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,14 @@ Create segments of UI content.
:toctree: reference/

ui.nav
ui.nav_item
ui.nav_control
ui.nav_spacer
ui.nav_menu
ui.navs_tab
ui.navs_tab_card
ui.navs_pill
ui.navs_pill_card
ui.navs_pill_list
ui.navset_tab
ui.navset_tab_card
ui.navset_pill
ui.navset_pill_card
ui.navset_pill_list


UI panels
Expand Down
2 changes: 1 addition & 1 deletion examples/event/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
print the number of clicks in the console twice.
"""
),
ui.navs_tab_card(
ui.navset_tab_card(
ui.nav(
"Sync",
ui.input_action_button("btn", "Click me"),
Expand Down
2 changes: 1 addition & 1 deletion examples/inputs-update/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
multiple=True,
),
),
ui.navs_tab(
ui.navset_tab(
ui.nav("panel1", h2("This is the first panel.")),
ui.nav("panel2", h2("This is the second panel.")),
id="inTabset",
Expand Down
2 changes: 1 addition & 1 deletion examples/inputs/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
),
ui.panel_main(
ui.output_plot("plot"),
ui.navs_tab_card(
ui.navset_tab_card(
# TODO: output_plot() within a tab not working?
ui.nav("Inputs", ui.output_ui("inputs"), icon=icon_svg("code")),
ui.nav(
Expand Down
4 changes: 2 additions & 2 deletions parity.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ output: html_document

## Tabs

✓ `tabsetPanel()` -> `navs_tab()`/`navs_pill()`
✓ `navlistPanel()` -> `navs_pill_list()`
✓ `tabsetPanel()` -> `navset_tab()`/`navset_pill()`
✓ `navlistPanel()` -> `navset_pill_list()`
✓ `tabPanel()` -> `nav()`
✓ `navbarMenu()` -> `nav_menu()`
`tabPanelBody()` -> `navs_content()`
Expand Down
17 changes: 6 additions & 11 deletions scripts/htmlDependencies.R
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,9 @@ withr::with_options(
lapply(deps, copyDependencyToDir, "shiny/www/shared")
)

# For JSX based nav() implementation
bslib <- file.path(www, "shared", "bslib")
dir.create(bslib)
withr::with_tempdir({
cmd <- paste("git clone --depth 1 --branch jsx https://github.com/rstudio/bslib")
system(cmd)
file.copy(
"bslib/inst/navs/dist",
bslib, recursive = TRUE
)
})
# This additional bs3compat HTMLDependency() only holds
# the JS shim for tab panel logic, which we don't need
# since we're generating BS5+ tab markup. Note, however,
# we still do have bs3compat's CSS on the page, which
# comes in via the bootstrap HTMLDependency()
unlink("shiny/www/shared/bs3compat/", recursive = TRUE)
2 changes: 1 addition & 1 deletion shiny/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""A package for building reactive web applications."""

__version__ = "0.2.0.9001"
__version__ = "0.2.0.9002"

from ._shinyenv import is_pyodide as _is_pyodide

Expand Down
37 changes: 30 additions & 7 deletions shiny/_utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
import contextlib
import functools
import importlib
import inspect
import os
import random
import secrets
import sys
import tempfile

from typing import (
Callable,
Awaitable,
Expand All @@ -8,13 +18,6 @@
Any,
cast,
)
import functools
import os
import sys
import tempfile
import importlib
import inspect
import secrets

if sys.version_info >= (3, 10):
from typing import TypeGuard
Expand All @@ -33,6 +36,26 @@ def rand_hex(bytes: int) -> str:
return format_str.format(secrets.randbits(bytes * 8))


def private_random_int(min: int, max: int) -> str:
with private_seed():
return str(random.randint(min, max))


@contextlib.contextmanager
def private_seed():
state = random.getstate()
global own_random_state
try:
if own_random_state is not None:
random.setstate(own_random_state)
yield
finally:
own_random_state = random.getstate()
random.setstate(state)


own_random_state = None

# ==============================================================================
# Async-related functions
# ==============================================================================
Expand Down
40 changes: 24 additions & 16 deletions shiny/examples/nav/app.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from typing import List

from shiny import *
from htmltools import JSXTag, h4
from shiny.types import NavSetArg
from shiny.ui import h4
from fontawesome import icon_svg as icon


def nav_items(prefix: str) -> List[JSXTag]:
def nav_controls(prefix: str) -> List[NavSetArg]:
return [
ui.nav("a", prefix + ": tab a content"),
ui.nav("b", prefix + ": tab b content"),
ui.nav_item(
ui.nav_control(
ui.a(
icon("github"),
"Shiny",
Expand All @@ -21,7 +22,10 @@ def nav_items(prefix: str) -> List[JSXTag]:
ui.nav_menu(
"Other links",
ui.nav("c", prefix + ": tab c content"),
ui.nav_item(
"----",
"Plain text",
"----",
ui.nav_control(
ui.a(
icon("r-project"),
"RStudio",
Expand All @@ -35,28 +39,32 @@ def nav_items(prefix: str) -> List[JSXTag]:


app_ui = ui.page_navbar(
*nav_items("page_navbar"),
*nav_controls("page_navbar"),
title="page_navbar()",
bg="#0062cc",
inverse=True,
id="navbar_id",
footer=ui.div(
{"style": "width:80%;margin: 0 auto"},
ui.h4("navs_tab()"),
ui.navs_tab(*nav_items("navs_tab()")),
h4("navs_pill()"),
ui.navs_pill(*nav_items("navs_pill()")),
h4("navs_tab_card()"),
ui.navs_tab_card(*nav_items("navs_tab_card()")),
h4("navs_pill_card()"),
ui.navs_pill_card(*nav_items("navs_pill_card()")),
h4("navs_pill_list()"),
ui.navs_pill_list(*nav_items("navs_pill_list()")),
h4("navset_tab()"),
# ui.nav_menu("F", ui.nav("G", "g")),
ui.navset_tab(*nav_controls("navset_tab()")),
h4("navset_pill()"),
ui.navset_pill(*nav_controls("navset_pill()")),
h4("navset_tab_card()"),
ui.navset_tab_card(*nav_controls("navset_tab_card()")),
h4("navset_pill_card()"),
ui.navset_pill_card(*nav_controls("navset_pill_card()")),
h4("navset_pill_list()"),
ui.navset_pill_list(*nav_controls("navset_pill_list()")),
)
)


def server(input: Inputs, output: Outputs, session: Session):
pass
@reactive.Effect()
def _():
print("Current navbar page: ", input.navbar_id())


app = App(app_ui, server)
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@
ui.input_radio_buttons("controller", "Controller", ["1", "2", "3"], "1")
),
ui.panel_main(
ui.navs_hidden(
ui.nav_content("panel1", "Panel 1 content"),
ui.nav_content("panel2", "Panel 2 content"),
ui.nav_content("panel3", "Panel 3 content"),
ui.navset_hidden(
ui.nav(None, "Panel 1 content", value="panel1"),
ui.nav(None, "Panel 2 content", value="panel2"),
ui.nav(None, "Panel 3 content", value="panel3"),
id="hidden_tabs",
)
)
),
)
)


def server(input: Inputs, output: Outputs, session: Session):
@reactive.Effect()
@event(input.controller)
Expand Down
2 changes: 1 addition & 1 deletion shiny/examples/update_navs/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
ui.layout_sidebar(
ui.panel_sidebar(ui.input_slider("controller", "Controller", 1, 3, 1)),
ui.panel_main(
ui.navs_tab_card(
ui.navset_tab_card(
ui.nav("Panel 1", "Panel 1 content", value="panel1"),
ui.nav("Panel 2", "Panel 2 content", value="panel2"),
ui.nav("Panel 3", "Panel 3 content", value="panel3"),
Expand Down
41 changes: 40 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, Dict, Any

# 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,35 @@ class SilentCancelOutputException(Exception):

class ActionButtonValue(int):
pass


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

def resolve(
self, selected: Optional[str], context: Dict[str, Any] = {}
) -> Tuple[TagChildArg, TagChildArg]:
"""
Resolve information provided by the navigation container.

Parameters
----------
selected
The value of the navigation item to be shown on page load.
context
Additional context supplied by the navigation container.
"""
...

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).
"""
...
28 changes: 2 additions & 26 deletions shiny/ui/_html_dependencies.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from typing import List, Union
from typing import List

from htmltools import HTML, HTMLDependency

from ..html_dependencies import jquery_deps


def bootstrap_deps(bs3compat: bool = True) -> List[HTMLDependency]:
def bootstrap_deps() -> List[HTMLDependency]:
dep = HTMLDependency(
name="bootstrap",
version="5.0.1",
Expand All @@ -14,33 +14,9 @@ def bootstrap_deps(bs3compat: bool = True) -> List[HTMLDependency]:
stylesheet={"href": "bootstrap.min.css"},
)
deps = [jquery_deps(), dep]
if bs3compat:
deps.append(bs3compat_deps())
return deps


# TODO: if we want to support glyphicons we'll need to bundle font files, too
def bs3compat_deps() -> HTMLDependency:
return HTMLDependency(
name="bs3-compat",
version="1.0",
source={"package": "shiny", "subdir": "www/shared/bs3compat/"},
script=[{"src": "transition.js"}, {"src": "tabs.js"}, {"src": "bs3compat.js"}],
)


def nav_deps(
include_bootstrap: bool = True,
) -> Union[HTMLDependency, List[HTMLDependency]]:
dep = HTMLDependency(
name="bslib-navs",
version="1.0",
source={"package": "shiny", "subdir": "www/shared/bslib/dist/"},
script={"src": "navs.min.js"},
)
return [dep, *bootstrap_deps()] if include_bootstrap else dep


def ionrangeslider_deps() -> List[HTMLDependency]:
return [
HTMLDependency(
Expand Down
4 changes: 2 additions & 2 deletions shiny/ui/_input_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -657,8 +657,8 @@ def update_navs(

See Also
-------
~shiny.ui.navs_tab
~shiny.ui.navs_pill
~shiny.ui.navset_tab
~shiny.ui.navset_pill
~shiny.ui.page_navbar
"""

Expand Down
Loading