javascriptrshinydt

How to make DT datatable factor filter dropdowns keyboard accessible (ADA compliant) in R Shiny?


The Goal

I am building an R Shiny application with a DT::datatable that has column filters (filter = 'top'). For columns that are factors, DT correctly creates a dropdown menu (using selectize.js) that can be used with a mouse.

My goal is to make these dropdown filters fully keyboard accessible for ADA compliance. A user should be able to:

  1. Use the Tab key to navigate to a factor column's filter input.
  2. Press Enter or Space to open the dropdown list of options.
  3. Use the arrow keys to select an option and Enter to confirm.

The Problem

While I can tab to the filter input, pressing Enter or Space does nothing. The dropdown menu does not open, making it impossible for a keyboard-only user to filter these columns. The text input filters work fine with the keyboard, but the selectize.js factor filters do not.

What I've Tried

I have tried to use both Gemini and ChatGPT to come up with a solution but neither of them is able to deliver it. According to Gemini, "I understand that this requires custom JavaScript, likely in a DT callback. I have tried multiple approaches using both drawCallback and initComplete to attach keydown event listeners, but they have all failed, likely due to timing issues where the JavaScript runs before the selectize inputs are fully initialized by the DT library, or the event listeners are not being attached correctly. The solutions either don't work at all or work unreliably"

Minimal Reproducible Example

Here is a simple, self-contained Shiny app that demonstrates the exact problem using the standard iris dataset.

# install.packages(c("shiny", "DT", "dplyr"))
library(shiny)
library(DT)
library(dplyr)

# 1. Prepare the data with a factor column
iris_data <- iris %>%
  mutate(Species = as.factor(Species))

# 2. Define the UI
ui <- fluidPage(
  titlePanel("DT Accessibility Issue Example"),
  h4("Goal: Use the keyboard (Tab, then Enter/Space) to open the 'Species' filter dropdown."),
  hr(),
  DT::DTOutput("my_table")
)

# 3. Define the Server
server <- function(input, output, session) {
  output$my_table <- DT::renderDT({
    DT::datatable(
      iris_data,
      filter = "top",
      rownames = FALSE,
      options = list(
        pageLength = 5
        # I have tried adding various JS callbacks here without success
      )
    )
  })
}

# 4. Run the app
shinyApp(ui, server)

The Question

What is the definitive, robust JavaScript code—likely using a callback like initComplete or drawCallback—required to make the selectize.js dropdown filters in a DT table keyboard accessible? Specifically, how can I reliably attach a keydown event listener for the "Enter" and "Space" keys to trigger the opening of the dropdown menu?


Solution

  • You can add keypress event listeners to each queried input element using a for.Each loop and if the key is pressed, trigger a click-event. Pressing any key whilst the input is selected will trigger a click which will then show the dropdown.

    Specifically, how can I reliably attach a keydown event listener for the "Enter" and "Space" keys to trigger the opening of the dropdown menu?

    You can filter for the keycode, to only click, if space or enter were pressed

    if(event.keyCode == 13 | event.keyCode == 32) el.click();
    

    Me personnally, I would ommit this filter, because then the user can just free-type search and the dropdown will show fields based on that. It makes for better UX in my opinion.


    library(shiny)
    library(DT)
    library(dplyr)
    
    iris_data <- iris %>%
      mutate(Species = as.factor(Species))
    
    ui <- fluidPage(
      titlePanel("DT Accessibility Issue Example"),
      h4("Goal: Use the keyboard (Tab, then Enter/Space) to open the 'Species' filter dropdown."),
      hr(),
      DT::DTOutput("my_table")
    )
    
    server <- \(input, output, session) {
      output$my_table <- DT::renderDT({
        DT::datatable(
          iris_data,
          filter = "top",
          rownames = FALSE,
          options = list(
            pageLength = nrow(iris_data)
          ),
          callback = JS("
            document.querySelectorAll(\"div.form-group input[type='search']\").forEach(function (el, ind) {
               el.addEventListener(\"keypress\", (event) => {
                  // only click on enter (13) and space (32)  
                  if(event.keyCode == 13 | event.keyCode == 32) el.click();
               });
            });
          ")
        )
      })
    }
    
    shinyApp(ui, server)
    

    ou