diff --git a/news/changelog-1.11.md b/news/changelog-1.11.md index 4e98d750868..dc85f38003f 100644 --- a/news/changelog-1.11.md +++ b/news/changelog-1.11.md @@ -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 diff --git a/src/resources/projects/website/listing/item-default.ejs.md b/src/resources/projects/website/listing/item-default.ejs.md index d949ee50cee..c309964a887 100644 --- a/src/resources/projects/website/listing/item-default.ejs.md +++ b/src/resources/projects/website/listing/item-default.ejs.md @@ -88,16 +88,20 @@ print(`
${listing.utilities.outputLi <% if (fields.includes('description')) { %> +::: {.delink .listing-description} + ```{=html} - + ``` +::: + <% } %> ::: diff --git a/src/resources/projects/website/listing/item-grid.ejs.md b/src/resources/projects/website/listing/item-grid.ejs.md index 0069d20a242..fffc56e06e6 100644 --- a/src/resources/projects/website/listing/item-grid.ejs.md +++ b/src/resources/projects/website/listing/item-grid.ejs.md @@ -97,15 +97,11 @@ return !["title", "image", "image-alt", "date", "author", "subtitle", "descripti <% if (showField('description')) { %> -```{=html} - -``` +::: <% } %> diff --git a/tests/docs/playwright/listings/external-links/custom-icon.qmd b/tests/docs/playwright/listings/external-links/custom-icon.qmd index 1a854b62ca7..588a5661f15 100644 --- a/tests/docs/playwright/listings/external-links/custom-icon.qmd +++ b/tests/docs/playwright/listings/external-links/custom-icon.qmd @@ -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 --- diff --git a/tests/docs/smoke-all/listings/description-links/.gitignore b/tests/docs/smoke-all/listings/description-links/.gitignore new file mode 100644 index 00000000000..812977e5f74 --- /dev/null +++ b/tests/docs/smoke-all/listings/description-links/.gitignore @@ -0,0 +1,4 @@ +/.quarto/ +_site/ + +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/listings/description-links/_quarto.yml b/tests/docs/smoke-all/listings/description-links/_quarto.yml new file mode 100644 index 00000000000..bd7c4278398 --- /dev/null +++ b/tests/docs/smoke-all/listings/description-links/_quarto.yml @@ -0,0 +1,5 @@ +project: + type: website + +website: + title: "Listing descriptions with links" diff --git a/tests/docs/smoke-all/listings/description-links/default.qmd b/tests/docs/smoke-all/listings/description-links/default.qmd new file mode 100644 index 00000000000..7339bf79165 --- /dev/null +++ b/tests/docs/smoke-all/listings/description-links/default.qmd @@ -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 + - ".quarto-listing-container-default .listing-description a[href='https://example.org/docs']" + ensureFileRegexMatches: + - + # the link text is kept + - 'docs' +--- + +::: {#description-links} +::: + +The default listing wraps the item description in a link to the item. A link +inside the description would nest one `` in another, which the HTML parser +splits into empty, unnamed links. The description link must be turned into +plain text instead. diff --git a/tests/docs/smoke-all/listings/description-links/grid.qmd b/tests/docs/smoke-all/listings/description-links/grid.qmd new file mode 100644 index 00000000000..e50a23af516 --- /dev/null +++ b/tests/docs/smoke-all/listings/description-links/grid.qmd @@ -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 + - ".quarto-listing-container-grid .listing-description a[href='https://example.org/docs']" + ensureFileRegexMatches: + - + # the link text is kept + - 'docs' +--- + +::: {#description-links} +::: + +The grid listing wraps the whole item card in a link to the item. A link +inside the description would nest one `` in another, which the HTML parser +splits into empty, unnamed links. The description link must be turned into +plain text instead. diff --git a/tests/integration/playwright/tests/html-listing-external-links.spec.ts b/tests/integration/playwright/tests/html-listing-external-links.spec.ts index 7460367accc..7e2b216714d 100644 --- a/tests/integration/playwright/tests/html-listing-external-links.spec.ts +++ b/tests/integration/playwright/tests/html-listing-external-links.spec.ts @@ -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 , - // 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( @@ -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"); - }); } }); }, @@ -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); }); });