diff --git a/news/changelog-1.11.md b/news/changelog-1.11.md index 754f3da5a1..baf61c1e92 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 diff --git a/src/resources/formats/html/axe/axe-check.js b/src/resources/formats/html/axe/axe-check.js index d8b489caaf..cf77a08140 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 547df4d7d2..ecebb340c4 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');