From d286f683ba7d7d32c749e2ebca0ffde7dcfceef2 Mon Sep 17 00:00:00 2001 From: Marjanne Plasmans Date: Thu, 8 Oct 2026 07:45:23 +0200 Subject: [PATCH] Added reactable table. --- DESCRIPTION | 1 + NAMESPACE | 3 + R/ROvis.table-package.R | 1 + R/ro_reactable_theme.R | 234 +++++++++++++++++++++++ man/ro_reactable_css.Rd | 49 +++++ man/ro_reactable_theme.Rd | 28 +++ tests/testthat/test-ro_reactable_theme.R | 14 ++ 7 files changed, 330 insertions(+) create mode 100644 R/ro_reactable_theme.R create mode 100644 man/ro_reactable_css.Rd create mode 100644 man/ro_reactable_theme.Rd create mode 100644 tests/testthat/test-ro_reactable_theme.R diff --git a/DESCRIPTION b/DESCRIPTION index 96f860d..04cee0a 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -14,6 +14,7 @@ Imports: DT (>= 0.34.0), gt (>= 1.3.0), magrittr (>= 2.0.3), + reactable (>= 0.4.5), rlang (>= 1.1.0), shiny (>= 1.14.0), ROvis.utils diff --git a/NAMESPACE b/NAMESPACE index 64f66a5..cd4b45b 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,6 +3,8 @@ export(ro_dt_table) export(ro_dt_theme) export(ro_gt_theme) +export(ro_reactable_css) +export(ro_reactable_theme) import(rlang) importFrom(DT, JS, @@ -31,6 +33,7 @@ importFrom(gt, where ) importFrom(magrittr,"%>%") +importFrom(reactable,reactable) importFrom(shiny, HTML, tags diff --git a/R/ROvis.table-package.R b/R/ROvis.table-package.R index 3d66aeb..534417a 100644 --- a/R/ROvis.table-package.R +++ b/R/ROvis.table-package.R @@ -9,6 +9,7 @@ #' @importFrom magrittr %>% #' @importFrom DT datatable #' @importFrom gt gt +#' @importFrom reactable reactable #' @importFrom ROvis.utils ro_check_if_font_available ## usethis namespace: end NULL diff --git a/R/ro_reactable_theme.R b/R/ro_reactable_theme.R new file mode 100644 index 0000000..3986273 --- /dev/null +++ b/R/ro_reactable_theme.R @@ -0,0 +1,234 @@ +#' Reactable theme following Rijksoverheid huisstijl. +#' @description +#' Creates a `reactableTheme` object with table styling according to the +#' Rijksoverheid huisstijl. +#' +#' @importFrom ROvis.utils ro_check_if_font_available +#' +#' @param base_family Character. Font family to use for table text. Default is +#' "RijksoverheidSansWebText". If not installed, Verdana is used, then Arial, +#' then the first font found on your device, whichever is installed first. +#' @param sorting Logical. If `TRUE`, extra space is added to the right +#' in the column header for sort arrows. Default `FALSE`. +#' +#' @return A `reactable::reactableTheme` object. +#' @export +#' @family reactable +ro_reactable_theme <- function( + base_family = "RijksoverheidSansWebText", + sorting = FALSE +) { + check_string(base_family) + + base_family <- ROvis.utils::ro_check_if_font_available( + target_font_family = base_family + ) + + header_padding <- if (sorting) "10px" else "3px" + + reactable::reactableTheme( + tableStyle = list( + border = "1px solid #bababa", + borderCollapse = "collapse", + fontFamily = base_family + ), + headerStyle = list( + fontFamily = base_family, + fontWeight = "normal", + fontStyle = "normal", + fontSize = "13px", + textAlign = "right", + paddingTop = "1px", + paddingBottom = "1px", + paddingLeft = "3px", + paddingRight = header_padding, + borderBottom = "1px solid #222222", + borderLeft = "none", + borderRight = "none", + borderTop = "none" + ), + cellStyle = list( + fontFamily = base_family, + fontWeight = "normal", + fontStyle = "normal", + fontSize = "13px", + textAlign = "right", + paddingTop = "1px", + paddingBottom = "1px", + paddingLeft = "3px", + paddingRight = "3px", + border = "none" + ) + ) +} + +#' Custom CSS for reactable tables +#' +#' @description +#' Returns a Shiny tags object with custom CSS for reactable tables, +#' with table styling according to the Rijksoverheid huisstijl. +#' +#' @importFrom shiny tags HTML +#' +#' @param sorting Logical. If `TRUE`, custom sorting arrows are shown in the +#' column headers. If `FALSE`, only the default arrows are hidden. +#' Default `TRUE`. +#' +#' @return A `shiny.tag` with a `` tag that contains custom CSS +#' for reactable tables. +#' @export +#' @family reactable +#' +#' @examples +#' if (interactive()) { +#' library(shiny) +#' library(reactable) +#' +#' ui <- fluidPage( +#' ro_reactable_css(sorting = TRUE), +#' reactableOutput("mytable") +#' ) +#' +#' server <- function(input, output, session) { +#' output$mytable <- renderReactable( +#' reactable( +#' iris, +#' defaultColDef = colDef(align = "right", sortable = TRUE), +#' theme = ro_reactable_theme(sorting = TRUE) +#' ) +#' ) +#' } +#' +#' shinyApp(ui, server) +#' } +ro_reactable_css <- function(sorting = TRUE) { + arrow_css <- if (sorting) { + " + /* Hide the extra sort arrow span */ + .reactable .rt-th .rt-sort-left { + display: none !important; + } + + /* Custom sorting arrows based on aria-sort */ + .reactable .rt-th { + position: relative; + cursor: pointer; + } + + /* Unsorted: double arrow */ + .reactable .rt-th[aria-sort='none']::after, + .reactable .rt-th:not([aria-sort])::after { + content: '\\2195'; + position: absolute; + right: 4px; + top: 50%; + transform: translateY(-50%); + font-size: 16px; + line-height: 1; + color: #888888; + } + + /* Ascending: up arrow */ + .reactable .rt-th[aria-sort='ascending']::after { + content: '\\2191'; + position: absolute; + right: 4px; + top: 50%; + transform: translateY(-50%); + font-size: 16px; + line-height: 1; + color: #007bff; + font-weight: 700; + } + + /* Descending: down arrow */ + .reactable .rt-th[aria-sort='descending']::after { + content: '\\2193'; + position: absolute; + right: 4px; + top: 50%; + transform: translateY(-50%); + font-size: 16px; + color: #007bff; + font-weight: 700; + } + " + } else { + " + /* Hide the extra sort arrow span */ + .reactable .rt-th .rt-sort-left { + display: none !important; + } + " + } + + shiny::tags$head( + shiny::tags$style( + shiny::HTML(paste0( + " + /* Caption styling (if a caption is used) */ + .reactable .rt-table caption { + caption-side: top; + text-align: right; + font-weight: normal; + font-style: normal; + font-size: 14px; + padding: 1px 3px; + color: #505050; + } + + /* Vertical line after first column header */ + .reactable .rt-thead .rt-tr > .rt-th:first-child { + border-right: 1px solid #222222 !important; + } + + /* Vertical line after first column in body */ + .reactable .rt-tbody .rt-tr-group .rt-tr > .rt-td:first-child { + border-right: 1px solid #222222 !important; + } + + /* Zebra striping */ + .reactable .rt-tbody .rt-tr-group:nth-child(odd) { + background-color: #f2f2f2; + } + .reactable .rt-tbody .rt-tr-group:nth-child(even) { + background-color: #ffffff; + } + + /* Hover effect */ + .reactable .rt-tbody .rt-tr-group:hover { + background-color: #44688f; + color: #ffffff; + } + + /* Selected row */ + .reactable .rt-tr-group.rt-selected, + .reactable .rt-tr-group.rt-selected .rt-td { + background-color: #44688f !important; + color: #ffffff !important; + } + + /* Focus indicators: table cells & header */ + .reactable .rt-td:focus, + .reactable .rt-th:focus { + outline: 2px dotted #000 !important; + box-shadow: none !important; + border: none !important; + } + + /* Pagination buttons */ + .reactable .rt-pagination button:focus, + .reactable .rt-pagination button:focus-visible { + outline: 2px dotted #000 !important; + outline-offset: 2px !important; + box-shadow: none !important; + } + .reactable .rt-pagination button[aria-current='page'] { + border: 2px solid #949494 !important; + } + ", + arrow_css + )) + ) + ) +} diff --git a/man/ro_reactable_css.Rd b/man/ro_reactable_css.Rd new file mode 100644 index 0000000..be41503 --- /dev/null +++ b/man/ro_reactable_css.Rd @@ -0,0 +1,49 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ro_reactable_theme.R +\name{ro_reactable_css} +\alias{ro_reactable_css} +\title{Custom CSS for reactable tables} +\usage{ +ro_reactable_css(sorting = TRUE) +} +\arguments{ +\item{sorting}{Logical. If \code{TRUE}, custom sorting arrows are shown in the +column headers. If \code{FALSE}, only the default arrows are hidden. +Default \code{TRUE}.} +} +\value{ +A \code{shiny.tag} with a \verb{} tag that contains custom CSS +for reactable tables. +} +\description{ +Returns a Shiny tags object with custom CSS for reactable tables, +with table styling according to the Rijksoverheid huisstijl. +} +\examples{ +if (interactive()) { + library(shiny) + library(reactable) + + ui <- fluidPage( + ro_reactable_css(sorting = TRUE), + reactableOutput("mytable") + ) + + server <- function(input, output, session) { + output$mytable <- renderReactable( + reactable( + iris, + defaultColDef = colDef(align = "right", sortable = TRUE), + theme = ro_reactable_theme(sorting = TRUE) + ) + ) + } + + shinyApp(ui, server) +} +} +\seealso{ +Other reactable: +\code{\link[=ro_reactable_theme]{ro_reactable_theme()}} +} +\concept{reactable} diff --git a/man/ro_reactable_theme.Rd b/man/ro_reactable_theme.Rd new file mode 100644 index 0000000..4e97609 --- /dev/null +++ b/man/ro_reactable_theme.Rd @@ -0,0 +1,28 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ro_reactable_theme.R +\name{ro_reactable_theme} +\alias{ro_reactable_theme} +\title{Reactable theme following Rijksoverheid huisstijl.} +\usage{ +ro_reactable_theme(base_family = "RijksoverheidSansWebText", sorting = FALSE) +} +\arguments{ +\item{base_family}{Character. Font family to use for table text. Default is +"RijksoverheidSansWebText". If not installed, Verdana is used, then Arial, +then the first font found on your device, whichever is installed first.} + +\item{sorting}{Logical. If \code{TRUE}, extra space is added to the right +in the column header for sort arrows. Default \code{FALSE}.} +} +\value{ +A \code{reactable::reactableTheme} object. +} +\description{ +Creates a \code{reactableTheme} object with table styling according to the +Rijksoverheid huisstijl. +} +\seealso{ +Other reactable: +\code{\link[=ro_reactable_css]{ro_reactable_css()}} +} +\concept{reactable} diff --git a/tests/testthat/test-ro_reactable_theme.R b/tests/testthat/test-ro_reactable_theme.R new file mode 100644 index 0000000..d4b323f --- /dev/null +++ b/tests/testthat/test-ro_reactable_theme.R @@ -0,0 +1,14 @@ +test_that("ro_reactable_theme applies the font resolved by ro_check_if_font_available", { + local_mocked_bindings( + ro_check_if_font_available = function(target_font_family) "Verdana" + ) + css <- ro_dt_theme()$children[[1]]$children[[1]] + expect_true(grepl("Verdana", css, fixed = TRUE)) +}) + +test_that("ro_reactable_theme errors for an unavailable custom font", { + expect_error( + ro_dt_theme(base_family = "Not an installed font"), + "Can't find" + ) +})