Skip to content

feat: Move navbar_options into single argument with helper navbar_options() #1822

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

Merged
merged 20 commits into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [UNRELEASED]

### Breaking changes

* The navbar-related style options of `ui.page_navbar()` and `ui.navset_bar()` have been consolidated into a single `navbar_options` argument that pairs with a new `ui.navbar_options()` helper. Using the direct `position`, `bg`, `inverse`, `collapsible`, and `underline` arguments will continue to work with a deprecation message.

Related to this change, `ui.navset_bar()` now defaults to using `underline=True` so that it uses the same set of default `ui.navbar_options()` as the page variant. In `ui.navbar_options()`, `inverse` is replaced by `theme`, which takes values `"light"` (dark text on a **light** background), `"dark"` (light text on a **dark** background), or `"auto"` (follow page settings).
Comment on lines +10 to +14
Copy link
Collaborator

@cpsievert cpsievert Feb 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the "breaking" change here could be considered more of an improvement than a regression, I'd be in favor of renaming this section to ### Changes (or even ### Improvements and putting it just after ### New features.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had the same thought, but we do emit deprecation warnings that say, effectively, "your current code is being deprecated, please update it". I know it's not strictly breaking but I'd personally rather read a changelog that differentiates between "you might need to take action" and "you might want to know about this" items.


### New features

* Added a new `ui.MarkdownStream()` component for performantly streaming in chunks of markdown/html strings into the UI. This component is primarily useful for text-based generative AI where responses are received incrementally. (#1782)
Expand Down
1 change: 1 addition & 0 deletions docs/_quartodoc-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions docs/_quartodoc-express.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
36 changes: 36 additions & 0 deletions shiny/api-examples/navbar_options/app-core.py
Original file line number Diff line number Diff line change
@@ -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)
34 changes: 34 additions & 0 deletions shiny/api-examples/navbar_options/app-express.py
Original file line number Diff line number Diff line change
@@ -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()
2 changes: 2 additions & 0 deletions shiny/express/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
notification_show,
notification_remove,
nav_spacer,
navbar_options,
Progress,
Theme,
value_box_theme,
Expand Down Expand Up @@ -288,6 +289,7 @@
"navset_pill_list",
"navset_tab",
"navset_underline",
"navbar_options",
"value_box",
"panel_well",
"panel_conditional",
Expand Down
75 changes: 51 additions & 24 deletions shiny/express/ui/_cm_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
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,
NavbarOptionsPositionType,
NavMenu,
NavPanel,
NavSet,
NavSetBar,
NavSetCard,
)
from ...ui._sidebar import SidebarOpenSpec, SidebarOpenValue
from ...ui.css import CssUnit
from .._recall_context import RecallContextManager
Expand Down Expand Up @@ -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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel somewhat on the fence about this, but would you be open to having types.NavbarOptions inherit from TypedDict? The benefit being that we wouldn't need ui.navbar_options(). FWIW, the animate argument of input_slider() follows that pattern.

Copy link
Collaborator Author

@gadenbuie gadenbuie Jan 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally open to it and it's certainly a route I thought about taking. It's a little more complicated because there are methods on NavbarOptions, so we'd need a user-facing NavbarOptions that inherits from TypedDict and a counterpart NavbarOptionsResolved (or similar name) class that resolves the user-facing options.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out it's a bit more complicated because, unlike AnimationOptions, the navbar options are not a closed set, which means we can't just call NavbarOptionsResolved(**navbar_options) without making the type checker angry.

Code sample in pyright playground

from typing import TypedDict

class NavbarOptions(TypedDict, total = False):
    underline: bool
    __extra_items__: str

def test(x: NavbarOptions) -> bool:
    return x.get("underline", False)


test({"underline": True})
test({"class": "my-class"}) # we want this to be okay
#> Argument of type "dict[str, str]" cannot be assigned to parameter "x" of type "NavbarOptions" in function "test"
#>  "class" is an undefined item in type "NavbarOptions"  (reportArgumentType)

test({"underline": True, "class": "my-class"})
#> Argument of type "dict[str, bool | str]" cannot be assigned to parameter "x" of type "NavbarOptions" in function "test"
#>  "class" is an undefined item in type "NavbarOptions"  (reportArgumentType)

It turns out there's a PEP for this but it's still in draft mode: https://peps.python.org/pep-0728/

fluid: bool = True,
# Deprecated ----
position: NavbarOptionsPositionType | MISSING_TYPE = DEPRECATED,
bg: str | None | MISSING_TYPE = DEPRECATED,
inverse: bool | MISSING_TYPE = DEPRECATED,
underline: bool | MISSING_TYPE = DEPRECATED,
collapsible: bool | MISSING_TYPE = DEPRECATED,
) -> RecallContextManager[NavSetBar]:
"""
Context manager for a set of nav items as a tabset inside a card container.
Expand All @@ -1095,16 +1102,15 @@ 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
of `nav()`s to be filled, may also be provided. Note that, if a `sidebar` is
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
Expand All @@ -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,
Expand All @@ -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,
),
)

Expand Down
5 changes: 3 additions & 2 deletions shiny/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,17 @@
if TYPE_CHECKING:
from matplotlib.figure import Figure

T = TypeVar("T")


# Sentinel value - indicates a missing value in a function call.
class MISSING_TYPE:
pass


MISSING: MISSING_TYPE = MISSING_TYPE()
DEPRECATED: MISSING_TYPE = MISSING_TYPE() # A MISSING that communicates deprecation


T = TypeVar("T")
ListOrTuple = Union[List[T], Tuple[T, ...]]


Expand Down
2 changes: 2 additions & 0 deletions shiny/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
nav_menu,
nav_panel,
nav_spacer,
navbar_options,
navset_bar,
navset_card_pill,
navset_card_tab,
Expand Down Expand Up @@ -295,6 +296,7 @@
"navset_pill_list",
"navset_hidden",
"navset_bar",
"navbar_options",
# _notification
"notification_show",
"notification_remove",
Expand Down
Loading
Loading