From 9912eff50e35230b4760d50a619f1e8843ed8a8e Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 5 Apr 2023 17:48:48 -0500 Subject: [PATCH 1/8] Update pkgdown articles to better describe/reflect new fillable/fill concepts --- .gitignore | 10 +- DESCRIPTION | 2 + _pkgdown.yml | 40 +- pkgdown/extra.css | 41 ++ vignettes/cards.Rmd | 307 ++++++---- vignettes/fillable-fill.Rmd | 365 +++++++++++ vignettes/filling.Rmd | 314 ++++++++++ vignettes/layouts.Rmd | 21 +- vignettes/sidebar-create-contents.R | 25 +- vignettes/sidebars.Rmd | 904 +++++++++++++++++++++------- 10 files changed, 1641 insertions(+), 388 deletions(-) create mode 100644 vignettes/fillable-fill.Rmd create mode 100644 vignettes/filling.Rmd diff --git a/.gitignore b/.gitignore index d323a105e..adcb040ce 100644 --- a/.gitignore +++ b/.gitignore @@ -19,10 +19,6 @@ inst/yarn.lock inst/rmarkdown/templates/*/skeleton/skeleton.html -vignettes/navbar-global -vignettes/navbar-global-fillable -vignettes/navbar-local-scroll -vignettes/navbar-local-fill -vignettes/page-scroll -vignettes/page-fill -vignettes/page-fill-double +vignettes/*.html +# rendered examples that appear in iframes +vignettes/examples diff --git a/DESCRIPTION b/DESCRIPTION index ee13faf41..fc41e0426 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -90,9 +90,11 @@ Config/Needs/website: crosstalk, dplyr, DT, + ggplot2, glue, htmlwidgets, leaflet, + lorem, plotly, purrr, rprojroot, diff --git a/_pkgdown.yml b/_pkgdown.yml index 6ef775561..c7cab75d7 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -10,7 +10,7 @@ template: bslib: info: "#E6F2FD" base_font: - google: "Atkinson Hyperlegible" + google: {family: "Atkinson Hyperlegible", wght: [400, 700], ital: [0, 1]} headings-color: "#206B72" link-color: "#216B73" link-decoration: underline var(--bs-info) dotted @@ -24,29 +24,36 @@ navbar: bg: info type: light structure: - left: [intro, articles, integration] + left: [intro, theming, components, layouts] right: [reference, news, github] components: home: ~ - articles: - text: Articles + theming: + text: Theming + menu: + - text: Theming variables + href: articles/bs5-variables.html + - text: Utility Classes + href: articles/utility-classes.html + - text: Custom components + href: articles/custom-components.html + components: + text: Components menu: - #- text: Why Sass over CSS? - # href: articles/why-sass.html - text: Cards href: articles/cards.html - text: Value Boxes href: articles/value-boxes.html - text: Sidebars href: articles/sidebars.html - - text: Layouts + layouts: + text: Layouts + menu: + - text: Filling layouts + href: articles/filling.html + - text: Column-first layout href: articles/layouts.html - - text: Theming variables - href: articles/bs5-variables.html - - text: Utility Classes - href: articles/utility-classes.html - - text: Custom components - href: articles/custom-components.html + reference: text: Reference href: reference/index.html @@ -131,3 +138,10 @@ reference: - theme_bootswatch - bootswatch_themes - version_default + + + redirects: + - ["articles/cards.html#fixed-sizing", "articles/cards.html#scrolling-contents"] + - ["articles/cards.html#responsive-sizing", "articles/cards.html#filling-contents"] + - ["articles/cards.html#fixed-responsive-sizing", "articles/cards.html#scrolling-filling"] + - ["articles/cards.html#dynamic-rendering-shiny", "articles/cards.html#shiny"] diff --git a/pkgdown/extra.css b/pkgdown/extra.css index c0652cb61..baf103f62 100644 --- a/pkgdown/extra.css +++ b/pkgdown/extra.css @@ -23,3 +23,44 @@ a:focus, a:hover { table.dataTable.table-bordered { border-collapse: collapse !important; } + +main h2 { + margin-top: 1lh; +} + +/* ---- Callouts ---- */ +.callout { + position: relative; + border: 1px solid gray; + padding: 1rem; + margin: 1rem 0; + border-radius: var(--bs-border-radius); +} + +.callout > :last-child { + margin-bottom: 0; +} + +.callout.callout-warning { + border-color: gold +} + +.callout.callout-warning > h3 { + position: relative; + color: #473D00; + background-color: hsl(51 100% 94% / 1); + margin: -1rem; + margin-bottom: 10px; + padding: 1rem; + font-size: 1.5rem; +} + +.callout.callout-warning > h3 > .anchor { + display: none; +} + +.callout.callout-warning > h3:before { + /* https://icons.getbootstrap.com/icons/exclamation-diamond-fill/ */ + content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1.25rem' height='1.25rem' fill='%23473D00' class='bi bi-exclamation-diamond-fill' viewBox='0 0 16 16'%3E%3Cpath d='M9.05.435c-.58-.58-1.52-.58-2.1 0L.436 6.95c-.58.58-.58 1.519 0 2.098l6.516 6.516c.58.58 1.519.58 2.098 0l6.516-6.516c.58-.58.58-1.519 0-2.098L9.05.435zM8 4c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995A.905.905 0 0 1 8 4zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z'/%3E%3C/svg%3E"); + margin-right: 0.5em +} diff --git a/vignettes/cards.Rmd b/vignettes/cards.Rmd index a39bf89e4..1c40824e3 100644 --- a/vignettes/cards.Rmd +++ b/vignettes/cards.Rmd @@ -6,19 +6,12 @@ resource_files: Cards are a common organizing unit for modern user interfaces (UI). At their core, they're just rectangular containers with borders and padding. However, when utilized properly to group related information, they help users better digest, engage, and navigate through content. This is why most successful dashboard/UI frameworks make cards a core feature of their component library. This article provides an overview of the API that bslib provides to create [Bootstrap cards](https://getbootstrap.com/docs/5.0/components/card/). -One major feature that bslib adds to Bootstrap cards is the ability to expand the card to a [full screen view](#responsive-sizing). Often this feature wants [output that resizes itself to fit its card container](#responsive-sizing). To do this as advertised, make sure you have the latest version of shiny and htmlwidgets: +## Setup code -```r -install.packages("shiny") -install.packages("htmlwidgets") -``` - -Since this article is statically hosted (i.e., not powered by Shiny), it uses statically rendered [htmlwidgets](http://www.htmlwidgets.org/) like `{plotly}` and `{leaflet}` (but don't worry, `card()`s [work in Shiny equally as well](#dynamic-rendering-shiny)). Here's some code to create those widgets: +Since this article is statically hosted and not powered by Shiny, it uses statically rendered [htmlwidgets](http://www.htmlwidgets.org/) like `{plotly}` and `{leaflet}`. Don't worry, `card()`s [work in Shiny equally as well](#shiny). Here's some code to create those widgets: ```{r setup, include=FALSE} -knitr::opts_chunk$set(echo = TRUE, warning = FALSE, message = FALSE) - -lorem_ipsum_dolor_sit_amet <- "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Id nibh tortor id aliquet lectus proin nibh nisl. Adipiscing at in tellus integer feugiat. Arcu bibendum at varius vel pharetra vel turpis nunc eget. Cursus sit amet dictum sit amet justo. Sit amet consectetur adipiscing elit. Vestibulum mattis ullamcorper velit sed ullamcorper. Enim facilisis gravida neque convallis a. Elit duis tristique sollicitudin nibh sit amet. Magna eget est lorem ipsum. Gravida dictum fusce ut placerat orci nulla pellentesque dignissim. Mauris in aliquam sem fringilla ut morbi. Id semper risus in hendrerit gravida rutrum quisque non tellus. At erat pellentesque adipiscing commodo elit at imperdiet dui. Fames ac turpis egestas maecenas pharetra convallis posuere morbi. Duis convallis convallis tellus id interdum velit laoreet id. Aliquet lectus proin nibh nisl. Nunc vel risus commodo viverra maecenas accumsan lacus vel facilisis. Bibendum enim facilisis gravida neque convallis a." +knitr::opts_chunk$set(echo = TRUE, warning = FALSE, message = FALSE, collapse = "#>") # pkgdown really wants BS5+ markup for tabs, and this is currently the best way to achieving that :( # (note this isn't a problem for any format based on html_document_base) @@ -77,7 +70,7 @@ leaflet_widget <- leaflet() %>% ::: row ::: col-md-6 -A `card()` is designed to handle any number of "known" card items (e.g., `card_header()`, `card_body()`, etc) as unnamed arguments (i.e., children). As we'll see shotly, `card()` also has some useful named arguments (e.g., `full_screen`, `height`, etc). +A `card()` is designed to handle any number of "known" card items (e.g., `card_header()`, `card_body()`, etc) as unnamed arguments (i.e., children). As we'll see shortly, `card()` also has some useful named arguments (e.g., `full_screen`, `height`, etc). At their core, `card()` and card items are just an HTML `div()` with a special Bootstrap class, so you can use Bootstrap's utility classes to customize things like [colors](https://getbootstrap.com/docs/5.2/utilities/background/), [text](https://getbootstrap.com/docs/5.2/utilities/text/), [borders](https://getbootstrap.com/docs/5.2/utilities/borders), etc. ::: @@ -113,150 +106,187 @@ card( class = "bg-dark", "A header" ), - markdown("Some text with a [link](https://github.com)") + markdown("Some text with a [link](https://github.com).") ) ``` ::: ::: -## Fixed sizing +## Restricting growth ::: row ::: col-md-6 -By default, a `card()`'s size grows to accommodate the size of it's contents. Thus, if some portion of the `card_body()` contains a large amount of text, table(s), etc., consider setting a fixed `height`. And in that case, if the contents exceed the specified height, they'll be scrollable. +By default, a `card()`'s size grows to accommodate the size of it's contents. Thus, if a `card_body()` contains a large amount of text, tables, etc., you may want to specify a `height` or `max_height`. That said, when laying out [multiple cards](#multiple-cards), you probably shouldn't constrain the height on the card-level. Instead, the height should probably come from a containing element, like `layout_column_wrap()`. + +Although scrolling is convenient for reducing the amount of space required to park lots of content, it can also be a nuisance to the user. To help reduce the need for scrolling, consider pairing scrolling with `full_screen = TRUE` (which adds an icon to expand the card's size to the browser window). Notice how, when the card is expanded to full-screen, `max_height`/`height` won't effect the full-screen size of the card. ::: ::: col-md-6 ```{r} card( + max_height = 250, + full_screen = TRUE, card_header( "A long, scrolling, description" ), - card_body( - height = 150, - lorem_ipsum_dolor_sit_amet + lorem::ipsum(paragraphs = 3, sentences = 5), + card_footer( + class = "fs-6", + "Copyright 2023 RStudio, PBC" ) ) ``` ::: ::: + +## Filling outputs + ::: row ::: col-md-6 -Alternatively, you can also set the `height` of the card to a fixed size and set `fill = TRUE` to have the `card_body()` container shrink/grow to fit the available space in a `card()`. Note that, by doing this, the _children_ of the `card_body()` aren't necessarily allowed to shrink/grow to fit the `card_body()`, which `card_body_fill()` (aka "[responsize sizing](#responsize-sizing)") is designed to do. +A `card()`'s default behavior is optimized for facilitating [filling layouts](filling.html). For example, if **fill item**(s), like `plotly_widget`, appear as _direct children_ of the card (or a `card_body()`), they grow/shrink in size to fit their `card_body()` container. + ::: ::: col-md-6 ```{r} card( - height = 200, - card_header( - "A long, scrolling, description" - ), - card_body( - fill = TRUE, - lorem_ipsum_dolor_sit_amet - ) + max_height = 250, + full_screen = TRUE, + card_header("A filling plot"), + plotly_widget ) ``` ::: ::: +::: row +::: col-md-6 +Although most outputs (e.g., `shiny::plotOutput()`, etc.) are **fill items**, most other UI elements (e.g., `shiny::div()`) are not, so wrapping an output container in additional `shiny::div()` (which something like `shiny::uiOutput()` does implicitly) breaks filling behavior. In this case, you can leverage `as_fill_carrier()` to "carry" the potential to fill from the `card()` down to the relevant contents. +::: -## Responsive sizing - -
-
+::: col-md-6 +```{r} +is_fill(plotly_widget) +is_fill(uiOutput("some_output")) -Unlike `card_body()`, `card_body_fill()` encourages its children to grow and shrink vertically as needed in response to its `card()`'s height. Responsive sizing is particularly useful for `card(full_screen = TRUE, ...)`, which adds an icon (displayed on hover) to expand the `card()` to a full screen view. +card( + max_height = 250, + full_screen = TRUE, + card_header("A filling plot"), + uiOutput("some_output") |> + as_fill_carrier() +) +``` +::: +::: -Since many htmlwidgets (like `plotly::plot_ly()`) and Shiny output bindings (like `shiny::plotOutput()`) default to a fixed height of 400 pixels, but are actually capable of responsive sizing, you'll get a better result with `card_body_fill()` instead of `card_body()` in these cases (compare the "Responsive" with the "Fixed" result using the tabs to the right). -
+::: row +::: col-md-6 +Beware that, by default, there are no limits to how large or small **fill item**(s) may become, so -
+::: - -
-
+::: col-md-6 ```{r} card( - height = 250, full_screen = TRUE, - card_header("Responsive sizing"), - card_body_fill(plotly_widget), - card_footer( - class = "fs-6", - "Copyright 2022 RStudio, PBC" - ) + max_height = 300, + full_screen = TRUE, + card_header("A filling plot"), + plotly_widget, + plotly_widget ) ``` -
-
+::: +::: + +::: {.callout .callout-warning} +

What's a fill item?

+ + See the [article on filling](filling.html) to learn more. +::: + + +## Multiple `card_body()` + +::: {.row .mt-3} +::: col-md-6 +A `card()` can have multiple `card_body()`s, which is especially useful for putting different grow/shrink limits on different sections of the body. For example, if we had lots of paragraphs + +1. For combining both filling and scrolling contents (as shown on the example). +2. For having multiple rows of filling outputs, each with their own restrictions on the min/max height, etc. + +Note that, when combining filling (i.e., ``) +::: + +::: col-md-6 ```{r} card( - height = 250, full_screen = TRUE, - card_header("Fixed sizing"), - plotly_widget, - card_footer( - class = "fs-6", - "Copyright 2022 RStudio, PBC" - ) + max_height = 300, + full_screen = TRUE, + card_header( + "A filling plot and scrolling description" + ), + card_body( + min_height = 150, + plotly_widget + ), + lorem::ipsum(paragraphs = 10, sentences = 5) ) ``` -
-
-
-
+::: +::: + + +## Multiple columns ::: row ::: col-md-6 -Under-the-hood, `card_body_fill()` achieves its behavior because it is a [flex container](https://css-tricks.com/snippets/css/a-guide-to-flexbox/), which makes its direct children flex items. This can lead to suprising, yet useful, differences in behavior from `card_body()`. For example, each inline element (like text, `actionLink()`, `actionButton()`, etc) is placed in a new row and stretches horizontally (as shown in the example). In the case where you want particular elements inside of `card_body_fill()` to behave as though they're in `card_body()` (i.e., have the `actionLink()` and `actionButton()` appear inline on the same line), just wrap those elements in a `div()`. +Leverage `layout_column_wrap()` for multiple columns that still support [filling outputs](#filling-outputs) within a card (which can also be used to layout [multiple cards](#multiple-cards)), especially if the height of those columns should grow/shrink as needed. ::: ::: col-md-6 ```{r} card( - height = 250, full_screen = TRUE, - card_header("A plot with an action links"), - card_body_fill( - plotly_widget, - actionLink( - "go", "Action link", - class = "link-primary align-self-center" - ), - actionButton( - "go_btn", "Action button", - class = "btn-primary rounded-0" + max_height = 300, + full_screen = TRUE, + card_header("A multi-column filling layout"), + card_body( + min_height = 150, + layout_column_wrap( + width = 1/2, + plotOutput("p1"), + plotOutput("p2") ) - ) + ), + lorem::ipsum(paragraphs = 3, sentences = 5) ) ``` ::: ::: +## Sidebars ::: row ::: col-md-6 -Sometimes it's useful to put a limit on how much the contents of `card_body_fill()` may grow or shrink. For example, here's a case where the plot won't expand over 400 pixels (try expanding to full screen). + ::: ::: col-md-6 ```{r} card( - height = 200, full_screen = TRUE, - card_header("Try expanding full screen"), - card_body_fill( - plotly_widget, - max_height = "400px" + max_height = 300, + full_screen = TRUE, + card_header("A card-based sidebar layout"), + layout_sidebar( + fillable = TRUE, + sidebar( + width = 100, + actionButton("btn", "A button") + ), + plotly_widget ) ) ``` @@ -264,33 +294,66 @@ card( ::: -## Fixed & responsive sizing -::: row +## Flexbox + +::: {.row .mt-3} ::: col-md-6 -Sometimes it's desirable to combine both `card_body_fill()` with `card_body()` to allow some portion of the body to grow/shrink as needed, but also keep another portion at a fixed/defined height. +`card_body()` is a CSS [flexbox container](https://css-tricks.com/snippets/css/a-guide-to-flexbox/) by default, which is a great for enabling [filling output](#filling-output), but does lead to surprising behavior with things like inline tags (e.g., `shiny::actionButton()`, `shiny::span()`, etc). In this case, consider setting `fillable = FALSE` to get the typical layout experience. Also, for textual content, consider using `shiny::markdown()` (or `shiny::includeMarkdown()`), which effectively yields the same result regardless of `fillable`[^markdown]. ::: +[^markdown]: This is essentially because each paragraph gets wrapped in a `

` tag, which means the children of the `

` tag won't be treated as flex items. + ::: col-md-6 ```{r} card( - height = 300, full_screen = TRUE, - card_header("Plot with long description"), - card_body_fill(plotly_widget), + "Here's some", tags$i("inline"), "text", + actionButton("btn1", "A button"), + card_body(fillable = FALSE, + "Here's some", tags$i("inline"), "text" + ), + markdown("Here's some _inline_ text"), + card_body(fillable = FALSE, + actionButton("btn2", "A button") + ) +) +``` +::: +::: + + +::: {.row .mt-3} +::: col-md-6 +Keep in mind though, sometimes the `fillable = TRUE` behavior of stretching each inline element horizontally can be pretty useful. Say, for example, making a button full-width below some filling output. Also, if you're willing to spend some time learning [flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/), it makes some spacing and alignment issues much easier to address. Say, for example, centering an `actionLink()` (done here with Bootstrap's [flex utility classes](https://getbootstrap.com/docs/5.3/utilities/flex/)): +::: + +::: col-md-6 +```{r} +card( + height = 250, full_screen = TRUE, + card_header("A plot with an action links"), card_body( - height = "30%", - lorem_ipsum_dolor_sit_amet + plotly_widget, + actionButton( + "go_btn", "Action button", + class = "btn-primary rounded-0" + ), + actionLink( + "go", "Action link", + class = "link-primary align-self-center py-1" + ) ) ) ``` ::: ::: + ## Spacing & alignment ::: row ::: col-md-6 -Both `card_body()` and `card_body_fill()` include padding between their contents and the `card()` container by default. In either case, you can override those defaults with Bootstrap's [spacing utility classes](https://getbootstrap.com/docs/5.2/utilities/spacing/), like `"p-0"` to remove the padding altogether. This is especially useful if +Both `card_body()` and `card_body_fillable()` include padding between their contents and the `card()` container by default. In either case, you can override those defaults with Bootstrap's [spacing utility classes](https://getbootstrap.com/docs/5.2/utilities/spacing/), like applying `"p-0"` to remove the padding altogether. This is especially useful if 1. The content itself already provides sufficient padding. 2. The content's background color is different from the card.[^2] @@ -302,8 +365,8 @@ Both `card_body()` and `card_body_fill()` include padding between their contents ```{r} card( height = 250, full_screen = TRUE, - card_header("A stretchy plot with no padding"), - card_body_fill( + card_header("A filling plot (with no padding)"), + card_body( class = "p-0", plotOutput("id") ) @@ -316,7 +379,7 @@ card( ::: row ::: col-md-6 -Utility classes are really useful since they not only also help with spacing and alignment of stuff _within_ a `card_body()` (or `card_body_fill()`), but more generally enable easy customization of colors, fonts, and more. +Not only are utility classes useful to help with spacing and alignment of stuff _within_ a `card_body()` (or `card_body_fillable()`), but they also enable easy customization of [colors](https://getbootstrap.com/docs/5.3/utilities/colors/), [fonts](https://getbootstrap.com/docs/5.3/utilities/text/), and [more](https://getbootstrap.com/docs/5.3/utilities/). ::: ::: col-md-6 @@ -337,13 +400,13 @@ card( ::: row ::: col-md-6 -In the case of `card_body_fill()`, since it's based on [CSS flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/), you can add uniform spacing between children via the `gap` argument. Note there is a similar way to space between [multiple columns](#multiple-columns). +In the case of `card_body_fillable()`, since it's based on [CSS flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/), you can add uniform spacing between children via the `gap` argument. Note there is a similar way to space between [multiple columns](#multiple-columns). ::: ::: col-md-6 ```{r} card( - card_body_fill( + card_body( gap = "1rem", class = "p-3", div(class = "bg-secondary", "Thing 1"), div(class = "bg-secondary", "Thing 2"), @@ -356,14 +419,14 @@ card( ::: row ::: col-md-6 -Again, thanks to CSS flexbox, if the contents of a `card_body_fill()` aren't full width, you can pretty easily horizontally center them via [flex utility classes](https://getbootstrap.com/docs/5.2/utilities/flex/) (note that you could handle similar alignment issues with `card_body()` by making it a flexbox container with `card_body(class = "d-flex")`. +Again, thanks to CSS flexbox, if the contents of a `card_body_fillable()` aren't full width, you can pretty easily horizontally center them via [flex utility classes](https://getbootstrap.com/docs/5.2/utilities/flex/) (note that you could handle similar alignment issues with `card_body()` by making it a flexbox container with `card_body(class = "d-flex")`. ::: ::: col-md-6 ```{r} card( height = 150, full_screen = TRUE, - card_body_fill( + card_body( class = "p-3 align-items-center", plotOutput("id", width = "50%") ) @@ -372,7 +435,7 @@ card( ::: ::: -## Dynamic rendering (Shiny) +## Shiny ::: row ::: col-md-6 @@ -386,10 +449,10 @@ One neat thing about dynamic rendering is that you can leverage `shiny::getCurre # UI logic ui <- page_fluid( card( - height = 200, + max_height = 200, full_screen = TRUE, card_header("A dynamically rendered plot"), - card_body_fill(plotOutput("plot_id")) + plotOutput("plot_id") ) ) @@ -440,7 +503,7 @@ card( ::: row ::: col-md-6 -`navs_tab_card()` (as well as `navs_pill_card()`) makes it easy to create cards with multiple tabs (or pills). These functions have the same `full_screen` capabilities as normal `card()`s as well some other options like `title` (since there is no natural place for a `card_header()` to be used). Note that, similar to `card()`, the children of each `nav()` panel will be implicitly wrapped in a `card_body()` call, so use `card_body_fill()` where appropriate to get [responsive sizing](#responsive-sizing). +`navs_tab_card()` and `navs_pill_card()` make it easy to create cards with multiple tabs or pills. These functions have the same `full_screen` capabilities as normal `card()`s as well some other options like `title` (since there is no natural place for a `card_header()` to be used). Note that, similar to `card()`, the children of each `nav()` panel will be implicitly wrapped in a `card_body()` call, and so, the children of each `nav()` are rendered as [flex items](#flexbox). To avoid this, wrap those children in an explicit `card_body(fillable = FALSE, ...)`, as done here: ::: @@ -453,17 +516,20 @@ navs_tab_card( nav( "Plotly", card_title("A plotly plot", class = "pt-1"), - card_body_fill(plotly_widget) + plotly_widget ), nav( "Leaflet", card_title("A leaflet plot", class = "pt-1"), - card_body_fill(leaflet_widget) + leaflet_widget ), nav( shiny::icon("circle-info"), - "Learn more about", - tags$a("htmlwidgets", href = "http://www.htmlwidgets.org/") + card_body( + fillable = FALSE, + "Learn more about", + tags$a("htmlwidgets", href = "http://www.htmlwidgets.org/") + ) ) ) ``` @@ -471,30 +537,7 @@ navs_tab_card( ::: -## Multiple columns -::: row -::: col-md-6 -To create multiple columns within a card, it's recommended to use `layout_column_wrap()` (which can also be used to layout [multiple cards](#multiple-cards)), especially if the height of those columns should grow/shrink as needed. -::: - -::: col-md-6 -```{r} -card( - height = 300, full_screen = TRUE, - layout_column_wrap( - width = 1/2, class = "p-3", - plotOutput("p1"), - plotOutput("p2") - ), - card_body( - height = "30%", class = "pt-0", - lorem_ipsum_dolor_sit_amet - ) -) -``` -::: -::: ## Multiple cards @@ -506,7 +549,7 @@ See the article on layout, specifically the [section on `layout_column_wrap()`]( The following CSS is used to give `plotOutput()` a background color; it's necessary here because this documentation page is not actually hooked up to a Shiny app, so we can't show a real plot. ```{css} -.shiny-plot-output { +.shiny-plot-output, .shiny-html-output { background-color: #216B7288; height: 400px; width: 100%; diff --git a/vignettes/fillable-fill.Rmd b/vignettes/fillable-fill.Rmd new file mode 100644 index 000000000..5fb53f702 --- /dev/null +++ b/vignettes/fillable-fill.Rmd @@ -0,0 +1,365 @@ +--- +title: "Introduction to Fillabilty" +--- + +```{r setup, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + echo = FALSE, + comment = "#>" +) + +library(bslib) +library(htmltools) +``` + +```{r setup-functions} +fillable_container <- function(..., tooltip = NULL, resizable = FALSE, padding = TRUE) { + gap <- "0.25rem" + card( + "data-bs-toggle" = if (!is.null(tooltip)) "tooltip", + "data-bs-title" = paste(tooltip, collapse = "
"), + "data-bs-html" = "true", + "data-bs-placement" = "top", + "data-bs-custom-class" = "tip-pink", + style = css( + "--bs-card-border-color" = "var(--bs-pink)", + "--bs-card-border-width" = "0.12rem", + gap = if (padding) gap, + resize = if (resizable) "vertical" + ), + ..., + wrapper = function(...) { + card_body_fillable( + class = if (padding) "p-1" else "p-0", + gap = if (padding) gap, + ... + ) + } + ) +} + + +child <- function(..., fill = TRUE, fillable = FALSE, tooltip = NULL, height = 400) { + both <- fill && fillable + color <- if (both) "purple" else if (fill) "blue" else "orange" + div( + class = "border border-1 border-dark rounded text-center", + class = paste0("bg-fill-", color), + "data-bs-toggle" = if (!is.null(tooltip)) "tooltip", + "data-bs-title" = paste(tooltip, collapse = "
"), + "data-bs-html" = "true", + #"data-bs-placement" = if (both || neither) "left" else "right", + "data-bs-placement" = "right", + "data-bs-custom-class" = paste0("tip-", color), + style = css( + color = "white", + "--bs-tooltip-bg" = sprintf("var(--bs-%s)", color), + height = validateCssUnit(height), + flex_shrink = if (!fill) 0, + padding = "0.2rem" + ), + ... + ) |> + bindFillRole(item = fill, container = fillable) +} +``` + +```{scss, echo = FALSE} +.tip-pink { + --bs-tooltip-bg: var(--bs-pink); +} +.tip-blue { + --bs-tooltip-bg: var(--bs-blue); +} +.tip-orange { + --bs-tooltip-bg: var(--bs-orange); +} +.tip-purple { + --bs-tooltip-bg: var(--bs-purple); +} + +.b-pink { + font-weight: bold; + color: var(--bs-pink); +} +.b-orange { + font-weight: bold; + color: var(--bs-orange); +} +.b-blue { + font-weight: bold; + color: hsl(202deg, 100%, 38%); +} +.b-purple { + font-weight: bold; + color: var(--bs-purple); +} + +.bg-fill-blue { + background-image: linear-gradient( + 180deg, + hsl(202deg, 100%, 38%) 0%, + hsl(206deg, 58%, 55%) 25%, + hsl(206deg, 58%, 66%) 50%, + hsl(205deg, 61%, 77%) 75%, + hsl(203deg, 69%, 87%) 100% + ); +} + +.bg-fill-orange { + background-image: linear-gradient( + 180deg, + hsl(27deg, 98%, 54%) 0%, + hsl(27deg, 97%, 64%) 25%, + hsl(27deg, 93%, 73%) 50%, + hsl(28deg, 79%, 82%) 75%, + hsl(28deg, 27%, 90%) 100% + ); +} + +.bg-fill-purple { + background: var(--bs-purple); +} + +``` + +## Fundamentals + +This section covers the building blocks of filling layouts: **fillable container**s and **fill item**s. Keep in mind, most bslib components, as well as many Shiny outputs (e.g., `shiny::plotOutput()`, `plotly::plotlyOutput()`, etc) classify as **fill item**s by default. As we'll learn, that means these UI elements have the potential to grow/shrink to fit their container, but that potential is only activated when their _immediate parent_ is a [fillable container]{.b-pink} _with a defined height_. + +### Activating fill + +::: row +::: col-md-6 +Just like any other HTML **container**, a [fillable container]{.b-pink}'s default height depends on the height of it's children. So, for example, if there's a single [fill item]{.b-blue} with a defined height of `400px` (the default for most Shiny outputs), the [fillable container]{.b-pink}'s height is also `400px` (plus any padding, border, etc). +::: + +::: col-md-6 +```{r} +fillable_container( + tooltip = c("Fillable container", "Computed height: 400px"), + child( + tooltip = c("Fill child", "Defined height: 400px") + ) +) +``` +::: +::: + + +::: {.row .mt-3} +::: col-md-6 +Defining the height of a [fillable container]{.b-pink} activates its immediate children's potential to [fill]{.b-blue}. So, for example, if [fillable container]{.b-pink}'s height is set to `200px`, the [fill child]{.b-blue} would shrink to about `200px`: +::: + +::: col-md-6 +```{r} +fillable_container( + tooltip = c("Fillable", "Defined height: 200px"), + child( + tooltip = c("Fill", "Defined height: 400px", "Computed height: 200px") + ), + height = 200 +) +``` +::: +::: + + +::: {.row .mt-3} +::: col-md-6 +If multiple [fill items]{.b-blue} were immediate children of this [fillable container]{.b-pink}, they'd keep shrinking (in this case, to about `100px` each): +::: + +::: col-md-6 +```{r} +fillable_container( + tooltip = c("Fillable", "Defined height: 200px"), + child( + tooltip = c("Fill", "Defined height: 400px", "Computed height: 100px") + ), + child( + tooltip = c("Fill", "Defined height: 400px", "Computed height: 100px") + ), + height = 200 +) +``` +::: +::: + + +::: {.row .mt-3} +::: col-md-6 +Adding a [non-fill]{.b-orange} item (e.g., `htmltools::p()`-aragraph of text) won't cause that particular item to grow/shrink, but the [fill]{.b-blue} items divvy up any remaining space (**careful:** if [non-fill]{.b-orange} item(s) are larger than the [fillable container]{.b-pink}, the [fill]{.b-blue} items won't be visible!). + +> TODO: explain options for setting min-height? +::: + +::: col-md-6 +```{r} +fillable_container( + tooltip = c("Fillable container"), + child(tooltip = "Fill"), + child(tooltip = "Fill"), + child( + fill = FALSE, height = 100, + tooltip = "Non-fill" + ), + height = 200, + resizable = TRUE +) +``` +::: +::: + + +::: {.callout .callout-warning} +

Resizable example

+ + +Notice the resizing handle on the lower-right hand corner of the [fillable container]{.b-pink} above. Use it to change the size of the [fillable container]{.b-pink} and compare the behavior between [fill]{.b-blue} and [non-fill]{.b-orange} items. +::: + + +### Carrying fill + +The previous section focuses on the fairly simple case of _one_ parent container. However, in practice, you'll likely be working with multiple levels of parents, which quickly complicates things, especially because: + +1. [Fill items]{.b-blue} require their _immediate_ parent to be a [fillable container]{.b-pink} in order to fill. +2. All "raw" HTML tags (e.g., `shiny::div()`, `shiny::p()`, etc.) as well as many Shiny UI elements (e.g., `shiny::wellPanel()`, etc.) are neither [fillable]{.b-pink} nor [fill]{.b-blue} (i.e., we'll call these [non-fill]{.b-orange} elements). + + +::: {.row .mt-3} +::: col-md-6 +As a result, a common way in which (1) breaks down is that a [non-fill]{.b-orange} element, like a `shiny::div()`, is a parent of the [fill]{.b-blue} and a child of the [fillable container]{.b-pink}. In fact, you'll run into this exact behavior when using `shiny::uiOutput()` to insert a dynamically rendered [fill]{.b-blue} item into a [fillable container]{.b-pink}. +::: + +::: col-md-6 +```{r} +fillable_container( + tooltip = "Fillable container", + child( + fill = FALSE, + tooltip = "Non-fill", + "data-bs-offset" = "-140,0", + child( + tooltip = "Fill", + "data-bs-offset" = "-100,0" + ) + ), + height = 200, + resizable = TRUE +) +``` +::: +::: + + +::: {.callout .callout-warning} +

Fill carriers

+ + +Fill carriers +::: + +::: {.row .mt-3} +::: col-md-6 +Assuming the goal is for the [fill item]{.b-blue} to fit the [fillable container]{.b-pink}, it's useful to coerce the [non-fill]{.b-orange} element into both [fill item]{.b-blue} _and_ a [fillable container]{.b-pink}, which we call a [fill carriers]{.b-purple}. Any UI element can be coerced into a [fill carrier]{.b-purple} with `as_fill_carrier()`. +::: + +::: col-md-6 +```{r} +fillable_container( + tooltip = "Fillable container", + child( + fillable = TRUE, + tooltip = "Fill carrier", + "data-bs-offset" = "-40,0", + child(tooltip = "Fill") + ), + height = 200, + resizable = TRUE +) +``` +::: +::: + + +::: {.row .mt-3} +::: col-md-6 +This concept of a [fill carrier]{.b-purple} is especially useful and relevant for [cards](cards.html). In most cases, a [card]{.b-pink} has numerous children like a [header]{.b-orange} and a [body]{.b-purple}, and the body commonly contains [fill item]{.b-blue}(s) (to ensure [fill item]{.b-blue}s ) +::: + +::: col-md-6 +```{r} +fillable_container( + tooltip = c("card() (fillable container)"), + child( + fill = FALSE, height = 50, + tooltip = "card_header()" + ), + child( + fillable = TRUE, + tooltip = "card_body_fillable()", + child( + tooltip = "Fill", + "data-bs-offset" = "-30,0" + ) + ), + height = 200, + resizable = TRUE +) +``` +::: +::: + +::: {.row .mt-3} +::: col-md-6 +This is why, when `card()` implicitly wraps non-card items, it wraps them in `card_body()`{.b-purple} and not `card_body_text()`{.b-blue}. You might wonder, why does `card_body_text()` exist at all? The main reason is that [fillable containers]{.b-pink} are powered by [CSS flexbox](#flexbox), which changes the way in which it's children are rendered. And, although those changes are great for "stretchy" children, there are downsides for providing textual content. +::: + +::: col-md-6 +```{r} +fillable_container( + tooltip = c("card() (fillable container)"), + child( + fill = FALSE, height = 50, + tooltip = "card_header() (not fill)" + ), + child( + fill = TRUE, + tooltip = "card_body() (fill)", + child( + tooltip = "Fill", + "data-bs-offset" = "-100,0" + ) + ), + height = 200, + resizable = TRUE +) +``` +::: +::: + +### Don't use `fluidRow()` + + + +### Fillable = Flexbox {#flexbox} + +In order to activate and carry fill, it's useful to + + + + +```{=html} + +``` diff --git a/vignettes/filling.Rmd b/vignettes/filling.Rmd new file mode 100644 index 000000000..e66ea2cc4 --- /dev/null +++ b/vignettes/filling.Rmd @@ -0,0 +1,314 @@ +--- +title: "Filling layouts" +resource_files: + - infobox.svg + - examples/filling +--- + +```{r setup, include=FALSE} +library(knitr) +library(htmltools) +library(bslib) + +# TODO: put this stuff in a common.R script? +render_as_iframe <- function(x, options, ...) { + lbl <- opts_current$get("label") + doc_name <- sub("[.]Rmd", "", current_input()) + lbl_dir <- file.path("examples", doc_name, lbl) + if (!dir.exists(lbl_dir)) { + dir.create(lbl_dir, recursive = TRUE) + } + file <- file.path(lbl_dir, paste0(lbl, ".html")) + x <- tagList(x, tags$head(tags$style(".modebar-container { display: none; }"))) + tryCatch( + save_html(x, file), + error = function(e) { + stop("Don't know how to render ", class(x)[[1]], " as an