Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions news/changelog-1.11.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ All changes included in 1.11:
- ([#14818](https://github.com/quarto-dev/quarto-cli/issues/14818)): Fix a dashboard with more than one page going blank when the URL hash does not name a page, such as a footnote link or a cross-reference. Such a hash is now left alone, so the current page stays visible.
- ([#14819](https://github.com/quarto-dev/quarto-cli/issues/14819)): Fix keyboard focus starting inside the page content when a dashboard opens at a page hash, such as `dashboard.html#sales`. Tabbing forward could not reach the navbar. Focus now starts at the top of the document.

### `html`

- ([#14684](https://github.com/quarto-dev/quarto-cli/issues/14684)): Add a "Skip to main content" link to Bootstrap-themed HTML output (documents, websites, books, dashboards) so keyboard users can bypass navigation blocks (WCAG 2.4.1). The link is the first tab stop on every page, visually hidden until focused, and its text is customizable via the `language` key `skip-to-content`. Dashboards additionally mark their content container as the `main` landmark.

## Engines

### `knitr`
Expand Down
2 changes: 2 additions & 0 deletions src/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ export const kToggleNavigation = "toggle-navigation";
export const kCopyButtonTooltip = "copy-button-tooltip";
export const kCopyButtonTooltipSuccess = "copy-button-tooltip-success";
export const kBackToTop = "back-to-top";
export const kSkipToContent = "skip-to-content";
export const kRepoActionLinksEdit = "repo-action-links-edit";
export const kRepoActionLinksSource = "repo-action-links-source";
export const kRepoActionLinksIssue = "repo-action-links-issue";
Expand Down Expand Up @@ -436,6 +437,7 @@ export const kLanguageDefaultsKeys = [
kCopyButtonTooltip,
kCopyButtonTooltipSuccess,
kBackToTop,
kSkipToContent,
kRepoActionLinksEdit,
kRepoActionLinksSource,
kRepoActionLinksIssue,
Expand Down
2 changes: 2 additions & 0 deletions src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ import {
kSelfContainedMath,
kShiftHeadingLevelBy,
kShortcodes,
kSkipToContent,
kSlideLevel,
kSourceNotebookPrefix,
kStandalone,
Expand Down Expand Up @@ -696,6 +697,7 @@ export interface FormatLanguage {
[kCopyButtonTooltip]?: string;
[kCopyButtonTooltipSuccess]?: string;
[kBackToTop]?: string;
[kSkipToContent]?: string;
[kToggleDarkMode]?: string;
[kToggleNavigation]?: string;
[kToggleReaderMode]?: string;
Expand Down
3 changes: 3 additions & 0 deletions src/format/dashboard/format-dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,9 @@ function dashboardHtmlPostProcessor(
// Mark the page container with layout instructions
const containerEl = doc.querySelector("div.page-layout-custom");
if (containerEl) {
// dashboards have no <main> element, so mark the content container
// as the main landmark for assistive technology
containerEl.setAttribute("role", "main");
const containerClz = [
"quarto-dashboard-content",
"bslib-gap-spacing",
Expand Down
36 changes: 36 additions & 0 deletions src/format/html/format-html-bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
kQuartoTemplateParams,
kRelatedFormatsTitle,
kSectionDivs,
kSkipToContent,
kTocDepth,
kTocExpand,
kTocLocation,
Expand Down Expand Up @@ -526,6 +527,10 @@ function bootstrapHtmlPostprocessor(
doc.body.insertBefore(beforeBodyScript, doc.body.firstChild);
}

// add a skip-to-content link as the first element of the body so that
// keyboard users can bypass navigation (WCAG 2.4.1 Bypass Blocks)
injectSkipLink(doc, format);

// Process the elements of this document into an appendix
if (
format.metadata[kAppendixStyle] !== false &&
Expand All @@ -545,6 +550,37 @@ function bootstrapHtmlPostprocessor(
};
}

function injectSkipLink(doc: Document, format: Format) {
// resolve the link target, from most to least specific: the article-layout
// main content element, a hand-authored main, the custom-layout container
// (standalone custom layouts and dashboards), or the website custom-layout
// content container
const target = doc.querySelector("#quarto-document-content") ||
doc.querySelector("main") ||
doc.querySelector("div.page-layout-custom") ||
doc.querySelector("#quarto-content");
if (!target) {
return;
}
if (!target.id) {
target.id = "quarto-document-content";
}
// tabindex=-1 so that activating the link moves keyboard focus into the
// content (not just scroll position) across browsers and assistive tech
target.setAttribute("tabindex", "-1");

const skipLink = doc.createElement("a");
skipLink.id = "quarto-skip-link";
skipLink.classList.add("visually-hidden-focusable");
skipLink.setAttribute("href", `#${target.id}`);
skipLink.appendChild(
doc.createTextNode(
format.language[kSkipToContent] || "Skip to main content",
),
);
doc.body.insertBefore(skipLink, doc.body.firstChild);
}

function createLinkChild(formatLink: AlternateLink, doc: Document) {
const link = doc.createElement("a");
link.setAttribute("href", formatLink.href);
Expand Down
7 changes: 4 additions & 3 deletions src/resources/editor/tools/vs-code.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10373,6 +10373,7 @@ var require_yaml_intelligence_resources = __commonJS({
"search-no-results-text": "string",
"copy-button-tooltip": "string",
"copy-button-tooltip-success": "string",
"skip-to-content": "string",
"repo-action-links-edit": "string",
"repo-action-links-source": "string",
"repo-action-links-issue": "string",
Expand Down Expand Up @@ -25387,12 +25388,12 @@ var require_yaml_intelligence_resources = __commonJS({
mermaid: "%%"
},
"handlers/mermaid/schema.yml": {
_internalId: 223031,
_internalId: 223755,
type: "object",
description: "be an object",
properties: {
"mermaid-format": {
_internalId: 223023,
_internalId: 223747,
type: "enum",
enum: [
"png",
Expand All @@ -25408,7 +25409,7 @@ var require_yaml_intelligence_resources = __commonJS({
exhaustiveCompletions: true
},
theme: {
_internalId: 223030,
_internalId: 223754,
type: "anyOf",
anyOf: [
{
Expand Down

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions src/resources/editor/tools/yaml/web-worker.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -3341,6 +3341,7 @@
"search-no-results-text": "string",
"copy-button-tooltip": "string",
"copy-button-tooltip-success": "string",
"skip-to-content": "string",
"repo-action-links-edit": "string",
"repo-action-links-source": "string",
"repo-action-links-issue": "string",
Expand Down Expand Up @@ -18355,12 +18356,12 @@
"mermaid": "%%"
},
"handlers/mermaid/schema.yml": {
"_internalId": 223031,
"_internalId": 223755,
"type": "object",
"description": "be an object",
"properties": {
"mermaid-format": {
"_internalId": 223023,
"_internalId": 223747,
"type": "enum",
"enum": [
"png",
Expand All @@ -18376,7 +18377,7 @@
"exhaustiveCompletions": true
},
"theme": {
"_internalId": 223030,
"_internalId": 223754,
"type": "anyOf",
"anyOf": [
{
Expand Down
19 changes: 19 additions & 0 deletions src/resources/formats/dashboard/quarto-dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,25 @@ window.document.addEventListener("DOMContentLoaded", function (_event) {
}
});

// Move focus for the skip-to-content link without changing the hash.
// A plain anchor jump pushes a history entry that names no page, so Back
// would land on a dead step: the URL changes but the page does not, and
// the reader has to press Back twice. Without JS the link still works as
// an ordinary in-page anchor.
const skipLinkEl = document.getElementById("quarto-skip-link");
if (skipLinkEl) {
skipLinkEl.addEventListener("click", function (e) {
const targetId = QuartoDashboardUtils.urlHash(
skipLinkEl.getAttribute("href") || ""
).substring(1);
const targetEl = targetId ? document.getElementById(targetId) : null;
if (targetEl) {
e.preventDefault();
targetEl.focus();
}
});
}

// Hook tabs and use that to update history / active tabs
const navItems = document.querySelectorAll(".navbar .nav-item .nav-link");
for (const navItem of navItems) {
Expand Down
23 changes: 23 additions & 0 deletions src/resources/formats/html/bootstrap/_bootstrap-rules.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,29 @@ ol > li:not(:has(> p)) > ol > li:has(> p) {
margin-top: 1rem;
}

// skip-to-content link (WCAG 2.4.1 Bypass Blocks); visually hidden via
// Bootstrap's .visually-hidden-focusable helper until it receives focus
#quarto-skip-link:focus {
position: fixed;
top: $spacer * 0.5;
inset-inline-start: $spacer * 0.5;
z-index: $zindex-skip-link;
padding: $spacer * 0.5 $spacer;
background-color: $body-bg;
color: $link-color;
text-decoration: none;
border: 1px solid $border-color;
border-radius: $border-radius;
box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.25);
}

// the skip-link target receives programmatic focus (tabindex="-1"); don't
// draw a focus ring around the entire content area
#quarto-document-content:focus,
#quarto-content:focus {
outline: none;
}

// Grid layout
body {
margin: 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,9 @@ $enable-cssgrid: true;

$zindex-pagelayout: 998;

// above the fixed/headroom navbar ($zindex-fixed: 1030)
$zindex-skip-link: 1070 !default;

$popover-bg: if(variable-exists(body-bg), $body-bg, null) !default;
$input-bg: if(variable-exists(body-bg), $body-bg, null) !default;

Expand Down
2 changes: 2 additions & 0 deletions src/resources/language/_language-de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ repo-action-links-issue: "Problem melden"

back-to-top: "Zurück nach oben"

skip-to-content: "Zum Hauptinhalt springen"

search-no-results-text: "Keine Treffer"
search-matching-documents-text: "Treffer"
search-copy-link-title: "Link in die Suche kopieren"
Expand Down
2 changes: 2 additions & 0 deletions src/resources/language/_language-es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ repo-action-links-issue: "Informar sobre problema"

back-to-top: "Volver arriba"

skip-to-content: "Saltar al contenido principal"

search-no-results-text: "No se han encontrado resultados"
search-matching-documents-text: "documentos encontrados"
search-copy-link-title: "Copiar el enlace en la búsqueda"
Expand Down
2 changes: 2 additions & 0 deletions src/resources/language/_language-fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ repo-action-links-issue: "Faire part d'un problème"

back-to-top: "Retour au sommet"

skip-to-content: "Aller au contenu principal"

search-no-results-text: "Pas de résultats"
search-matching-documents-text: "documents trouvés"
search-copy-link-title: "Copier le lien vers la recherche"
Expand Down
2 changes: 2 additions & 0 deletions src/resources/language/_language-it.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ repo-action-links-issue: "Segnala un problema"

back-to-top: "Torna in cima"

skip-to-content: "Vai al contenuto principale"

search-no-results-text: "Nessun risultato"
search-matching-documents-text: "documenti trovati"
search-copy-link-title: "Copiare il link nella ricerca"
Expand Down
2 changes: 2 additions & 0 deletions src/resources/language/_language-ja.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ repo-action-links-issue: "問題を報告"

back-to-top: "トップに戻る"

skip-to-content: "メインコンテンツにスキップ"

search-no-results-text: "一致なし"
search-matching-documents-text: "一致した文書"
search-copy-link-title: "検索へのリンクをコピー"
Expand Down
2 changes: 2 additions & 0 deletions src/resources/language/_language-nl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ repo-action-links-issue: "Een probleem melden"

back-to-top: "Terug naar boven"

skip-to-content: "Ga naar de hoofdinhoud"

search-no-results-text: "Geen resultaten"
search-matching-documents-text: "Gevonden documenten"
search-copy-link-title: "Kopieer link om te zoeken"
Expand Down
2 changes: 2 additions & 0 deletions src/resources/language/_language-pt-BR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ repo-action-links-issue: "Criar uma issue"

back-to-top: "De volta ao topo"

skip-to-content: "Pular para o conteúdo principal"

search-no-results-text: "Nenhum resultado"
search-matching-documents-text: "documentos correspondentes"
search-copy-link-title: "Copiar link para a busca"
Expand Down
2 changes: 2 additions & 0 deletions src/resources/language/_language-pt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ repo-action-links-issue: "Criar uma issue"

back-to-top: "De volta ao topo"

skip-to-content: "Saltar para o conteúdo principal"

search-no-results-text: "Nenhum resultado"
search-matching-documents-text: "documentos correspondentes"
search-copy-link-title: "Copiar link para a busca"
Expand Down
2 changes: 2 additions & 0 deletions src/resources/language/_language-zh-TW.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ repo-action-links-issue: "回報問題"

back-to-top: "回到頂端"

skip-to-content: "跳至主要內容"

search-no-results-text: "沒有結果"
search-matching-documents-text: "符合的文件"
search-copy-link-title: "複製搜尋連結"
Expand Down
2 changes: 2 additions & 0 deletions src/resources/language/_language-zh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ repo-action-links-issue: "反馈问题"

back-to-top: "回到顶部"

skip-to-content: "跳至主要内容"

search-no-results-text: "没有结果"
search-matching-documents-text: "匹配的文档"
search-copy-link-title: "复制搜索链接"
Expand Down
2 changes: 2 additions & 0 deletions src/resources/language/_language.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ repo-action-links-issue: "Report an issue"

back-to-top: "Back to top"

skip-to-content: "Skip to main content"

search-no-results-text: "No results"
search-matching-documents-text: "matching documents"
search-copy-link-title: "Copy link to search"
Expand Down
1 change: 1 addition & 0 deletions src/resources/schema/definitions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1393,6 +1393,7 @@
search-no-results-text: string
copy-button-tooltip: string
copy-button-tooltip-success: string
skip-to-content: string
repo-action-links-edit: string
repo-action-links-source: string
repo-action-links-issue: string
Expand Down
3 changes: 3 additions & 0 deletions src/resources/schema/json-schemas.json
Original file line number Diff line number Diff line change
Expand Up @@ -1728,6 +1728,9 @@
"copy-button-tooltip-success": {
"type": "string"
},
"skip-to-content": {
"type": "string"
},
"repo-action-links-edit": {
"type": "string"
},
Expand Down
1 change: 1 addition & 0 deletions src/resources/types/schema-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,7 @@ export type FormatLanguage = {
"search-no-results-text"?: string;
"copy-button-tooltip"?: string;
"copy-button-tooltip-success"?: string;
"skip-to-content"?: string;
"repo-action-links-edit"?: string;
"repo-action-links-source"?: string;
"repo-action-links-issue"?: string;
Expand Down
Loading
Loading