Found while adding automated Playwright self-scan coverage for the axe accessibility-checker's own report chrome (it audits the page, then injects a report UI that never gets audited itself unless a test scans it directly).
axe rule: scrollable-region-focusable · impact: serious · tag: wcag2a (2.1.1, 2.1.3)
Targets: the RevealJS report slide (section.quarto-axe-report-slide) and the Dashboard offcanvas body (.offcanvas-body inside #quarto-axe-offcanvas).
When the report grows past its container's height, both the reveal report slide and the dashboard offcanvas body become scrollable (overflow-y: auto), but neither element nor any descendant is keyboard-focusable, so keyboard-only users can't reach the rest of the violations.
The HTML overlay variant already handles this case:
|
createReportOverlay() { |
|
const reportElement = this.createReportElement(); |
|
// The fixed overlay scrolls (max-height: 50vh + overflow-y: auto), so it |
|
// must be keyboard-focusable or it fails axe's own |
|
// scrollable-region-focusable rule once the report overflows. The role and |
|
// label give the resulting tab stop an accessible name. The reveal slide |
|
// and dashboard offcanvas variants don't scroll the report element itself, |
|
// so they skip the extra tab stop. |
|
reportElement.tabIndex = 0; |
|
reportElement.setAttribute("role", "region"); |
|
reportElement.setAttribute("aria-label", "Accessibility report"); |
|
(document.querySelector("main") || document.body).appendChild(reportElement); |
|
} |
The comment there reasons the reveal/dashboard variants don't need the same tab stop because "they don't scroll the report element itself" — true of the inner .quarto-axe-report, but the scrolling ancestor (the reveal slide / .offcanvas-body) is a different element the comment doesn't account for:
|
createReportSlide() { |
|
const slidesContainer = document.querySelector(".reveal .slides"); |
|
if (!slidesContainer) { |
|
this.createReportOverlay(); |
|
return; |
|
} |
|
|
|
const section = document.createElement("section"); |
|
section.className = "slide quarto-axe-report-slide scrollable"; |
|
// Force a white slide background so the report stays readable regardless |
|
// of the deck's brand/theme colors. Reveal applies this to the slide's |
|
// generated `.slide-background` element during sync(). |
|
section.setAttribute("data-background-color", "#fff"); |
|
|
|
const title = document.createElement("h2"); |
|
title.textContent = "Accessibility Report"; |
|
section.appendChild(title); |
|
|
|
section.appendChild(this.createReportElement()); |
|
slidesContainer.appendChild(section); |
|
|
|
// sync() registers the new slide but doesn't update visibility classes. |
|
// Re-navigating to the current slide triggers the visibility update so |
|
// the report slide gets the correct past/present/future class. |
|
const indices = Reveal.getIndices(); |
|
Reveal.sync(); |
|
Reveal.slide(indices.h, indices.v); |
|
} |
|
createReportOffcanvas() { |
|
const offcanvas = document.createElement("div"); |
|
offcanvas.className = "offcanvas offcanvas-end quarto-axe-offcanvas"; |
|
offcanvas.id = "quarto-axe-offcanvas"; |
|
offcanvas.tabIndex = -1; |
|
offcanvas.setAttribute("aria-labelledby", "quarto-axe-offcanvas-label"); |
|
offcanvas.setAttribute("data-bs-scroll", "true"); |
|
offcanvas.setAttribute("data-bs-backdrop", "false"); |
|
|
|
const header = document.createElement("div"); |
|
header.className = "offcanvas-header"; |
|
|
|
const title = document.createElement("h5"); |
|
title.className = "offcanvas-title"; |
|
title.id = "quarto-axe-offcanvas-label"; |
|
title.textContent = "Accessibility Report"; |
|
header.appendChild(title); |
|
|
|
const closeBtn = document.createElement("button"); |
|
closeBtn.type = "button"; |
|
closeBtn.className = "btn-close"; |
|
closeBtn.setAttribute("data-bs-dismiss", "offcanvas"); |
|
closeBtn.setAttribute("aria-label", "Close"); |
|
header.appendChild(closeBtn); |
|
|
|
offcanvas.appendChild(header); |
|
|
|
const body = document.createElement("div"); |
|
body.className = "offcanvas-body"; |
|
body.appendChild(this.createReportElement()); |
|
offcanvas.appendChild(body); |
Confirmed directly with axe-core on an overflowing report in both formats: reveal's report slide (scrollHeight 901 vs clientHeight 700) and dashboard's offcanvas body (scrollHeight 774 vs clientHeight 658 — this one overflows even with a small, default-sized report) both get flagged.
We could mirror the overlay's fix — tabIndex = 0 / role="region" / aria-label on the scrolling ancestor in each case (the reveal <section> and the offcanvas body <div>), and correct the now-inaccurate comment above.
Related to #14378 (same axe rule, different target — document code blocks rather than the checker's own report UI). Part of #8706.
Found while adding automated Playwright self-scan coverage for the axe accessibility-checker's own report chrome (it audits the page, then injects a report UI that never gets audited itself unless a test scans it directly).
axe rule:
scrollable-region-focusable· impact: serious · tag: wcag2a (2.1.1, 2.1.3)Targets: the RevealJS report slide (
section.quarto-axe-report-slide) and the Dashboard offcanvas body (.offcanvas-bodyinside#quarto-axe-offcanvas).When the report grows past its container's height, both the reveal report slide and the dashboard offcanvas body become scrollable (
overflow-y: auto), but neither element nor any descendant is keyboard-focusable, so keyboard-only users can't reach the rest of the violations.The HTML overlay variant already handles this case:
quarto-cli/src/resources/formats/html/axe/axe-check.js
Lines 346 to 358 in 13b7e59
The comment there reasons the reveal/dashboard variants don't need the same tab stop because "they don't scroll the report element itself" — true of the inner
.quarto-axe-report, but the scrolling ancestor (the reveal slide /.offcanvas-body) is a different element the comment doesn't account for:quarto-cli/src/resources/formats/html/axe/axe-check.js
Lines 360 to 387 in 13b7e59
quarto-cli/src/resources/formats/html/axe/axe-check.js
Lines 389 to 419 in 13b7e59
Confirmed directly with axe-core on an overflowing report in both formats: reveal's report slide (scrollHeight 901 vs clientHeight 700) and dashboard's offcanvas body (scrollHeight 774 vs clientHeight 658 — this one overflows even with a small, default-sized report) both get flagged.
We could mirror the overlay's fix —
tabIndex = 0/role="region"/aria-labelon the scrolling ancestor in each case (the reveal<section>and the offcanvas body<div>), and correct the now-inaccurate comment above.Related to #14378 (same axe rule, different target — document code blocks rather than the checker's own report UI). Part of #8706.