Skip to content
Merged
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
1 change: 1 addition & 0 deletions news/changelog-1.11.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ All changes included in 1.11:
- ([#15080](https://github.com/quarto-dev/quarto-cli/issues/15080)): Fix footnote, citation, cross-reference and code annotation popups not closing with Esc, and staying open over the next focused element after keyboard focus leaves them.
- ([#15129](https://github.com/quarto-dev/quarto-cli/issues/15129)): Fix default listings rendering an empty, focusable metadata link for posts with no date, author or other listed field to show, flagged by axe-core (`link-name`).
- ([#15159](https://github.com/quarto-dev/quarto-cli/issues/15159), [#15160](https://github.com/quarto-dev/quarto-cli/issues/15160)): Fix the `revealjs` speaker view's Layout selector having no accessible name or visible keyboard focus, and its current slide, upcoming slide, time, and notes areas not being named regions. Changing layouts no longer forwards control keys to the slides, and the slide navigation buttons no longer introduce an unrelated complementary landmark. (author: @jooyoungseo)
- ([#15211](https://github.com/quarto-dev/quarto-cli/issues/15211)): Fix default and grid listings rendering empty, focusable links when an item description contains a link, flagged by axe-core (`link-name`), a regression introduced in v1.7.7. Links in descriptions are shown as text again, so the whole description links to the item.

## Formats

Expand Down
8 changes: 6 additions & 2 deletions src/resources/projects/website/listing/item-default.ejs.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,16 +88,20 @@ print(`<div class="metadata-value listing-${field}">${listing.utilities.outputLi

<% if (fields.includes('description')) { %>

::: {.delink .listing-description}

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

<%= item.description %>

```{=html}
</a></div>
</a>
```

:::

<% } %>

:::
Expand Down
8 changes: 2 additions & 6 deletions src/resources/projects/website/listing/item-grid.ejs.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,11 @@ return !["title", "image", "image-alt", "date", "author", "subtitle", "descripti

<% if (showField('description')) { %>

```{=html}
<div class="card-text listing-description delink">
```
::: {.card-text .listing-description .delink}

<%= item.description %>

```{=html}
</div>
```
:::

<% } %>

Expand Down
4 changes: 2 additions & 2 deletions tests/docs/playwright/listings/external-links/custom-icon.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ listing:
title: "Local Post"
- path: https://example.com/external-article
title: "External Article"
description: "An external article, see the [docs](https://example.org/docs)."
type: default
type: table
fields: [title]
sort: "title desc"
page-size: 1
---
Expand Down
4 changes: 4 additions & 0 deletions tests/docs/smoke-all/listings/description-links/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/.quarto/
_site/

**/*.quarto_ipynb
5 changes: 5 additions & 0 deletions tests/docs/smoke-all/listings/description-links/_quarto.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
project:
type: website

website:
title: "Listing descriptions with links"
33 changes: 33 additions & 0 deletions tests/docs/smoke-all/listings/description-links/default.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
title: "Default listing, description with a link"
listing:
id: description-links
type: default
contents:
- path: https://example.com/article
title: "Article"
description: "Read the article, see the [docs](https://example.org/docs)."
_quarto:
render-project: true
tests:
html:
ensureHtmlElements:
-
# the description stays inside the link to the item
- ".quarto-listing-container-default .listing-description > a[href='https://example.com/article'] span"
-
# the link must be removed, not nested in the item's wrapping <a>
- ".quarto-listing-container-default .listing-description a[href='https://example.org/docs']"
ensureFileRegexMatches:
-
# the link text is kept
- '<span>docs</span>'
---

::: {#description-links}
:::

The default listing wraps the item description in a link to the item. A link
inside the description would nest one `<a>` in another, which the HTML parser
splits into empty, unnamed links. The description link must be turned into
plain text instead.
33 changes: 33 additions & 0 deletions tests/docs/smoke-all/listings/description-links/grid.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
title: "Grid listing, description with a link"
listing:
id: description-links
type: grid
contents:
- path: https://example.com/article
title: "Article"
description: "Read the article, see the [docs](https://example.org/docs)."
_quarto:
render-project: true
tests:
html:
ensureHtmlElements:
-
# the description stays inside the card's link to the item
- ".quarto-listing-container-grid a.quarto-grid-link[href='https://example.com/article'] .listing-description span"
-
# the link must be removed, not nested in the item's wrapping <a>
- ".quarto-listing-container-grid .listing-description a[href='https://example.org/docs']"
ensureFileRegexMatches:
-
# the link text is kept
- '<span>docs</span>'
---

::: {#description-links}
:::

The grid listing wraps the whole item card in a link to the item. A link
inside the description would nest one `<a>` in another, which the HTML parser
splits into empty, unnamed links. The description link must be turned into
plain text instead.
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,10 @@ listingTypes.forEach(
const violations = await browserPage.evaluate(async (selector) => {
const axe = (window as any).axe;
const container = document.querySelector(selector)!;
// A description containing a link splits the item's wrapping <a>,
// leaving empty links that fail link-name; a separate bug.
const emptyWrappers = Array.from(
container.querySelectorAll("a"),
).filter((a) => a.children.length === 0 && a.textContent!.trim() === "");
// heading-order judges the listing heading levels against the
// page, which this test does not cover.
const result = await axe.run(
{ include: [container], exclude: emptyWrappers },
{ include: [container] },
{ rules: { "heading-order": { enabled: false } } },
);
return result.violations.map(
Expand All @@ -127,24 +122,6 @@ listingTypes.forEach(
await browserPage.evaluate(afterContent(itemAnchorSelector)),
).toBe("none");
});

test("description link keeps its external icon", async ({ page: browserPage }) => {
await browserPage.goto(`./listings/external-links/_site/${page}`);
await browserPage.waitForSelector(containerSelector);

const descriptionLink = browserPage
.locator(containerSelector)
.locator('a[href="https://example.org/docs"]');

await expect(descriptionLink).toHaveClass(/external/);
const content = await browserPage.evaluate(
afterContent(
`${containerSelector} a[href="https://example.org/docs"]`,
),
);
expect(content).not.toBe("none");
expect(content).not.toBe("missing");
});
}
});
},
Expand Down Expand Up @@ -183,18 +160,19 @@ test.describe("External links shown after listing updates", () => {
browserPage.locator("#custom-icon-listing-pagination .page.page-link", {
hasText: n,
});
const descriptionLink = browserPage.locator(
'.quarto-listing-default a[href="https://example.org/docs"]',
// Table listing titles are the only item links that keep the icon.
const titleLink = browserPage.locator(
'.quarto-listing-table a.listing-title[href="https://example.com/external-article"]',
);

for (const n of ["2", "1", "2", "1", "2"]) {
await pageLink(n).click();
}
await expect(descriptionLink).toBeVisible();
await expect(titleLink).toBeVisible();
// The icon is inserted as the next sibling of the link it decorates.
await expect(descriptionLink.locator("xpath=following-sibling::i[contains(@class, 'quarto-ext-icon')]"))
await expect(titleLink.locator("xpath=following-sibling::i[contains(@class, 'quarto-ext-icon')]"))
.toHaveCount(1);
await expect(descriptionLink).toHaveAttribute("aria-describedby", newWindowNoteId);
await expect(titleLink).toHaveAttribute("aria-describedby", newWindowNoteId);
});
});

Expand Down
Loading