-
-
Notifications
You must be signed in to change notification settings - Fork 3
Description
In April 2026, WCAG 2.1AA is going to be federally mandated in the US for all University-built websites/webapps, so we're being asked to make all our Shiny apps compliant in advance of that deadline.
Unfortunately, the disconnect message generated by shinydisconnect gets flagged by our Office of Digital Accessibility in several respects. I know I had raised this issue before, but I had only identified part of the issue at that time. The issues are:
- Text being stored in the content css property being inaccessible to screen readers.
- The disconnect screen not getting focus or being focusable or being announced to screen readers.
So my options were to A) Stop using shinydisconnect (not my preference!) or else patch this somehow.
FWIW, ChatGPT whipped up the following (with my coaching). It shows a reprex containing the problem and a drop-in JS-based fix that seems to work, at least so far as I can tell, along with its pattern of usage:
library(shiny)
library(shinydisconnect)
# JavaScript patch to make disconnect screen accessible
accessibleDisconnectMessage <- function() {
tags$script(HTML("
$(document).on('shiny:disconnected', function(event) {
var el = document.getElementById('ss-connect-dialog');
if (el) {
// Remove ::before content to avoid duplicate screen reader text
var style = document.createElement('style');
style.innerHTML = `
#ss-connect-dialog::before,
#ss-connect-dialog a::before {
content: none !important;
}
#ss-connect-dialog a {
display: inline !important;
font-size: unset !important;
}
`;
document.head.appendChild(style);
// Create accessible alert container
var msgContainer = document.createElement('div');
msgContainer.innerHTML = `
<p>The app has disconnected. This may be due to inactivity or a system error. To try again, please refresh the page.</p>
`;
msgContainer.setAttribute('role', 'alertdialog');
msgContainer.setAttribute('tabindex', '-1');
msgContainer.setAttribute('aria-label', 'Disconnected');
msgContainer.style.outline = 'none';
msgContainer.style.marginBottom = '1em';
// Insert at the beginning of the dialog
el.insertBefore(msgContainer, el.firstChild);
// Move focus for screen reader / keyboard users
msgContainer.focus();
// Fix empty reload link if needed
var reloadLink = document.getElementById('ss-reload-link');
if (reloadLink && reloadLink.innerText.trim() === '') {
reloadLink.innerText = 'Refresh the page';
}
}
});
"))
}
ui <- fluidPage(
titlePanel("Disconnect Accessibility Reprex"),
p("Leave this app idle or kill its R process to trigger a disconnect."),
disconnectMessage(
text = "Disconnected", # Gets overridden anyway
refresh = "Refresh", # Also overridden
width = "full",
top = "center",
size = 22,
background = "#7a0019",
colour = "white",
refreshColour = "#ffb71e",
css = "z-index: 100000 !important;"
),
tags$head(accessibleDisconnectMessage())
)
server <- function(input, output, session) {}
shinyApp(ui, server)
You can see that none of the errors/issues raised have anything to do with the disconnect message when you check it using the WAVE tool:
So I think this would satisfy from a compliance standpoint. So, I include it as a resource for anyone else who happens to find themselves in a similar boat.
FWIW, ChatGPT thinks that addressing the a11y concerns here could be done declaratively using htmltools without any JS needed, but I'm frankly far too web-stupid to know if that's actually true. Its fix would involve deleting this bit:
"#ss-connect-dialog::before { content: '{{text}}' }",
"#ss-connect-dialog a::before {
content: '{{refresh}}';
font-size: {{size}}px;
}",
Adding this bit instead:
htmltools::tags$div(
id = "ss-connect-dialog",
role = "alertdialog",
tabindex = "-1",
`aria-label` = "Disconnection message",
htmltools::tags$p(text),
if (refresh != "") htmltools::tags$a(
id = "ss-reload-link",
href = "",
style = "display: block !important; font-size: inherit;",
refresh
)
)
And adjusting this region:
"#ss-connect-dialog a {
display: {{ if (refresh == '') 'none' else 'block' }} !important;
color: {{refreshColour}} !important;
margin-top: {{size}}px !important;
font-size: {{size}}px !important;
font-weight: normal !important;
}"
I have no idea if that's just AI bluster/nonsense, but it doesn't make 0 sense to me. I appreciate the need to avoid JS for consistency/reliability sake, although the JS above does seem to work, though I am an n of 1 for how universal a fix it is...
Anyway, thought I'd share so that this package can continue to get used post-WCAG 2.1AA! LMK if you think a PR to try something like this would be worthwhile...