diff --git a/news/changelog-1.11.md b/news/changelog-1.11.md index e1b823c6e9e..66ba504b65b 100644 --- a/news/changelog-1.11.md +++ b/news/changelog-1.11.md @@ -31,6 +31,7 @@ All changes included in 1.11: - ([#14378](https://github.com/quarto-dev/quarto-cli/issues/14378)): Make scrollable code blocks and cell output keyboard-focusable in HTML output, so keyboard users can Tab to them and scroll with the arrow keys (axe rule `scrollable-region-focusable`, WCAG 2.1.1). While a region overflows it gets `tabindex="0"`, `role="group"`, and a localized `aria-label` (new `scrollable-code-label` and `scrollable-output-label` language keys, translated in all 34 built-in language files); when it fits again the attributes are removed. Chrome and Firefox already focus such regions natively; this adds Safari coverage and an accessible name. - ([#14380](https://github.com/quarto-dev/quarto-cli/issues/14380)): Fix default listings rendering an empty, focusable subtitle link for posts without a subtitle, flagged by axe-core (`link-name`). - ([#14795](https://github.com/quarto-dev/quarto-cli/issues/14795)): Fix keyboard focus in `revealjs` presentations reaching links and controls on nearby slides that are not shown. Tab now stays on the current slide, except in overview, scroll and print views where all slides are shown. +- ([#14844](https://github.com/quarto-dev/quarto-cli/issues/14844)): With `toc-location: left-body` or `right-body`, give the ids in the cloned body table of contents a `-body` suffix. The clone previously repeated every id from the sidebar copy, including the `toc-title` heading that `aria-labelledby` now points at. - ([#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`). - ([#14968](https://github.com/quarto-dev/quarto-cli/issues/14968)): The header of notebook preview pages (back link, notebook title, download link) is now a navigation landmark named after the notebook, so it is reachable through landmark navigation and no longer flagged by axe-core (`region`). - ([#14970](https://github.com/quarto-dev/quarto-cli/issues/14970)): Fix keyboard focus order of tabsets in `revealjs` presentations. Tab now follows the order of the slide: tabs and their panel content no longer come before other links on the slide, and two tabsets on the same slide are no longer mixed together. diff --git a/src/format/html/format-html-bootstrap.ts b/src/format/html/format-html-bootstrap.ts index 0f16ef7c556..4a8ddc53b88 100644 --- a/src/format/html/format-html-bootstrap.ts +++ b/src/format/html/format-html-bootstrap.ts @@ -355,11 +355,44 @@ function bootstrapHtmlPostprocessor( // Leave it where it is in the document, and just mutate it const clonedToc = toc.cloneNode(true) as Element; clonedToc.id = "TOC-body"; + const tocActionsEl = clonedToc.querySelector(".toc-actions"); if (tocActionsEl) { tocActionsEl.remove(); } + // Ids must be unique, so the clone can't keep the originals: suffix + // every id it carries, and repoint its aria-labelledby at the renamed + // heading. Nothing outside the nav references these ids, and the + // originals stay on `toc`, so existing lookups resolve as before. + // + // A fixed suffix can still collide: headings "Setup" and "Setup Body" + // give `toc-setup` and `toc-setup-body`, so `toc-setup` + `-body` is + // already taken. Check each candidate against the document and the + // ids handed out so far, and append a counter until it is free. + const assignedIds = new Set(); + const uniqueBodyId = (id: string) => { + const base = `${id}-body`; + let candidate = base; + for (let n = 1; doc.getElementById(candidate) || assignedIds.has(candidate); n++) { + candidate = `${base}-${n}`; + } + assignedIds.add(candidate); + return candidate; + }; + const renamedIds = new Map(); + const clonedIdEls = clonedToc.querySelectorAll("[id]"); + for (let i = 0; i < clonedIdEls.length; i++) { + const clonedIdEl = clonedIdEls[i] as Element; + const newId = uniqueBodyId(clonedIdEl.id); + renamedIds.set(clonedIdEl.id, newId); + clonedIdEl.id = newId; + } + const clonedLabelId = clonedToc.getAttribute("aria-labelledby"); + if (clonedLabelId && renamedIds.has(clonedLabelId)) { + clonedToc.setAttribute("aria-labelledby", renamedIds.get(clonedLabelId)!); + } + toc.parentElement?.insertBefore(clonedToc, toc); } diff --git a/src/project/types/website/website-navigation.ts b/src/project/types/website/website-navigation.ts index 37ae8f7ec58..ae36a7d962e 100644 --- a/src/project/types/website/website-navigation.ts +++ b/src/project/types/website/website-navigation.ts @@ -922,7 +922,10 @@ function makeBreadCrumbs( navEl.classList.add(cls); }); } - navEl.setAttribute("aria-label", language[kNavigationBreadcrumbsLabel]!); + navEl.setAttribute( + "aria-label", + language[kNavigationBreadcrumbsLabel] || "Breadcrumbs", + ); const olEl = doc.createElement("ol"); olEl.classList.add("breadcrumb"); diff --git a/src/resources/projects/website/templates/nav-before-body.ejs b/src/resources/projects/website/templates/nav-before-body.ejs index e9f2564a2b5..d44cabe5e89 100644 --- a/src/resources/projects/website/templates/nav-before-body.ejs +++ b/src/resources/projects/website/templates/nav-before-body.ejs @@ -131,4 +131,3 @@ if (nav.layout === "article" || nav.layout === "full") { - diff --git a/tests/docs/smoke-all/issues/3473-toc-side-body/left-body-id-collision.qmd b/tests/docs/smoke-all/issues/3473-toc-side-body/left-body-id-collision.qmd new file mode 100644 index 00000000000..21492b09954 --- /dev/null +++ b/tests/docs/smoke-all/issues/3473-toc-side-body/left-body-id-collision.qmd @@ -0,0 +1,33 @@ +--- +title: 3473-toc-left-body-id-collision +format: + html: + toc: true + toc-location: left-body + toc-title: Contents +_quarto: + tests: + html: + ensureHtmlElements: + - + - "nav#TOC a#toc-setup" + - "nav#TOC a#toc-setup-body" + - "nav#TOC-body a#toc-setup-body-1" + - "nav#TOC-body a#toc-setup-body-body" + - [] + ensureFileRegexMatches: + - [] + - ['id="toc-setup-body"[\s\S]*id="toc-setup-body"'] +--- + +The heading pair below slugs to `toc-setup` and `toc-setup-body`. A fixed +`-body` suffix on the cloned body TOC would hand `toc-setup` the id +`toc-setup-body`, which the sidebar TOC already holds (#14844). + +# Setup + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +# Setup Body + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. diff --git a/tests/docs/smoke-all/issues/3473-toc-side-body/left-body.qmd b/tests/docs/smoke-all/issues/3473-toc-side-body/left-body.qmd index fcb26bb2dd8..59f68285514 100644 --- a/tests/docs/smoke-all/issues/3473-toc-side-body/left-body.qmd +++ b/tests/docs/smoke-all/issues/3473-toc-side-body/left-body.qmd @@ -13,7 +13,16 @@ _quarto: ensureHtmlElements: - - "nav#TOC.toc-active" + - "nav#TOC h2#toc-title" + - 'nav#TOC[aria-labelledby="toc-title"]' - "nav#TOC-body" + - "nav#TOC-body h2#toc-title-body" + - 'nav#TOC-body[aria-labelledby="toc-title-body"]' + - "nav#TOC-body a#toc-section-1-body" + - + - "nav#TOC-body h2#toc-title" + - 'nav#TOC-body[aria-labelledby="toc-title"]' + - "nav#TOC-body a#toc-section-1" --- # Section 1 diff --git a/tests/docs/smoke-all/issues/3473-toc-side-body/right-body.qmd b/tests/docs/smoke-all/issues/3473-toc-side-body/right-body.qmd index 38af3483e6e..146ac410f19 100644 --- a/tests/docs/smoke-all/issues/3473-toc-side-body/right-body.qmd +++ b/tests/docs/smoke-all/issues/3473-toc-side-body/right-body.qmd @@ -13,7 +13,16 @@ _quarto: ensureHtmlElements: - - "nav#TOC.toc-active" + - "nav#TOC h2#toc-title" + - 'nav#TOC[aria-labelledby="toc-title"]' - "nav#TOC-body" + - "nav#TOC-body h2#toc-title-body" + - 'nav#TOC-body[aria-labelledby="toc-title-body"]' + - "nav#TOC-body a#toc-section-1-body" + - + - "nav#TOC-body h2#toc-title" + - 'nav#TOC-body[aria-labelledby="toc-title"]' + - "nav#TOC-body a#toc-section-1" --- # Section 1