From 3ee794deaff8d7a165225a616f5d7ce1ef5ba23b Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Mon, 12 Oct 2026 15:40:50 +0200 Subject: [PATCH 1/2] Keep the current RevealJS slide visible after an axe scan To let axe check every slide, the scan strips hidden, aria-hidden and inert from all slides and restores them afterwards. inert was already recomputed from the live Reveal state, but hidden and aria-hidden were replayed from the snapshot taken before the scan. Moving to another slide while axe.run is pending therefore put aria-hidden="true" back on the slide now on screen, hiding it from screen readers until the next navigation. The console and json output modes never navigate after the scan, so nothing corrected it. Skip replaying the saved attributes for the current slide (and the stack containing it) and apply the same check to inert. Other slides keep their saved values, and reveal.js has already hidden the slide that was left. --- src/resources/formats/html/axe/axe-check.js | 18 +++++++------ .../tests/axe-accessibility.spec.ts | 26 +++++++++++++++++++ 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/resources/formats/html/axe/axe-check.js b/src/resources/formats/html/axe/axe-check.js index d8b489caaf0..cf77a081409 100644 --- a/src/resources/formats/html/axe/axe-check.js +++ b/src/resources/formats/html/axe/axe-check.js @@ -493,16 +493,18 @@ class QuartoAxeChecker { revealRestoreSlides(saved) { if (!saved) return; - saved.forEach(({ el, hidden, ariaHidden }) => { - if (hidden) el.setAttribute("hidden", ""); - if (ariaHidden !== null) el.setAttribute("aria-hidden", ariaHidden); - }); - // Recompute inert from live state, mirroring reveal.js's own rule (6.0.2+), - // so a slide change during the scan cannot leave the current slide inert. + // Use live state for the current slide, mirroring reveal.js's own inert + // rule (6.0.2+), so a slide change during the scan cannot leave the + // current slide hidden, aria-hidden or inert. const allowAll = Reveal.isOverview() || Reveal.isScrollView() || Reveal.isPrintView(); const current = Reveal.getCurrentSlide(); - saved.forEach(({ el }) => { - el.toggleAttribute("inert", !allowAll && el !== current && !el.contains(current)); + saved.forEach(({ el, hidden, ariaHidden }) => { + const isCurrent = el === current || el.contains(current); + if (!isCurrent) { + if (hidden) el.setAttribute("hidden", ""); + if (ariaHidden !== null) el.setAttribute("aria-hidden", ariaHidden); + } + el.toggleAttribute("inert", !allowAll && !isCurrent); }); } diff --git a/tests/integration/playwright/tests/axe-accessibility.spec.ts b/tests/integration/playwright/tests/axe-accessibility.spec.ts index 547df4d7d25..ecebb340c48 100644 --- a/tests/integration/playwright/tests/axe-accessibility.spec.ts +++ b/tests/integration/playwright/tests/axe-accessibility.spec.ts @@ -267,6 +267,32 @@ test.describe('RevealJS axe — cross-slide scanning and state restoration', () await expect(highlightedImg).toBeAttached({ timeout: 3000 }); }); + test('slide navigated to during the scan stays accessible afterwards', async ({ page }) => { + // Navigate as soon as the scan unhides the slides, i.e. while axe.run is pending. + await page.addInitScript(() => { + document.addEventListener('DOMContentLoaded', () => { + const observer = new MutationObserver(() => { + const slide2 = document.querySelectorAll('.reveal .slides > section')[1]; + if (slide2 && !slide2.hasAttribute('aria-hidden')) { + observer.disconnect(); + Reveal.slide(1); + } + }); + observer.observe(document.body, { + subtree: true, attributes: true, attributeFilter: ['aria-hidden'], + }); + }); + }); + await page.goto('/revealjs/axe-console.html', { waitUntil: 'networkidle' }); + await waitForAxeCompletion(page); + + expect(await page.evaluate(() => Reveal.getIndices().h)).toBe(1); + const current = page.locator('.reveal .slides > section.present'); + await expect(current).not.toHaveAttribute('hidden'); + await expect(current).not.toHaveAttribute('aria-hidden'); + await expect(current).not.toHaveAttribute('inert'); + }); + test('detects image-alt violation on hidden vertical slide', async ({ page }) => { await page.goto('/revealjs/axe-accessibility-vertical.html', { waitUntil: 'networkidle' }); const reportSlide = page.locator('section.quarto-axe-report-slide'); From d902943b450539e6712412c802ce15cde6edd234 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Mon, 12 Oct 2026 16:23:57 +0200 Subject: [PATCH 2/2] Add changelog entry for axe current-slide fix --- news/changelog-1.11.md | 1 + 1 file changed, 1 insertion(+) diff --git a/news/changelog-1.11.md b/news/changelog-1.11.md index 754f3da5a13..baf61c1e923 100644 --- a/news/changelog-1.11.md +++ b/news/changelog-1.11.md @@ -24,6 +24,7 @@ All changes included in 1.11: - ([#14952](https://github.com/quarto-dev/quarto-cli/issues/14952)): Fix `revealjs` presentations disabling pinch-zoom on touch devices. The viewport `` tag no longer sets `maximum-scale=1.0` and `user-scalable=no`, which axe-core flagged (`meta-viewport`). - ([#15011](https://github.com/quarto-dev/quarto-cli/issues/15011)): Fix the slide menu of `revealjs` presentations being read by screen readers while it is closed and off screen, and Tab moving keyboard focus to it once it lists more slides than fit. (author: @jooyoungseo) - ([#15015](https://github.com/quarto-dev/quarto-cli/issues/15015)): Fix the "Back to Article" link of notebook previews in website projects not being reachable or usable with the keyboard. +- ([#15030](https://github.com/quarto-dev/quarto-cli/pull/15030)): Fix `axe` checks in `revealjs` presentations hiding the current slide from screen readers when moving to another slide while the scan runs. ## Formats