Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
7b4f323
`shiny.ui.sidebar` supports `SidebarOpen` and is now a thin wrapper a…
gadenbuie Feb 14, 2024
40b8425
`sidebar(open=)` might not be user-provided
gadenbuie Feb 14, 2024
e1c408a
layout_sidebar: Update to use new bslib features and improved Sidebar…
gadenbuie Feb 14, 2024
ebc2198
export ui.SidebarOpen
gadenbuie Feb 14, 2024
e534609
Add a dynamic example (currently unused)
gadenbuie Feb 14, 2024
ada1d63
Use `render.code` in Express example
gadenbuie Feb 14, 2024
fe912c0
Update shiny.express.ui.sidebar signature
gadenbuie Feb 14, 2024
dbd7fa7
`shiny.express.ui.sidebar()` now takes kwargs
gadenbuie Feb 15, 2024
2a189f9
update bslib again
gadenbuie Feb 15, 2024
a5e8b04
sidebar: Validate `open` on init, if provided
gadenbuie Feb 15, 2024
4053bd1
docs(sidebar): Update `open`, `max_height_mobile`
gadenbuie Feb 15, 2024
0dfb030
fix(sidebar): Fix logic for auto-generated sidebar id
gadenbuie Feb 15, 2024
48f5b13
refactor: Match color parameters of `sidebar` and `Sidebar`
gadenbuie Feb 15, 2024
342502f
docs(Sidebar): Update for new signature
gadenbuie Feb 15, 2024
2e2b674
docs(SidebarOpen): Finish docs
gadenbuie Feb 15, 2024
0531a98
re-export SidebarOpen from shiny.express.ui
gadenbuie Feb 15, 2024
a18f8bd
docs(SidebarOpen): Add to Core doc listing
gadenbuie Feb 15, 2024
66601b2
page-level sidebar default to mobile="always"
gadenbuie Feb 15, 2024
8de875d
express(sidebar): `open` is now `None` by default
gadenbuie Feb 15, 2024
fea04fb
sidebar params: position, open, width
gadenbuie Feb 15, 2024
18321d6
remove unused import
gadenbuie Feb 15, 2024
8f13804
sidebar: Improve setting default open value, get/set open and max_hei…
gadenbuie Feb 15, 2024
e3e2045
typo: resolve -> render
gadenbuie Feb 15, 2024
b9612b1
fix shiny.experimental.ui._deprecated.Sidebar
gadenbuie Feb 15, 2024
a70d9fc
a11y(card): Improve accessibility of full-screen card button
gadenbuie Feb 15, 2024
9a5cece
fix DeprecatedPanelSidebar
gadenbuie Feb 15, 2024
34f927a
tests: Improve `compare_annotations()` failure message
gadenbuie Feb 15, 2024
219280e
fix(sidebar): Fix open type and default value
gadenbuie Feb 15, 2024
ddb5e9a
tests(sidebar): Fix sidebar test
gadenbuie Feb 15, 2024
ab1eceb
docs: Add changelog notes
gadenbuie Feb 15, 2024
8847f2e
tests: Fix tests/playwright/shiny/bugs/0666-sidebar/test_sidebar_colo…
gadenbuie Feb 15, 2024
9a3a226
typo: adjust hypenation of full-screen
gadenbuie Feb 15, 2024
6ec7fa3
feat(resolve_id): add ID validations
gadenbuie Feb 15, 2024
330ad1c
fix(sidebar): resolve ID earlier
gadenbuie Feb 15, 2024
9ad19a1
chore: make format
gadenbuie Feb 15, 2024
416fe4c
Merge branch 'main' into sync-bslib-0-6-1-dev
gadenbuie Feb 15, 2024
d8c7abb
Apply suggestions from code review
gadenbuie Feb 15, 2024
5f2a99a
chore: make format
gadenbuie Feb 16, 2024
905f6b8
refactor: Apply review suggestions
gadenbuie Feb 16, 2024
3425044
fix: Don't need to unpack attrs
gadenbuie Feb 16, 2024
9d27f50
refactor(card): Fix types so that `str(attr["id"])` isn't required
gadenbuie Feb 16, 2024
9667eeb
refactor: Simplify nzchar check
gadenbuie Feb 16, 2024
a9d83c9
chore: make format
gadenbuie Feb 16, 2024
79fbb45
refactor: Use private_random_id() to create randomized IDs
gadenbuie Feb 16, 2024
dceb169
one more random id and remove unused import
gadenbuie Feb 16, 2024
3cb4fd1
feat(sidebar): Users supply SidebarOpenSpec dict, we upgrade to Sideb…
gadenbuie Feb 20, 2024
7323f81
Fix `Sidebar._as_open(open=)` type annotation
gadenbuie Feb 20, 2024
3e7eac3
don't need to export `SidebarOpen` from `express.ui`
gadenbuie Feb 20, 2024
b393eb4
Hide `SidebarOpen._VALUES` from print method
gadenbuie Feb 20, 2024
b598542
Merge branch 'main' into sync-bslib-0-6-1-dev
schloerke Feb 20, 2024
d4b42b1
Merge branch 'main' into sync-bslib-0-6-1-dev
gadenbuie Feb 21, 2024
b727b6f
refactor(Sidebar): open is now a method that gets/sets initial open s…
gadenbuie Feb 21, 2024
ff8ed30
refactor(Sidebar): Return self early if new default same as current
gadenbuie Feb 21, 2024
2d6c1b4
chore(Sidebar): Add comments about `_open` and `_default_open`
gadenbuie Feb 21, 2024
d066fa1
chore(Sidebar): Explicit default open value
gadenbuie Feb 21, 2024
3a82783
fix(Sidebar): Need same `id` for collapse/sidebar tags
gadenbuie Feb 21, 2024
3b95018
chore: make format
gadenbuie Feb 21, 2024
d4d5c5a
docs(Sidebar): Add some method docs
gadenbuie Feb 21, 2024
1df1010
chore(Sidebar.open): Let `None` also be valid to set `.open`
gadenbuie Feb 21, 2024
7a02069
chore(Sidebar): Improve validation error messages
gadenbuie Feb 21, 2024
6ab8af6
fix(Sidebar): no aria expanded/controls if always open
gadenbuie Feb 21, 2024
b8d26f1
refactor(Sidebar): Consolidate str/dict upgrade to SidebarOpen in _as…
gadenbuie Feb 21, 2024
a9b3203
tests(Sidebar): Add tests for Sidebar class
gadenbuie Feb 21, 2024
ea2ab21
chore: make format
gadenbuie Feb 22, 2024
4c4da4b
tests(sidebar): Fix type issues
gadenbuie Feb 22, 2024
c68456d
test(sidebar): from future...
gadenbuie Feb 22, 2024
06809b8
chore: Apply changes from code review
gadenbuie Feb 22, 2024
1d118bf
Merge branch 'main' into sync-bslib-0-6-1-dev
gadenbuie Feb 22, 2024
aa8374d
refactor(Sidebar): Rename `._get_sidebar_id()` method
gadenbuie Feb 22, 2024
65ebb93
refactor(Sidebar): remove `_set_default_open()` method, copy the side…
gadenbuie Feb 22, 2024
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
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [UNRELEASED] - YYYY-MM-DD

### Breaking Changes

* Page-level sidebars used in `ui.page_sidebar()` and `ui.page_navbar()` will now default to being initially open but collapsible on desktop devices and always open on mobile devices. You can adjust this default choice by setting `ui.sidebar(open=)`. (#1129)

* `ui.sidebar()` is now a thin wrapper for the internal `ui.Sidebar` class. The `ui.Sidebar` class has been updated to store the sidebar's contents and settings and to delay rendering until the sidebar HTML is actually used. Because most users call `ui.sidebar()` instead of using the class directly, this change is not expected to affect many apps. (#1129)

### New features

* `ui.sidebar(open=)` now accepts a dictionary with keys `desktop` and `mobile`, allowing you to independently control the initial state of the sidebar at desktop and mobile screen sizes. (#1129)

### Other changes

* We improved the accessibility of the full screen toggle button in cards created with `ui.card(full_screen=True)`. Full-screen cards are now also supported on mobile devices. (#1129)

* When entering and exiting full-screen card mode, Shiny now emits a client-side custom `bslib.card` event that JavaScript-oriented users can use to react to the full screen state change. (#1129)

* The sidebar's collapse toggle now has a high `z-index` value to ensure it always appears above elements in the main content area of `ui.layout_sidebar()`. The sidebar overlay also now receives the same high `z-index` on mobile layouts. (#1129)

### Bug fixes

* Fixed `input_task_button` not working in a Shiny module. (#1108)
Expand Down
2 changes: 1 addition & 1 deletion scripts/htmlDependencies.R
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ message("Installing GitHub packages: bslib, shiny, htmltools")
withr::local_temp_libpaths()
ignore <- capture.output({
pak::pkg_install(c(
"rstudio/bslib@py-shiny-v0.7.0",
"rstudio/bslib@main",
"rstudio/shiny@main",
"cran::htmltools"
))
Expand Down
4 changes: 4 additions & 0 deletions shiny/_namespaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ def resolve_id_or_none(id: Id | None) -> ResolvedId | None:


def validate_id(id: str) -> None:
if not isinstance(id, str):
raise ValueError("`id` must be a single string")
if id == "":
raise ValueError("`id` must be a non-empty string")
if not re_valid_id.match(id):
raise ValueError(
f"The string '{id}' is not a valid id; only letters, numbers, and "
Expand Down
8 changes: 8 additions & 0 deletions shiny/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,14 @@ def private_random_int(min: int, max: int) -> str:
return str(random.randint(min, max))


def private_random_id(prefix: str = "", bytes: int = 3) -> str:
if prefix != "" and not prefix.endswith("_"):
prefix += "_"

with private_seed():
return prefix + rand_hex(bytes)


@contextlib.contextmanager
def private_seed() -> Generator[None, None, None]:
state = random.getstate()
Expand Down
90 changes: 90 additions & 0 deletions shiny/api-examples/sidebar/app-core-dynamic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from shiny import App, Inputs, Outputs, Session, render, ui

app_ui = ui.page_fluid(
ui.card(
ui.card_header("ui.sidebar() Settings"),
ui.layout_column_wrap(
ui.input_select(
"desktop",
label="Desktop",
choices=["open", "closed", "always"],
selected="open",
),
ui.input_select(
"mobile",
label="Mobile",
choices=["open", "closed", "always"],
selected="closed",
),
ui.input_select(
"position", label="Position", choices=["left", "right"], selected="left"
),
width="100px",
),
),
ui.layout_column_wrap(
ui.card(
ui._card.card_body(
ui.output_ui("sidebar_dynamic", fill=True, fillable=True), class_="p-0"
),
),
ui.card(
ui.card_header("Sidebar Layout Code"),
ui.output_code("sidebar_code"),
),
width="500px",
),
)


def server(input: Inputs, output: Outputs, session: Session):
@render.ui
def sidebar_dynamic():
return ui.layout_sidebar(
ui.sidebar(
ui.markdown(
f"""
**Desktop**: {input.desktop()}

**Mobile**: {input.mobile()}

**Position**: {input.position()}
"""
),
title="Settings",
id="sidebar_dynamic",
open={"desktop": input.desktop(), "mobile": input.mobile()},
position=input.position(),
),
ui.h2("Dynamic sidebar"),
ui.output_text_verbatim("state_dynamic"),
)

@render.text
def state_dynamic():
return f"input.sidebar_dynamic(): {input.sidebar_dynamic()}"

@render.code
def sidebar_code():
if input.desktop() == input.mobile():
open = f'"{input.desktop()}"'
elif input.desktop() == "open" and input.mobile() == "closed":
open = '{"desktop": "open", "mobile": "closed"}'
else:
open = f'{{"desktop": "{input.desktop()}", "mobile": "{input.mobile()}}}'

return f"""\
ui.layout_sidebar(
ui.sidebar(
"Sidebar content...",
title="Sidebar title",
id="sidebar_id",
open={open},
position="{input.position()}"
),
"Main content...",
)
"""


app = App(app_ui, server)
8 changes: 4 additions & 4 deletions shiny/api-examples/sidebar/app-express.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
with ui.sidebar(id="sidebar_left", open="desktop"):
"Left sidebar content"

@render.text
@render.code
def state_left():
return f"input.sidebar_left(): {input.sidebar_left()}"

Expand All @@ -17,7 +17,7 @@ def state_left():
with ui.sidebar(id="sidebar_right", position="right", open="desktop"):
"Right sidebar content"

@render.text
@render.code
def state_right():
return f"input.sidebar_right(): {input.sidebar_right()}"

Expand All @@ -27,7 +27,7 @@ def state_right():
with ui.sidebar(id="sidebar_closed", open="closed"):
"Closed sidebar content"

@render.text
@render.code
def state_closed():
return f"input.sidebar_closed(): {input.sidebar_closed()}"

Expand All @@ -37,6 +37,6 @@ def state_closed():
with ui.sidebar(id="sidebar_always", open="always"):
"Always sidebar content"

@render.text
@render.code
def state_always():
return f"input.sidebar_always(): {input.sidebar_always()}"
44 changes: 28 additions & 16 deletions shiny/experimental/ui/_deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
from ...ui._plot_output_opts import HoverOpts as MainHoverOpts
from ...ui._sidebar import DeprecatedPanelMain, DeprecatedPanelSidebar
from ...ui._sidebar import Sidebar as MainSidebar
from ...ui._sidebar import SidebarOpenSpec as MainSidebarOpenSpec
from ...ui._sidebar import SidebarOpenValue as MainSidebarOpenValue
from ...ui._sidebar import layout_sidebar as main_layout_sidebar
from ...ui._sidebar import panel_main as main_panel_main
from ...ui._sidebar import panel_sidebar as main_panel_sidebar
Expand Down Expand Up @@ -398,29 +400,39 @@ class Sidebar(MainSidebar):

def __init__(
self,
tag: Tag,
collapse_tag: Optional[Tag],
position: Literal["left", "right"],
open: Literal["desktop", "open", "closed", "always"],
width: CssUnit,
max_height_mobile: Optional[str | float],
color_fg: Optional[str],
color_bg: Optional[str],
children: list[TagChild],
attrs: TagAttrs,
position: Literal["left", "right"] = "left",
open: Optional[MainSidebarOpenValue | MainSidebarOpenSpec] = None,
width: CssUnit = 250,
id: Optional[str] = None,
title: TagChild | str = None,
fg: Optional[str] = None,
bg: Optional[str] = None,
class_: Optional[str] = None,
max_height_mobile: Optional[str | float] = None,
gap: Optional[CssUnit] = None,
padding: Optional[CssUnit | list[CssUnit]] = None,
):
warn_deprecated(
"`shiny.experimental.ui.Sidebar` is deprecated. "
"This class will be removed in a future version, "
"please use :class:`shiny.ui.Sidebar` instead."
)
super().__init__(
tag,
collapse_tag,
position,
open,
width,
max_height_mobile,
color_fg,
color_bg,
children=children,
attrs=attrs,
position=position,
open=open,
width=width,
id=id,
title=title,
fg=fg,
bg=bg,
class_=class_,
max_height_mobile=max_height_mobile,
gap=gap,
padding=padding,
)


Expand Down
38 changes: 22 additions & 16 deletions shiny/express/ui/_cm_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from ...ui._card import CardItem
from ...ui._layout_columns import BreakpointsUser
from ...ui._navs import 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 @@ -42,17 +43,18 @@
@add_example()
def sidebar(
*,
width: CssUnit = 250,
position: Literal["left", "right"] = "left",
open: Literal["desktop", "open", "closed", "always"] = "always",
open: Optional[SidebarOpenSpec | SidebarOpenValue | Literal["desktop"]] = None,
width: CssUnit = 250,
id: Optional[str] = None,
title: TagChild | str = None,
bg: Optional[str] = None,
fg: Optional[str] = None,
class_: Optional[str] = None, # TODO-future; Consider using `**kwargs` instead
max_height_mobile: Optional[str | float] = "auto",
class_: Optional[str] = None,
max_height_mobile: Optional[str | float] = None,
gap: Optional[CssUnit] = None,
padding: Optional[CssUnit | list[CssUnit]] = None,
**kwargs: TagAttrValue,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Given we are wanting to support **kwargs, should we add *args: TagAttrs to the definition?

Running args through consolidate_attrs(), we could verify that length of children is 0.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This upgrading / check could be done with UI (not express) section of code

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sorry, I'm not sure if you're suggesting we use consolidate_attrs() in shiny.ui.sidebar() (like you suggested later) or if you have something else in mind?

) -> RecallContextManager[ui.Sidebar]:
"""
Context manager for sidebar element
Expand All @@ -64,20 +66,21 @@ def sidebar(
width
A valid CSS unit used for the width of the sidebar.
position
Where the sidebar should appear relative to the main content.
Where the sidebar should appear relative to the main content, one of `"left"` or
`"right"`.
open
The initial state of the sidebar.
The initial state of the sidebar. If a string, the possible values are:

* `"desktop"`: the sidebar starts open on desktop screen, closed on mobile
* `"open"` or `True`: the sidebar starts open
* `"closed"` or `False`: the sidebar starts closed
* `"always"` or `None`: the sidebar is always open and cannot be closed
* `"open"`: the sidebar starts open
* `"closed"`: the sidebar starts closed
* `"always"`: the sidebar is always open and cannot be closed

In :func:`~shiny.ui.update_sidebar`, `open` indicates the desired state of the
sidebar. Note that :func:`~shiny.ui.update_sidebar` can only open or close the
sidebar, so it does not support the `"desktop"` and `"always"` options.
Alternatively, you can provide a dictionary with keys `"desktop"` and `"mobile"`
to set different initial states for desktop and mobile. For example, when
`{"desktop": "open", "mobile": "closed"}` the sidebar is initialized in the
open state on desktop screens or in the closed state on mobile screens.
id
A character string. Required if wanting to re-actively read (or update) the
A character string. Required if wanting to reactively read (or update) the
`collapsible` state in a Shiny app.
title
A character title to be used as the sidebar title, which will be wrapped in a
Expand All @@ -92,8 +95,8 @@ def sidebar(
max_height_mobile
A CSS length unit (passed through :func:`~shiny.ui.css.as_css_unit`) defining
the maximum height of the horizontal sidebar when viewed on mobile devices. Only
applies to always-open sidebars that use `open = "always"`, where by default the
sidebar container is placed below the main content container on mobile devices.
applies to always-open sidebars on mobile, where by default the sidebar
container is placed below the main content container on mobile devices.
gap
A CSS length unit defining the vertical `gap` (i.e., spacing) between elements
provided to `*args`.
Expand All @@ -109,6 +112,8 @@ def sidebar(
and right, and the third will be bottom.
* If four, then the values will be interpreted as top, right, bottom, and left
respectively.
**kwargs
Named attributes are supplied to the sidebar content container.
"""
return RecallContextManager(
ui.sidebar,
Expand All @@ -124,6 +129,7 @@ def sidebar(
max_height_mobile=max_height_mobile,
gap=gap,
padding=padding,
**kwargs,
),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ def __init__(self, _fn: Optional[ValueFn[int]] = None, *, height: str = "200px")
self.height: str = height

# Transforms non-`None` values into a `Jsonifiable` object.
# If you'd like more control on when and how the value is resolved,
# please use the `async def resolve(self)` method.
# If you'd like more control on when and how the value is rendered,
# please use the `async def render(self)` method.
async def transform(self, value: int) -> Jsonifiable:
# Send the results to the client. Make sure that this is a serializable
# object and matches what is expected in the javascript code.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ def auto_output_ui(self) -> Tag:
# self.extra_arg: str = extra_arg

# Transforms non-`None` values into a `Jsonifiable` object.
# If you'd like more control on when and how the value is resolved,
# please use the `async def resolve(self)` method.
# If you'd like more control on when and how the value is rendered,
# please use the `async def render(self)` method.
async def transform(self, value: str) -> Jsonifiable:
# Send the results to the client. Make sure that this is a serializable
# object and matches what is expected in the javascript code.
Expand Down
7 changes: 3 additions & 4 deletions shiny/ui/_accordion.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
from __future__ import annotations

import random
from typing import TYPE_CHECKING, Literal, Optional, TypeVar

from htmltools import Tag, TagAttrs, TagAttrValue, TagChild, css, tags

from .._docstring import add_example
from .._namespaces import resolve_id_or_none
from .._utils import drop_none
from .._utils import drop_none, private_random_id
from ..session import require_active_session
from ..types import MISSING, MISSING_TYPE
from ._html_deps_shinyverse import components_dependency
Expand Down Expand Up @@ -266,7 +265,7 @@ def accordion(
# but only create a binding when it is provided
binding_class_value: TagAttrs | None = None
if id is None:
id = f"bslib_accordion_{random.randint(1000, 10000)}"
id = private_random_id("bslib_accordion")
binding_class_value = None
else:
binding_class_value = {"class": "bslib-accordion-input"}
Expand Down Expand Up @@ -349,7 +348,7 @@ def accordion_panel(
if not isinstance(value, str):
raise TypeError("`value` must be a string")

id = f"bslib-accordion-panel-{random.randint(1000, 10000)}"
id = private_random_id("bslib_accordion_panel")

return AccordionPanel(
*args,
Expand Down
Loading