diff --git a/CHANGELOG.md b/CHANGELOG.md index 822b0af31..fcf237af7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `playwright.controller.InputActionButton` gains a `expect_icon()` method. As a result, the already existing `expect_label()` no longer includes the icon. (#2020) +* `ui.sidebar()` gains a `fillable` argument to support vertical fill behavior in sidebars. (#2077) + ### Changes * `express.ui.insert_accordion_panel()`'s function signature has changed to be more ergonomic. Now you can pass the `panel_title` and `panel_contents` directly instead of `ui.hold()`ing the `ui.accordion_panel()` context manager. (#2042) diff --git a/shiny/express/ui/_cm_components.py b/shiny/express/ui/_cm_components.py index 651d61620..3f9f14dab 100644 --- a/shiny/express/ui/_cm_components.py +++ b/shiny/express/ui/_cm_components.py @@ -64,6 +64,7 @@ def sidebar( max_height_mobile: Optional[str | float] = None, gap: Optional[CssUnit] = None, padding: Optional[CssUnit | list[CssUnit]] = None, + fillable: bool = False, **kwargs: TagAttrValue, ) -> RecallContextManager[ui.Sidebar]: """ @@ -122,6 +123,10 @@ 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. + fillable + Whether or not the sidebar should be considered a fillable container. + When `True`, the sidebar and its content can use `fill` to consume + available vertical space. **kwargs Named attributes are supplied to the sidebar content container. """ @@ -139,6 +144,7 @@ def sidebar( max_height_mobile=max_height_mobile, gap=gap, padding=padding, + fillable=fillable, **kwargs, ), ) diff --git a/shiny/ui/_sidebar.py b/shiny/ui/_sidebar.py index a17d1957b..eb442434f 100644 --- a/shiny/ui/_sidebar.py +++ b/shiny/ui/_sidebar.py @@ -206,6 +206,10 @@ class directly. Instead, supply the :func:`~shiny.ui.sidebar` object to and right, and the third will be bottom. * If four, then the values will be interpreted as top, right, bottom, and left respectively. + fillable + Whether or not the sidebar should be considered a fillable container. + When `True`, the sidebar and its content can use `fill` to consume + available vertical space. Parameters ---------- @@ -283,6 +287,7 @@ def __init__( max_height_mobile: Optional[str | float] = None, gap: Optional[CssUnit] = None, padding: Optional[CssUnit | list[CssUnit]] = None, + fillable: bool = False, ): if isinstance(title, (str, int, float)): title = tags.header(str(title), class_="sidebar-title") @@ -292,6 +297,7 @@ def __init__( self.class_ = class_ self.gap = as_css_unit(gap) self.padding = as_css_padding(padding) + self.fillable = fillable # User-provided initial open state self._open: SidebarOpen | None = self._as_open(open) # Shiny or consumer-provided default open state, change with `_set_default_open()` @@ -403,7 +409,27 @@ def _sidebar_tag(self, id: str | None) -> Tag: self.open().desktop == "closed" or self.open().mobile == "closed" ) - return tags.aside( + # Create the sidebar content div + sidebar_content = div( + { + "class": "sidebar-content bslib-gap-spacing", + "style": css( + gap=self.gap, + padding=self.padding, + ), + }, + self.title, + *self.children, + self.attrs, + ) + + # Apply fill_item to the content div if fillable + if self.fillable: + sidebar_content = as_fill_item(sidebar_content) + sidebar_content = as_fillable_container(sidebar_content) + + # Create the sidebar tag + sidebar_tag = tags.aside( { "id": id, "class": "sidebar", @@ -411,21 +437,16 @@ def _sidebar_tag(self, id: str | None) -> Tag: }, # If the user provided an id, we make the sidebar an input to report state {"class": "bslib-sidebar-input"} if self.id is not None else None, - div( - { - "class": "sidebar-content bslib-gap-spacing", - "style": css( - gap=self.gap, - padding=self.padding, - ), - }, - self.title, - *self.children, - self.attrs, - ), + sidebar_content, class_=self.class_, ) + # Apply fillable container to the sidebar if needed + if self.fillable: + sidebar_tag = as_fillable_container(sidebar_tag) + + return sidebar_tag + def tagify(self) -> TagList: id = self._get_sidebar_id() taglist = TagList(self._sidebar_tag(id), self._collapse_tag(id)) @@ -446,6 +467,7 @@ def sidebar( max_height_mobile: Optional[str | float] = None, gap: Optional[CssUnit] = None, padding: Optional[CssUnit | list[CssUnit]] = None, + fillable: bool = False, **kwargs: TagAttrValue, ) -> Sidebar: # See [this article](https://rstudio.github.io/bslib/articles/sidebars.html) @@ -521,6 +543,10 @@ 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. + fillable + Whether or not the sidebar should be considered a fillable container. + When `True`, the sidebar and its content can use `fill` to consume + available vertical space. **kwargs Named attributes are supplied to the sidebar content container. @@ -567,6 +593,7 @@ def sidebar( max_height_mobile=max_height_mobile, gap=gap, padding=padding, + fillable=fillable, )