diff --git a/DESCRIPTION b/DESCRIPTION index 4f22cb5..7046cc0 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -24,7 +24,8 @@ Imports: shinyGovstyle, shinyjs, stringr, - styler + styler, + reactable Suggests: devtools, diffviewer, diff --git a/R/dfe_reactable.R b/R/dfe_reactable.R new file mode 100644 index 0000000..9c1fdd3 --- /dev/null +++ b/R/dfe_reactable.R @@ -0,0 +1,75 @@ +#' Department for Education Reactable Wrapper +#' +#' A wrapper around the `reactable` function for creating styled, accessible, +#' and user-friendly tables tailored to the Department for Education's +#' requirements. +#' +#' @param data A data frame to display in the table. +#' @param ... Additional arguments passed to `reactable::reactable`. +#' +#' @details +#' The `dfe_reactable` function provides a pre-configured version of +#' the `reactable` function with: +#' \itemize{ +#' \item **Highlighting**: Row highlighting enabled. +#' \item **Borderless Table**: Removes borders for a clean look. +#' \item **Sort Icons Hidden**: Sort icons are not displayed by default. +#' \item **Resizable Columns**: Users can resize columns. +#' \item **Full Width**: Table expands to the full width of the container. +#' \item **Default Column Definition**: Custom column header styles, +#' NA value handling, +#' and alignment. +#' \item **Custom Search Input**: A search bar styled to the Department +#' for Education's specifications. +#' \item **Custom Language**: Provides a user-friendly search placeholder +#' text. +#' } +#' +#' @return A `reactable` HTML widget object. +#' +#' @examples +#' if (interactive()) { +#' library(shiny) +#' library(dfeshiny) +#' ui <- fluidPage( +#' h1("Example of dfe_reactable in a Shiny app"), +#' dfe_reactable(mtcars) +#' ) +#' server <- function(input, output, session) {} +#' shinyApp(ui, server) +#' } +dfe_reactable <- function(data, ...) { + reactable::reactable( + data, + highlight = TRUE, + borderless = TRUE, + showSortIcon = TRUE, + resizable = TRUE, + fullWidth = TRUE, + defaultColDef = reactable::colDef( + headerClass = "govuk-table__header", + html = TRUE, + na = "NA", + minWidth = 65, + align = "left", + class = "govuk-table__cell" + ), + rowClass = "govuk-table__row", + language = reactable::reactableLang( + searchPlaceholder = "Search table..." + ), + theme = reactable::reactableTheme( + searchInputStyle = list( + float = "right", + width = "25%", + marginBottom = "10px", + padding = "5px", + fontSize = "14px", + border = "1px solid #ccc", + borderRadius = "5px" + ) + ), + class = "gov-table", + ... + ) +} diff --git a/_pkgdown.yml b/_pkgdown.yml index 1e51cb2..af1103b 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -32,3 +32,7 @@ reference: desc: A wrapper for shinyGovstyle::header() that automatically uses the DfE logo. contents: - header +- title: Charts and Tables + desc: Functions to create and manage tables and visualisations. + contents: + - dfe_reactable diff --git a/inst/www/css/reactable_wrapper.css b/inst/www/css/reactable_wrapper.css new file mode 100644 index 0000000..2f519ec --- /dev/null +++ b/inst/www/css/reactable_wrapper.css @@ -0,0 +1,51 @@ +.govuk-table { + font-family: GDS Transport, arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 400; + font-size: 1rem; + line-height: 1.25; + color: #0b0c0c; + width: 100%; + margin-bottom: 20px; + border-spacing: 0; + border-collapse: collapse +} + +@media print { + .govuk-table { + font-family: sans-serif + } +} + +@media (min-width:40.0625em) { + .govuk-table { + font-size: 1.1875rem; + line-height: 1.3157894737 + } +} + +@media print { + .govuk-table { + font-size: 14pt; + line-height: 1.15; + color: #000 + } +} + +@media (min-width:40.0625em) { + .govuk-table { + margin-bottom: 30px + } +} + +.govuk-table__header { + font-weight: 700 +} + +.govuk-table__cell, .govuk-table__header { + padding: 10px 20px 10px 0; + border-bottom: 1px solid #b1b4b6; + text-align: left; + vertical-align: top +} diff --git a/man/dfe_reactable.Rd b/man/dfe_reactable.Rd new file mode 100644 index 0000000..a1419a2 --- /dev/null +++ b/man/dfe_reactable.Rd @@ -0,0 +1,51 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/dfe_reactable.R +\name{dfe_reactable} +\alias{dfe_reactable} +\title{Department for Education Reactable Wrapper} +\usage{ +dfe_reactable(data, ...) +} +\arguments{ +\item{data}{A data frame to display in the table.} + +\item{...}{Additional arguments passed to \code{reactable::reactable}.} +} +\value{ +A \code{reactable} HTML widget object. +} +\description{ +A wrapper around the \code{reactable} function for creating styled, accessible, +and user-friendly tables tailored to the Department for Education's +requirements. +} +\details{ +The \code{dfe_reactable} function provides a pre-configured version of +the \code{reactable} function with: +\itemize{ +\item \strong{Highlighting}: Row highlighting enabled. +\item \strong{Borderless Table}: Removes borders for a clean look. +\item \strong{Sort Icons Hidden}: Sort icons are not displayed by default. +\item \strong{Resizable Columns}: Users can resize columns. +\item \strong{Full Width}: Table expands to the full width of the container. +\item \strong{Default Column Definition}: Custom column header styles, +NA value handling, +and alignment. +\item \strong{Custom Search Input}: A search bar styled to the Department +for Education's specifications. +\item \strong{Custom Language}: Provides a user-friendly search placeholder +text. +} +} +\examples{ +if (interactive()) { + library(shiny) + library(dfeshiny) + ui <- fluidPage( + h1("Example of dfe_reactable in a Shiny app"), + dfe_reactable(mtcars) + ) + server <- function(input, output, session) {} + shinyApp(ui, server) +} +} diff --git a/tests/test_dashboard/server.R b/tests/test_dashboard/server.R index 56103a8..a9bf5fe 100644 --- a/tests/test_dashboard/server.R +++ b/tests/test_dashboard/server.R @@ -15,4 +15,8 @@ server <- function(input, output, session) { input_cookies = shiny::reactive(input$cookies), google_analytics_key = ga_key # nolint: [object_usage_linter] ) + + output$reactable_example <- reactable::renderReactable( + dfe_reactable(mtcars |> dplyr::select("mpg", "cyl", "hp", "gear")) + ) } diff --git a/tests/test_dashboard/tests/testthat/_snaps/UI-04-table/table_example-001.json b/tests/test_dashboard/tests/testthat/_snaps/UI-04-table/table_example-001.json new file mode 100644 index 0000000..fd2ae4b --- /dev/null +++ b/tests/test_dashboard/tests/testthat/_snaps/UI-04-table/table_example-001.json @@ -0,0 +1,5 @@ +{ + "output": { + + } +} diff --git a/tests/test_dashboard/tests/testthat/test-UI-04-table.R b/tests/test_dashboard/tests/testthat/test-UI-04-table.R new file mode 100644 index 0000000..4f2b9bf --- /dev/null +++ b/tests/test_dashboard/tests/testthat/test-UI-04-table.R @@ -0,0 +1,12 @@ +app <- AppDriver$new( + name = "table_example", + expect_values_screenshot_args = FALSE +) + +app$wait_for_idle(50) + +test_that("Table renders as expected", { + # Check the initial rendering of the table + app$wait_for_idle(50) + app$expect_values(output = "reactable_example") +}) diff --git a/tests/test_dashboard/ui.R b/tests/test_dashboard/ui.R index 285e6e9..c9bf086 100644 --- a/tests/test_dashboard/ui.R +++ b/tests/test_dashboard/ui.R @@ -99,6 +99,14 @@ ui <- function(input, output, session) { ), " code in a cave without distractions." ) + ), + + ## Example table panel -------------------------------------------------------- + shiny::tabPanel( + value = "table_panel_ui", + "Table example", + shiny::tags$h1("Reactable example"), + reactable::reactableOutput("reactable_example") ) ) ) diff --git a/tests/testthat/test-dfe_reactable.R b/tests/testthat/test-dfe_reactable.R new file mode 100644 index 0000000..226ace8 --- /dev/null +++ b/tests/testthat/test-dfe_reactable.R @@ -0,0 +1,56 @@ +test_that("dfe_reactable produces a properly configured reactable object", { + # Generate a small sample dataset + sample_data <- data.frame( + Name = c("Alice", "Bob", "Charlie"), + Age = c(25, 30, 35), + Score = c(85.5, 90.3, 88.7) + ) + + # Run the function + table_output <- dfe_reactable(sample_data) + + # Test if the output is a reactable object + expect_s3_class(table_output, "reactable") + expect_s3_class(table_output, "htmlwidget") + + # Check the main attributes + expect_type(table_output$x, "list") + expect_true("tag" %in% names(table_output$x)) + + # Check the tag attribs + attribs <- table_output$x$tag$attribs + expect_type(attribs, "list") + + # Verify key preconfigured attributes + expect_equal(attribs$resizable, TRUE) + expect_equal(attribs$highlight, TRUE) + expect_equal(attribs$borderless, TRUE) + + # Verify column definitions + columns <- attribs$columns + expect_equal(length(columns), 3) # Ensure 3 columns are defined + + # Validate the first column's attributes + col1 <- columns[[1]] + expect_equal(col1$id, "Name") + expect_equal(col1$name, "Name") + expect_equal(col1$type, "character") + expect_equal(col1$html, TRUE) + expect_equal(col1$headerClassName, "govuk-table__header") + + # Validate language configuration + language <- attribs$language + expect_type(language, "list") + expect_equal(language$searchPlaceholder, "Search table...") + + # Validate theme configuration + theme <- attribs$theme + expect_type(theme, "list") + expect_equal(theme$searchInputStyle$float, "right") + expect_equal(theme$searchInputStyle$width, "25%") + expect_equal(theme$searchInputStyle$border, "1px solid #ccc") + + # Validate the widget's overall class and package + expect_equal(attr(table_output, "class"), c("reactable", "htmlwidget")) + expect_equal(attr(table_output, "package"), "reactable") +})