diff --git a/DESCRIPTION b/DESCRIPTION index 8aeaac8b..5bd7061b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -25,6 +25,7 @@ Depends: Imports: htmltools, jsonlite, + purrr, rlang, shiny, shiny.react (>= 0.3.0) @@ -37,7 +38,6 @@ Suggests: leaflet, mockery, plotly, - purrr, rcmdcheck, RColorBrewer, rmarkdown, diff --git a/NAMESPACE b/NAMESPACE index 26b79111..ebbf8890 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -19,6 +19,7 @@ export(ColorPicker.shinyInput) export(ComboBox) export(ComboBox.shinyInput) export(CommandBar) +export(CommandBar.shinyInput) export(CommandBarButton) export(CommandBarButton.shinyInput) export(CommandBarItem) diff --git a/R/extensions.R b/R/extensions.R index 9cd63db8..f4870dc4 100644 --- a/R/extensions.R +++ b/R/extensions.R @@ -1,3 +1,18 @@ +setInputValue <- function(inputId, value, event = TRUE) { + fmt <- if (event) { + "() => Shiny.setInputValue('%s', %s, { priority: 'event' })" + } else { + "() => Shiny.setInputValue('%s', %s)" + } + JS(sprintf(fmt, inputId, if (!is.numeric(value)) sprintf("'%s'", value) else value)) +} + +commandBarItem <- "CommandBarItem" + +isCommandBarItem <- function(x) { + isTRUE(attr(x, "componentName") == commandBarItem) +} + #' Command bar item #' #' Helper function for constructing items for `CommandBar`. @@ -10,18 +25,52 @@ #' @param text Text to be displayed on the menu. #' @param icon Optional name of an icon. #' @param subitems Optional list of CommandBar items. +#' @param onClick A JS function that runs on item click. By default it sends input value to `input[[key]]` #' @param ... Additional props to pass to CommandBarItem. #' @return Item suitable for use in the CommandBar. #' #' @seealso CommandBar #' @export -CommandBarItem <- function(text, icon = NULL, subitems = NULL, ...) { - props <- rlang::dots_list(...) +CommandBarItem <- function( + key, + text, + onClick = setInputValue(inputId = key, value = 0), + ... +) { + structure( + list( + key = key, + text = text, + onClick = onClick, + ... + ), + componentName = commandBarItem + ) +} - props$text <- text - if (is.character(icon)) props$iconProps <- list(iconName = icon) - if (!is.null(subitems)) props$subMenuProps <- list(items = subitems) - props +#' CommandBar.shinyInput +#' +#' CommandBar extension that sends values of clicked CommandBarItems +#' +#' @params inputId Input name +#' @return A CommandBar +#' +#' @seealso CommandBarItem +#' @rdname CommandBar +#' @export +CommandBar.shinyInput <- function( + inputId, + ..., + itemValueGetter = function(el) el$key +) { + attachOnClick <- function(el) { + el$onClick <- setInputValue(inputId, itemValueGetter(el)) + el + } + args <- list(...) + args$items <- recursiveModify(args$items, attachOnClick, isCommandBarItem) + args$farItems <- recursiveModify(args$farItems, attachOnClick, isCommandBarItem) + do.call(CommandBar, args) } #' Basic Fluent UI page diff --git a/R/utils.R b/R/utils.R new file mode 100644 index 00000000..847759d4 --- /dev/null +++ b/R/utils.R @@ -0,0 +1,12 @@ +recursiveModify <- function(x, modify = identity, when = function(x) TRUE) { + purrr::modify_tree( + x, + pre = function(el) { + if (!is.null(el) && when(el)) { + return(modify(el)) + } + el + }, + is_node = is.list + ) +} \ No newline at end of file diff --git a/inst/examples/CommandBar.R b/inst/examples/CommandBar.R index c0d7af9a..5df3198d 100644 --- a/inst/examples/CommandBar.R +++ b/inst/examples/CommandBar.R @@ -1,69 +1,91 @@ library(shiny) library(shiny.fluent) -items <- list( +items <- function(ns) { list( - key = "newItem", - text = "New", - cacheKey = "myCacheKey", - iconProps = list(iconName = "Add"), - subMenuProps = list( - items = list( - list( - key = "emailMessage", - text = "Email message", - iconProps = list(iconName = "Mail") - ), - list( - key = "calendarEvent", - text = "Calendar event", - iconProps = list(iconName = "Calendar") + CommandBarItem( + key = ns("newItem"), + text = "New", + cacheKey = "myCacheKey", + split = TRUE, + iconProps = list(iconName = "Add"), + subMenuProps = list( + items = list( + CommandBarItem( + key = ns("emailMessage"), + text = "Email message", + iconProps = list(iconName = "Mail") + ), + CommandBarItem( + key = ns("calendarEvent"), + text = "Calendar event", + iconProps = list(iconName = "Calendar") + ) ) ) + ), + CommandBarItem( + key = ns("upload"), + text = "Upload", + iconProps = list(iconName = "Upload") + ), + CommandBarItem( + key = ns("share"), + text = "Share", + iconProps = list(iconName = "Share") + ), + CommandBarItem( + key = ns("download"), + text = "Download", + iconProps = list(iconName = "Download") ) - ), - list( - key = "upload", - text = "Upload", - iconProps = list(iconName = "Upload") - ), - list( - key = "share", - text = "Share", - iconProps = list(iconName = "Share") - ), - list( - key = "download", - text = "Download", - iconProps = list(iconName = "Download") ) -) +} -farItems <- list( - list( - key = "tile", - text = "Grid view", - ariaLabel = "Grid view", - iconOnly = TRUE, - iconProps = list(iconName = "Tiles") - ), +farItems <- function(ns) { list( - key = "info", - text = "Info", - ariaLabel = "Info", - iconOnly = TRUE, - iconProps = list(iconName = "Info") + CommandBarItem( + key = ns("tile"), + text = "Grid view", + ariaLabel = "Grid view", + iconOnly = TRUE, + iconProps = list(iconName = "Tiles") + ), + CommandBarItem( + key = ns("info"), + text = "Info", + ariaLabel = "Info", + iconOnly = TRUE, + iconProps = list(iconName = "Info") + ) ) -) - +} ui <- function(id) { ns <- NS(id) - CommandBar(items = items, farItems = farItems) + tagList( + CommandBar( + items = items(ns), + farItems = farItems(ns) + ), + textOutput(ns("commandBarItems")), + CommandBar.shinyInput( + inputId = ns("commandBar"), + items = items(identity), + farItems = farItems(identity) + ), + textOutput(ns("commandBar")) + ) } server <- function(id) { - moduleServer(id, function(input, output, session) { }) + moduleServer(id, function(input, output, session) { + commandBarItemClicked <- reactiveVal() + observeEvent(input$newItem, commandBarItemClicked("newItem clicked")) + observeEvent(input$upload, commandBarItemClicked("upload clicked")) + output$commandBarItems <- renderText(commandBarItemClicked()) + output$commandBar <- renderText(input$commandBar) + }) } if (interactive()) { diff --git a/man/CommandBar.Rd b/man/CommandBar.Rd index 62c0406f..d73333d0 100644 --- a/man/CommandBar.Rd +++ b/man/CommandBar.Rd @@ -1,17 +1,21 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/components.R, R/documentation.R, R/examples.R +% Please edit documentation in R/components.R, R/documentation.R, R/examples.R, +% R/extensions.R \name{CommandBar} \alias{CommandBar} +\alias{CommandBar.shinyInput} \title{CommandBar} \usage{ CommandBar(...) + +CommandBar.shinyInput(inputId, ..., itemValueGetter = function(el) el$key) } \arguments{ \item{...}{Props to pass to the component. The allowed props are listed below in the \bold{Details} section.} } \value{ -Object with \code{shiny.tag} class suitable for use in the UI of a Shiny app. +A CommandBar } \description{ CommandBar is a surface that houses commands that operate on the content of @@ -40,6 +44,8 @@ commands should go into an overflow where text labels can be shown. For more details and examples visit the \href{https://developer.microsoft.com/en-us/fluentui#/controls/web/CommandBar}{official docs}. The R package cannot handle each and every case, so for advanced use cases you need to work using the original docs to achieve the desired result. + +CommandBar extension that sends values of clicked CommandBarItems } \details{ \itemize{ @@ -72,72 +78,97 @@ you need to work using the original docs to achieve the desired result. library(shiny) library(shiny.fluent) -items <- list( +items <- function(ns) { list( - key = "newItem", - text = "New", - cacheKey = "myCacheKey", - iconProps = list(iconName = "Add"), - subMenuProps = list( - items = list( - list( - key = "emailMessage", - text = "Email message", - iconProps = list(iconName = "Mail") - ), - list( - key = "calendarEvent", - text = "Calendar event", - iconProps = list(iconName = "Calendar") + CommandBarItem( + key = ns("newItem"), + text = "New", + cacheKey = "myCacheKey", + split = TRUE, + iconProps = list(iconName = "Add"), + subMenuProps = list( + items = list( + CommandBarItem( + key = ns("emailMessage"), + text = "Email message", + iconProps = list(iconName = "Mail") + ), + CommandBarItem( + key = ns("calendarEvent"), + text = "Calendar event", + iconProps = list(iconName = "Calendar") + ) ) ) + ), + CommandBarItem( + key = ns("upload"), + text = "Upload", + iconProps = list(iconName = "Upload") + ), + CommandBarItem( + key = ns("share"), + text = "Share", + iconProps = list(iconName = "Share") + ), + CommandBarItem( + key = ns("download"), + text = "Download", + iconProps = list(iconName = "Download") ) - ), - list( - key = "upload", - text = "Upload", - iconProps = list(iconName = "Upload") - ), - list( - key = "share", - text = "Share", - iconProps = list(iconName = "Share") - ), - list( - key = "download", - text = "Download", - iconProps = list(iconName = "Download") ) -) +} -farItems <- list( +farItems <- function(ns) { list( - key = "tile", - text = "Grid view", - ariaLabel = "Grid view", - iconOnly = TRUE, - iconProps = list(iconName = "Tiles") - ), - list( - key = "info", - text = "Info", - ariaLabel = "Info", - iconOnly = TRUE, - iconProps = list(iconName = "Info") + CommandBarItem( + key = ns("tile"), + text = "Grid view", + ariaLabel = "Grid view", + iconOnly = TRUE, + iconProps = list(iconName = "Tiles") + ), + CommandBarItem( + key = ns("info"), + text = "Info", + ariaLabel = "Info", + iconOnly = TRUE, + iconProps = list(iconName = "Info") + ) ) -) - +} ui <- function(id) { ns <- NS(id) - CommandBar(items = items, farItems = farItems) + tagList( + CommandBar( + items = items(ns), + farItems = farItems(ns) + ), + textOutput(ns("commandBarItems")), + CommandBar.shinyInput( + inputId = ns("commandBar"), + items = items(identity), + farItems = farItems(identity) + ), + textOutput(ns("commandBar")) + ) } server <- function(id) { - moduleServer(id, function(input, output, session) { }) + moduleServer(id, function(input, output, session) { + commandBarItemClicked <- reactiveVal() + observeEvent(input$newItem, commandBarItemClicked("newItem clicked")) + observeEvent(input$upload, commandBarItemClicked("upload clicked")) + output$commandBarItems <- renderText(commandBarItemClicked()) + output$commandBar <- renderText(input$commandBar) + }) } if (interactive()) { shinyApp(ui("app"), function(input, output) server("app")) } } +\seealso{ +CommandBarItem +} diff --git a/man/CommandBarItem.Rd b/man/CommandBarItem.Rd index 1e320ede..aeb4f27e 100644 --- a/man/CommandBarItem.Rd +++ b/man/CommandBarItem.Rd @@ -4,16 +4,23 @@ \alias{CommandBarItem} \title{Command bar item} \usage{ -CommandBarItem(text, icon = NULL, subitems = NULL, ...) +CommandBarItem( + key, + text, + onClick = setInputValue(inputId = key, value = 0), + ... +) } \arguments{ \item{text}{Text to be displayed on the menu.} +\item{onClick}{A JS function that runs on item click. By default it sends input value to \code{input[[key]]}} + +\item{...}{Additional props to pass to CommandBarItem.} + \item{icon}{Optional name of an icon.} \item{subitems}{Optional list of CommandBar items.} - -\item{...}{Additional props to pass to CommandBarItem.} } \value{ Item suitable for use in the CommandBar. diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R new file mode 100644 index 00000000..197068a4 --- /dev/null +++ b/tests/testthat/test-utils.R @@ -0,0 +1,45 @@ +describe("recursiveModify", { + it("applies modify function to all elements", { + # Arrange + makeItem <- function(...) { + structure( + list(...), + class = "item" + ) + } + data <- list( + makeItem( + x = 1, + subItems = list( + makeItem(x = 1), + makeItem(x = 2) + ) + ), + makeItem( + x = 2, + subItems = list( + makeItem(x = 1), + makeItem(x = 2) + ) + ), + makeItem( + x = 3, + subItems = list( + makeItem(x = 1), + makeItem(x = 2) + ) + ) + ) + modify_spy <- mockery::mock() + modify <- function(x) { + modify_spy() + x + } + + # Act + result <- recursiveModify(data, modify = modify, when = function(x) inherits(x, "item")) + + # Assert + mockery::expect_called(modify_spy, n = 9) + }) +}) \ No newline at end of file