Skip to content
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

feat(card_image): Improve card_image() API and usage #1076

Merged
merged 12 commits into from
Jul 15, 2024
113 changes: 95 additions & 18 deletions R/card.R
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ card <- function(

attribs <- args[nzchar(argnames)]
children <- as_card_items(args[!nzchar(argnames)], wrapper = wrapper)
children <- card_image_add_classes(children)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's investigate what this would mean for navset_card_*()

Copy link
Member Author

Choose a reason for hiding this comment

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

Turns out this works perfectly with navset_card_*().

Kapture.2024-07-15.at.10.30.52.mp4


is_shiny_input <- !is.null(id)

Expand Down Expand Up @@ -248,32 +249,78 @@ card_footer <- function(..., class = NULL) {
)
}

#' @describeIn card_body Include static (i.e., pre-generated) images.
#' @param file a file path pointing an image. The image will be base64 encoded
#' and provided to the `src` attribute of the `<img>`. Alternatively, you may
#' set this value to `NULL` and provide the `src` yourself.
#' @param href an optional URL to link to.
#' @param border_radius where to apply `border-radius` on the image.
#' @param mime_type the mime type of the `file`.
#' @param container a function to generate an HTML element to contain the image.
#' @param width Any valid [CSS unit][htmltools::validateCssUnit] (e.g., `width="100%"`).
#' @describeIn card_body Include static images in a card, for example as an
#' image cap at the top or bottom of the card.
#'
#' @param file A file path pointing an image. Local images (i.e. not a URI
#' starting with `https://` or similar) will be base64 encoded and provided to
#' the `src` attribute of the `<img>`. Alternatively, you may directly set
#' the image `src`, in which case `file` is ignored.
#' @param alt Alternate text for the image, used by screen readers and assistive
#' devices. Provide alt text with a description of the image for any images
#' with important content. If alt text is not provided, the image will be
#' considered to be decorative and will not be read or announced by screen
#' readers.
#'
#' For more information, the Web Accessibility Initiative (WAI) has a
#' [helpful tutorial on alt text](https://www.w3.org/WAI/tutorials/images/).
#' @param src The `src` attribute of the `<img>` tag. If provided, `file` is
#' ignored entirely. Use `src` to provide a relative path to a file that will
#' be served by the Shiny application and should not be base64 encoded.
#' @param href An optional URL to link to when a user clicks on the image.
#' @param border_radius Which side of the image should have rounded corners,
#' useful when `card_image()` is used as an image cap at the top or bottom of
#' the card.
#'
#' The value of `border_radius` determines whether the `card-img-top`
#' (`"top"`), `card-img-bottom` (`"bottom"`), or `card-img` (`"all"`)
#' [Bootstrap
#' classes](https://getbootstrap.com/docs/5.3/components/card/#images) are
#' applied to the card. The default `"auto"` value will use the image's
#' position within a `card()` to automatically choose the appropriate class.
#' @param mime_type The mime type of the `file` when it is base64 encoded. This
#' argument is available for advanced use cases where [mime::guess_type()] is
#' unable to automatically determine the file type.
#' @param container A function to generate an HTML element to contain the image.
#' Setting this value to `card_body()` places the image inside the card body
#' area, otherwise the image will extend to the edges of the card.
#' @param width Any valid [CSS unit][htmltools::validateCssUnit] (e.g.,
#' `width="100%"`).
#'
#' @export
card_image <- function(
file, ..., href = NULL, border_radius = c("top", "bottom", "all", "none"),
mime_type = NULL, class = NULL, height = NULL, fill = TRUE, width = NULL, container = card_body) {

src <- NULL
if (length(file) > 0) {
src <- base64enc::dataURI(
file = file, mime = mime_type %||% mime::guess_type(file)
)
file,
alt = "",
...,
gadenbuie marked this conversation as resolved.
Show resolved Hide resolved
src = NULL,
href = NULL,
border_radius = c("auto", "top", "bottom", "all", "none"),
mime_type = NULL,
class = NULL,
height = NULL,
fill = FALSE,
width = NULL,
container = NULL
) {
border_radius <- rlang::arg_match(border_radius)

if (is.null(src)) {
if (grepl("^([[:alnum:]]+:)?//|data:", file)) {
src <- file
} else {
src <- base64enc::dataURI(
file = file,
mime = mime_type %||% mime::guess_type(file)
)
}
}

image <- tags$img(
src = src,
alt = alt,
class = "img-fluid",
class = switch(
match.arg(border_radius),
border_radius,
all = "card-img",
top = "card-img-top",
bottom = "card-img-bottom",
Expand All @@ -295,11 +342,41 @@ card_image <- function(

if (is.function(container)) {
image <- container(image)
} else {
image <- as.card_item(image)
}

class(image) <- c(
if (border_radius == "auto") "card_image_auto",
"card_image",
class(image)
)

image
}

card_image_add_classes <- function(children) {
for (idx_child in seq_along(children)) {
if (inherits(children[[idx_child]], "card_image_auto")) {
card_img_class <-
if (length(children) == 1) {
"card-img"
} else if (idx_child == 1) {
"card-img-top"
} else if (idx_child == length(children)) {
"card-img-bottom"
}

children[[idx_child]] <- tagAppendAttributes(
children[[idx_child]],
class = card_img_class
)
}
}

children
}

#' @describeIn card_body Mark an object as a card item. This will prevent the
#' [card()] from putting the object inside a `wrapper` (i.e., a
#' `card_body()`).
Expand Down
Binary file modified R/sysdata.rda
Binary file not shown.
54 changes: 42 additions & 12 deletions man/card_body.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 47 additions & 0 deletions tests/testthat/_snaps/card.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# card_image()

Code
show_raw_html(card(card_image("https://example.com/image.jpg"), card_body(
"image cap on top of card")))
Output
<div class="card bslib-card bslib-mb-spacing html-fill-item html-fill-container" data-bslib-card-init data-require-bs-caller="card()" data-require-bs-version="5">
<img alt="" class="img-fluid card-img-top" src="https://example.com/image.jpg"/>
<div class="card-body bslib-gap-spacing html-fill-item html-fill-container" style="margin-top:auto;margin-bottom:auto;flex:1 1 auto;">image cap on top of card</div>
<script data-bslib-card-init>bslib.Card.initializeAllCards();</script>
</div>

---

Code
show_raw_html(card(card_body("image cap on bottom of card"), card_image(
"https://example.com/image.jpg")))
Output
<div class="card bslib-card bslib-mb-spacing html-fill-item html-fill-container" data-bslib-card-init data-require-bs-caller="card()" data-require-bs-version="5">
<div class="card-body bslib-gap-spacing html-fill-item html-fill-container" style="margin-top:auto;margin-bottom:auto;flex:1 1 auto;">image cap on bottom of card</div>
<img alt="" class="img-fluid card-img-bottom" src="https://example.com/image.jpg"/>
<script data-bslib-card-init>bslib.Card.initializeAllCards();</script>
</div>

---

Code
show_raw_html(card(card_header("header"), card_image(
"https://example.com/image.jpg"), card_body("image not a cap")))
Output
<div class="card bslib-card bslib-mb-spacing html-fill-item html-fill-container" data-bslib-card-init data-require-bs-caller="card()" data-require-bs-version="5">
<div class="card-header">header</div>
<img src="https://example.com/image.jpg" alt="" class="img-fluid"/>
<div class="card-body bslib-gap-spacing html-fill-item html-fill-container" style="margin-top:auto;margin-bottom:auto;flex:1 1 auto;">image not a cap</div>
<script data-bslib-card-init>bslib.Card.initializeAllCards();</script>
</div>

---

Code
show_raw_html(card(card_image("https://example.com/image.jpg", alt = "card-img")))
Output
<div class="card bslib-card bslib-mb-spacing html-fill-item html-fill-container" data-bslib-card-init data-require-bs-caller="card()" data-require-bs-version="5">
<img alt="card-img" class="img-fluid card-img" src="https://example.com/image.jpg"/>
<script data-bslib-card-init>bslib.Card.initializeAllCards();</script>
</div>

41 changes: 41 additions & 0 deletions tests/testthat/test-card.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
test_that("card_image()", {
show_raw_html <- function(x) {
cat(format(x))
}

expect_snapshot(
show_raw_html(
card(
card_image("https://example.com/image.jpg"),
card_body("image cap on top of card")
)
)
)

expect_snapshot(
show_raw_html(
card(
card_body("image cap on bottom of card"),
card_image("https://example.com/image.jpg")
)
)
)

expect_snapshot(
show_raw_html(
card(
card_header("header"),
card_image("https://example.com/image.jpg"),
card_body("image not a cap")
)
)
)

expect_snapshot(
show_raw_html(
card(
card_image("https://example.com/image.jpg", alt = "card-img")
)
)
)
})
Loading