├── .github ├── .gitignore ├── FUNDING.yml └── workflows │ └── R-CMD-check.yaml ├── NAMESPACE ├── .gitignore ├── LICENSE ├── tests ├── testthat.R └── testthat │ ├── test-check_contrast.R │ ├── test-make_tabbable.R │ └── test-to_skiplinks.R ├── man ├── figures │ ├── README-pressure-1.png │ ├── README-unnamed-chunk-3-1.png │ ├── README-unnamed-chunk-4-1.png │ ├── README-unnamed-chunk-6-1.png │ └── README-unnamed-chunk-7-1.png ├── create_invisible_anchor.Rd ├── check_contrast.Rd ├── check_contrast_raw.Rd ├── make_skiplinks.Rd ├── make_tabable.Rd ├── describe_using.Rd └── add_description.Rd ├── .Rbuildignore ├── NEWS.md ├── savonliquide.Rproj ├── cran-comments.md ├── DESCRIPTION ├── R ├── create_invisible_anchor.R ├── make_tabbable.R ├── make_skiplinks.R ├── check_contrast_raw.R ├── check_contrast.R └── add_description.R ├── LICENSE.md ├── CODE_OF_CONDUCT.md ├── README.Rmd └── README.md /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | exportPattern("^[[:alpha:]]+") 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rhistory 2 | .RData 3 | .Rproj.user 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://www.buymeacoffee.com/Fodil'] 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2020 2 | COPYRIGHT HOLDER: Mohamed El Fodil Ihaddaden 3 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(savonliquide) 3 | 4 | test_check("savonliquide") 5 | -------------------------------------------------------------------------------- /man/figures/README-pressure-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feddelegrand7/savonliquide/HEAD/man/figures/README-pressure-1.png -------------------------------------------------------------------------------- /man/figures/README-unnamed-chunk-3-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feddelegrand7/savonliquide/HEAD/man/figures/README-unnamed-chunk-3-1.png -------------------------------------------------------------------------------- /man/figures/README-unnamed-chunk-4-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feddelegrand7/savonliquide/HEAD/man/figures/README-unnamed-chunk-4-1.png -------------------------------------------------------------------------------- /man/figures/README-unnamed-chunk-6-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feddelegrand7/savonliquide/HEAD/man/figures/README-unnamed-chunk-6-1.png -------------------------------------------------------------------------------- /man/figures/README-unnamed-chunk-7-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feddelegrand7/savonliquide/HEAD/man/figures/README-unnamed-chunk-7-1.png -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^LICENSE\.md$ 4 | ^README\.Rmd$ 5 | ^CODE_OF_CONDUCT\.md$ 6 | ^cran-comments\.md$ 7 | ^CRAN-RELEASE$ 8 | ^\.github$ 9 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # savonliquide 0.2.0 2 | 3 | * Added the following functions: 4 | * `add_description()` 5 | * `describe_using()` 6 | * `create_invisible_anchor()` 7 | * `make_skiplinks()` 8 | * `make_tabbable()` 9 | 10 | * minor bug fixing. 11 | 12 | 13 | # savonliquide 0.1.0 14 | 15 | * Added a `NEWS.md` file to track changes to the package. 16 | * First CRAN release 17 | -------------------------------------------------------------------------------- /savonliquide.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | ## Test environments 2 | * local R installation, R 4.0.2 3 | * ubuntu 16.04 (on travis-ci), R 4.0.2 4 | * win-builder (devel) 5 | 6 | ## R CMD check results 7 | 8 | -- R CMD check results - savonliquide 0.2.0 ---- 9 | Duration: 29s 10 | 11 | 0 errors √ | 0 warnings √ | 0 notes √ 12 | 13 | 14 | + I've added new functions. 15 | + I've added new tests. 16 | + I've modified the description of the package. 17 | + I've used the skip_on_cran function to skip tests that call the API. 18 | -------------------------------------------------------------------------------- /man/create_invisible_anchor.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/create_invisible_anchor.R 3 | \name{create_invisible_anchor} 4 | \alias{create_invisible_anchor} 5 | \title{Create an HTML invisible anchor} 6 | \usage{ 7 | create_invisible_anchor(id, text, href = NULL) 8 | } 9 | \arguments{ 10 | \item{id}{id of the anchor} 11 | 12 | \item{text}{text of the anchor} 13 | 14 | \item{href}{of the anchor. Defaults to NULL.} 15 | } 16 | \value{ 17 | an invisible HTML anchor element 18 | } 19 | \description{ 20 | Make an element invisible so that it can only be read 21 | by screen readers 22 | } 23 | -------------------------------------------------------------------------------- /man/check_contrast.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check_contrast.R 3 | \name{check_contrast} 4 | \alias{check_contrast} 5 | \title{Color Contrast Accessibility Report} 6 | \usage{ 7 | check_contrast(fg_col, bg_col) 8 | } 9 | \arguments{ 10 | \item{fg_col}{the Foreground Color} 11 | 12 | \item{bg_col}{the Background Color} 13 | } 14 | \value{ 15 | Color Contrast Report 16 | } 17 | \description{ 18 | returns a report from the Contrast Checker API about color contrast for accessibility 19 | } 20 | \examples{ 21 | 22 | check_contrast(fg_col = "#21EA06", bg_col = "#483D3D") 23 | 24 | 25 | } 26 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: savonliquide 2 | Title: Accessibility Toolbox for 'R' Users 3 | Version: 0.2.0 4 | Authors@R: 5 | person(given = "Mohamed El Fodil", 6 | family = "Ihaddaden", 7 | role = c("aut", "cre"), 8 | email = "ihaddaden.fodeil@gmail.com") 9 | Description: Provides a toolbox that allows the user to implement accessibility related concepts. 10 | License: MIT + file LICENSE 11 | Encoding: UTF-8 12 | LazyData: true 13 | Roxygen: list(markdown = TRUE) 14 | RoxygenNote: 7.1.1 15 | Suggests: 16 | testthat, 17 | spelling 18 | URL: https://github.com/feddelegrand7/savonliquide 19 | BugReports: https://github.com/feddelegrand7/savonliquide/issues 20 | Imports: 21 | glue, 22 | htmltools, 23 | httr, 24 | crayon 25 | Language: en-US 26 | -------------------------------------------------------------------------------- /man/check_contrast_raw.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check_contrast_raw.R 3 | \name{check_contrast_raw} 4 | \alias{check_contrast_raw} 5 | \title{Color Contrast Accessibility Report in a Raw Format} 6 | \usage{ 7 | check_contrast_raw(fg_col, bg_col) 8 | } 9 | \arguments{ 10 | \item{fg_col}{the Foreground Color} 11 | 12 | \item{bg_col}{the Background Color} 13 | } 14 | \value{ 15 | Color Contrast Report in a raw format 16 | } 17 | \description{ 18 | returns a report from the Contrast Checker API about color contrast for accessibility in a list format 19 | so that the information provided can be extracted and piped into other functions. 20 | } 21 | \examples{ 22 | 23 | check_contrast_raw(fg_col = "#21EA06", bg_col = "#483D3D") 24 | 25 | 26 | } 27 | -------------------------------------------------------------------------------- /R/create_invisible_anchor.R: -------------------------------------------------------------------------------- 1 | 2 | 3 | #' Create an HTML invisible anchor 4 | #' 5 | #' @param id id of the anchor 6 | #' @param text text of the anchor 7 | #' @param href of the anchor. Defaults to NULL. 8 | #' @description Make an element invisible so that it can only be read 9 | #' by screen readers 10 | #' @return an invisible HTML anchor element 11 | #' @export 12 | #' 13 | #' @examples 14 | 15 | create_invisible_anchor <- function(id, text, href = NULL) { 16 | 17 | href <- if(is.null(href)) { 18 | NULL 19 | } else { 20 | href 21 | } 22 | 23 | htmltools::tagList( 24 | make_tabable(htmltools::tags$a(text, id = id, href = href)), 25 | 26 | htmltools::tags$head(htmltools::tags$style(glue::glue( 27 | " 28 | #{id} {{ 29 | position: absolute; 30 | left: -1000px; 31 | width: 1px; 32 | height: 1px; 33 | overflow: hidden; 34 | }} 35 | " 36 | ))) 37 | 38 | ) 39 | 40 | 41 | } 42 | -------------------------------------------------------------------------------- /tests/testthat/test-check_contrast.R: -------------------------------------------------------------------------------- 1 | 2 | 3 | test_that("the function returns a character", { 4 | 5 | testthat::skip_on_cran() 6 | 7 | expect_is(object = check_contrast(fg_col = "#21EA06", bg_col = "#483D3D"), 8 | class = "character") 9 | 10 | }) 11 | 12 | 13 | test_that("check if the function returns the same output when inputing color with and without #", { 14 | 15 | testthat::skip_on_cran() 16 | 17 | expect_equivalent( 18 | 19 | check_contrast(fg_col = "#21EA06", bg_col = "#483D3D"), 20 | 21 | check_contrast(fg_col = "21EA06", bg_col = "483D3D") 22 | 23 | 24 | ) 25 | 26 | 27 | }) 28 | 29 | 30 | test_that("check if the function throw an error when inputing wrong colors", { 31 | 32 | expect_error( 33 | 34 | check_contrast(fg_col = "23", bg_col = "#4") 35 | 36 | ) 37 | 38 | expect_error( 39 | 40 | check_contrast(fg_col = "", bg_col = "#334") 41 | 42 | ) 43 | 44 | 45 | expect_error( 46 | 47 | check_contrast(fg_col = "#333", bg_col = "#34") 48 | 49 | ) 50 | 51 | expect_error( 52 | 53 | check_contrast(fg_col = "#333", bg_col = "34") 54 | 55 | ) 56 | 57 | 58 | 59 | 60 | }) 61 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2020 Mohamed El Fodil Ihaddaden 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /man/make_skiplinks.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/make_skiplinks.R 3 | \name{make_skiplinks} 4 | \alias{make_skiplinks} 5 | \title{Transform an HTML element to a Skip Link} 6 | \usage{ 7 | make_skiplinks(element, skip_to, bg_color = "#002240", col = "#FFFFFF") 8 | } 9 | \arguments{ 10 | \item{element}{the element to use as a Skip Link} 11 | 12 | \item{skip_to}{the HTML element to skip to} 13 | 14 | \item{bg_color}{the background color of the element to use as a Skip Link} 15 | 16 | \item{col}{the color of the element to use as a Skip Link} 17 | } 18 | \value{ 19 | a Skip Link HTML element 20 | } 21 | \description{ 22 | Transform an HTML element to a Skip Link 23 | } 24 | \examples{ 25 | 26 | if (interactive()) { 27 | ui <- fluidPage( 28 | tags$a("do you want to be redirected to google.com ?", 29 | id = "skip-link" 30 | ) \%>\% 31 | make_skiplinks( 32 | skip_to = "https://google.com", 33 | bg_color = "red", 34 | col = "white" 35 | ), 36 | 37 | h1("accessibility is not a detail") 38 | ) 39 | 40 | server <- function(input, output, session) {} 41 | 42 | shinyApp(ui, server) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /man/make_tabable.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/make_tabbable.R 3 | \name{make_tabable} 4 | \alias{make_tabable} 5 | \title{Make HTML elements tabable} 6 | \usage{ 7 | make_tabable(element, tab_index = 0) 8 | } 9 | \arguments{ 10 | \item{element}{the HTML element to be tabable (if not by default)} 11 | 12 | \item{tab_index}{takes either 0, a negative or a positive value according to the required state of the element. 13 | 0 will make the element tabable with its relative order defined by the platform convention. 14 | a negative value will make the element untabable. 15 | a positive value will make the element tabable and its relative order defined by the provided value.} 16 | } 17 | \value{ 18 | a tabable HTML element 19 | } 20 | \description{ 21 | Make HTML elements tabable 22 | } 23 | \examples{ 24 | 25 | if (interactive()) { 26 | ui <- fluidPage( 27 | textInput(inputId = "inp1", label = "input"), 28 | 29 | div(h1("Not tabable")) \%>\% 30 | make_tabable(tab_index = -1), 31 | div(h2("Tabable ! with priority")) \%>\% 32 | make_tabable(tab_index = 1), 33 | div(h2("Simply Tabable")) \%>\% 34 | make_tabable(tab_index = 0) 35 | ) 36 | 37 | server <- function(input, output, session) {} 38 | 39 | shinyApp(ui = ui, server = server) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /man/describe_using.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/add_description.R 3 | \name{describe_using} 4 | \alias{describe_using} 5 | \title{Describe an HTML element by another one} 6 | \usage{ 7 | describe_using(element, descID) 8 | } 9 | \arguments{ 10 | \item{element}{the HTML element to describe} 11 | 12 | \item{descID}{one or a vector of many HTML elements' 13 | that will be used to describe the 'element' parameter} 14 | } 15 | \value{ 16 | an HTML element described by another HTML element 17 | } 18 | \description{ 19 | Describe an HTML element by another one 20 | } 21 | \examples{ 22 | if (interactive()) { 23 | ui <- fluidPage( 24 | h2("Using a screen reader 25 | hit Tab and Shift + Tab to 26 | navigate between the buttons 27 | and stop at button 2 to see the difference"), 28 | 29 | div( 30 | id = "paragraph", 31 | p("The following paragraph tag will be used as a descriptor") 32 | ), 33 | 34 | actionButton( 35 | inputId = "inp1", 36 | label = "button 1" 37 | ), 38 | actionButton( 39 | inputId = "inp2", 40 | label = "button 2" 41 | ) \%>\% 42 | describe_using( 43 | descID = "paragraph" 44 | ) 45 | ) 46 | 47 | server <- function(input, output, session) {} 48 | 49 | shinyApp(ui, server) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /R/make_tabbable.R: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #' Make HTML elements tabable 5 | #' 6 | #' @param element the HTML element to be tabable (if not by default) 7 | #' @param tab_index 8 | #' takes either 0, a negative or a positive value according to the required state of the element. 9 | #' 0 will make the element tabable with its relative order defined by the platform convention. 10 | #' a negative value will make the element untabable. 11 | #' a positive value will make the element tabable and its relative order defined by the provided value. 12 | #' @return a tabable HTML element 13 | #' @export 14 | #' 15 | #' @examples 16 | #' 17 | #' if (interactive()) { 18 | #' ui <- fluidPage( 19 | #' textInput(inputId = "inp1", label = "input"), 20 | #' 21 | #' div(h1("Not tabable")) %>% 22 | #' make_tabable(tab_index = -1), 23 | #' div(h2("Tabable ! with priority")) %>% 24 | #' make_tabable(tab_index = 1), 25 | #' div(h2("Simply Tabable")) %>% 26 | #' make_tabable(tab_index = 0) 27 | #' ) 28 | #' 29 | #' server <- function(input, output, session) {} 30 | #' 31 | #' shinyApp(ui = ui, server = server) 32 | #' } 33 | make_tabable <- function(element, tab_index = 0) { 34 | if (tab_index > 32767) { 35 | stop("tab_index maximum value is 32767") 36 | } 37 | 38 | tab_index <- as.character(tab_index) 39 | 40 | htmltools::tagAppendAttributes(element, `tabindex` = tab_index) 41 | } 42 | -------------------------------------------------------------------------------- /tests/testthat/test-make_tabbable.R: -------------------------------------------------------------------------------- 1 | 2 | test_that("expect error when tabindex gt 32767", { 3 | 4 | expect_error(make_tabable(div("hello world"), 5 | tab_index = 32768)) 6 | 7 | }) 8 | 9 | 10 | test_that("expect savonliquide way equal to htmltools way", { 11 | 12 | 13 | element <- htmltools::div("tabable stuff going on here") 14 | 15 | htmltools0 <- htmltools::tagAppendAttributes(element, `tabindex` = "0") 16 | 17 | savon_liquid0 <- htmltools::div("tabable stuff going on here") %>% 18 | make_tabable(0) 19 | 20 | expect_equal(htmltools0, savon_liquid0) 21 | 22 | 23 | htmltools1 <- htmltools::tagAppendAttributes(element, `tabindex` = "1") 24 | 25 | savon_liquid1 <- htmltools::div("tabable stuff going on here") %>% 26 | make_tabable(1) 27 | 28 | expect_equal(htmltools1, savon_liquid1) 29 | 30 | htmltools_20 <- htmltools::tagAppendAttributes(element, `tabindex` = "-20") 31 | 32 | savon_liquid_20 <- htmltools::div("tabable stuff going on here") %>% 33 | make_tabable(-20) 34 | 35 | expect_equal(htmltools_20, savon_liquid_20) 36 | 37 | 38 | 39 | }) 40 | 41 | test_that("expecting the output of class shiny.tag", { 42 | 43 | 44 | savon_liquid <- htmltools::div("tabable stuff going on here") %>% 45 | make_tabable(0) 46 | 47 | expect_s3_class(savon_liquid, "shiny.tag") 48 | 49 | }) 50 | 51 | 52 | test_that("expecting the output to be of type list", { 53 | 54 | 55 | savon_liquid <- htmltools::div("tabable stuff going on here") %>% 56 | make_tabable(0) 57 | 58 | expect_type(savon_liquid, "list") 59 | 60 | }) 61 | -------------------------------------------------------------------------------- /man/add_description.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/add_description.R 3 | \name{add_description} 4 | \alias{add_description} 5 | \title{Add a description to an HTML element} 6 | \usage{ 7 | add_description(element, descID, description, visible = FALSE) 8 | } 9 | \arguments{ 10 | \item{element}{an HTML element to describe} 11 | 12 | \item{descID}{the ID of the div that will describe the HTML element} 13 | 14 | \item{description}{the description of the HTML element} 15 | 16 | \item{visible}{should the description be visible ? Defaults to FALSE} 17 | } 18 | \value{ 19 | an HTML element with a description attached to it 20 | } 21 | \description{ 22 | Add a description to an HTML element 23 | } 24 | \examples{ 25 | 26 | if (interactive()) { 27 | ui <- fluidPage( 28 | h2("Using a screen reader 29 | hit or to 30 | navigate between the buttons 31 | and stop at button 5 to see the difference"), 32 | 33 | actionButton( 34 | inputId = "inp1", 35 | label = "button 1" 36 | ), 37 | actionButton( 38 | inputId = "inp2", 39 | label = "button 2" 40 | ), 41 | actionButton( 42 | inputId = "inp3", 43 | label = "button 3" 44 | ), 45 | actionButton( 46 | inputId = "inp4", 47 | label = "button 4" 48 | ), 49 | actionButton( 50 | inputId = "inp5", 51 | label = "button 5" 52 | ) \%>\% 53 | add_description( 54 | description = "hello this is a button 55 | when you click it you'll have a 56 | thing, when you don't click it you'll 57 | have another thing", 58 | descID = "chkoup" 59 | ) 60 | ) 61 | 62 | server <- function(input, output, session) {} 63 | 64 | shinyApp(ui, server) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/testthat/test-to_skiplinks.R: -------------------------------------------------------------------------------- 1 | 2 | test_that("the function returns a shiny.tag.list class", { 3 | 4 | 5 | output <- make_skiplinks( 6 | htmltools::tags$a(id = "kla", "Do you want to skip the content ?"), 7 | skip_to = "#inp7") 8 | 9 | expect_s3_class(output, "shiny.tag.list") 10 | 11 | 12 | }) 13 | 14 | 15 | test_that("the function returns a list type", { 16 | 17 | 18 | output <- make_skiplinks( 19 | htmltools::tags$a(id = "kla", "Do you want to skip the content ?"), 20 | skip_to = "#inp7") 21 | 22 | testthat::expect_type(output, "list") 23 | 24 | 25 | }) 26 | 27 | 28 | test_that("the function gives back error when mandatory parameters missing", { 29 | 30 | 31 | expect_error(make_skiplinks()) 32 | 33 | expect_error(make_skiplinks(element = htmltools::tags$a(id = "kla", 34 | "Do you want to skip the content ?"))) 35 | 36 | expect_error(make_skiplinks(, skip_to = "#inp7")) 37 | 38 | 39 | 40 | }) 41 | 42 | test_that("expecting error when the id is missing", { 43 | 44 | 45 | expect_error( 46 | htmltools::tags$a("Do you want to skip the content ?") %>% 47 | make_skiplinks(skip_to = "#inp7")) 48 | 49 | 50 | }) 51 | 52 | 53 | test_that("expecting error when the parameters are not character", { 54 | 55 | 56 | expect_error( 57 | 58 | htmltools::tags$a(id = "kla", "Do you want to skip the content ?") %>% 59 | make_skiplinks(skip_to = 123) 60 | 61 | ) 62 | 63 | expect_error( 64 | 65 | htmltools::tags$a(id = "kla", "Do you want to skip the content ?") %>% 66 | make_skiplinks(skip_to = "efef", bg_color = 24234) 67 | 68 | ) 69 | 70 | expect_error( 71 | 72 | htmltools::tags$a(id = "kla", "Do you want to skip the content ?") %>% 73 | make_skiplinks(skip_to = "efef", bg_color = "blue", col = 23342) 74 | 75 | ) 76 | 77 | 78 | }) 79 | 80 | -------------------------------------------------------------------------------- /R/make_skiplinks.R: -------------------------------------------------------------------------------- 1 | 2 | 3 | #' Transform an HTML element to a Skip Link 4 | #' 5 | #' @param element the element to use as a Skip Link 6 | #' @param skip_to the HTML element to skip to 7 | #' @param bg_color the background color of the element to use as a Skip Link 8 | #' @param col the color of the element to use as a Skip Link 9 | #' 10 | #' 11 | #' @return a Skip Link HTML element 12 | #' @export 13 | #' 14 | #' @examples 15 | #' 16 | #' if (interactive()) { 17 | #' ui <- fluidPage( 18 | #' tags$a("do you want to be redirected to google.com ?", 19 | #' id = "skip-link" 20 | #' ) %>% 21 | #' make_skiplinks( 22 | #' skip_to = "https://google.com", 23 | #' bg_color = "red", 24 | #' col = "white" 25 | #' ), 26 | #' 27 | #' h1("accessibility is not a detail") 28 | #' ) 29 | #' 30 | #' server <- function(input, output, session) {} 31 | #' 32 | #' shinyApp(ui, server) 33 | #' } 34 | make_skiplinks <- function(element, 35 | skip_to, 36 | bg_color = "#002240", 37 | col = "#FFFFFF") { 38 | if (any(missing(element), missing(skip_to))) { 39 | stop("'element' and 'skip_to' are mondatory parameters") 40 | } 41 | 42 | if (!all( 43 | is.character(skip_to), 44 | is.character(bg_color), 45 | is.character(col) 46 | )) { 47 | stop("'skip_to', 'bg_color', 'col' must be provided as character strings") 48 | } 49 | 50 | id <- htmltools::tagGetAttribute(element, attr = "id") 51 | 52 | if (is.null(id)) { 53 | stop("the element must have an ID attribute") 54 | } 55 | 56 | 57 | htmltools::tagList( 58 | htmltools::tagAppendAttributes( 59 | element, 60 | href = skip_to 61 | ), 62 | 63 | htmltools::tags$head(htmltools::tags$style(glue::glue( 64 | " 65 | #{id} {{ 66 | position: absolute; 67 | top: -40px; 68 | left: 0; 69 | background: {bg_color}; 70 | color: {col}; 71 | padding: 8px; 72 | z-index: 9999; 73 | }} 74 | 75 | #{id}:focus {{ 76 | top: 0; 77 | }} 78 | " 79 | ))) 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /R/check_contrast_raw.R: -------------------------------------------------------------------------------- 1 | 2 | #' Color Contrast Accessibility Report in a Raw Format 3 | #' 4 | #' @description returns a report from the Contrast Checker API about color contrast for accessibility in a list format 5 | #' so that the information provided can be extracted and piped into other functions. 6 | #' 7 | #' @param fg_col the Foreground Color 8 | #' @param bg_col the Background Color 9 | #' 10 | #' @return Color Contrast Report in a raw format 11 | #' @export 12 | #' 13 | #' @examples 14 | #' 15 | #' check_contrast_raw(fg_col = "#21EA06", bg_col = "#483D3D") 16 | #' 17 | #' 18 | 19 | 20 | 21 | 22 | check_contrast_raw <- function(fg_col, bg_col){ 23 | 24 | # We need to be sure that the user provide valid HEX colors 25 | # here we focus on the minimum length (nchar) which is 26 | # 4 if # is included or 3 if # is not included 27 | 28 | 29 | if(grepl(pattern = "#", x = fg_col) && nchar(fg_col) < 4){ 30 | 31 | stop("invalid color provided in fg_color, please use valid HEX colors") 32 | 33 | } 34 | 35 | if(!grepl(pattern = "#", x = fg_col) && nchar(fg_col) < 3){ 36 | 37 | stop("invalid color provided in fg_color, few characters provided") 38 | 39 | } 40 | 41 | 42 | # Same stuff (copy and paste) for bg_col 43 | 44 | 45 | if(grepl(pattern = "#", x = bg_col) && nchar(bg_col) < 4){ 46 | 47 | stop("invalid color provided in bg_col, please use valid HEX colors") 48 | 49 | } 50 | 51 | if(!grepl(pattern = "#", x = bg_col) && nchar(bg_col) < 3){ 52 | 53 | stop("invalid color provided in bg_col, few characters provided") 54 | 55 | } 56 | 57 | 58 | # The API doesn't take into account the # so let's remove it. 59 | 60 | fg_col <- gsub(pattern = "#", replacement = "", x = fg_col) 61 | 62 | bg_col <- gsub(pattern = "#", replacement = "", x = bg_col) 63 | 64 | 65 | 66 | tryCatch( 67 | 68 | expr = { 69 | 70 | 71 | # Getting the response from the API. Here I use the glue package so that we 72 | # can interpolate the arguments (fg_col and bg_col) into the API call. 73 | 74 | 75 | response <- httr::GET(glue::glue("https://webaim.org/resources/contrastchecker/?fcolor={fg_col}&bcolor={bg_col}&api")) 76 | 77 | 78 | # Now we need to parse the response 79 | return(httr::content(response, as = "parsed", encoding = "UTF-8")) 80 | 81 | 82 | 83 | 84 | }, 85 | 86 | 87 | error = function(cond){ 88 | 89 | # If there is an error, it would be cool to return the API response so that 90 | # the error can be investigated, right ? 91 | 92 | message(glue::glue("Error: here the API response status: {response$status_code}")) 93 | 94 | # in the case of an error, let's just return an NA so 95 | # that we don't break stuffs 96 | 97 | return(NA) 98 | 99 | } 100 | 101 | 102 | ) 103 | } 104 | 105 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # For help debugging build failures open an issue on the RStudio community with the 'github-actions' tag. 2 | # https://community.rstudio.com/new-topic?category=Package%20development&tags=github-actions 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | pull_request: 9 | branches: 10 | - main 11 | - master 12 | 13 | name: R-CMD-check 14 | 15 | jobs: 16 | R-CMD-check: 17 | runs-on: ${{ matrix.config.os }} 18 | 19 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | config: 25 | - {os: windows-latest, r: 'release'} 26 | - {os: macOS-latest, r: 'release'} 27 | - {os: ubuntu-20.04, r: 'release', rspm: "https://packagemanager.rstudio.com/cran/__linux__/focal/latest"} 28 | - {os: ubuntu-20.04, r: 'devel', rspm: "https://packagemanager.rstudio.com/cran/__linux__/focal/latest"} 29 | 30 | env: 31 | R_REMOTES_NO_ERRORS_FROM_WARNINGS: true 32 | RSPM: ${{ matrix.config.rspm }} 33 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | steps: 36 | - uses: actions/checkout@v2 37 | 38 | - uses: r-lib/actions/setup-r@v1 39 | with: 40 | r-version: ${{ matrix.config.r }} 41 | 42 | - uses: r-lib/actions/setup-pandoc@v1 43 | 44 | - name: Query dependencies 45 | run: | 46 | install.packages('remotes') 47 | saveRDS(remotes::dev_package_deps(dependencies = TRUE), ".github/depends.Rds", version = 2) 48 | writeLines(sprintf("R-%i.%i", getRversion()$major, getRversion()$minor), ".github/R-version") 49 | shell: Rscript {0} 50 | 51 | - name: Cache R packages 52 | if: runner.os != 'Windows' 53 | uses: actions/cache@v2 54 | with: 55 | path: ${{ env.R_LIBS_USER }} 56 | key: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-${{ hashFiles('.github/depends.Rds') }} 57 | restore-keys: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1- 58 | 59 | - name: Install system dependencies 60 | if: runner.os == 'Linux' 61 | run: | 62 | while read -r cmd 63 | do 64 | eval sudo $cmd 65 | done < <(Rscript -e 'writeLines(remotes::system_requirements("ubuntu", "20.04"))') 66 | 67 | - name: Install dependencies 68 | run: | 69 | remotes::install_deps(dependencies = TRUE) 70 | remotes::install_cran("rcmdcheck") 71 | shell: Rscript {0} 72 | 73 | - name: Check 74 | env: 75 | _R_CHECK_CRAN_INCOMING_REMOTE_: false 76 | run: rcmdcheck::rcmdcheck(args = c("--no-manual", "--as-cran"), error_on = "warning", check_dir = "check") 77 | shell: Rscript {0} 78 | 79 | - name: Upload check results 80 | if: failure() 81 | uses: actions/upload-artifact@main 82 | with: 83 | name: ${{ runner.os }}-r${{ matrix.config.r }}-results 84 | path: check 85 | -------------------------------------------------------------------------------- /R/check_contrast.R: -------------------------------------------------------------------------------- 1 | 2 | #' Color Contrast Accessibility Report 3 | #' 4 | #' @description returns a report from the Contrast Checker API about color contrast for accessibility 5 | #' 6 | #' @param fg_col the Foreground Color 7 | #' @param bg_col the Background Color 8 | #' 9 | #' @return Color Contrast Report 10 | #' @export 11 | #' 12 | #' @examples 13 | #' 14 | #' check_contrast(fg_col = "#21EA06", bg_col = "#483D3D") 15 | #' 16 | #' 17 | 18 | 19 | 20 | 21 | check_contrast <- function(fg_col, bg_col){ 22 | 23 | # We need to be sure that the user provide valid HEX colors 24 | # here we focus on the minimum length (nchar) which is 25 | # 4 if # is included or 3 if # is not included 26 | 27 | 28 | if(grepl(pattern = "#", x = fg_col) && nchar(fg_col) < 4){ 29 | 30 | stop("invalid color provided in fg_color, please use valid HEX colors") 31 | 32 | } 33 | 34 | if(!grepl(pattern = "#", x = fg_col) && nchar(fg_col) < 3){ 35 | 36 | stop("invalid color provided in fg_color, few characters provided") 37 | 38 | } 39 | 40 | 41 | # Same stuff (copy and paste) for bg_col 42 | 43 | 44 | if(grepl(pattern = "#", x = bg_col) && nchar(bg_col) < 4){ 45 | 46 | stop("invalid color provided in bg_col, please use valid HEX colors") 47 | 48 | } 49 | 50 | if(!grepl(pattern = "#", x = bg_col) && nchar(bg_col) < 3){ 51 | 52 | stop("invalid color provided in bg_col, few characters provided") 53 | 54 | } 55 | 56 | 57 | # The API doesn't take into account the # so let's remove it. 58 | 59 | fg_col <- gsub(pattern = "#", replacement = "", x = fg_col) 60 | 61 | bg_col <- gsub(pattern = "#", replacement = "", x = bg_col) 62 | 63 | 64 | 65 | tryCatch( 66 | 67 | expr = { 68 | 69 | 70 | # Getting the response from the API. Here I use the glue package so that we 71 | # can interpolate the arguments (fg_col and bg_col) into the API call. 72 | 73 | 74 | response <- httr::GET(glue::glue("https://webaim.org/resources/contrastchecker/?fcolor={fg_col}&bcolor={bg_col}&api")) 75 | 76 | 77 | # Now we need to parse the response 78 | text <- httr::content(response, as = "parsed", encoding = "UTF-8") 79 | 80 | 81 | # Now let's print the results of the contrast checker 82 | # in the form of a report. I'll use the glue package 83 | # in conjunction with crayon to print out a nice 84 | # formatted and colorized text output to the console. 85 | 86 | return(glue::glue(" 87 | 88 | * The Contrast Ratio is {crayon::bold(text$ratio)} 89 | 90 | * The result for the AA check is : {if(text$AA == 'pass') 91 | {{crayon::green('PASS')}} else {{crayon::red('FAIL')}}} 92 | 93 | * The result for the AALarge check is : {if(text$AALarge == 'pass') 94 | {{crayon::green('PASS')}} else {{crayon::red('FAIL')}}} 95 | 96 | * The result for the AAA check is : {if(text$AAA == 'pass') 97 | {{crayon::green('PASS')}} else {{crayon::red('FAIL')}}} 98 | 99 | * The result for the AAALarge check is : {if(text$AAALarge == 'pass') 100 | {{crayon::green('PASS')}} else {{crayon::red('FAIL')}}} 101 | 102 | " 103 | 104 | ))}, 105 | 106 | 107 | error = function(cond){ 108 | 109 | # If there is an error, it would be cool to return the API response so that 110 | # the error can be investigated, right ? 111 | 112 | message(glue::glue("Error: here the API response status: {response$status_code}")) 113 | 114 | # in the case of an error, let's just return an NA so 115 | # that we don't break stuffs 116 | 117 | return(NA) 118 | 119 | } 120 | 121 | 122 | ) 123 | } 124 | -------------------------------------------------------------------------------- /R/add_description.R: -------------------------------------------------------------------------------- 1 | #' Add a description to an HTML element 2 | #' 3 | #' @param element an HTML element to describe 4 | #' @param descID the ID of the div that will describe the HTML element 5 | #' @param description the description of the HTML element 6 | #' @param visible should the description be visible ? Defaults to FALSE 7 | #' 8 | #' @return an HTML element with a description attached to it 9 | #' @export 10 | #' 11 | #' @examples 12 | #' 13 | #' if (interactive()) { 14 | #' ui <- fluidPage( 15 | #' h2("Using a screen reader 16 | #' hit or to 17 | #' navigate between the buttons 18 | #' and stop at button 5 to see the difference"), 19 | #' 20 | #' actionButton( 21 | #' inputId = "inp1", 22 | #' label = "button 1" 23 | #' ), 24 | #' actionButton( 25 | #' inputId = "inp2", 26 | #' label = "button 2" 27 | #' ), 28 | #' actionButton( 29 | #' inputId = "inp3", 30 | #' label = "button 3" 31 | #' ), 32 | #' actionButton( 33 | #' inputId = "inp4", 34 | #' label = "button 4" 35 | #' ), 36 | #' actionButton( 37 | #' inputId = "inp5", 38 | #' label = "button 5" 39 | #' ) %>% 40 | #' add_description( 41 | #' description = "hello this is a button 42 | #' when you click it you'll have a 43 | #' thing, when you don't click it you'll 44 | #' have another thing", 45 | #' descID = "chkoup" 46 | #' ) 47 | #' ) 48 | #' 49 | #' server <- function(input, output, session) {} 50 | #' 51 | #' shinyApp(ui, server) 52 | #' } 53 | add_description <- function(element, 54 | descID, 55 | description, 56 | visible = FALSE) { 57 | if (!visible) { 58 | htmltools::tagList( 59 | htmltools::tagAppendAttributes( 60 | element, 61 | "aria-describedby" = descID 62 | ), 63 | 64 | htmltools::tags$div( 65 | id = descID, 66 | description 67 | ), 68 | 69 | htmltools::tags$head(htmltools::tags$style(glue::glue( 70 | " 71 | #{descID} {{ 72 | position: absolute; 73 | left: -1000px; 74 | width: 1px; 75 | height: 1px; 76 | overflow: hidden; 77 | }} 78 | " 79 | ))) 80 | ) 81 | } else { 82 | htmltools::tagList( 83 | htmltools::tagAppendAttributes( 84 | element, 85 | "aria-describedby" = descID 86 | ), 87 | 88 | htmltools::tags$div( 89 | id = descID, 90 | description 91 | ) 92 | ) 93 | } 94 | } 95 | 96 | #' Describe an HTML element by another one 97 | #' 98 | #' @param element the HTML element to describe 99 | #' @param descID one or a vector of many HTML elements' 100 | #' that will be used to describe the 'element' parameter 101 | #' 102 | #' @return an HTML element described by another HTML element 103 | #' @export 104 | #' 105 | #' @examples 106 | #' if (interactive()) { 107 | #' ui <- fluidPage( 108 | #' h2("Using a screen reader 109 | #' hit Tab and Shift + Tab to 110 | #' navigate between the buttons 111 | #' and stop at button 2 to see the difference"), 112 | #' 113 | #' div( 114 | #' id = "paragraph", 115 | #' p("The following paragraph tag will be used as a descriptor") 116 | #' ), 117 | #' 118 | #' actionButton( 119 | #' inputId = "inp1", 120 | #' label = "button 1" 121 | #' ), 122 | #' actionButton( 123 | #' inputId = "inp2", 124 | #' label = "button 2" 125 | #' ) %>% 126 | #' describe_using( 127 | #' descID = "paragraph" 128 | #' ) 129 | #' ) 130 | #' 131 | #' server <- function(input, output, session) {} 132 | #' 133 | #' shinyApp(ui, server) 134 | #' } 135 | describe_using <- function(element, 136 | descID) { 137 | 138 | # vectors in R are atomic so if the first element is 139 | # a character the other elements must follow 140 | # so let's just assert for the first element 141 | 142 | firstElement <- descID[1] 143 | 144 | if (!is.character(firstElement)) { 145 | stop("'descID' parameter must be provided as a character string") 146 | } 147 | 148 | combine_ids <- function(..., sep = " ") { 149 | paste(..., collapse = sep) 150 | } 151 | 152 | htmltools::tagAppendAttributes( 153 | element, 154 | "aria-describedby" = combine_ids(descID) 155 | ) 156 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards 42 | of acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies 54 | when an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail 56 | address, posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at [INSERT CONTACT 63 | METHOD]. All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series of 85 | actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or permanent 92 | ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within the 112 | community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.0, 118 | available at https://www.contributor-covenant.org/version/2/0/ 119 | code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at https:// 128 | www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | 6 | 7 | ```{r, include = FALSE} 8 | knitr::opts_chunk$set( 9 | collapse = TRUE, 10 | comment = "#>", 11 | fig.path = "man/figures/README-", 12 | out.width = "100%" 13 | ) 14 | ``` 15 | 16 | # savonliquide 17 | 18 | 19 | 20 | [![CRAN_Status_Badge](https://www.r-pkg.org/badges/version/savonliquide)](https://cran.r-project.org/package=savonliquide) 21 | [![CRAN_time_from_release](https://www.r-pkg.org/badges/ago/savonliquide)](https://cran.r-project.org/package=savonliquide) 22 | [![metacran 23 | downloads](https://cranlogs.r-pkg.org/badges/savonliquide)](https://cran.r-project.org/package=savonliquide) 24 | [![metacran 25 | downloads](https://cranlogs.r-pkg.org/badges/grand-total/savonliquide)](https://cran.r-project.org/package=savonliquide) 26 | [![license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://choosealicense.com/licenses/mit/) 27 | [![R 28 | badge](https://img.shields.io/badge/Build%20with-♥%20and%20R-blue)](https://github.com/feddelegrand7/savonliquide) 29 | [![R build status](https://github.com/feddelegrand7/savonliquide/workflows/R-CMD-check/badge.svg)](https://github.com/feddelegrand7/savonliquide/actions) 30 | 31 | 32 | 33 | `savonliquide` is a package that contains a set of functions that helps you deal with accessibility issues in R, RMarkdown and Shiny. 34 | 35 | ## Installation 36 | 37 | You can install the stable version of `savonliquide` from [CRAN](https://CRAN.R-project.org) with: 38 | 39 | ```{r, eval=FALSE} 40 | 41 | install.packages("savonliquide") 42 | 43 | ``` 44 | 45 | or you can install the development version from `Github` with: 46 | 47 | ```{r, eval=FALSE} 48 | 49 | remotes::install_github("feddelegrand7/savonliquide") 50 | 51 | ``` 52 | 53 | # 1. `check_contrast()` and `check_contrast_raw()` 54 | 55 | `check_contrast()` allows you to generate a report from the [Contrast Checker API](https://webaim.org/resources/contrastchecker/). The report will display a contrast ratio among other useful information that will judge the accessibility level of your color contrast. 56 | 57 | ## Examples 58 | 59 | The following plot is awful in terms of color contrast. The two main colors used are : `#FDF6E3` (for points) and `#C6F6E3"` for the background. 60 | 61 | ```{r} 62 | 63 | library(ggplot2) 64 | 65 | ggplot(mpg, aes(hwy, cty)) + 66 | geom_point(color = "#FDF6E3") + 67 | theme(panel.background = element_rect(fill = "#C6F6E3")) 68 | 69 | 70 | 71 | ``` 72 | 73 | Let's check the contrast: 74 | 75 | ```{r} 76 | library(savonliquide) 77 | # fg_col stands for foreground color 78 | # bg_col stands for background color 79 | 80 | 81 | check_contrast(fg_col = "#FDF6E3", 82 | bg_col = "#C6F6E3") 83 | 84 | 85 | ``` 86 | 87 | You can see that the report indicates that we've a very low Contrast Ratio and that we've failed all the recommended standards (In the web development industry we consider mostly the AA standard). 88 | 89 | Now let's experiment other colors combinations. Let's go for: 90 | 91 | - Foreground color: #0000FF 92 | 93 | - Background color: #FFFFFF 94 | 95 | ```{r} 96 | 97 | check_contrast(fg_col = "#0000FF", 98 | bg_col = "#FFFFFF") 99 | 100 | 101 | ``` 102 | 103 | Awesome ! we've got an excellent Contrast Ratio and we've passed all the accessibility standards. Let's use the above colors to render our plot again: 104 | 105 | ```{r} 106 | 107 | ggplot(mpg, aes(hwy, cty)) + 108 | geom_point(color = "#0000FF") + 109 | theme(panel.background = element_rect(fill = "#FFFFFF")) 110 | 111 | ``` 112 | 113 | The `check_contrast_raw()` function allows you to get the same information but in a raw format. To be more precise, you get a `list` object. 114 | 115 | ```{r} 116 | 117 | check_contrast_raw(fg_col = "#0000FF", bg_col = "#FFFFFF") 118 | 119 | 120 | 121 | ``` 122 | 123 | ```{r} 124 | 125 | paste0("This is a good Contrast Ratio ", 126 | check_contrast_raw(fg_col = "#0000FF", bg_col = "#FFFFFF")$ratio) 127 | 128 | 129 | ``` 130 | 131 | # 2. `add_description()` 132 | 133 | The `add_description()` function works in RMarkdown documents (HTML output) and Shiny application. It creates an HTML element that will be used to describe another element. When a screen-reader user focuses on the former, he will hear the vocal description provided by the latter. Let's dig into an example in Shiny. Note that in order to experiment the following example, you must install and enable a screen reader of your choice (I'm using the Chrome Vox Google Chrome extension). 134 | 135 | ```{r, eval = FALSE} 136 | library(savonliquide) 137 | library(shiny) 138 | library(magrittr) 139 | 140 | ui <- fluidPage( 141 | 142 | h2("Using a screen reader 143 | hit Tab and Shift + Tab to 144 | navigate between the buttons 145 | and stop at button 2 to see the difference"), 146 | 147 | actionButton( 148 | inputId = "inp1", 149 | label = "button 1" 150 | ), 151 | actionButton( 152 | inputId = "inp2", 153 | label = "button 2" 154 | ) %>% 155 | add_description( 156 | description = "hello this is a button 157 | when you click it you'll have a 158 | thing, when you don't click it you'll 159 | have another thing", 160 | descID = "descriptor" # the ID of the element that will be created and used as a descriptor 161 | ) 162 | ) 163 | 164 | server <- function(input, output, session) {} 165 | 166 | shinyApp(ui, server) 167 | ``` 168 | 169 | By default the description will be invisible for non screen-reader users but you can set it to be visible by setting the parameter `visible` to `TRUE`. 170 | 171 | 172 | # 3. `describe_using()` 173 | 174 | `describe_using()` relies on a **preexisting** HTML element to describe another element. Note that you can use many elements to describe one. 175 | 176 | ```{r, eval = FALSE} 177 | library(savonliquide) 178 | library(shiny) 179 | library(magrittr) 180 | 181 | ui <- fluidPage( 182 | 183 | h2("Using a screen reader 184 | hit Tab and Shift + Tab to 185 | navigate between the buttons 186 | and stop at button 2 to see the difference"), 187 | 188 | div( 189 | id = "paragraph1", 190 | p("The following paragraph tag will be used as the first descriptor") 191 | ), 192 | 193 | div( 194 | id = "paragraph2", 195 | p("The following paragraph tag will be used as the second descriptor") 196 | ), 197 | 198 | actionButton( 199 | inputId = "inp1", 200 | label = "button 1" 201 | ), 202 | actionButton( 203 | inputId = "inp2", 204 | label = "button 2" 205 | ) %>% 206 | describe_using( 207 | descID = c("paragraph1", "paragraph2") 208 | ) 209 | ) 210 | 211 | server <- function(input, output, session) {} 212 | 213 | shinyApp(ui, server) 214 | ``` 215 | 216 | # 4. `create_invisible_anchor()` 217 | 218 | This functions allows you to create an invisible HTML anchor tag that will be heard only by screen-reader users. The anchor will be used to redirect them to a particular web page that could be more adapted to their condition. 219 | 220 | 221 | ```{r, eval = FALSE} 222 | library(savonliquide) 223 | library(shiny) 224 | library(magrittr) 225 | 226 | ui <- fluidPage( 227 | 228 | h2("Using a screen reader 229 | hit or to 230 | navigate until you discover the invisible anchor tag"), 231 | 232 | actionButton( 233 | inputId = "inp1", 234 | label = "button 1" 235 | ), 236 | actionButton( 237 | inputId = "inp2", 238 | label = "button 2" 239 | ), 240 | 241 | create_invisible_anchor( 242 | id = "invisible-anchor", 243 | text = "Hit ENTER if you want to be redirected to google.com", 244 | href = "https://google.com" 245 | ) 246 | ) 247 | 248 | server <- function(input, output, session) {} 249 | 250 | shinyApp(ui, server) 251 | ``` 252 | 253 | # 5. `make_skiplinks()` 254 | 255 | Often, in heavy website or application, you might want a screen-reader user to have the choice to skip superfluous HTML elements and go directly to the main-content. In such situation, you might consider skiplinks! just try out with the following example: 256 | 257 | ```{r, eval = FALSE} 258 | 259 | library(savonliquide) 260 | library(shiny) 261 | library(magrittr) 262 | 263 | ui <- fluidPage( 264 | 265 | h2("Using a screen reader 266 | hit or to 267 | navigate until you get asked 268 | if you want to skip to the main content"), 269 | 270 | a("do you want to be redirected to the main content ?", 271 | id = "skiplink") %>% 272 | make_skiplinks( 273 | skip_to = "#main-content", # note that we need to append '#' to the ID 274 | bg_color = "red", 275 | col = "white" 276 | ), 277 | 278 | actionButton( 279 | inputId = "inp1", 280 | label = "button 1" 281 | ), 282 | actionButton( 283 | inputId = "inp2", 284 | label = "button 2" 285 | ), 286 | 287 | div( 288 | id = "main-content", 289 | h1("The main content starts here") 290 | ) 291 | 292 | ) 293 | 294 | server <- function(input, output, session) {} 295 | 296 | shinyApp(ui, server) 297 | ``` 298 | 299 | # 6. `make_tabbable()` 300 | 301 | Some HTML elements (for example `buttons` or `anchors`) get focusable by default when you hit or to navigate within a web page/app, you can say that these elements are **tabbable**, in other words the screen-reader focuses on theses elements when you hit tab. When developing a website/app you might consider enabling this feature for elements that are not tabbable by default (for example paragraph and titles) or desabling this feature for elements that are tabbable by default. Consider the following example: 302 | 303 | 304 | ```{r, eval = FALSE} 305 | 306 | library(savonliquide) 307 | library(shiny) 308 | library(magrittr) 309 | 310 | ui <- fluidPage( 311 | 312 | h2("Using a screen reader 313 | hit or to 314 | navigate and see which element is tabbable or not"), 315 | 316 | actionButton( 317 | inputId = "inp1", 318 | label = "button 1" 319 | ), 320 | 321 | p("This is the first paragraph"), 322 | 323 | 324 | actionButton( 325 | inputId = "inp2", 326 | label = "button 2" 327 | ), 328 | 329 | p("This is the second paragraph") 330 | 331 | ) 332 | 333 | server <- function(input, output, session) {} 334 | 335 | shinyApp(ui, server) 336 | 337 | ``` 338 | 339 | When you navigate the application, you can see that only the buttons get focusable when you hit the key. We can change this behavior, let's make the title and the two paragraphs tabbable and the buttons untabbable and observe the difference. 340 | 341 | ```{r, eval=FALSE} 342 | 343 | library(savonliquide) 344 | library(shiny) 345 | library(magrittr) 346 | 347 | ui <- fluidPage( 348 | 349 | h2("Using a screen reader 350 | hit or to 351 | navigate and see which element is tabbable or not") %>% 352 | make_tabable(tab_index = 0), 353 | 354 | actionButton( 355 | inputId = "inp1", 356 | label = "button 1" 357 | ) %>% 358 | make_tabable(tab_index = -1) # a negative value will make it untabbable 359 | , 360 | 361 | p("This is the first paragraph") %>% 362 | make_tabable(tab_index = 0), 363 | 364 | 365 | actionButton( 366 | inputId = "inp2", 367 | label = "button 2" 368 | ) %>% 369 | make_tabable(tab_index = -1), 370 | 371 | p("This is the second paragraph") %>% 372 | make_tabable(tab_index = 0) 373 | 374 | ) 375 | 376 | server <- function(input, output, session) {} 377 | 378 | shinyApp(ui, server) 379 | 380 | ``` 381 | ## Code of Conduct 382 | 383 | Please note that the savonliquide project is released with a [Contributor Code of Conduct](https://contributor-covenant.org/version/2/0/CODE_OF_CONDUCT.html). By contributing to this project, you agree to abide by its terms. 384 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # savonliquide 5 | 6 | 7 | 8 | [![CRAN\_Status\_Badge](https://www.r-pkg.org/badges/version/savonliquide)](https://cran.r-project.org/package=savonliquide) 9 | [![CRAN\_time\_from\_release](https://www.r-pkg.org/badges/ago/savonliquide)](https://cran.r-project.org/package=savonliquide) 10 | [![metacran 11 | downloads](https://cranlogs.r-pkg.org/badges/savonliquide)](https://cran.r-project.org/package=savonliquide) 12 | [![metacran 13 | downloads](https://cranlogs.r-pkg.org/badges/grand-total/savonliquide)](https://cran.r-project.org/package=savonliquide) 14 | [![license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://choosealicense.com/licenses/mit/) 15 | [![R 16 | badge](https://img.shields.io/badge/Build%20with-♥%20and%20R-blue)](https://github.com/feddelegrand7/savonliquide) 17 | [![R build 18 | status](https://github.com/feddelegrand7/savonliquide/workflows/R-CMD-check/badge.svg)](https://github.com/feddelegrand7/savonliquide/actions) 19 | 20 | 21 | 22 | `savonliquide` is a package that contains a set of functions that helps 23 | you deal with accessibility issues in R, RMarkdown and Shiny. 24 | 25 | ## Installation 26 | 27 | You can install the stable version of `savonliquide` from 28 | [CRAN](https://CRAN.R-project.org) with: 29 | 30 | ``` r 31 | install.packages("savonliquide") 32 | ``` 33 | 34 | or you can install the development version from `Github` with: 35 | 36 | ``` r 37 | remotes::install_github("feddelegrand7/savonliquide") 38 | ``` 39 | 40 | ### List of functions: 41 | ----------------------------------- 42 | - [1. `check_contrast()` and `check_contrast_raw()`](#%201.%20`check_contrast()`%20and%20`check_contrast_raw()`) 43 | - [2. `add_description()`](#%202.%20`add_description()`) 44 | - [3. `describe_using()`](#%203.%20`describe_using()`) 45 | - [4. `create_invisible_anchor()`](#%204.%20`create_invisible_anchor()`) 46 | - [5. `make_skiplinks()`](#%205.%20`make_skiplinks()`) 47 | - [6. `make_tabbable()`](#%206.%20`make_tabbable()`) 48 | 49 | # 1. `check_contrast()` and `check_contrast_raw()` 50 | 51 | `check_contrast()` allows you to generate a report from the [Contrast 52 | Checker API](https://webaim.org/resources/contrastchecker/). The report 53 | will display a contrast ratio among other useful information that will 54 | judge the accessibility level of your color contrast. 55 | 56 | ## Examples 57 | 58 | The following plot is awful in terms of color contrast. The two main 59 | colors used are : `#FDF6E3` (for points) and `#C6F6E3"` for the 60 | background. 61 | 62 | ``` r 63 | library(ggplot2) 64 | #> Warning: package 'ggplot2' was built under R version 4.0.3 65 | 66 | ggplot(mpg, aes(hwy, cty)) + 67 | geom_point(color = "#FDF6E3") + 68 | theme(panel.background = element_rect(fill = "#C6F6E3")) 69 | ``` 70 | 71 | 72 | 73 | Let’s check the contrast: 74 | 75 | ``` r 76 | library(savonliquide) 77 | # fg_col stands for foreground color 78 | # bg_col stands for background color 79 | 80 | 81 | check_contrast(fg_col = "#FDF6E3", 82 | bg_col = "#C6F6E3") 83 | #> 84 | #> * The Contrast Ratio is 1.10 85 | #> 86 | #> * The result for the AA check is : FAIL 87 | #> 88 | #> * The result for the AALarge check is : FAIL 89 | #> 90 | #> * The result for the AAA check is : FAIL 91 | #> 92 | #> * The result for the AAALarge check is : FAIL 93 | ``` 94 | 95 | You can see that the report indicates that we’ve a very low Contrast 96 | Ratio and that we’ve failed all the recommended standards (In the web 97 | development industry we consider mostly the AA standard). 98 | 99 | Now let’s experiment other colors combinations. Let’s go for: 100 | 101 | - Foreground color: \#0000FF 102 | 103 | - Background color: \#FFFFFF 104 | 105 | ``` r 106 | check_contrast(fg_col = "#0000FF", 107 | bg_col = "#FFFFFF") 108 | #> 109 | #> * The Contrast Ratio is 8.59 110 | #> 111 | #> * The result for the AA check is : PASS 112 | #> 113 | #> * The result for the AALarge check is : PASS 114 | #> 115 | #> * The result for the AAA check is : PASS 116 | #> 117 | #> * The result for the AAALarge check is : PASS 118 | ``` 119 | 120 | Awesome ! we’ve got an excellent Contrast Ratio and we’ve passed all the 121 | accessibility standards. Let’s use the above colors to render our plot 122 | again: 123 | 124 | ``` r 125 | ggplot(mpg, aes(hwy, cty)) + 126 | geom_point(color = "#0000FF") + 127 | theme(panel.background = element_rect(fill = "#FFFFFF")) 128 | ``` 129 | 130 | 131 | 132 | The `check_contrast_raw()` function allows you to get the same 133 | information but in a raw format. To be more precise, you get a `list` 134 | object. 135 | 136 | ``` r 137 | check_contrast_raw(fg_col = "#0000FF", bg_col = "#FFFFFF") 138 | #> $ratio 139 | #> [1] "8.59" 140 | #> 141 | #> $AA 142 | #> [1] "pass" 143 | #> 144 | #> $AALarge 145 | #> [1] "pass" 146 | #> 147 | #> $AAA 148 | #> [1] "pass" 149 | #> 150 | #> $AAALarge 151 | #> [1] "pass" 152 | ``` 153 | 154 | ``` r 155 | paste0("This is a good Contrast Ratio ", 156 | check_contrast_raw(fg_col = "#0000FF", bg_col = "#FFFFFF")$ratio) 157 | #> [1] "This is a good Contrast Ratio 8.59" 158 | ``` 159 | 160 | # 2. `add_description()` 161 | 162 | The `add_description()` function works in RMarkdown documents (HTML 163 | output) and Shiny application. It creates an HTML element that will be 164 | used to describe another element. When a screen-reader user focuses on 165 | the latter, he will hear the vocal description provided by the form (the HTML element created by the function). 166 | Let’s dig into an example in Shiny. Note that in order to experiment the 167 | following example, you must install and enable the screen reader of your 168 | choice (I’m using the Chrome Vox Google Chrome extension). 169 | 170 | ``` r 171 | library(savonliquide) 172 | library(shiny) 173 | library(magrittr) 174 | 175 | ui <- fluidPage( 176 | 177 | h2("Using a screen reader 178 | hit Tab and Shift + Tab to 179 | navigate between the buttons 180 | and stop at button 2 to see the difference"), 181 | 182 | actionButton( 183 | inputId = "inp1", 184 | label = "button 1" 185 | ), 186 | actionButton( 187 | inputId = "inp2", 188 | label = "button 2" 189 | ) %>% 190 | add_description( 191 | description = "hello this is a button 192 | when you click it you'll have a 193 | thing, when you don't click it you'll 194 | have another thing", 195 | descID = "descriptor" # the ID of the element that will be created and used as a descriptor 196 | ) 197 | ) 198 | 199 | server <- function(input, output, session) {} 200 | 201 | shinyApp(ui, server) 202 | ``` 203 | 204 | By default the description will be invisible for non screen-reader users 205 | but you can set it to be visible by setting the parameter `visible` to 206 | `TRUE`. 207 | 208 | # 3. `describe_using()` 209 | 210 | `describe_using()` relies on a **preexisting** HTML element to describe 211 | another element. Note that you can use many elements to describe one. 212 | 213 | ``` r 214 | library(savonliquide) 215 | library(shiny) 216 | library(magrittr) 217 | 218 | ui <- fluidPage( 219 | 220 | h2("Using a screen reader 221 | hit Tab and Shift + Tab to 222 | navigate between the buttons 223 | and stop at button 2 to see the difference"), 224 | 225 | div( 226 | id = "paragraph1", 227 | p("The following paragraph tag will be used as the first descriptor") 228 | ), 229 | 230 | div( 231 | id = "paragraph2", 232 | p("The following paragraph tag will be used as the second descriptor") 233 | ), 234 | 235 | actionButton( 236 | inputId = "inp1", 237 | label = "button 1" 238 | ), 239 | actionButton( 240 | inputId = "inp2", 241 | label = "button 2" 242 | ) %>% 243 | describe_using( 244 | descID = c("paragraph1", "paragraph2") 245 | ) 246 | ) 247 | 248 | server <- function(input, output, session) {} 249 | 250 | shinyApp(ui, server) 251 | ``` 252 | 253 | # 4. `create_invisible_anchor()` 254 | 255 | This functions allows you to create an invisible HTML anchor tag that 256 | will be heard only by screen-reader users. The anchor will be used to 257 | redirect them to a particular web page that could be more adapted to 258 | their condition. 259 | 260 | ``` r 261 | library(savonliquide) 262 | library(shiny) 263 | library(magrittr) 264 | 265 | ui <- fluidPage( 266 | 267 | h2("Using a screen reader 268 | hit or to 269 | navigate until you discover the invisible anchor tag"), 270 | 271 | actionButton( 272 | inputId = "inp1", 273 | label = "button 1" 274 | ), 275 | actionButton( 276 | inputId = "inp2", 277 | label = "button 2" 278 | ), 279 | 280 | create_invisible_anchor( 281 | id = "invisible-anchor", 282 | text = "Hit ENTER if you want to be redirected to google.com", 283 | href = "https://google.com" 284 | ) 285 | ) 286 | 287 | server <- function(input, output, session) {} 288 | 289 | shinyApp(ui, server) 290 | ``` 291 | 292 | # 5. `make_skiplinks()` 293 | 294 | Often, in heavy website or application, you might want a screen-reader 295 | user to have the choice to skip superfluous HTML elements and go 296 | directly to the main-content. In such situation, you might consider 297 | skiplinks! just try out with the following example: 298 | 299 | ``` r 300 | library(savonliquide) 301 | library(shiny) 302 | library(magrittr) 303 | 304 | ui <- fluidPage( 305 | 306 | h2("Using a screen reader 307 | hit or to 308 | navigate until you get asked 309 | if you want to skip to the main content"), 310 | 311 | a("do you want to be redirected to the main content ?", 312 | id = "skiplink") %>% 313 | make_skiplinks( 314 | skip_to = "#main-content", # note that we need to append '#' to the ID 315 | bg_color = "red", 316 | col = "white" 317 | ), 318 | 319 | actionButton( 320 | inputId = "inp1", 321 | label = "button 1" 322 | ), 323 | actionButton( 324 | inputId = "inp2", 325 | label = "button 2" 326 | ), 327 | 328 | div( 329 | id = "main-content", 330 | h1("The main content starts here") 331 | ) 332 | 333 | ) 334 | 335 | server <- function(input, output, session) {} 336 | 337 | shinyApp(ui, server) 338 | ``` 339 | 340 | # 6. `make_tabbable()` 341 | 342 | Some HTML elements (for example `buttons` or `anchors`) get focusable by 343 | default when you hit or <SHIFT+Tab> to navigate within a web 344 | page/app, you can say that these elements are **tabbable**, in other 345 | words the screen-reader focuses on theses elements when you hit tab. 346 | When developing a website/app you might consider enabling this feature 347 | for elements that are not tabbable by default (for example paragraph and 348 | titles) or desabling this feature for elements that are tabbable by 349 | default. Consider the following example: 350 | 351 | ``` r 352 | library(savonliquide) 353 | library(shiny) 354 | library(magrittr) 355 | 356 | ui <- fluidPage( 357 | 358 | h2("Using a screen reader 359 | hit or to 360 | navigate and see which element is tabbable or not"), 361 | 362 | actionButton( 363 | inputId = "inp1", 364 | label = "button 1" 365 | ), 366 | 367 | p("This is the first paragraph"), 368 | 369 | 370 | actionButton( 371 | inputId = "inp2", 372 | label = "button 2" 373 | ), 374 | 375 | p("This is the second paragraph") 376 | 377 | ) 378 | 379 | server <- function(input, output, session) {} 380 | 381 | shinyApp(ui, server) 382 | ``` 383 | 384 | When you navigate the application, you can see that only the buttons get 385 | focusable when you hit the key. We can change this behavior, let’s 386 | make the title and the two paragraphs tabbable and the buttons 387 | untabbable and observe the difference. 388 | 389 | ``` r 390 | library(savonliquide) 391 | library(shiny) 392 | library(magrittr) 393 | 394 | ui <- fluidPage( 395 | 396 | h2("Using a screen reader 397 | hit or to 398 | navigate and see which element is tabbable or not") %>% 399 | make_tabable(tab_index = 0), 400 | 401 | actionButton( 402 | inputId = "inp1", 403 | label = "button 1" 404 | ) %>% 405 | make_tabable(tab_index = -1) # a negative value will make it untabbable 406 | , 407 | 408 | p("This is the first paragraph") %>% 409 | make_tabable(tab_index = 0), 410 | 411 | 412 | actionButton( 413 | inputId = "inp2", 414 | label = "button 2" 415 | ) %>% 416 | make_tabable(tab_index = -1), 417 | 418 | p("This is the second paragraph") %>% 419 | make_tabable(tab_index = 0) 420 | 421 | ) 422 | 423 | server <- function(input, output, session) {} 424 | 425 | shinyApp(ui, server) 426 | ``` 427 | 428 | ## Code of Conduct 429 | 430 | Please note that the savonliquide project is released with a 431 | [Contributor Code of 432 | Conduct](https://contributor-covenant.org/version/2/0/CODE_OF_CONDUCT.html). 433 | By contributing to this project, you agree to abide by its terms. 434 | --------------------------------------------------------------------------------