Skip to content

Commit ae03c7a

Browse files
authored
fix(revealjs): name the slide menu button for screen readers (#14251)
The Reveal.js slide menu button is an icon-only link with no accessible name, so it failed the axe `link-name` rule (WCAG 2.4.4). The `menu-ready` handler now sets its `aria-label` from a new `revealjs-menu-button-label` language key ("Slide menu"), so authors can translate it through `language:`. The label applies to normal decks and to `delay-init: true` builds. - Set the button's `aria-label` in `quarto-menu.js`, next to the `inert` handling from #15011 - Add `revealjs-menu-button-label` to `_language.yml`; the other translations fall back to English until they add it - Escape `<` in config values written into the `Reveal.initialize` script, so a value containing `</script>` cannot end it early - Insert that config with a replacer function, so `$&`, `$'` and other replacement patterns in a value are written literally Fixes #14249
1 parent b23fc50 commit ae03c7a

21 files changed

Lines changed: 132 additions & 26 deletions

File tree

‎news/changelog-1.11.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ All changes included in 1.11:
2020
- ([#5514](https://github.com/quarto-dev/quarto-cli/issues/5514)): Fix `fig-alt` being ignored in `docx` output. Images and figures, including computational figures, now use it as their Word alternative text instead of the caption or an empty description, and the visible caption is unchanged. (author: @jooyoungseo)
2121
- ([#7058](https://github.com/quarto-dev/quarto-cli/issues/7058)): Move the navbar search control after the navigation links and tools in the DOM so keyboard and screen-reader order matches the visual order. The rendered layout is unchanged (navbar positioning is controlled by CSS `order`), but custom CSS or JS that relied on `#quarto-search` preceding the navbar links in the DOM may need updating.
2222
- ([#13463](https://github.com/quarto-dev/quarto-cli/issues/13463)): The dark/light mode toggle is now a switch (`button` with `role="switch"`, `aria-checked`, and a localized `aria-label`) instead of a link.
23+
- ([#14249](https://github.com/quarto-dev/quarto-cli/issues/14249)): Fix the `revealjs` slide menu button having no accessible name for screen readers. The name can be translated with the `revealjs-menu-button-label` language key. (author: @mcanouil)
2324
- ([#14615](https://github.com/quarto-dev/quarto-cli/issues/14615)): Fix invalid `role="menu"` on the website navbar's collapse toggle button, flagged by axe-core (`aria-allowed-role`) and WAVE (`aria-role-mismatch`).
2425
- ([#12116](https://github.com/quarto-dev/quarto-cli/issues/12116), [#4935](https://github.com/quarto-dev/quarto-cli/issues/4935)): Website sidebar section toggles are now `<button>` elements, so they are keyboard accessible and properly announced.
2526
- ([#14774](https://github.com/quarto-dev/quarto-cli/issues/14774)): Fix missing keyboard focus indicator on the code tools button and on a website's sidebar toggle and sidebar search buttons.

‎src/config/constants.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ export const kToggleReaderMode = "toggle-reader-mode";
306306
export const kToggleNavigation = "toggle-navigation";
307307
export const kScrollableCodeLabel = "scrollable-code-label";
308308
export const kScrollableOutputLabel = "scrollable-output-label";
309+
export const kRevealjsMenuButtonLabel = "revealjs-menu-button-label";
309310

310311
export const kNavigationSiteLabel = "navigation-site-label";
311312
export const kNavigationSectionLabel = "navigation-section-label";
@@ -449,6 +450,7 @@ export const kLanguageDefaultsKeys = [
449450
kNavigationBreadcrumbsLabel,
450451
kScrollableCodeLabel,
451452
kScrollableOutputLabel,
453+
kRevealjsMenuButtonLabel,
452454
kCopyButtonTooltip,
453455
kCopyButtonTooltipSuccess,
454456
kBackToTop,

‎src/config/types.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ import {
196196
kResourcePath,
197197
kScrollableCodeLabel,
198198
kScrollableOutputLabel,
199+
kRevealjsMenuButtonLabel,
199200
kSearchClearButtonTitle,
200201
kSearchCopyLinkTitle,
201202
kSearchDetatchedCancelButtonTitle,
@@ -717,6 +718,7 @@ export interface FormatLanguage {
717718
[kNavigationBreadcrumbsLabel]?: string;
718719
[kScrollableCodeLabel]?: string;
719720
[kScrollableOutputLabel]?: string;
721+
[kRevealjsMenuButtonLabel]?: string;
720722
[kSearchMatchingDocumentsText]?: string;
721723
[kSearchCopyLinkTitle]?: string;
722724
[kSearchMoreMatchText]?: string;

‎src/format/reveal/format-reveal-plugin.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66

77
import { ensureDirSync, existsSync } from "../../deno_ral/fs.ts";
88
import { basename, join } from "../../deno_ral/path.ts";
9-
import { kIncludeInHeader, kSelfContained } from "../../config/constants.ts";
9+
import {
10+
kIncludeInHeader,
11+
kRevealjsMenuButtonLabel,
12+
kSelfContained,
13+
} from "../../config/constants.ts";
1014

1115
import { error } from "../../deno_ral/log.ts";
1216

@@ -383,6 +387,9 @@ function revealMenuPlugin(format: Format) {
383387
content: revealMenuTools(format),
384388
}],
385389
openButton: format.metadata[kRevealMenu] !== false,
390+
// read by quarto-menu.js, which names the button with it
391+
openButtonLabel: format.language[kRevealjsMenuButtonLabel] ||
392+
"Slide menu",
386393
},
387394
},
388395
};

‎src/format/reveal/format-reveal.ts‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -546,26 +546,30 @@ const fixupRevealJsInitialization = (
546546
);
547547
}
548548

549+
// Config values can hold author-supplied text, such as metadata or
550+
// language strings, so `<` is escaped: a value holding `</script>` would
551+
// otherwise close this script early.
552+
const configValue = (value: unknown) =>
553+
(JSON.stringify(value) ?? "undefined").replace(/</g, "\\u003c");
554+
549555
// Write any additional configuration of reveal
550556
const configJs: string[] = [];
551557
Object.keys(extraConfig).forEach((key) => {
552-
configJs.push(
553-
`'${key}': ${JSON.stringify(extraConfig[key])}`,
554-
);
558+
configJs.push(`'${key}': ${configValue(extraConfig[key])}`);
555559
});
556560

557561
// Plugin initialization
558562
Object.keys(pluginInit.revealConfig).forEach((key) => {
559-
configJs.push(
560-
`'${key}': ${JSON.stringify(pluginInit.revealConfig[key])}`,
561-
);
563+
configJs.push(`'${key}': ${configValue(pluginInit.revealConfig[key])}`);
562564
});
563565

564566
const configStr = configJs.join(",\n");
565567

568+
// A replacer function inserts configStr literally, so `$&` or `$'` in a
569+
// config value is not read as a replacement pattern.
566570
scriptEl.innerText = scriptEl.innerText.replace(
567571
"Reveal.initialize({",
568-
`Reveal.initialize({\n${configStr},\n`,
572+
() => `Reveal.initialize({\n${configStr},\n`,
569573
);
570574
}
571575
}

‎src/resources/editor/tools/vs-code.mjs‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10532,6 +10532,7 @@ var require_yaml_intelligence_resources = __commonJS({
1053210532
"skip-to-content": "string",
1053310533
"scrollable-code-label": "string",
1053410534
"scrollable-output-label": "string",
10535+
"revealjs-menu-button-label": "string",
1053510536
"repo-action-links-edit": "string",
1053610537
"repo-action-links-source": "string",
1053710538
"repo-action-links-issue": "string",
@@ -25826,12 +25827,12 @@ var require_yaml_intelligence_resources = __commonJS({
2582625827
mermaid: "%%"
2582725828
},
2582825829
"handlers/mermaid/schema.yml": {
25829-
_internalId: 221293,
25830+
_internalId: 221295,
2583025831
type: "object",
2583125832
description: "be an object",
2583225833
properties: {
2583325834
"mermaid-format": {
25834-
_internalId: 221285,
25835+
_internalId: 221287,
2583525836
type: "enum",
2583625837
enum: [
2583725838
"png",
@@ -25847,7 +25848,7 @@ var require_yaml_intelligence_resources = __commonJS({
2584725848
exhaustiveCompletions: true
2584825849
},
2584925850
theme: {
25850-
_internalId: 221292,
25851+
_internalId: 221294,
2585125852
type: "anyOf",
2585225853
anyOf: [
2585325854
{

‎src/resources/editor/tools/yaml/all-schema-definitions.json‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎src/resources/editor/tools/yaml/web-worker.js‎

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/resources/editor/tools/yaml/yaml-intelligence-resources.json‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3500,6 +3500,7 @@
35003500
"skip-to-content": "string",
35013501
"scrollable-code-label": "string",
35023502
"scrollable-output-label": "string",
3503+
"revealjs-menu-button-label": "string",
35033504
"repo-action-links-edit": "string",
35043505
"repo-action-links-source": "string",
35053506
"repo-action-links-issue": "string",
@@ -18794,12 +18795,12 @@
1879418795
"mermaid": "%%"
1879518796
},
1879618797
"handlers/mermaid/schema.yml": {
18797-
"_internalId": 221293,
18798+
"_internalId": 221295,
1879818799
"type": "object",
1879918800
"description": "be an object",
1880018801
"properties": {
1880118802
"mermaid-format": {
18802-
"_internalId": 221285,
18803+
"_internalId": 221287,
1880318804
"type": "enum",
1880418805
"enum": [
1880518806
"png",
@@ -18815,7 +18816,7 @@
1881518816
"exhaustiveCompletions": true
1881618817
},
1881718818
"theme": {
18818-
"_internalId": 221292,
18819+
"_internalId": 221294,
1881918820
"type": "anyOf",
1882018821
"anyOf": [
1882118822
{

‎src/resources/formats/revealjs/plugins/menu/quarto-menu.js‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ window.RevealMenuToolHandlers = {
4545
})
4646
};
4747

48+
// The plugin's button is a bare `<a href="#">` holding only an icon, so give it
49+
// an accessible name from the language files. The plugin builds the button and
50+
// the menu in the same call, so the button exists whenever the menu does.
51+
// See https://github.com/quarto-dev/quarto-cli/issues/14249
52+
const labelMenuButton = function () {
53+
const label = Reveal.getConfig().menu?.openButtonLabel;
54+
const button = document.querySelector(".slide-menu-button a");
55+
if (label && button && !button.hasAttribute("aria-label")) {
56+
button.setAttribute("aria-label", label);
57+
}
58+
};
59+
4860
// Keep the slide menu out of the tab order and the accessibility tree while it
4961
// is closed. Closing only moves it off screen, so screen readers still read it,
5062
// and Chrome and Firefox make its slide list a Tab stop once the list scrolls.
@@ -65,6 +77,7 @@ document.addEventListener("menu-ready", function () {
6577

6678
const menu = document.querySelector(".slide-menu");
6779
if (menu) {
80+
labelMenuButton();
6881
followMenu(menu);
6982
return;
7083
}
@@ -76,6 +89,7 @@ document.addEventListener("menu-ready", function () {
7689
const builtMenu = container.querySelector(".slide-menu");
7790
if (builtMenu) {
7891
observer.disconnect();
92+
labelMenuButton();
7993
followMenu(builtMenu);
8094
}
8195
});

0 commit comments

Comments
 (0)