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
@@ -1,5 +1,9 @@
All changes included in 1.11:

## Accessibility

- ([#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 nameless link, in all three places it is created: website navbars/sidebars, plain documents with a light/dark theme pair, and multi-page dashboards. Screen readers now announce the control's name, role, and which mode is active, and Space activates it as well as Enter. Custom CSS targeting `a.quarto-color-scheme-toggle` should now target the `button` element instead.

## Engines

### `knitr`
Expand Down
15 changes: 12 additions & 3 deletions src/format/dashboard/format-dashboard-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* Copyright (C) 2020-2022 Posit Software, PBC
*/

import { kToggleDarkMode } from "../../config/constants.ts";
import { FormatLanguage } from "../../config/types.ts";
import { Document, Element } from "../../core/deno-dom.ts";
import { recursiveApplyFillClasses } from "./format-dashboard-layout.ts";
import {
Expand All @@ -25,7 +27,11 @@ interface NavItem {
scrolling: boolean;
}

export function processPages(doc: Document, dashboardMeta: DashboardMeta) {
export function processPages(
doc: Document,
dashboardMeta: DashboardMeta,
language: FormatLanguage,
) {
// Find the pages, if any
const pageNodes = doc.querySelectorAll(`.${kPageClass}`);
if (pageNodes.length === 0) {
Expand Down Expand Up @@ -64,10 +70,13 @@ export function processPages(doc: Document, dashboardMeta: DashboardMeta) {
// Add a dark mode toggle if needed
// If dark and light themes are provided, inject a toggle into the correct spot
if (dashboardMeta.hasDarkMode) {
const toggleEl = makeEl("a", {
const toggleEl = makeEl("button", {
classes: ["quarto-color-scheme-toggle"],
attributes: {
href: "",
type: "button",
role: "switch",
"aria-checked": "false",
"aria-label": language[kToggleDarkMode] || "Toggle dark mode",
onclick: "window.quartoToggleColorScheme(); return false;",
},
}, doc);
Expand Down
6 changes: 4 additions & 2 deletions src/format/dashboard/format-dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
DependencyHtmlFile,
Format,
FormatExtras,
FormatLanguage,
kDependencies,
kHtmlPostprocessors,
kSassBundles,
Expand Down Expand Up @@ -154,7 +155,7 @@ export function dashboardFormat() {
extras.html[kHtmlPostprocessors] = extras.html[kHtmlPostprocessors] ||
[];
extras.html[kHtmlPostprocessors].push(
dashboardHtmlPostProcessor(dashboard),
dashboardHtmlPostProcessor(dashboard, format.language),
);

extras.metadata = extras.metadata || {};
Expand Down Expand Up @@ -317,6 +318,7 @@ registerWriterFormatHandler((format) => {

function dashboardHtmlPostProcessor(
dashboardMeta: DashboardMeta,
language: FormatLanguage,
) {
return (doc: Document): Promise<HtmlPostProcessResult> => {
const result: HtmlPostProcessResult = {
Expand Down Expand Up @@ -391,7 +393,7 @@ function dashboardHtmlPostProcessor(
processNavigation(doc);

// Process pages that may be present in the document
processPages(doc, dashboardMeta);
processPages(doc, dashboardMeta, language);

// Process Navbar buttons
processNavButtons(doc, dashboardMeta);
Expand Down
7 changes: 7 additions & 0 deletions src/resources/formats/html/bootstrap/_bootstrap-rules.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1680,6 +1680,13 @@ div.callout.callout-style-default > .callout-header {
}

// dark mode
button.quarto-color-scheme-toggle {
background: transparent;
border: none;
padding: 0;
cursor: pointer;
}

.quarto-reader-toggle .bi::before,
.quarto-color-scheme-toggle .bi::before {
display: inline-block;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@

// Ensure there is a toggle, if there isn't float one in the top right
if (window.document.querySelector('.quarto-color-scheme-toggle') === null) {
const a = window.document.createElement('a');
const a = window.document.createElement('button');
a.type = "button";
a.setAttribute("role", "switch");
a.setAttribute("aria-checked", "false");
a.classList.add('top-right');
a.classList.add('quarto-color-scheme-toggle');
a.href = "";
a.setAttribute("aria-label", "<%- language['toggle-dark-mode'] %>");
a.onclick = function() { try { window.quartoToggleColorScheme(); } catch {} return false; };

const i = window.document.createElement("i");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
for (let i=0; i < toggles.length; i++) {
const toggle = toggles[i];
if (toggle) {
toggle.setAttribute("aria-checked", alternate ? "true" : "false");
if (alternate) {
toggle.classList.add("alternate");
} else {
Expand Down
2 changes: 1 addition & 1 deletion src/resources/projects/website/templates/navdarktoggle.ejs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<a href="" class="quarto-color-scheme-toggle <%= classes %>" onclick="window.quartoToggleColorScheme(); return false;" title="<%- language['toggle-dark-mode'] %>"><i class="bi"></i></a>
<button type="button" role="switch" aria-checked="false" class="quarto-color-scheme-toggle <%= classes %>" onclick="window.quartoToggleColorScheme(); return false;" title="<%- language['toggle-dark-mode'] %>" aria-label="<%- language['toggle-dark-mode'] %>"><i class="bi"></i></button>
25 changes: 25 additions & 0 deletions tests/docs/smoke-all/dark-mode/color-scheme-toggle-dashboard.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
title: "Dashboard color-scheme toggle is a named switch (#13463)"
format:
dashboard:
theme:
light: flatly
dark: darkly
_quarto:
tests:
dashboard:
ensureHtmlElements:
-
- 'button.quarto-color-scheme-toggle[type="button"][role="switch"][aria-checked="false"][aria-label="Toggle dark mode"]'
-
- 'a.quarto-color-scheme-toggle'
---

# Page A

Multi-page so `processPages()` injects the navbar toggle (single-page
dashboards get the JS-injected fallback toggle instead).

# Page B

Second page.
24 changes: 24 additions & 0 deletions tests/docs/smoke-all/dark-mode/color-scheme-toggle-fallback.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
title: "Fallback color-scheme toggle is a named switch (#13463)"
format:
html:
theme:
light: flatly
dark: darkly
_quarto:
tests:
html:
ensureFileRegexMatches:
-
- "createElement\\('button'\\)"
- 'setAttribute\("role", "switch"\)'
- 'setAttribute\("aria-label"'
- 'setAttribute\("aria-checked", alternate'
-
- 'a\.href = ""'
---

The toggle on a plain document (no navbar) is injected by the after-body
script at `DOMContentLoaded`, so these assertions check the script text: the
fallback creates a `button` switch with a name, `setColorSchemeToggle()` keeps
`aria-checked` in sync, and the old nameless-anchor creation is gone.
15 changes: 15 additions & 0 deletions tests/docs/smoke-all/dark-mode/no-toggle-without-dark.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
title: "No color-scheme toggle without a dark theme"
format:
html:
theme: flatly
_quarto:
tests:
html:
ensureFileRegexMatches:
- []
-
- 'quarto-color-scheme-toggle'
---

A light-only document must not emit any toggle markup or toggle script.
5 changes: 5 additions & 0 deletions tests/docs/smoke-all/website/color-scheme-toggle/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/.quarto/
*.html
*.json
site_libs/
**/*.quarto_ipynb
20 changes: 20 additions & 0 deletions tests/docs/smoke-all/website/color-scheme-toggle/_quarto.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
project:
type: website
output-dir: .

website:
title: "Website"
reader-mode: true
navbar:
left:
- href: index.qmd
text: Home
tools:
- icon: github
href: https://github.com/quarto-dev/quarto-cli

format:
html:
theme:
light: flatly
dark: darkly
16 changes: 16 additions & 0 deletions tests/docs/smoke-all/website/color-scheme-toggle/index.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
title: "Website"
_quarto:
tests:
html:
ensureHtmlElements:
-
- 'button.quarto-color-scheme-toggle[type="button"][role="switch"][aria-checked="false"][aria-label="Toggle dark mode"][title="Toggle dark mode"]'
- 'a.quarto-reader-toggle[title]'
- 'a.quarto-navigation-tool[href="https://github.com/quarto-dev/quarto-cli"]'
-
- 'a.quarto-color-scheme-toggle'
---

The navbar color-scheme toggle is a named switch (#13463); the reader-mode
toggle and navbar tool links keep their existing shape.
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ async function check_backgrounds(page, class_, primary, secondary) {
const locatr = await page.locator('body').first();
await expect(locatr).toHaveClass(`fullcontent ${class_}`);
await expect(locatr).toHaveCSS('background-color', primary);
await page.locator("a.quarto-color-scheme-toggle").click();
await page.locator("button.quarto-color-scheme-toggle").click();
const locatr2 = await page.locator('body').first();
await expect(locatr2).toHaveCSS('background-color', secondary);
}

async function check_toggle(page, alternate) {
const locatr = await page.locator("a.quarto-color-scheme-toggle");
const locatr = await page.locator("button.quarto-color-scheme-toggle");
await expect(locatr).toHaveClass(`top-right quarto-color-scheme-toggle${alternate?" alternate":""}`)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ async function check_backgrounds(page, class_, primary, secondary) {
const locatr = await page.locator('body').first();
await expect(locatr).toHaveClass(`fullcontent ${class_}`);
await expect(locatr).toHaveCSS('background-color', primary);
await page.locator("a.quarto-color-scheme-toggle").click();
await page.locator("button.quarto-color-scheme-toggle").click();
const locatr2 = await page.locator('body').first();
await expect(locatr2).toHaveCSS('background-color', secondary);
}


async function check_toggle(page, alternate) {
const locatr = await page.locator("a.quarto-color-scheme-toggle");
const locatr = await page.locator("button.quarto-color-scheme-toggle");
await expect(locatr).toHaveClass(`top-right quarto-color-scheme-toggle${alternate?" alternate":""}`)
}

Expand Down
8 changes: 4 additions & 4 deletions tests/integration/playwright/tests/html-dark-mode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ async function check_theme_overrides(page) {
const locatr = await page.locator('body').first();
await expect(locatr).toHaveClass('fullcontent quarto-light');
await expect(locatr).toHaveCSS('background-color', 'rgb(252, 252, 252)');
await page.locator("a.quarto-color-scheme-toggle").click();
await page.locator("button.quarto-color-scheme-toggle").click();
const locatr2 = await page.locator('body').first();
await expect(locatr2).toHaveCSS('background-color', 'rgb(6, 6, 6)');
}
Expand All @@ -31,7 +31,7 @@ test('Brand false remove project brand', async ({ page }) => {
await expect(locatr).toHaveClass('fullcontent quarto-light');
await expect(locatr).toHaveCSS('background-color', 'rgb(255, 255, 255)');
// no toggle
expect(await page.locator('a.quarto-color-scheme-toggle').count()).toEqual(0);
expect(await page.locator('button.quarto-color-scheme-toggle').count()).toEqual(0);
});


Expand All @@ -50,7 +50,7 @@ test('Syntax highlighting, a11y, with JS', async ({ page }) => {
// light highlight stylesheet
await expect(importKeyword).toHaveCSS('color', 'rgb(84, 84, 84)');

await page.locator("a.quarto-color-scheme-toggle").click();
await page.locator("button.quarto-color-scheme-toggle").click();

// dark inline code
await expect(pythonCode).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
Expand All @@ -75,7 +75,7 @@ test('Syntax highlighting, arrow, with JS', async ({ page }) => {
const link = await page.locator('span.al').first();
await expect(link).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)'); // transparent

await page.locator("a.quarto-color-scheme-toggle").click();
await page.locator("button.quarto-color-scheme-toggle").click();
// dark inline code
await expect(pythonCode).toHaveCSS('background-color', 'rgba(37, 41, 46, 0.65)');
await expect(pythonCode).toHaveCSS('color', 'rgb(122, 130, 136)');
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/playwright/tests/html-themes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ test('Dark and light theme respect user themes', async ({ page }) => {
await page.goto('./html/dark-light-theme-custom/');
const locatr = await page.locator('div').filter({ hasText: 'Quarto Playground' }).first()
await expect(locatr).toHaveCSS('background-color', 'rgb(255, 0, 0)');
await page.locator("a.quarto-color-scheme-toggle").click();
await page.locator("button.quarto-color-scheme-toggle").click();
const locatr2 = await page.locator('div').filter({ hasText: 'Quarto Playground' }).first()
await expect(locatr2).toHaveCSS('background-color', 'rgb(255, 0, 0)');
});
Expand All @@ -17,7 +17,7 @@ test('Dark theming toggle change to dark background ', async ({ page }) => {
const locatr = page.getByText('Quarto Playground This is a');
await expect(locatr).toHaveCSS('background-color', 'rgb(255, 255, 255)');
// switching to dark theme using toggle
await page.locator("a.quarto-color-scheme-toggle").click();
await page.locator("button.quarto-color-scheme-toggle").click();
const locatr2 = await page.locator('div').filter({ hasText: 'Quarto Playground' }).first()
await expect(locatr2).toHaveCSS('background-color', 'rgb(255, 0, 0)');
});
Expand Down
Loading
Loading