Skip to content

Commit ccd5d2f

Browse files
gadenbuieschloerkewch
authored
Sync with latest bslib sidebar and card changes (#1129)
Co-authored-by: Barret Schloerke <[email protected]> Co-authored-by: Winston Chang <[email protected]>
1 parent a52a4c6 commit ccd5d2f

File tree

26 files changed

+720
-216
lines changed

26 files changed

+720
-216
lines changed

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
## [UNRELEASED] - YYYY-MM-DD
1010

11+
### Breaking Changes
12+
13+
* 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)
14+
15+
* `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)
16+
17+
### New features
18+
19+
* `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)
20+
21+
### Other changes
22+
23+
* 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)
24+
25+
* 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)
26+
27+
* 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)
28+
1129
### Bug fixes
1230

1331
* Fixed `input_task_button` not working in a Shiny module. (#1108)

scripts/htmlDependencies.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ message("Installing GitHub packages: bslib, shiny, htmltools")
1212
withr::local_temp_libpaths()
1313
ignore <- capture.output({
1414
pak::pkg_install(c(
15-
"rstudio/bslib@py-shiny-v0.7.0",
15+
"rstudio/bslib@main",
1616
"rstudio/shiny@main",
1717
"cran::htmltools"
1818
))

shiny/_namespaces.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ def resolve_id_or_none(id: Id | None) -> ResolvedId | None:
8181

8282

8383
def validate_id(id: str) -> None:
84+
if not isinstance(id, str):
85+
raise ValueError("`id` must be a single string")
86+
if id == "":
87+
raise ValueError("`id` must be a non-empty string")
8488
if not re_valid_id.match(id):
8589
raise ValueError(
8690
f"The string '{id}' is not a valid id; only letters, numbers, and "

shiny/_utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,14 @@ def private_random_int(min: int, max: int) -> str:
199199
return str(random.randint(min, max))
200200

201201

202+
def private_random_id(prefix: str = "", bytes: int = 3) -> str:
203+
if prefix != "" and not prefix.endswith("_"):
204+
prefix += "_"
205+
206+
with private_seed():
207+
return prefix + rand_hex(bytes)
208+
209+
202210
@contextlib.contextmanager
203211
def private_seed() -> Generator[None, None, None]:
204212
state = random.getstate()
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from shiny import App, Inputs, Outputs, Session, render, ui
2+
3+
app_ui = ui.page_fluid(
4+
ui.card(
5+
ui.card_header("ui.sidebar() Settings"),
6+
ui.layout_column_wrap(
7+
ui.input_select(
8+
"desktop",
9+
label="Desktop",
10+
choices=["open", "closed", "always"],
11+
selected="open",
12+
),
13+
ui.input_select(
14+
"mobile",
15+
label="Mobile",
16+
choices=["open", "closed", "always"],
17+
selected="closed",
18+
),
19+
ui.input_select(
20+
"position", label="Position", choices=["left", "right"], selected="left"
21+
),
22+
width="100px",
23+
),
24+
),
25+
ui.layout_column_wrap(
26+
ui.card(
27+
ui._card.card_body(
28+
ui.output_ui("sidebar_dynamic", fill=True, fillable=True), class_="p-0"
29+
),
30+
),
31+
ui.card(
32+
ui.card_header("Sidebar Layout Code"),
33+
ui.output_code("sidebar_code"),
34+
),
35+
width="500px",
36+
),
37+
)
38+
39+
40+
def server(input: Inputs, output: Outputs, session: Session):
41+
@render.ui
42+
def sidebar_dynamic():
43+
return ui.layout_sidebar(
44+
ui.sidebar(
45+
ui.markdown(
46+
f"""
47+
**Desktop**: {input.desktop()}
48+
49+
**Mobile**: {input.mobile()}
50+
51+
**Position**: {input.position()}
52+
"""
53+
),
54+
title="Settings",
55+
id="sidebar_dynamic",
56+
open={"desktop": input.desktop(), "mobile": input.mobile()},
57+
position=input.position(),
58+
),
59+
ui.h2("Dynamic sidebar"),
60+
ui.output_text_verbatim("state_dynamic"),
61+
)
62+
63+
@render.text
64+
def state_dynamic():
65+
return f"input.sidebar_dynamic(): {input.sidebar_dynamic()}"
66+
67+
@render.code
68+
def sidebar_code():
69+
if input.desktop() == input.mobile():
70+
open = f'"{input.desktop()}"'
71+
elif input.desktop() == "open" and input.mobile() == "closed":
72+
open = '{"desktop": "open", "mobile": "closed"}'
73+
else:
74+
open = f'{{"desktop": "{input.desktop()}", "mobile": "{input.mobile()}}}'
75+
76+
return f"""\
77+
ui.layout_sidebar(
78+
ui.sidebar(
79+
"Sidebar content...",
80+
title="Sidebar title",
81+
id="sidebar_id",
82+
open={open},
83+
position="{input.position()}"
84+
),
85+
"Main content...",
86+
)
87+
"""
88+
89+
90+
app = App(app_ui, server)

shiny/api-examples/sidebar/app-express.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
with ui.sidebar(id="sidebar_left", open="desktop"):
88
"Left sidebar content"
99

10-
@render.text
10+
@render.code
1111
def state_left():
1212
return f"input.sidebar_left(): {input.sidebar_left()}"
1313

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

20-
@render.text
20+
@render.code
2121
def state_right():
2222
return f"input.sidebar_right(): {input.sidebar_right()}"
2323

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

30-
@render.text
30+
@render.code
3131
def state_closed():
3232
return f"input.sidebar_closed(): {input.sidebar_closed()}"
3333

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

40-
@render.text
40+
@render.code
4141
def state_always():
4242
return f"input.sidebar_always(): {input.sidebar_always()}"

shiny/experimental/ui/_deprecated.py

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@
5252
from ...ui._plot_output_opts import HoverOpts as MainHoverOpts
5353
from ...ui._sidebar import DeprecatedPanelMain, DeprecatedPanelSidebar
5454
from ...ui._sidebar import Sidebar as MainSidebar
55+
from ...ui._sidebar import SidebarOpenSpec as MainSidebarOpenSpec
56+
from ...ui._sidebar import SidebarOpenValue as MainSidebarOpenValue
5557
from ...ui._sidebar import layout_sidebar as main_layout_sidebar
5658
from ...ui._sidebar import panel_main as main_panel_main
5759
from ...ui._sidebar import panel_sidebar as main_panel_sidebar
@@ -398,29 +400,39 @@ class Sidebar(MainSidebar):
398400

399401
def __init__(
400402
self,
401-
tag: Tag,
402-
collapse_tag: Optional[Tag],
403-
position: Literal["left", "right"],
404-
open: Literal["desktop", "open", "closed", "always"],
405-
width: CssUnit,
406-
max_height_mobile: Optional[str | float],
407-
color_fg: Optional[str],
408-
color_bg: Optional[str],
403+
children: list[TagChild],
404+
attrs: TagAttrs,
405+
position: Literal["left", "right"] = "left",
406+
open: Optional[MainSidebarOpenValue | MainSidebarOpenSpec] = None,
407+
width: CssUnit = 250,
408+
id: Optional[str] = None,
409+
title: TagChild | str = None,
410+
fg: Optional[str] = None,
411+
bg: Optional[str] = None,
412+
class_: Optional[str] = None,
413+
max_height_mobile: Optional[str | float] = None,
414+
gap: Optional[CssUnit] = None,
415+
padding: Optional[CssUnit | list[CssUnit]] = None,
409416
):
410417
warn_deprecated(
411418
"`shiny.experimental.ui.Sidebar` is deprecated. "
412419
"This class will be removed in a future version, "
413420
"please use :class:`shiny.ui.Sidebar` instead."
414421
)
415422
super().__init__(
416-
tag,
417-
collapse_tag,
418-
position,
419-
open,
420-
width,
421-
max_height_mobile,
422-
color_fg,
423-
color_bg,
423+
children=children,
424+
attrs=attrs,
425+
position=position,
426+
open=open,
427+
width=width,
428+
id=id,
429+
title=title,
430+
fg=fg,
431+
bg=bg,
432+
class_=class_,
433+
max_height_mobile=max_height_mobile,
434+
gap=gap,
435+
padding=padding,
424436
)
425437

426438

shiny/express/ui/_cm_components.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from ...ui._card import CardItem
1414
from ...ui._layout_columns import BreakpointsUser
1515
from ...ui._navs import NavMenu, NavPanel, NavSet, NavSetBar, NavSetCard
16+
from ...ui._sidebar import SidebarOpenSpec, SidebarOpenValue
1617
from ...ui.css import CssUnit
1718
from .._recall_context import RecallContextManager
1819

@@ -42,17 +43,18 @@
4243
@add_example()
4344
def sidebar(
4445
*,
45-
width: CssUnit = 250,
4646
position: Literal["left", "right"] = "left",
47-
open: Literal["desktop", "open", "closed", "always"] = "always",
47+
open: Optional[SidebarOpenSpec | SidebarOpenValue | Literal["desktop"]] = None,
48+
width: CssUnit = 250,
4849
id: Optional[str] = None,
4950
title: TagChild | str = None,
5051
bg: Optional[str] = None,
5152
fg: Optional[str] = None,
52-
class_: Optional[str] = None, # TODO-future; Consider using `**kwargs` instead
53-
max_height_mobile: Optional[str | float] = "auto",
53+
class_: Optional[str] = None,
54+
max_height_mobile: Optional[str | float] = None,
5455
gap: Optional[CssUnit] = None,
5556
padding: Optional[CssUnit | list[CssUnit]] = None,
57+
**kwargs: TagAttrValue,
5658
) -> RecallContextManager[ui.Sidebar]:
5759
"""
5860
Context manager for sidebar element
@@ -64,20 +66,21 @@ def sidebar(
6466
width
6567
A valid CSS unit used for the width of the sidebar.
6668
position
67-
Where the sidebar should appear relative to the main content.
69+
Where the sidebar should appear relative to the main content, one of `"left"` or
70+
`"right"`.
6871
open
69-
The initial state of the sidebar.
72+
The initial state of the sidebar. If a string, the possible values are:
7073
71-
* `"desktop"`: the sidebar starts open on desktop screen, closed on mobile
72-
* `"open"` or `True`: the sidebar starts open
73-
* `"closed"` or `False`: the sidebar starts closed
74-
* `"always"` or `None`: the sidebar is always open and cannot be closed
74+
* `"open"`: the sidebar starts open
75+
* `"closed"`: the sidebar starts closed
76+
* `"always"`: the sidebar is always open and cannot be closed
7577
76-
In :func:`~shiny.ui.update_sidebar`, `open` indicates the desired state of the
77-
sidebar. Note that :func:`~shiny.ui.update_sidebar` can only open or close the
78-
sidebar, so it does not support the `"desktop"` and `"always"` options.
78+
Alternatively, you can provide a dictionary with keys `"desktop"` and `"mobile"`
79+
to set different initial states for desktop and mobile. For example, when
80+
`{"desktop": "open", "mobile": "closed"}` the sidebar is initialized in the
81+
open state on desktop screens or in the closed state on mobile screens.
7982
id
80-
A character string. Required if wanting to re-actively read (or update) the
83+
A character string. Required if wanting to reactively read (or update) the
8184
`collapsible` state in a Shiny app.
8285
title
8386
A character title to be used as the sidebar title, which will be wrapped in a
@@ -92,8 +95,8 @@ def sidebar(
9295
max_height_mobile
9396
A CSS length unit (passed through :func:`~shiny.ui.css.as_css_unit`) defining
9497
the maximum height of the horizontal sidebar when viewed on mobile devices. Only
95-
applies to always-open sidebars that use `open = "always"`, where by default the
96-
sidebar container is placed below the main content container on mobile devices.
98+
applies to always-open sidebars on mobile, where by default the sidebar
99+
container is placed below the main content container on mobile devices.
97100
gap
98101
A CSS length unit defining the vertical `gap` (i.e., spacing) between elements
99102
provided to `*args`.
@@ -109,6 +112,8 @@ def sidebar(
109112
and right, and the third will be bottom.
110113
* If four, then the values will be interpreted as top, right, bottom, and left
111114
respectively.
115+
**kwargs
116+
Named attributes are supplied to the sidebar content container.
112117
"""
113118
return RecallContextManager(
114119
ui.sidebar,
@@ -124,6 +129,7 @@ def sidebar(
124129
max_height_mobile=max_height_mobile,
125130
gap=gap,
126131
padding=padding,
132+
**kwargs,
127133
),
128134
)
129135

shiny/templates/package-templates/js-output/custom_component/custom_component.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ def __init__(self, _fn: Optional[ValueFn[int]] = None, *, height: str = "200px")
3737
self.height: str = height
3838

3939
# Transforms non-`None` values into a `Jsonifiable` object.
40-
# If you'd like more control on when and how the value is resolved,
41-
# please use the `async def resolve(self)` method.
40+
# If you'd like more control on when and how the value is rendered,
41+
# please use the `async def render(self)` method.
4242
async def transform(self, value: int) -> Jsonifiable:
4343
# Send the results to the client. Make sure that this is a serializable
4444
# object and matches what is expected in the javascript code.

shiny/templates/package-templates/js-react/custom_component/custom_component.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ def auto_output_ui(self) -> Tag:
4949
# self.extra_arg: str = extra_arg
5050

5151
# Transforms non-`None` values into a `Jsonifiable` object.
52-
# If you'd like more control on when and how the value is resolved,
53-
# please use the `async def resolve(self)` method.
52+
# If you'd like more control on when and how the value is rendered,
53+
# please use the `async def render(self)` method.
5454
async def transform(self, value: str) -> Jsonifiable:
5555
# Send the results to the client. Make sure that this is a serializable
5656
# object and matches what is expected in the javascript code.

0 commit comments

Comments
 (0)