From 052748845121e7c3a3e8af6f11222788e07bc3f5 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 15 Jan 2025 22:30:20 -0500 Subject: [PATCH 01/11] feat(navbar_options): Consolidate options and use in `navset_bar()` --- shiny/express/ui/__init__.py | 2 + shiny/types.py | 12 +- shiny/ui/__init__.py | 2 + shiny/ui/_navs.py | 237 +++++++++++++++++++++++++++++------ tests/pytest/test_navs.py | 123 ++++++++++++++++++ 5 files changed, 336 insertions(+), 40 deletions(-) diff --git a/shiny/express/ui/__init__.py b/shiny/express/ui/__init__.py index 2dd603287..686b3739c 100644 --- a/shiny/express/ui/__init__.py +++ b/shiny/express/ui/__init__.py @@ -108,6 +108,7 @@ notification_show, notification_remove, nav_spacer, + navbar_options, Progress, Theme, value_box_theme, @@ -282,6 +283,7 @@ "navset_pill_list", "navset_tab", "navset_underline", + "navbar_options", "value_box", "panel_well", "panel_conditional", diff --git a/shiny/types.py b/shiny/types.py index 562ec5d4b..c03327853 100644 --- a/shiny/types.py +++ b/shiny/types.py @@ -31,11 +31,13 @@ from htmltools import TagChild from ._docstring import add_example -from ._typing_extensions import NotRequired, TypedDict +from ._typing_extensions import NotRequired, TypedDict, TypeIs if TYPE_CHECKING: from matplotlib.figure import Figure +T = TypeVar("T") + # Sentinel value - indicates a missing value in a function call. class MISSING_TYPE: @@ -43,12 +45,16 @@ class MISSING_TYPE: MISSING: MISSING_TYPE = MISSING_TYPE() +DEPRECATED: MISSING_TYPE = MISSING_TYPE() # A MISSING that communicates deprecation +MaybeMissing = Union[T, MISSING_TYPE] - -T = TypeVar("T") ListOrTuple = Union[List[T], Tuple[T, ...]] +def is_missing(x: Any) -> TypeIs[MISSING_TYPE]: + return x is MISSING or isinstance(x, MISSING_TYPE) + + # Information about a single file, with a structure like: # {'name': 'mtcars.csv', 'size': 1303, 'type': 'text/csv', 'datapath: '/...../mtcars.csv'} # The incoming data doesn't include 'datapath'; that field is added by the diff --git a/shiny/ui/__init__.py b/shiny/ui/__init__.py index b674cfeb3..85475e76b 100644 --- a/shiny/ui/__init__.py +++ b/shiny/ui/__init__.py @@ -115,6 +115,7 @@ nav_menu, nav_panel, nav_spacer, + navbar_options, navset_bar, navset_card_pill, navset_card_tab, @@ -291,6 +292,7 @@ "navset_pill_list", "navset_hidden", "navset_bar", + "navbar_options", # _notification "notification_show", "notification_remove", diff --git a/shiny/ui/_navs.py b/shiny/ui/_navs.py index a87d8bb9d..05b8fcbca 100644 --- a/shiny/ui/_navs.py +++ b/shiny/ui/_navs.py @@ -3,13 +3,14 @@ import collections.abc import copy import re -from typing import Any, Literal, Optional, Sequence, cast +from typing import Any, Generic, Literal, Optional, Sequence, TypeVar, cast from htmltools import ( HTML, MetadataNode, Tag, TagAttrs, + TagAttrValue, TagChild, TagList, css, @@ -17,10 +18,11 @@ tags, ) +from .._deprecated import warn_deprecated from .._docstring import add_example from .._namespaces import resolve_id_or_none from .._utils import private_random_int -from ..types import NavSetArg +from ..types import DEPRECATED, MISSING, MaybeMissing, NavSetArg, is_missing from ._bootstrap import column, row from ._card import CardItem, WrapperCallable, card, card_body, card_footer, card_header from ._html_deps_shinyverse import components_dependencies @@ -42,8 +44,11 @@ "navset_pill_list", "navset_hidden", "navset_bar", + "navbar_options", ) +T = TypeVar("T") + # ----------------------------------------------------------------------------- # Navigation items @@ -985,18 +990,170 @@ def navset_pill_list( ) +class Default(Generic[T]): + def __init__(self, value: T): + self._default = value + + +NavbarOptionsPositionT = Literal[ + "static-top", "fixed-top", "fixed-bottom", "sticky-top" +] +NavbarOptionsTypeT = Literal["auto", "light", "dark"] + + +class NavbarOptions: + position: NavbarOptionsPositionT + bg: Optional[str] + type: NavbarOptionsTypeT + underline: bool + collapsible: bool + attrs: dict[str, Any] + _is_default: dict[str, bool] + + def __init__( + self, + *, + position: MaybeMissing[NavbarOptionsPositionT] = MISSING, + bg: MaybeMissing[str | None] = MISSING, + type: MaybeMissing[NavbarOptionsTypeT] = MISSING, + underline: MaybeMissing[bool] = MISSING, + collapsible: MaybeMissing[bool] = MISSING, + **attrs: TagAttrValue, + ): + self._is_default = {} + + self.position = self._maybe_default("position", position, default="static-top") + self.bg = self._maybe_default("bg", bg, default=None) + self.type = self._maybe_default("type", type, default="auto") + self.underline = self._maybe_default("underline", underline, default=True) + self.collapsible = self._maybe_default("collapsible", collapsible, default=True) + + if "inverse" in attrs: + warn_deprecated("`inverse` is deprecated, please use `type_` instead.") + del attrs["inverse"] + + self.attrs = attrs + + def _maybe_default(self, name: str, value: Any, default: Any): + if is_missing(value): + self._is_default[name] = True + return default + return value + + def __eq__(self, other: Any): + if not isinstance(other, NavbarOptions): + return False + + return ( + self.position == other.position + and self.bg == other.bg + and self.type == other.type + and self.underline == other.underline + and self.collapsible == other.collapsible + and self.attrs == other.attrs + ) + + def __repr__(self): + fields: list[str] = [] + for key, value in self.__dict__.items(): + if key == "_is_default": + continue + if not self._is_default.get(key, False): + if key == "attrs" and len(value) == 0: + continue + fields.append(f"{key}={value!r}") + + return f"navbar_options({', '.join(fields)})" + + +def navbar_options( + position: MaybeMissing[NavbarOptionsPositionT] = MISSING, + bg: MaybeMissing[str | None] = MISSING, + type: MaybeMissing[NavbarOptionsTypeT] = MISSING, + underline: MaybeMissing[bool] = MISSING, + collapsible: MaybeMissing[bool] = MISSING, + **attrs: TagAttrValue, +) -> NavbarOptions: + return NavbarOptions( + position=position, + bg=bg, + type=type, + underline=underline, + collapsible=collapsible, + **attrs, + ) + + +def navbar_options_resolve_deprecated( + options_user: Optional[NavbarOptions] = None, + position: MaybeMissing[NavbarOptionsPositionT] = DEPRECATED, + bg: MaybeMissing[str | None] = DEPRECATED, + inverse: MaybeMissing[bool] = DEPRECATED, + underline: MaybeMissing[bool] = DEPRECATED, + collapsible: MaybeMissing[bool] = DEPRECATED, + fn_caller: str = "navset_bar", +) -> Any: + options_old = { + "position": position, + "bg": bg, + "inverse": inverse, + "collapsible": collapsible, + "underline": underline, + } + options_old = {k: v for k, v in options_old.items() if not is_missing(v)} + + args_deprecated = list(options_old.keys()) + + if args_deprecated: + args_deprecated = ", ".join([f"`{arg}`" for arg in args_deprecated]) + warn_deprecated( + f"The arguments of `{fn_caller}()` for navbar options (including {args_deprecated}) " + f"have been consolidated into a single `navbar_options` argument." + ) + + if "inverse" in options_old: + inverse_old = options_old["inverse"] + del options_old["inverse"] + + if not isinstance(inverse_old, bool): + raise ValueError(f"Invalid `inverse` value: {inverse}") + + options_old["type"] = "dark" if inverse_old else "light" + + options_user = options_user if options_user is not None else navbar_options() + + options_resolved = { + k: v + for k, v in vars(options_user).items() + if k != "_is_default" and not options_user._is_default.get(k, False) + } + + ignored: list[str] = [] + for opt in options_old: + if opt not in options_resolved: + options_resolved[opt] = options_old[opt] + elif options_old[opt] != options_resolved[opt]: + ignored.append("inverse" if opt == "type" else opt) + + if ignored: + warn_deprecated( + f"`{', '.join(ignored)}` {'was' if len(ignored) == 1 else 'were'} provided twice: once directly and once in `navbar_options`.\nThe deprecated direct option(s) will be ignored and the values from `navbar_options` will be used." + ) + + attrs = options_resolved.pop("attrs", {}) + + return navbar_options(**options_resolved, **attrs) + + class NavSetBar(NavSet): title: TagChild sidebar: Optional[Sidebar] fillable: bool | list[str] gap: Optional[CssUnit] padding: Optional[CssUnit | list[CssUnit]] - position: Literal["static-top", "fixed-top", "fixed-bottom", "sticky-top"] - bg: Optional[str] - inverse: bool - underline: bool - collapsible: bool fluid: bool + navbar_options: NavbarOptions + # Internal ---- _is_page_level: bool def __init__( @@ -1010,17 +1167,16 @@ def __init__( fillable: bool | list[str] = False, gap: Optional[CssUnit], padding: Optional[CssUnit | list[CssUnit]], - position: Literal[ - "static-top", "fixed-top", "fixed-bottom", "sticky-top" - ] = "static-top", + fluid: bool = True, header: TagChild = None, footer: TagChild = None, - bg: Optional[str] = None, - # TODO: default to 'auto', like we have in R (parse color via webcolors?) - inverse: bool = False, - underline: bool = True, - collapsible: bool = True, - fluid: bool = True, + navbar_options: Optional[NavbarOptions] = None, + # Deprecated ---- + position: MaybeMissing[NavbarOptionsPositionT] = DEPRECATED, + bg: MaybeMissing[str | None] = DEPRECATED, + inverse: MaybeMissing[bool] = DEPRECATED, + underline: MaybeMissing[bool] = DEPRECATED, + collapsible: MaybeMissing[bool] = DEPRECATED, ) -> None: super().__init__( *args, @@ -1035,11 +1191,14 @@ def __init__( self.fillable = fillable self.gap = gap self.padding = padding - self.position = position - self.bg = bg - self.inverse = inverse - self.underline = underline - self.collapsible = collapsible + self.navbar_options = navbar_options_resolve_deprecated( + options_user=navbar_options or NavbarOptions(), + position=position, + bg=bg, + inverse=inverse, + underline=underline, + collapsible=collapsible, + ) self.fluid = fluid self._is_page_level = False @@ -1048,7 +1207,7 @@ def layout(self, nav: Tag, content: Tag) -> TagList: {"class": "container-fluid" if self.fluid else "container"}, tags.span({"class": "navbar-brand"}, self.title), ) - if self.collapsible: + if self.navbar_options.collapsible: collapse_id = "navbar-collapse-" + nav_random_int() nav_container.append( tags.button( @@ -1067,15 +1226,19 @@ def layout(self, nav: Tag, content: Tag) -> TagList: nav_container.append(nav) nav_final = tags.nav({"class": "navbar navbar-expand-md"}, nav_container) - if self.position != "static-top": - nav_final.add_class(self.position) + if self.navbar_options.position != "static-top": + nav_final.add_class(self.navbar_options.position) # bslib supports navbar-default/navbar-inverse (which is no longer # a thing in Bootstrap 5) in a way that's still useful, especially Bootswatch. - nav_final.add_class(f"navbar-{'inverse' if self.inverse else 'default'}") + nav_final.add_class( + "navbar-inverse" if self.navbar_options.type == "dark" else "navbar-default" + ) - if self.bg: - nav_final.add_style(f"background-color: {self.bg} !important;") + if self.navbar_options.bg: + nav_final.add_style( + f"background-color: {self.navbar_options.bg} !important;" + ) content = _make_tabs_fillable( content, @@ -1177,17 +1340,16 @@ def navset_bar( fillable: bool | list[str] = True, gap: Optional[CssUnit] = None, padding: Optional[CssUnit | list[CssUnit]] = None, - position: Literal[ - "static-top", "fixed-top", "fixed-bottom", "sticky-top" - ] = "static-top", header: TagChild = None, footer: TagChild = None, - bg: Optional[str] = None, - # TODO: default to 'auto', like we have in R (parse color via webcolors?) - inverse: bool = False, - underline: bool = True, - collapsible: bool = True, fluid: bool = True, + navbar_options: Optional[NavbarOptions] = None, + # Deprecated ---- + position: MaybeMissing[NavbarOptionsPositionT] = DEPRECATED, + bg: MaybeMissing[str | None] = DEPRECATED, + inverse: MaybeMissing[bool] = DEPRECATED, + underline: MaybeMissing[bool] = DEPRECATED, + collapsible: MaybeMissing[bool] = DEPRECATED, ) -> NavSetBar: """ Render nav items as a navbar. @@ -1285,14 +1447,15 @@ def navset_bar( gap=gap, padding=padding, title=title, - position=position, header=header, footer=footer, + fluid=fluid, + navbar_options=navbar_options, + position=position, bg=bg, inverse=inverse, underline=underline, collapsible=collapsible, - fluid=fluid, ) diff --git a/tests/pytest/test_navs.py b/tests/pytest/test_navs.py index 1ac453360..ab10cd2ed 100644 --- a/tests/pytest/test_navs.py +++ b/tests/pytest/test_navs.py @@ -5,10 +5,17 @@ import textwrap from typing import Generator +import pytest from htmltools import TagList from shiny import ui +from shiny._deprecated import ShinyDeprecationWarning from shiny._utils import private_seed +from shiny.ui._navs import ( + NavbarOptions, + navbar_options, + navbar_options_resolve_deprecated, +) # Fix the randomness of these functions to make the tests deterministic @@ -176,3 +183,119 @@ def test_navset_bar_markup(): Page footer """ ) + + +# navbar_options() ------------------------------------------------------------------- + + +def test_navbar_options_no_deprecated_arguments(): + options_user = navbar_options() + result = navbar_options_resolve_deprecated(options_user) + assert isinstance(result, NavbarOptions) + assert result == navbar_options() + + +def test_navbar_options_deprecated_arguments(): + options_user = navbar_options() + assert options_user._is_default.get("position", False) + assert options_user._is_default.get("underline", False) + + with pytest.warns(ShinyDeprecationWarning, match="`position`, `underline`"): + result = navbar_options_resolve_deprecated( + options_user, + position="static-top", + underline=True, + ) + + assert isinstance(result, NavbarOptions) + assert result == navbar_options() + + +def test_navbar_options_inverse_true(): + options_user = navbar_options() + with pytest.warns(ShinyDeprecationWarning, match="`inverse`"): + result = navbar_options_resolve_deprecated(options_user, inverse=True) + assert isinstance(result, NavbarOptions) + assert result.type == "dark" + + +def test_navbar_options_inverse_false(): + options_user = navbar_options() + with pytest.warns(ShinyDeprecationWarning, match="`inverse`"): + result = navbar_options_resolve_deprecated(options_user, inverse=False) + assert isinstance(result, NavbarOptions) + assert result.type == "light" + + +def test_navbar_options_inverse_invalid(): + options_user = navbar_options() + with pytest.warns(ShinyDeprecationWarning, match="`inverse`"): + with pytest.raises(ValueError, match="Invalid `inverse` value: 42"): + navbar_options_resolve_deprecated(options_user, inverse=42) # type: ignore + + +def test_navbar_options_conflicting_options(): + options_user = navbar_options(position="fixed-top") + with pytest.warns(ShinyDeprecationWarning, match="`position`"): + with pytest.warns( + ShinyDeprecationWarning, match="`position` was provided twice" + ): + result = navbar_options_resolve_deprecated( + options_user, position="fixed-bottom" + ) + assert isinstance(result, NavbarOptions) + assert result.position == "fixed-top" + + +def test_navbar_options_attribs_in_options_user(): + options_user = navbar_options(class_="my-navbar") + result = navbar_options_resolve_deprecated(options_user) + assert isinstance(result, NavbarOptions) + assert result.attrs == {"class_": "my-navbar"} + + +def test_navbar_options_mixed_options(): + options_user = navbar_options(position="fixed-bottom", bg="light") + assert not options_user._is_default.get("position", False) + assert not options_user._is_default.get("bg", False) + + with pytest.warns(ShinyDeprecationWarning, match="`bg`"): + with pytest.warns(ShinyDeprecationWarning, match="`bg` was provided twice"): + result = navbar_options_resolve_deprecated(options_user, bg="dark") + + assert isinstance(result, NavbarOptions) + assert result.position == "fixed-bottom" + assert result.bg == "light" + + +def test_navbar_options_all_deprecated_arguments(): + options_user = navbar_options() + with pytest.warns( + ShinyDeprecationWarning, + match="The arguments of `navset_bar\\(\\)` for navbar options", + ): + result = navbar_options_resolve_deprecated( + options_user, + position="static-top", + bg="dark", + inverse=True, + collapsible=True, + underline=True, + ) + assert isinstance(result, NavbarOptions) + assert result.type == "dark" + + +def test_navbar_options_fn_caller_custom(): + options_user = navbar_options() + with pytest.warns( + ShinyDeprecationWarning, + match="The arguments of `custom_caller\\(\\)` for navbar options", + ): + result = navbar_options_resolve_deprecated( + options_user, + position="static-top", + fn_caller="custom_caller", + ) + assert isinstance(result, NavbarOptions) + assert result == navbar_options() From 2f4b42b3fba6d119380960fe426a16263c5d3956 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 16 Jan 2025 11:26:45 -0500 Subject: [PATCH 02/11] feat(navbar_options): Use in `page_navbar()` too --- shiny/ui/_page.py | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/shiny/ui/_page.py b/shiny/ui/_page.py index cd5695f5a..5be039170 100644 --- a/shiny/ui/_page.py +++ b/shiny/ui/_page.py @@ -31,12 +31,18 @@ from .._docstring import add_example, no_example from .._namespaces import resolve_id_or_none -from ..types import MISSING, MISSING_TYPE, NavSetArg +from ..types import DEPRECATED, MISSING, MISSING_TYPE, MaybeMissing, NavSetArg from ._bootstrap import panel_title from ._html_deps_external import Theme, ThemeProvider, shiny_page_theme_deps from ._html_deps_py_shiny import page_output_dependency from ._html_deps_shinyverse import components_dependencies -from ._navs import NavMenu, NavPanel, navset_bar +from ._navs import ( + NavbarOptions, + NavMenu, + NavPanel, + navbar_options_resolve_deprecated, + navset_bar, +) from ._sidebar import Sidebar, SidebarOpen, layout_sidebar from ._tag import consolidate_attrs from ._utils import get_window_title @@ -161,17 +167,21 @@ def page_navbar( fillable_mobile: bool = False, gap: Optional[CssUnit] = None, padding: Optional[CssUnit | list[CssUnit]] = None, - position: Literal["static-top", "fixed-top", "fixed-bottom"] = "static-top", header: Optional[TagChild] = None, footer: Optional[TagChild] = None, - bg: Optional[str] = None, - inverse: bool = False, - underline: bool = True, - collapsible: bool = True, + navbar_options: Optional[NavbarOptions] = None, fluid: bool = True, window_title: str | MISSING_TYPE = MISSING, lang: Optional[str] = None, theme: Optional[str | Path | Theme | ThemeProvider] = None, + # Deprecated ---- + position: MaybeMissing[ + Literal["static-top", "fixed-top", "fixed-bottom"] + ] = DEPRECATED, + bg: MaybeMissing[str | None] = DEPRECATED, + inverse: MaybeMissing[bool] = DEPRECATED, + underline: MaybeMissing[bool] = DEPRECATED, + collapsible: MaybeMissing[bool] = DEPRECATED, ) -> Tag: """ Create a page with a navbar and a title. @@ -276,6 +286,16 @@ def page_navbar( tagAttrs: TagAttrs = {"class": pageClass} + navbar_options = navbar_options_resolve_deprecated( + fn_caller="page_navbar", + options_user=navbar_options, + position=position, + bg=bg, + inverse=inverse, + underline=underline, + collapsible=collapsible, + ) + navbar = navset_bar( *args, title=title, @@ -285,13 +305,9 @@ def page_navbar( fillable=fillable, gap=gap, padding=padding, - position=position, + navbar_options=navbar_options, header=header, footer=footer, - bg=bg, - inverse=inverse, - underline=underline, - collapsible=collapsible, fluid=fluid, ) # This is a page-level navbar, so opt into page-level layouts (in particular for From 0309d761b35e75ddfb47fb62b4c756d0f9d696c2 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 16 Jan 2025 12:00:25 -0500 Subject: [PATCH 03/11] chore: Update deprecation warnings --- shiny/ui/_navs.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/shiny/ui/_navs.py b/shiny/ui/_navs.py index 05b8fcbca..8a89cd7f8 100644 --- a/shiny/ui/_navs.py +++ b/shiny/ui/_navs.py @@ -1029,7 +1029,9 @@ def __init__( self.collapsible = self._maybe_default("collapsible", collapsible, default=True) if "inverse" in attrs: - warn_deprecated("`inverse` is deprecated, please use `type_` instead.") + warn_deprecated( + "`navbar_options()` does not support `inverse`, please use `type_` instead." + ) del attrs["inverse"] self.attrs = attrs @@ -1107,7 +1109,8 @@ def navbar_options_resolve_deprecated( if args_deprecated: args_deprecated = ", ".join([f"`{arg}`" for arg in args_deprecated]) warn_deprecated( - f"The arguments of `{fn_caller}()` for navbar options (including {args_deprecated}) " + "In shiny v1.3.0, the arguments of " + f"`{fn_caller}()` for navbar options (including {args_deprecated}) " f"have been consolidated into a single `navbar_options` argument." ) @@ -1137,7 +1140,9 @@ def navbar_options_resolve_deprecated( if ignored: warn_deprecated( - f"`{', '.join(ignored)}` {'was' if len(ignored) == 1 else 'were'} provided twice: once directly and once in `navbar_options`.\nThe deprecated direct option(s) will be ignored and the values from `navbar_options` will be used." + f"`{', '.join(ignored)}` {'was' if len(ignored) == 1 else 'were'} provided twice: " + "once directly and once in `navbar_options`.\n" + "The deprecated direct option(s) will be ignored and the values from `navbar_options` will be used." ) attrs = options_resolved.pop("attrs", {}) From 5f50b5af4e773ce0e4a72e204ce19f71bb078e52 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 16 Jan 2025 13:48:26 -0500 Subject: [PATCH 04/11] chore(navbar_options): Add docs, rename `type` -> `theme` --- shiny/ui/_navs.py | 93 +++++++++++++++++++++++++++++++-------- shiny/ui/_page.py | 47 ++++++++++++++------ tests/pytest/test_navs.py | 10 ++--- 3 files changed, 112 insertions(+), 38 deletions(-) diff --git a/shiny/ui/_navs.py b/shiny/ui/_navs.py index 8a89cd7f8..705088274 100644 --- a/shiny/ui/_navs.py +++ b/shiny/ui/_navs.py @@ -998,13 +998,13 @@ def __init__(self, value: T): NavbarOptionsPositionT = Literal[ "static-top", "fixed-top", "fixed-bottom", "sticky-top" ] -NavbarOptionsTypeT = Literal["auto", "light", "dark"] +NavbarOptionsThemeT = Literal["auto", "light", "dark"] class NavbarOptions: position: NavbarOptionsPositionT bg: Optional[str] - type: NavbarOptionsTypeT + theme: NavbarOptionsThemeT underline: bool collapsible: bool attrs: dict[str, Any] @@ -1015,7 +1015,7 @@ def __init__( *, position: MaybeMissing[NavbarOptionsPositionT] = MISSING, bg: MaybeMissing[str | None] = MISSING, - type: MaybeMissing[NavbarOptionsTypeT] = MISSING, + theme: MaybeMissing[NavbarOptionsThemeT] = MISSING, underline: MaybeMissing[bool] = MISSING, collapsible: MaybeMissing[bool] = MISSING, **attrs: TagAttrValue, @@ -1024,13 +1024,13 @@ def __init__( self.position = self._maybe_default("position", position, default="static-top") self.bg = self._maybe_default("bg", bg, default=None) - self.type = self._maybe_default("type", type, default="auto") + self.theme = self._maybe_default("theme", theme, default="auto") self.underline = self._maybe_default("underline", underline, default=True) self.collapsible = self._maybe_default("collapsible", collapsible, default=True) if "inverse" in attrs: warn_deprecated( - "`navbar_options()` does not support `inverse`, please use `type_` instead." + "`navbar_options()` does not support `inverse`, please use `theme` instead." ) del attrs["inverse"] @@ -1049,7 +1049,7 @@ def __eq__(self, other: Any): return ( self.position == other.position and self.bg == other.bg - and self.type == other.type + and self.theme == other.theme and self.underline == other.underline and self.collapsible == other.collapsible and self.attrs == other.attrs @@ -1071,15 +1071,51 @@ def __repr__(self): def navbar_options( position: MaybeMissing[NavbarOptionsPositionT] = MISSING, bg: MaybeMissing[str | None] = MISSING, - type: MaybeMissing[NavbarOptionsTypeT] = MISSING, + theme: MaybeMissing[NavbarOptionsThemeT] = MISSING, underline: MaybeMissing[bool] = MISSING, collapsible: MaybeMissing[bool] = MISSING, **attrs: TagAttrValue, ) -> NavbarOptions: + """ + Configure the appearance and behavior of the navbar. + + Parameters: + ----------- + position + Determines whether the navbar should be displayed at the top of the page with + normal scrolling behavior (`"static-top"`), pinned at the top (`"fixed-top"`), + or pinned at the bottom (`"fixed-bottom"`). Note that using `"fixed-top"` or + `"fixed-bottom"` will cause the navbar to overlay your body content, unless you + add padding (e.g., `tags.style("body {padding-top: 70px;}")`) + + bg + Background color of the navbar (a CSS color). + + theme + The navbar theme: either `"dark"` for a light text color (on a **dark** + background) or `"light"` for a dark text color (on a **light** background). If + `"auto"` (the default) and `bg` is provided, the best contrast to `bg` is + chosen. + + underline + If `True`, adds an underline effect to the navbar. + + collapsible + If `True`, automatically collapses the elements into an expandable menu on + mobile devices or narrow window widths. + + **attrs : dict + Additional HTML attributes to apply to the navbar container element. + + Returns: + -------- + NavbarOptions + A NavbarOptions object configured with the specified options. + """ return NavbarOptions( position=position, bg=bg, - type=type, + theme=theme, underline=underline, collapsible=collapsible, **attrs, @@ -1121,7 +1157,7 @@ def navbar_options_resolve_deprecated( if not isinstance(inverse_old, bool): raise ValueError(f"Invalid `inverse` value: {inverse}") - options_old["type"] = "dark" if inverse_old else "light" + options_old["theme"] = "dark" if inverse_old else "light" options_user = options_user if options_user is not None else navbar_options() @@ -1136,7 +1172,7 @@ def navbar_options_resolve_deprecated( if opt not in options_resolved: options_resolved[opt] = options_old[opt] elif options_old[opt] != options_resolved[opt]: - ignored.append("inverse" if opt == "type" else opt) + ignored.append("inverse" if opt == "theme" else opt) if ignored: warn_deprecated( @@ -1237,7 +1273,7 @@ def layout(self, nav: Tag, content: Tag) -> TagList: # bslib supports navbar-default/navbar-inverse (which is no longer # a thing in Bootstrap 5) in a way that's still useful, especially Bootswatch. nav_final.add_class( - "navbar-inverse" if self.navbar_options.type == "dark" else "navbar-default" + "navbar-inverse" if self.navbar_options.theme == "dark" else "navbar-default" ) if self.navbar_options.bg: @@ -1347,8 +1383,8 @@ def navset_bar( padding: Optional[CssUnit | list[CssUnit]] = None, header: TagChild = None, footer: TagChild = None, - fluid: bool = True, navbar_options: Optional[NavbarOptions] = None, + fluid: bool = True, # Deprecated ---- position: MaybeMissing[NavbarOptionsPositionT] = DEPRECATED, bg: MaybeMissing[str | None] = DEPRECATED, @@ -1390,24 +1426,43 @@ def navset_bar( be used for top, the second will be left and right, and the third will be bottom. If four, then the values will be interpreted as top, right, bottom, and left respectively. This value is only used when the navbar is `fillable`. + header + UI to display above the selected content. + footer + UI to display below the selected content. + fluid + ``True`` to use fluid layout; ``False`` to use fixed layout. + navbar_options + Configure the appearance and behavior of the navbar using + :func:`~shiny.ui.navbar_options` to set properties like position, background + color, and more. + + `navbar_options` was added in v1.3.0 and replaces deprecated arguments + `position`, `bg`, `inverse`, `collapsible`, and `underline`. position + Deprecated in v1.3.0. Please use `navbar_options` instead; see + :func:`~shiny.ui.navbar_options` for details. + Determines whether the navbar should be displayed at the top of the page with normal scrolling behavior ("static-top"), pinned at the top ("fixed-top"), or pinned at the bottom ("fixed-bottom"). Note that using "fixed-top" or "fixed-bottom" will cause the navbar to overlay your body content, unless you add padding (e.g., ``tags.style("body {padding-top: 70px;}")``). - header - UI to display above the selected content. - footer - UI to display below the selected content. bg + Deprecated in v1.3.0. Please use `navbar_options` instead; see + :func:`~shiny.ui.navbar_options` for details. + Background color of the navbar (a CSS color). inverse + Deprecated in v1.3.0. Please use `navbar_options` instead; see + :func:`~shiny.ui.navbar_options` for details. + Either ``True`` for a light text color or ``False`` for a dark text color. collapsible - ``True`` to automatically collapse the navigation elements into an expandable menu on mobile devices or narrow window widths. - fluid - ``True`` to use fluid layout; ``False`` to use fixed layout. + Deprecated in v1.3.0. Please use `navbar_options` instead; see + :func:`~shiny.ui.navbar_options` for details. + + ``True`` to automatically collapse the elements into an expandable menu on mobile devices or narrow window widths. See Also -------- diff --git a/shiny/ui/_page.py b/shiny/ui/_page.py index 5be039170..db02bd4a9 100644 --- a/shiny/ui/_page.py +++ b/shiny/ui/_page.py @@ -218,24 +218,10 @@ def page_navbar( be used for top, the second will be left and right, and the third will be bottom. If four, then the values will be interpreted as top, right, bottom, and left respectively. This value is only used when the navbar is _fillable_. - position - Determines whether the navbar should be displayed at the top of the page with - normal scrolling behavior ("static-top"), pinned at the top ("fixed-top"), or - pinned at the bottom ("fixed-bottom"). Note that using "fixed-top" or - "fixed-bottom" will cause the navbar to overlay your body content, unless you - add padding (e.g., ``tags.style("body {padding-top: 70px;}")``). header UI to display above the selected content. footer UI to display below the selected content. - bg - Background color of the navbar (a CSS color). - inverse - Either ``True`` for a light text color or ``False`` for a dark text color. - collapsible - ``True`` to automatically collapse the elements into an expandable menu on mobile devices or narrow window widths. - fluid - ``True`` to use fluid layout; ``False`` to use fixed layout. window_title The browser's window title (defaults to the host URL of the page). Can also be set as a side effect via :func:`~shiny.ui.panel_title`. @@ -254,6 +240,39 @@ def page_navbar( To modify the theme of an app without replacing the Bootstrap CSS entirely, use :func:`~shiny.ui.include_css` to add custom CSS. + fluid + ``True`` to use fluid layout; ``False`` to use fixed layout. + navbar_options + Configure the appearance and behavior of the navbar using + :func:`~shiny.ui.navbar_options` to set properties like position, background + color, and more. + + `navbar_options` was added in v1.3.0 and replaces deprecated arguments + `position`, `bg`, `inverse`, `collapsible`, and `underline`. + position + Deprecated in v1.3.0. Please use `navbar_options` instead; see + :func:`~shiny.ui.navbar_options` for details. + + Determines whether the navbar should be displayed at the top of the page with + normal scrolling behavior ("static-top"), pinned at the top ("fixed-top"), or + pinned at the bottom ("fixed-bottom"). Note that using "fixed-top" or + "fixed-bottom" will cause the navbar to overlay your body content, unless you + add padding (e.g., ``tags.style("body {padding-top: 70px;}")``). + bg + Deprecated in v1.3.0. Please use `navbar_options` instead; see + :func:`~shiny.ui.navbar_options` for details. + + Background color of the navbar (a CSS color). + inverse + Deprecated in v1.3.0. Please use `navbar_options` instead; see + :func:`~shiny.ui.navbar_options` for details. + + Either ``True`` for a light text color or ``False`` for a dark text color. + collapsible + Deprecated in v1.3.0. Please use `navbar_options` instead; see + :func:`~shiny.ui.navbar_options` for details. + + ``True`` to automatically collapse the elements into an expandable menu on mobile devices or narrow window widths. Returns ------- diff --git a/tests/pytest/test_navs.py b/tests/pytest/test_navs.py index ab10cd2ed..b42224f12 100644 --- a/tests/pytest/test_navs.py +++ b/tests/pytest/test_navs.py @@ -216,7 +216,7 @@ def test_navbar_options_inverse_true(): with pytest.warns(ShinyDeprecationWarning, match="`inverse`"): result = navbar_options_resolve_deprecated(options_user, inverse=True) assert isinstance(result, NavbarOptions) - assert result.type == "dark" + assert result.theme == "dark" def test_navbar_options_inverse_false(): @@ -224,7 +224,7 @@ def test_navbar_options_inverse_false(): with pytest.warns(ShinyDeprecationWarning, match="`inverse`"): result = navbar_options_resolve_deprecated(options_user, inverse=False) assert isinstance(result, NavbarOptions) - assert result.type == "light" + assert result.theme == "light" def test_navbar_options_inverse_invalid(): @@ -272,7 +272,7 @@ def test_navbar_options_all_deprecated_arguments(): options_user = navbar_options() with pytest.warns( ShinyDeprecationWarning, - match="The arguments of `navset_bar\\(\\)` for navbar options", + match="arguments of `navset_bar\\(\\)` for navbar options", ): result = navbar_options_resolve_deprecated( options_user, @@ -283,14 +283,14 @@ def test_navbar_options_all_deprecated_arguments(): underline=True, ) assert isinstance(result, NavbarOptions) - assert result.type == "dark" + assert result.theme == "dark" def test_navbar_options_fn_caller_custom(): options_user = navbar_options() with pytest.warns( ShinyDeprecationWarning, - match="The arguments of `custom_caller\\(\\)` for navbar options", + match="arguments of `custom_caller\\(\\)` for navbar options", ): result = navbar_options_resolve_deprecated( options_user, From d29617dbc9138fb02d70b4356c8bfe8c617f27a0 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 16 Jan 2025 13:53:05 -0500 Subject: [PATCH 05/11] docs: Add navbar_options to index --- docs/_quartodoc-core.yml | 1 + docs/_quartodoc-express.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/_quartodoc-core.yml b/docs/_quartodoc-core.yml index d31b7f989..52d4f4930 100644 --- a/docs/_quartodoc-core.yml +++ b/docs/_quartodoc-core.yml @@ -83,6 +83,7 @@ quartodoc: - ui.navset_card_underline - ui.navset_pill_list - ui.navset_hidden + - ui.navbar_options - title: UI panels desc: Visually group together a section of UI components. contents: diff --git a/docs/_quartodoc-express.yml b/docs/_quartodoc-express.yml index 2b1f8e50c..6086b6fa2 100644 --- a/docs/_quartodoc-express.yml +++ b/docs/_quartodoc-express.yml @@ -77,6 +77,7 @@ quartodoc: - express.ui.navset_underline - express.ui.navset_pill_list - express.ui.navset_hidden + - express.ui.navbar_options - title: Chat interface desc: Build a chatbot interface contents: From d7827b44bd84c4ce152f11c6f13d8297158b56a0 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 16 Jan 2025 14:03:54 -0500 Subject: [PATCH 06/11] chore: Remove deprecated args from internal NavsetBar class --- shiny/ui/_navs.py | 55 +++++++++++++++++++++-------------------------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/shiny/ui/_navs.py b/shiny/ui/_navs.py index 705088274..5476ee324 100644 --- a/shiny/ui/_navs.py +++ b/shiny/ui/_navs.py @@ -1130,7 +1130,9 @@ def navbar_options_resolve_deprecated( underline: MaybeMissing[bool] = DEPRECATED, collapsible: MaybeMissing[bool] = DEPRECATED, fn_caller: str = "navset_bar", -) -> Any: +) -> NavbarOptions: + options_user = options_user if options_user is not None else navbar_options() + options_old = { "position": position, "bg": bg, @@ -1142,13 +1144,15 @@ def navbar_options_resolve_deprecated( args_deprecated = list(options_old.keys()) - if args_deprecated: - args_deprecated = ", ".join([f"`{arg}`" for arg in args_deprecated]) - warn_deprecated( - "In shiny v1.3.0, the arguments of " - f"`{fn_caller}()` for navbar options (including {args_deprecated}) " - f"have been consolidated into a single `navbar_options` argument." - ) + if not args_deprecated: + return options_user + + args_deprecated = ", ".join([f"`{arg}`" for arg in args_deprecated]) + warn_deprecated( + "In shiny v1.3.0, the arguments of " + f"`{fn_caller}()` for navbar options (including {args_deprecated}) " + f"have been consolidated into a single `navbar_options` argument." + ) if "inverse" in options_old: inverse_old = options_old["inverse"] @@ -1159,7 +1163,6 @@ def navbar_options_resolve_deprecated( options_old["theme"] = "dark" if inverse_old else "light" - options_user = options_user if options_user is not None else navbar_options() options_resolved = { k: v @@ -1212,12 +1215,6 @@ def __init__( header: TagChild = None, footer: TagChild = None, navbar_options: Optional[NavbarOptions] = None, - # Deprecated ---- - position: MaybeMissing[NavbarOptionsPositionT] = DEPRECATED, - bg: MaybeMissing[str | None] = DEPRECATED, - inverse: MaybeMissing[bool] = DEPRECATED, - underline: MaybeMissing[bool] = DEPRECATED, - collapsible: MaybeMissing[bool] = DEPRECATED, ) -> None: super().__init__( *args, @@ -1232,14 +1229,7 @@ def __init__( self.fillable = fillable self.gap = gap self.padding = padding - self.navbar_options = navbar_options_resolve_deprecated( - options_user=navbar_options or NavbarOptions(), - position=position, - bg=bg, - inverse=inverse, - underline=underline, - collapsible=collapsible, - ) + self.navbar_options = navbar_options if navbar_options is not None else NavbarOptions() self.fluid = fluid self._is_page_level = False @@ -1493,8 +1483,18 @@ def navset_bar( else: new_args.append(cast(NavSetArg, arg)) + navbar_opts = navbar_options_resolve_deprecated( + fn_caller="navset_bar", + options_user=navbar_options or NavbarOptions(), + position=position, + bg=bg, + inverse=inverse, + underline=underline, + collapsible=collapsible, + ) + ul_class = "nav navbar-nav" - if underline: + if navbar_opts.underline: ul_class += " nav-underline" return NavSetBar( @@ -1510,12 +1510,7 @@ def navset_bar( header=header, footer=footer, fluid=fluid, - navbar_options=navbar_options, - position=position, - bg=bg, - inverse=inverse, - underline=underline, - collapsible=collapsible, + navbar_options=navbar_opts, ) From c93a25727ec1f6f15651023e356034b0e4d3f2fe Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 16 Jan 2025 14:10:26 -0500 Subject: [PATCH 07/11] chore(express.ui.navbar_options): Reflect changes from core --- shiny/express/ui/_cm_components.py | 75 ++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 24 deletions(-) diff --git a/shiny/express/ui/_cm_components.py b/shiny/express/ui/_cm_components.py index 9d5b5e517..727a5aba3 100644 --- a/shiny/express/ui/_cm_components.py +++ b/shiny/express/ui/_cm_components.py @@ -8,11 +8,19 @@ from ... import ui from ..._docstring import add_example, no_example -from ...types import MISSING, MISSING_TYPE +from ...types import DEPRECATED, MISSING, MISSING_TYPE, MaybeMissing from ...ui._accordion import AccordionPanel from ...ui._card import CardItem from ...ui._layout_columns import BreakpointsUser -from ...ui._navs import NavMenu, NavPanel, NavSet, NavSetBar, NavSetCard +from ...ui._navs import ( + NavbarOptions, + NavbarOptionsPositionT, + NavMenu, + NavPanel, + NavSet, + NavSetBar, + NavSetCard, +) from ...ui._sidebar import SidebarOpenSpec, SidebarOpenValue from ...ui.css import CssUnit from .._recall_context import RecallContextManager @@ -1067,17 +1075,16 @@ def navset_bar( fillable: bool | list[str] = True, gap: Optional[CssUnit] = None, padding: Optional[CssUnit | list[CssUnit]] = None, - position: Literal[ - "static-top", "fixed-top", "fixed-bottom", "sticky-top" - ] = "static-top", header: TagChild = None, footer: TagChild = None, - bg: Optional[str] = None, - # TODO: default to 'auto', like we have in R (parse color via webcolors?) - inverse: bool = False, - underline: bool = True, - collapsible: bool = True, + navbar_options: Optional[NavbarOptions] = None, fluid: bool = True, + # Deprecated ---- + position: MaybeMissing[NavbarOptionsPositionT] = DEPRECATED, + bg: MaybeMissing[str | None] = DEPRECATED, + inverse: MaybeMissing[bool] = DEPRECATED, + underline: MaybeMissing[bool] = DEPRECATED, + collapsible: MaybeMissing[bool] = DEPRECATED, ) -> RecallContextManager[NavSetBar]: """ Context manager for a set of nav items as a tabset inside a card container. @@ -1095,8 +1102,7 @@ def navset_bar( Choose a particular nav item to select by default value (should match it's ``value``). sidebar - A :class:`~shiny.ui.Sidebar` component to display on every - :func:`~shiny.ui.nav_panel` page. + A :class:`~shiny.ui.Sidebar` component to display on every :func:`~shiny.ui.nav_panel` page. fillable Whether or not to allow fill items to grow/shrink to fit the browser window. If `True`, all `nav()` pages are fillable. A character vector, matching the value @@ -1104,7 +1110,7 @@ def navset_bar( provided, `fillable` makes the main content portion fillable. gap A CSS length unit defining the gap (i.e., spacing) between elements provided to - `*args`. + `*args`. This value is only used when the navbar is `fillable`. padding Padding to use for the body. This can be a numeric vector (which will be interpreted as pixels) or a character vector with valid CSS lengths. The length @@ -1113,26 +1119,45 @@ def navset_bar( the second value will be used for left and right. If three, then the first will be used for top, the second will be left and right, and the third will be bottom. If four, then the values will be interpreted as top, right, bottom, and - left respectively. + left respectively. This value is only used when the navbar is `fillable`. + header + UI to display above the selected content. + footer + UI to display below the selected content. + fluid + ``True`` to use fluid layout; ``False`` to use fixed layout. + navbar_options + Configure the appearance and behavior of the navbar using + :func:`~shiny.ui.navbar_options` to set properties like position, background + color, and more. + + `navbar_options` was added in v1.3.0 and replaces deprecated arguments + `position`, `bg`, `inverse`, `collapsible`, and `underline`. position + Deprecated in v1.3.0. Please use `navbar_options` instead; see + :func:`~shiny.ui.navbar_options` for details. + Determines whether the navbar should be displayed at the top of the page with normal scrolling behavior ("static-top"), pinned at the top ("fixed-top"), or pinned at the bottom ("fixed-bottom"). Note that using "fixed-top" or "fixed-bottom" will cause the navbar to overlay your body content, unless you add padding (e.g., ``tags.style("body {padding-top: 70px;}")``). - header - UI to display above the selected content. - footer - UI to display below the selected content. bg + Deprecated in v1.3.0. Please use `navbar_options` instead; see + :func:`~shiny.ui.navbar_options` for details. + Background color of the navbar (a CSS color). inverse + Deprecated in v1.3.0. Please use `navbar_options` instead; see + :func:`~shiny.ui.navbar_options` for details. + Either ``True`` for a light text color or ``False`` for a dark text color. collapsible - ``True`` to automatically collapse the navigation elements into an expandable - menu on mobile devices or narrow window widths. - fluid - ``True`` to use fluid layout; ``False`` to use fixed layout. + Deprecated in v1.3.0. Please use `navbar_options` instead; see + :func:`~shiny.ui.navbar_options` for details. + + ``True`` to automatically collapse the elements into an expandable menu on + mobile devices or narrow window widths. """ return RecallContextManager( ui.navset_bar, @@ -1144,14 +1169,16 @@ def navset_bar( fillable=fillable, gap=gap, padding=padding, - position=position, header=header, footer=footer, + fluid=fluid, + navbar_options=navbar_options, + # Deprecated -- v1.3.0 2025-01 ---- + position=position, bg=bg, inverse=inverse, underline=underline, collapsible=collapsible, - fluid=fluid, ), ) From b483f316300cefed40f892cfdddb38187da0073b Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 16 Jan 2025 14:10:38 -0500 Subject: [PATCH 08/11] chore: add version and date for deprecations --- shiny/ui/_navs.py | 5 +++-- shiny/ui/_page.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/shiny/ui/_navs.py b/shiny/ui/_navs.py index 5476ee324..a9649c9f0 100644 --- a/shiny/ui/_navs.py +++ b/shiny/ui/_navs.py @@ -1375,7 +1375,7 @@ def navset_bar( footer: TagChild = None, navbar_options: Optional[NavbarOptions] = None, fluid: bool = True, - # Deprecated ---- + # Deprecated -- v1.3.0 2025-01 ---- position: MaybeMissing[NavbarOptionsPositionT] = DEPRECATED, bg: MaybeMissing[str | None] = DEPRECATED, inverse: MaybeMissing[bool] = DEPRECATED, @@ -1452,7 +1452,8 @@ def navset_bar( Deprecated in v1.3.0. Please use `navbar_options` instead; see :func:`~shiny.ui.navbar_options` for details. - ``True`` to automatically collapse the elements into an expandable menu on mobile devices or narrow window widths. + ``True`` to automatically collapse the elements into an expandable menu on + mobile devices or narrow window widths. See Also -------- diff --git a/shiny/ui/_page.py b/shiny/ui/_page.py index db02bd4a9..d1d7e169f 100644 --- a/shiny/ui/_page.py +++ b/shiny/ui/_page.py @@ -174,7 +174,7 @@ def page_navbar( window_title: str | MISSING_TYPE = MISSING, lang: Optional[str] = None, theme: Optional[str | Path | Theme | ThemeProvider] = None, - # Deprecated ---- + # Deprecated -- v1.3.0 2025-01 ---- position: MaybeMissing[ Literal["static-top", "fixed-top", "fixed-bottom"] ] = DEPRECATED, From 7f5b578ae4f834ff5c83864ab88a6d0200b5707a Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 16 Jan 2025 14:10:49 -0500 Subject: [PATCH 09/11] docs: Add navbar_options examples --- shiny/api-examples/navbar_options/app-core.py | 36 +++++++++++++++++++ .../navbar_options/app-express.py | 34 ++++++++++++++++++ shiny/ui/_navs.py | 1 + 3 files changed, 71 insertions(+) create mode 100644 shiny/api-examples/navbar_options/app-core.py create mode 100644 shiny/api-examples/navbar_options/app-express.py diff --git a/shiny/api-examples/navbar_options/app-core.py b/shiny/api-examples/navbar_options/app-core.py new file mode 100644 index 000000000..1080bacff --- /dev/null +++ b/shiny/api-examples/navbar_options/app-core.py @@ -0,0 +1,36 @@ +from shiny import App, render, ui + +app_ui = ui.page_fluid( + ui.navset_bar( + ui.nav_panel("A", "Panel A content"), + ui.nav_panel("B", "Panel B content"), + ui.nav_panel("C", "Panel C content"), + ui.nav_menu( + "Other links", + ui.nav_panel("D", "Panel D content"), + "----", + "Description:", + ui.nav_control( + ui.a("Shiny", href="https://shiny.posit.co", target="_blank") + ), + ), + id="selected_navset_bar", + title="Navset Bar", + navbar_options=ui.navbar_options( + bg="#B73A85", + theme="dark", + underline=False, + ), + ), + ui.h5("Selected:"), + ui.output_code("selected"), +) + + +def server(input, output, session): + @render.code + def selected(): + return input.selected_navset_bar() + + +app = App(app_ui, server) diff --git a/shiny/api-examples/navbar_options/app-express.py b/shiny/api-examples/navbar_options/app-express.py new file mode 100644 index 000000000..46631c040 --- /dev/null +++ b/shiny/api-examples/navbar_options/app-express.py @@ -0,0 +1,34 @@ +from shiny.express import input, render, ui + +with ui.navset_bar( + title="Navset Bar", + id="selected_navset_bar", + navbar_options=ui.navbar_options( + bg="#B73A85", + theme="dark", + underline=False, + ), +): + with ui.nav_panel("A"): + "Panel A content" + + with ui.nav_panel("B"): + "Panel B content" + + with ui.nav_panel("C"): + "Panel C content" + + with ui.nav_menu("Other links"): + with ui.nav_panel("D"): + "Page D content" + + "----" + "Description:" + with ui.nav_control(): + ui.a("Shiny", href="https://shiny.posit.co", target="_blank") +ui.h5("Selected:") + + +@render.code +def _(): + return input.selected_navset_bar() diff --git a/shiny/ui/_navs.py b/shiny/ui/_navs.py index a9649c9f0..7d4728db6 100644 --- a/shiny/ui/_navs.py +++ b/shiny/ui/_navs.py @@ -1068,6 +1068,7 @@ def __repr__(self): return f"navbar_options({', '.join(fields)})" +@add_example() def navbar_options( position: MaybeMissing[NavbarOptionsPositionT] = MISSING, bg: MaybeMissing[str | None] = MISSING, From 87e4abb4041d66ae5d83e3a4e75dd6d6225feefa Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 16 Jan 2025 14:16:19 -0500 Subject: [PATCH 10/11] docs: Fix navbar_optoins parameters --- shiny/ui/_navs.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/shiny/ui/_navs.py b/shiny/ui/_navs.py index 7d4728db6..c1f8cb18e 100644 --- a/shiny/ui/_navs.py +++ b/shiny/ui/_navs.py @@ -1080,7 +1080,7 @@ def navbar_options( """ Configure the appearance and behavior of the navbar. - Parameters: + Parameters ----------- position Determines whether the navbar should be displayed at the top of the page with @@ -1088,23 +1088,18 @@ def navbar_options( or pinned at the bottom (`"fixed-bottom"`). Note that using `"fixed-top"` or `"fixed-bottom"` will cause the navbar to overlay your body content, unless you add padding (e.g., `tags.style("body {padding-top: 70px;}")`) - bg Background color of the navbar (a CSS color). - theme The navbar theme: either `"dark"` for a light text color (on a **dark** background) or `"light"` for a dark text color (on a **light** background). If `"auto"` (the default) and `bg` is provided, the best contrast to `bg` is chosen. - underline If `True`, adds an underline effect to the navbar. - collapsible If `True`, automatically collapses the elements into an expandable menu on mobile devices or narrow window widths. - **attrs : dict Additional HTML attributes to apply to the navbar container element. From afc0dd23e0d2ff0c0fdb8021faac4b36f241acbe Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 16 Jan 2025 14:37:17 -0500 Subject: [PATCH 11/11] chore: format --- shiny/ui/_navs.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/shiny/ui/_navs.py b/shiny/ui/_navs.py index c1f8cb18e..46becf30a 100644 --- a/shiny/ui/_navs.py +++ b/shiny/ui/_navs.py @@ -1159,7 +1159,6 @@ def navbar_options_resolve_deprecated( options_old["theme"] = "dark" if inverse_old else "light" - options_resolved = { k: v for k, v in vars(options_user).items() @@ -1225,7 +1224,9 @@ def __init__( self.fillable = fillable self.gap = gap self.padding = padding - self.navbar_options = navbar_options if navbar_options is not None else NavbarOptions() + self.navbar_options = ( + navbar_options if navbar_options is not None else NavbarOptions() + ) self.fluid = fluid self._is_page_level = False @@ -1259,7 +1260,9 @@ def layout(self, nav: Tag, content: Tag) -> TagList: # bslib supports navbar-default/navbar-inverse (which is no longer # a thing in Bootstrap 5) in a way that's still useful, especially Bootswatch. nav_final.add_class( - "navbar-inverse" if self.navbar_options.theme == "dark" else "navbar-default" + "navbar-inverse" + if self.navbar_options.theme == "dark" + else "navbar-default" ) if self.navbar_options.bg: