Skip to content

Commit 6f4faf6

Browse files
committed
Keep the code copy button clear of the annotation gutter
Since the copy button moved out of the pre into the outer scaffold, the rule that shifted it left of the annotation gutter matched nothing, so the button sat on top of the marker of an annotated first line and swallowed its hover or click. The scaffold stays: it keeps the button from scrolling with the code while line numbers need visible overflow. The rule now targets the button through the scaffold that holds an annotated pre. Its margin can no longer be 1.25em: that relied on the button inheriting the code font size, and as a sibling of div.sourceCode its em is now the body size. The margin is the gutter width, calc(20px + 0.5em) in code font size, rewritten as 0.5 times $code-block-font-size so it holds when a theme changes that size, plus the default 5px. The button and the pre resolve em from the same parent, so the two stay in step. The test now also covers select mode, a theme with a 0.75em code block font size, and checks that the button of a block without annotations keeps its default position. Fixes #13444
1 parent b5e53db commit 6f4faf6

7 files changed

Lines changed: 115 additions & 29 deletions

File tree

‎news/changelog-1.11.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ All changes included in 1.11:
22

33
## Regression fixes
44

5+
- ([#13444](https://github.com/quarto-dev/quarto-cli/issues/13444)): Fix the code copy button in `html` covering the code annotation marker on the first line of an annotated code block, which stopped the annotation from showing on hover or click (regression introduced in v1.8.15).
56
- ([#14920](https://github.com/quarto-dev/quarto-cli/issues/14920)): Fix Windows ARM diagnostics for x64 R crashes to preserve the specialized error in `quarto check`, JSON output, and render failures, and update the native ARM64 R installation guidance.
67
- ([#14731](https://github.com/quarto-dev/quarto-cli/pull/14731)): Fix `QUARTO_VENDOR_BINARIES=false` builds silently succeeding with missing binaries. The `configure` step's "is it on PATH?" check for `typst-gather`, `typst`, `pandoc` and `esbuild` compared an unawaited `Promise` against `undefined`, so the guard never fired. A build with `typst-gather` neither vendored nor on PATH reported success and produced a Quarto tree without it, surfacing only at render time as `typst-gather analyze failed; staging all packages as fallback`. The missing-binary error message now also names the path that was checked and the `QUARTO_TYPST_GATHER` environment variable.
78
- ([#14741](https://github.com/quarto-dev/quarto-cli/issues/14741)): Fix PDF rendering of a cross-referenced table failing with `! Extra }, or forgotten \endgroup.` when the `endfloat` package defers `longtable` environments (`\DeclareDelayedFloatFlavor*{longtable}{table}`).

‎src/resources/formats/html/bootstrap/_bootstrap-rules.scss‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1997,8 +1997,11 @@ textarea {
19971997

19981998
// Code Annotation
19991999

2000-
.code-annotated .code-copy-button {
2001-
margin-right: 1.25em;
2000+
// The copy button is a sibling of div.sourceCode, so its em is not the code
2001+
// font size. Clear the gutter (calc(20px + 0.5em) in code font size) plus the
2002+
// default 5px margin.
2003+
.code-copy-outer-scaffold:has(pre.code-annotated) > .code-copy-button {
2004+
margin-right: calc(20px + 0.5 * #{$code-block-font-size} + 5px);
20022005
margin-top: 0;
20032006
padding-bottom: 0;
20042007
padding-top: 3px;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
title: "Code copy button and code annotation marker (code block font size)"
3+
format:
4+
html:
5+
theme: [default, code-annotation-copy-button-font-size.scss]
6+
code-annotations: hover
7+
---
8+
9+
```{.python}
10+
import polars as pl # <1>
11+
```
12+
13+
1. This is a code block with a copy button and an annotation.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/*-- scss:defaults --*/
2+
$code-block-font-size: 0.75em;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
title: "Code copy button and code annotation marker (select)"
3+
format: html
4+
code-annotations: select
5+
---
6+
7+
```{.python}
8+
import polars as pl # <1>
9+
```
10+
11+
1. This is a code block with a copy button and an annotation.

‎tests/docs/playwright/html/code-annotation-copy-button.qmd‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,9 @@ import polars as pl # <1>
99
```
1010

1111
1. This is a code block with a copy button and an annotation.
12+
13+
A code block without annotations keeps the copy button in its default place.
14+
15+
```{.python}
16+
import polars as pl
17+
```
Lines changed: 77 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,85 @@
1-
import { expect, test } from "@playwright/test";
1+
import { expect, Locator, test } from "@playwright/test";
22
import { getUrl } from "../src/utils";
33

44
// Regression test for #13444. The code copy button sits in the top-right
55
// corner of the code block, where the annotation gutter shows the marker of
66
// an annotated first line. The button must stay clear of the gutter so the
7-
// marker can still be hovered to show its annotation.
8-
test("code copy button does not cover the code annotation marker", async ({
9-
page,
10-
}) => {
11-
await page.goto(getUrl("html/code-annotation-copy-button.html"), {
12-
waitUntil: "load",
13-
});
7+
// marker can still be hovered (or clicked) to show its annotation.
148

15-
const anchor = page.locator("button.code-annotation-anchor");
16-
await expect(anchor).toHaveCount(1);
17-
const box = await anchor.boundingBox();
9+
const rightEdge = async (locator: Locator) => {
10+
const box = await locator.boundingBox();
1811
expect(box).not.toBeNull();
19-
const x = box!.x + box!.width / 2;
20-
const y = box!.y + box!.height / 2;
21-
22-
// Hovering the code block reveals the copy button, so check the marker is
23-
// still the top-most element at its own center while the button is shown.
24-
await page.mouse.move(x, y);
25-
const hit = await page.evaluate(
26-
([px, py]) =>
27-
document.elementFromPoint(px, py)?.closest(
28-
".code-annotation-anchor, .code-copy-button",
29-
)?.className ?? null,
30-
[x, y],
31-
);
32-
expect(hit).toContain("code-annotation-anchor");
33-
34-
await expect(page.locator(".code-annotation-tip-content")).toBeVisible();
12+
return box!.x + box!.width;
13+
};
14+
15+
const testCases = [
16+
{ name: "hover", url: "html/code-annotation-copy-button.html" },
17+
{ name: "select", url: "html/code-annotation-copy-button-select.html" },
18+
{
19+
name: "hover with a custom code block font size",
20+
url: "html/code-annotation-copy-button-font-size.html",
21+
},
22+
];
23+
24+
test.describe("code copy button and code annotation marker", () => {
25+
for (const { name, url } of testCases) {
26+
test(`${name} — copy button does not cover the marker`, async ({ page }) => {
27+
await page.goto(getUrl(url), { waitUntil: "load" });
28+
29+
const scaffold = page.locator(".code-copy-outer-scaffold").filter({
30+
has: page.locator("pre.code-annotated"),
31+
});
32+
const anchor = scaffold.locator("button.code-annotation-anchor");
33+
await expect(anchor).toHaveCount(1);
34+
const box = await anchor.boundingBox();
35+
expect(box).not.toBeNull();
36+
const x = box!.x + box!.width / 2;
37+
const y = box!.y + box!.height / 2;
38+
39+
// Hovering the code block reveals the copy button, so check the marker
40+
// is still the top-most element at its own center while the button is
41+
// shown.
42+
await page.mouse.move(x, y);
43+
const hit = await page.evaluate(
44+
([px, py]) =>
45+
document.elementFromPoint(px, py)?.closest(
46+
".code-annotation-anchor, .code-copy-button",
47+
)?.className ?? null,
48+
[x, y],
49+
);
50+
expect(hit).toContain("code-annotation-anchor");
51+
52+
// The button sits the default 5px left of the gutter, whatever the code
53+
// block font size the gutter width is derived from.
54+
const gutterLeft = (await scaffold.locator(".code-annotation-gutter")
55+
.boundingBox())!.x;
56+
const buttonRight = await rightEdge(
57+
scaffold.locator(".code-copy-button"),
58+
);
59+
expect(Math.abs(gutterLeft - buttonRight - 5)).toBeLessThanOrEqual(1);
60+
61+
if (name === "select") {
62+
await page.mouse.click(x, y);
63+
}
64+
await expect(page.locator(".code-annotation-tip-content")).toBeVisible();
65+
});
66+
}
67+
68+
test("copy button of a block without annotations is unchanged", async ({
69+
page,
70+
}) => {
71+
await page.goto(getUrl("html/code-annotation-copy-button.html"), {
72+
waitUntil: "load",
73+
});
74+
75+
const scaffold = page.locator(".code-copy-outer-scaffold").filter({
76+
hasNot: page.locator("pre.code-annotated"),
77+
});
78+
await expect(scaffold).toHaveCount(1);
79+
const button = scaffold.locator(".code-copy-button");
80+
await expect(button).toHaveCSS("right", "0px");
81+
await expect(button).toHaveCSS("margin-right", "5px");
82+
expect((await rightEdge(scaffold)) - (await rightEdge(button)))
83+
.toBeCloseTo(5, 0);
84+
});
3585
});

0 commit comments

Comments
 (0)