Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4c57c89
fix: `link-external-newwindow: true` not respected by default listing
mcanouil Apr 24, 2024
6c7bc4c
fix: add rule to hide external icon in default listing
mcanouil Feb 1, 2025
a483f46
test: add external links test suite
mcanouil Jan 15, 2026
6e18d12
test: increase coverage with other listing types
mcanouil Jan 15, 2026
49b77d6
test: cover listing icons and new-tab announcement
mcanouil Oct 25, 2026
c1e20b7
fix: keep listing item anchors in a new window without an icon
mcanouil Oct 25, 2026
d97014c
test: skip heading order in the listing axe scan
mcanouil Oct 25, 2026
a1df290
feat: announce links opened in a new tab
mcanouil Oct 25, 2026
c27713a
docs: add changelog entries for #9468
mcanouil Oct 25, 2026
b86bc0b
test: wait for the external-link script before reading the icon style
mcanouil Oct 26, 2026
307b951
test: cover paginated listing items and minimal HTML
mcanouil Oct 26, 2026
bd251a8
test: sort the paginated listing so the external item is on page 2
mcanouil Oct 26, 2026
b30fddc
fix: decorate external links shown after listing pagination
mcanouil Oct 26, 2026
1747ea9
fix: strip no-external-icon from llms output
mcanouil Oct 26, 2026
424b185
fix: keep the new-tab announcement hidden in minimal HTML
mcanouil Oct 26, 2026
c8e8c0e
test: check that listing classes stay out of llms output
mcanouil Oct 26, 2026
8f6b6be
docs: name the listing types that keep the icon off items
mcanouil Oct 26, 2026
36c8c20
fix: hide the empty thumbnail link of listing items without a file
mcanouil Oct 26, 2026
0d91827
test: cover the thumbnail link of imageless external items
mcanouil Oct 26, 2026
787a7bd
docs: add changelog entry for the hidden thumbnail link
mcanouil Oct 26, 2026
4fc7ffc
fix: stop marking decorated external links
mcanouil Oct 26, 2026
d62b2f6
Merge remote-tracking branch 'upstream/main' into fix/issue9468
mcanouil Oct 26, 2026
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
3 changes: 3 additions & 0 deletions news/changelog-1.11.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ All changes included in 1.11:
- ([#3873](https://github.com/quarto-dev/quarto-cli/issues/3873), [#9003](https://github.com/quarto-dev/quarto-cli/issues/9003)): Honor `fig-alt` for Mermaid and Graphviz diagrams in HTML, revealjs, DOCX, and Typst output while preserving visible captions. (author: @jooyoungseo)
- ([#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)
- ([#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.
- ([#9469](https://github.com/quarto-dev/quarto-cli/pull/9469)): Announce links that `link-external-newwindow` opens in a new tab to screen readers, with a visually hidden "(opens in a new tab)" that can be translated with the `link-external-newwindow-label` language key. (author: @mcanouil)
- ([#9469](https://github.com/quarto-dev/quarto-cli/pull/9469)): Hide the empty thumbnail link of default listing items that have no image and no file, such as external items listed in `contents:`, from assistive technology and the tab order, as [#15066](https://github.com/quarto-dev/quarto-cli/issues/15066) already does for posts. (author: @mcanouil)
- ([#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.
- ([#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)
- ([#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`).
Expand Down Expand Up @@ -114,6 +116,7 @@ All changes included in 1.11:

### Websites

- ([#9468](https://github.com/quarto-dev/quarto-cli/issues/9468)): Fix listing items not opening in a new tab with `link-external-newwindow: true`, while keeping the external link icon off default and grid listing items. (author: @mcanouil)
- ([#10756](https://github.com/quarto-dev/quarto-cli/issues/10756)): Fix the notebook preview of an embedded `.qmd` file going missing from the output directory when a project is rendered again, which broke its source notebook link.
- ([#12014](https://github.com/quarto-dev/quarto-cli/issues/12014)): Fix `twitter-card` `image-width` and `image-height` being listed as options although Twitter Cards have no image size tags. They are no longer documented, and setting them prints a warning pointing to `open-graph`, where an image size is used. The size Quarto measures from a local image is no longer written as `twitter:image-height` and `twitter:image-width` tags, only as `og:image:height` and `og:image:width`.
- ([#12016](https://github.com/quarto-dev/quarto-cli/issues/12016)): Fix `open-graph` `image-width` and `image-height` being dropped when `site-url` is set and Quarto cannot measure the image, such as a remote image. A size declared in the site `open-graph` options no longer applies when a page sets its own `image`, even the same file: declare `image-width` and `image-height` in the page `open-graph` options or in shared metadata such as `_metadata.yml`.
Expand Down
2 changes: 2 additions & 0 deletions src/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ export const kToggleReaderMode = "toggle-reader-mode";
export const kToggleNavigation = "toggle-navigation";
export const kScrollableCodeLabel = "scrollable-code-label";
export const kScrollableOutputLabel = "scrollable-output-label";
export const kLinkExternalNewwindowLabel = "link-external-newwindow-label";
export const kRevealjsMenuButtonLabel = "revealjs-menu-button-label";

export const kNavigationSiteLabel = "navigation-site-label";
Expand Down Expand Up @@ -450,6 +451,7 @@ export const kLanguageDefaultsKeys = [
kNavigationBreadcrumbsLabel,
kScrollableCodeLabel,
kScrollableOutputLabel,
kLinkExternalNewwindowLabel,
kRevealjsMenuButtonLabel,
kCopyButtonTooltip,
kCopyButtonTooltipSuccess,
Expand Down
2 changes: 2 additions & 0 deletions src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ import {
kResourcePath,
kScrollableCodeLabel,
kScrollableOutputLabel,
kLinkExternalNewwindowLabel,
kRevealjsMenuButtonLabel,
kSearchClearButtonTitle,
kSearchCopyLinkTitle,
Expand Down Expand Up @@ -718,6 +719,7 @@ export interface FormatLanguage {
[kNavigationBreadcrumbsLabel]?: string;
[kScrollableCodeLabel]?: string;
[kScrollableOutputLabel]?: string;
[kLinkExternalNewwindowLabel]?: string;
[kRevealjsMenuButtonLabel]?: string;
[kSearchMatchingDocumentsText]?: string;
[kSearchCopyLinkTitle]?: string;
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 @@ -10532,6 +10532,7 @@ var require_yaml_intelligence_resources = __commonJS({
"skip-to-content": "string",
"scrollable-code-label": "string",
"scrollable-output-label": "string",
"link-external-newwindow-label": "string",
"revealjs-menu-button-label": "string",
"repo-action-links-edit": "string",
"repo-action-links-source": "string",
Expand Down Expand Up @@ -25827,12 +25828,12 @@ var require_yaml_intelligence_resources = __commonJS({
mermaid: "%%"
},
"handlers/mermaid/schema.yml": {
_internalId: 221295,
_internalId: 221297,
type: "object",
description: "be an object",
properties: {
"mermaid-format": {
_internalId: 221287,
_internalId: 221289,
type: "enum",
enum: [
"png",
Expand All @@ -25848,7 +25849,7 @@ var require_yaml_intelligence_resources = __commonJS({
exhaustiveCompletions: true
},
theme: {
_internalId: 221294,
_internalId: 221296,
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 @@ -3500,6 +3500,7 @@
"skip-to-content": "string",
"scrollable-code-label": "string",
"scrollable-output-label": "string",
"link-external-newwindow-label": "string",
"revealjs-menu-button-label": "string",
"repo-action-links-edit": "string",
"repo-action-links-source": "string",
Expand Down Expand Up @@ -18795,12 +18796,12 @@
"mermaid": "%%"
},
"handlers/mermaid/schema.yml": {
"_internalId": 221295,
"_internalId": 221297,
"type": "object",
"description": "be an object",
"properties": {
"mermaid-format": {
"_internalId": 221287,
"_internalId": 221289,
"type": "enum",
"enum": [
"png",
Expand All @@ -18816,7 +18817,7 @@
"exhaustiveCompletions": true
},
"theme": {
"_internalId": 221294,
"_internalId": 221296,
"type": "anyOf",
"anyOf": [
{
Expand Down
1 change: 1 addition & 0 deletions src/resources/filters/llms/llms.lua
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ local removable_classes = {
["md"] = true,
["code-with-copy"] = true,
["no-external"] = true,
["no-external-icon"] = true,
["no-anchor"] = true,
}
local removable_attributes = {
Expand Down
23 changes: 21 additions & 2 deletions src/resources/formats/html/templates/quarto-html-after-body.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,15 @@
<% } else { %>
var filterRegex = new RegExp('/' + window.location.host + '/');
<% } %>
<% if (linkExternalNewwindow) { %>
const newWindowLabel = <%= JSON.stringify(language['link-external-newwindow-label']).replace(/</g, '\\u003c') %>;
<% } %>
var isInternal = (href) => {
return filterRegex.test(href) || localhostRegex.test(href) || mailtoRegex.test(href);
}
// Inspect non-navigation links and adorn them if external
var links = window.document.querySelectorAll('a[href]:not(.nav-link):not(.navbar-brand):not(.toc-action):not(.sidebar-link):not(.sidebar-item-toggle):not(.pagination-link):not(.no-external):not([aria-hidden]):not(.dropdown-item):not(.quarto-navigation-tool):not(.about-link)');
window.quartoDecorateExternalLinks = function (root) {
var links = (root || window.document).querySelectorAll('a[href]:not(.nav-link):not(.navbar-brand):not(.toc-action):not(.sidebar-link):not(.sidebar-item-toggle):not(.pagination-link):not(.no-external):not([aria-hidden]):not(.dropdown-item):not(.quarto-navigation-tool):not(.about-link)');
for (var i=0; i<links.length; i++) {
const link = links[i];
if (!isInternal(link.href)) {
Expand All @@ -245,9 +249,21 @@
if (link.getAttribute("rel") === null) {
link.setAttribute("rel", "noopener");
}
if (!link.querySelector(".quarto-ext-newwindow")) {
var newWindowNote = window.document.createElement("span");
newWindowNote.classList.add("visually-hidden", "quarto-ext-newwindow");
// minimal HTML ships no .visually-hidden rule
newWindowNote.style.cssText = "border:0;clip:rect(0 0 0 0);height:1px;margin:0;overflow:hidden;padding:0;position:absolute;width:1px;white-space:nowrap;";
newWindowNote.textContent = " " + newWindowLabel;
link.appendChild(newWindowNote);
if (link.hasAttribute("aria-label")) {
link.setAttribute("aria-label", link.getAttribute("aria-label") + " " + newWindowLabel);
}
}
<% } %>


if (!link.classList.contains('no-external-icon')) {
<% if (linkExternalIcon && typeof(linkExternalIcon) === 'string') { %>
// external icon, if provided
var extIcon = window.document.createElement('i');
Expand All @@ -258,8 +274,11 @@
// default icon
link.classList.add("external");
<% } %>
}
}
}
};
window.quartoDecorateExternalLinks(window.document);



Expand Down Expand Up @@ -786,4 +805,4 @@
}).replace(/</g, '\\u003c') %>;
</script>
<% } %>


3 changes: 3 additions & 0 deletions src/resources/language/_language.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ navigation-breadcrumbs-label: "Breadcrumbs"
scrollable-code-label: "Scrollable code"
scrollable-output-label: "Scrollable output"

# Announced by screen readers after a link that opens in a new tab (never shown as visible text).
link-external-newwindow-label: "(opens in a new tab)"

# Accessible name for the Reveal.js slide menu button (never shown as visible text).
revealjs-menu-button-label: "Slide menu"

Expand Down
10 changes: 5 additions & 5 deletions src/resources/projects/website/listing/item-default.ejs.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ print(`<div class="metadata-value listing-${field}">${listing.utilities.outputLi
<% if (fields.includes('image')) { %>

```{=html}
<div class="thumbnail"><a href="<%- item.path %>" class="no-external">
<div class="thumbnail"><a href="<%- item.path %>" class="no-external-icon"<%= !item.image && !item.outputHref ? ' aria-hidden="true" tabindex="-1"' : '' %>>
<% if (item.image) { %>
<%= listing.utilities.img(itemNumber, item.image, "thumbnail-image", item['image-alt'], item['image-lazy-loading'] ?? listing['image-lazy-loading']) %>
<% } else { %>
Expand All @@ -68,9 +68,9 @@ print(`<div class="metadata-value listing-${field}">${listing.utilities.outputLi
::: {.body}

<% if (fields.includes('title')) { %>
<h3 class="no-anchor listing-title"><a href="<%- item.path %>" class="no-external"><%= item.title %></a></h3>
<h3 class="no-anchor listing-title"><a href="<%- item.path %>" class="no-external-icon"><%= item.title %></a></h3>
<% if (fields.includes('subtitle') && hasText(item.subtitle)) { %>
<div class="listing-subtitle"><a href="<%- item.path %>" class="no-external"><%= item.subtitle %></a></div>
<div class="listing-subtitle"><a href="<%- item.path %>" class="no-external-icon"><%= item.subtitle %></a></div>
<% } %>
<% } %>

Expand All @@ -89,7 +89,7 @@ print(`<div class="metadata-value listing-${field}">${listing.utilities.outputLi
<% if (fields.includes('description')) { %>

```{=html}
<div class="delink listing-description"><a href="<%- item.path %>" class="no-external">
<div class="delink listing-description"><a href="<%- item.path %>" class="no-external-icon">
```

<%= item.description %>
Expand All @@ -107,7 +107,7 @@ print(`<div class="metadata-value listing-${field}">${listing.utilities.outputLi
<% if (hasMetadata(item)) { %>

```{=html}
<a href="<%- item.path %>" class="no-external">
<a href="<%- item.path %>" class="no-external-icon">
```

<% } %>
Expand Down
2 changes: 1 addition & 1 deletion src/resources/projects/website/listing/item-grid.ejs.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ return !["title", "image", "image-alt", "date", "author", "subtitle", "descripti
::: {.g-col-1 <%= listing.utilities.metadataAttrs(item) %> }

```{=html}
<a href="<%- item.path %>" class="quarto-grid-link">
<a href="<%- item.path %>" class="quarto-grid-link no-external-icon">
<div class="quarto-grid-item card h-100 <%= `card-${align}` %><%= hideBorders ? ' borderless' : '' %>">
```

Expand Down
1 change: 1 addition & 0 deletions src/resources/projects/website/listing/quarto-listing.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ window["quarto-listing-loaded"] = () => {
// and refresh any newly visible items.
list.on("updated", function () {
renderVisibleProgressiveImages(list);
window.quartoDecorateExternalLinks?.(list.list);
setTimeout(() => refreshPaginationHandlers(listingId));

// Show or hide the no matching message
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 @@ -1528,6 +1528,7 @@
skip-to-content: string
scrollable-code-label: string
scrollable-output-label: string
link-external-newwindow-label: string
revealjs-menu-button-label: string
repo-action-links-edit: string
repo-action-links-source: 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 @@ -1931,6 +1931,9 @@
"scrollable-output-label": {
"type": "string"
},
"link-external-newwindow-label": {
"type": "string"
},
"revealjs-menu-button-label": {
"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 @@ -786,6 +786,7 @@ export type FormatLanguage = {
"skip-to-content"?: string;
"scrollable-code-label"?: string;
"scrollable-output-label"?: string;
"link-external-newwindow-label"?: string;
"revealjs-menu-button-label"?: string;
"repo-action-links-edit"?: string;
"repo-action-links-source"?: string;
Expand Down
1 change: 1 addition & 0 deletions src/resources/types/zod/schema-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,7 @@ export const ZodFormatLanguage = z.object({
"skip-to-content": z.string(),
"scrollable-code-label": z.string(),
"scrollable-output-label": z.string(),
"link-external-newwindow-label": z.string(),
"revealjs-menu-button-label": z.string(),
"repo-action-links-edit": z.string(),
"repo-action-links-source": z.string(),
Expand Down
3 changes: 3 additions & 0 deletions tests/docs/playwright/listings/external-links/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/.quarto/
**/*.quarto_ipynb
/_site/
13 changes: 13 additions & 0 deletions tests/docs/playwright/listings/external-links/_quarto.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
project:
type: website

website:
title: "Testing external links in listings"

link-external-newwindow: true
link-external-icon: true

format: html

axe:
output: console
16 changes: 16 additions & 0 deletions tests/docs/playwright/listings/external-links/grid.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
title: "External Links Test - Grid"
listing:
id: grid-listing
contents:
- path: https://example.com/external-article
title: "External Article"
description: "An external article that should open in new window, see the [docs](https://example.org/docs)."
- path: local-post.qmd
title: "Local Post"
description: "A local post that should NOT open in new window."
type: grid
---

::: {#grid-listing}
:::
16 changes: 16 additions & 0 deletions tests/docs/playwright/listings/external-links/index.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
title: "External Links Test - Default"
listing:
id: external-listing
contents:
- path: https://example.com/external-article
title: "External Article"
description: "An external article that should open in new window, see the [docs](https://example.org/docs)."
- path: local-post.qmd
title: "Local Post"
description: "A local post that should NOT open in new window."
type: default
---

::: {#external-listing}
:::
5 changes: 5 additions & 0 deletions tests/docs/playwright/listings/external-links/local-post.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: "Local Post"
---

Local content.
8 changes: 8 additions & 0 deletions tests/docs/playwright/listings/external-links/minimal.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: "External Links Test - Minimal HTML"
format:
html:
minimal: true
---

An [Outside](https://example.com/minimal) link.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: "External Links Test - Option Absent"
link-external-newwindow: false
---

An [Outside](https://example.com/outside) link.
Loading
Loading