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
15 changes: 15 additions & 0 deletions docs/e2e/annotations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,21 @@ test('the note opens out of its marker with room after the line, and under it ot
}
});

test('the note opens out of its marker when only the empty space of a longer line next to it is under the box', async ({
page,
isMobile,
}) => {
test.skip(isMobile, 'A phone has no room beside the line.');
const block = example(page);
const marker = block.getByRole('button', { name: 'Annotation 1' });
await marker.evaluate((el) => {
const above = el.closest('.ec-line')?.previousElementSibling?.querySelector('.code');
above?.append(' # a longer line above');
});
const { note } = await open(block, 1);
await expect(note).toHaveClass(/scb-annotation-end/);
});

test('with the keyboard, several notes can stay open, the badge shows focus, and Escape closes them', async ({
page,
}) => {
Expand Down
23 changes: 11 additions & 12 deletions packages/starlight-codeblocks/src/client/annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,26 @@ const END = 'scb-annotation-end';
const WAIT = 'scb-annotation-wait';
const stops = new WeakMap<Element, () => void>();

/** The right edge of a line's code text, without its markers and popovers. */
function textEnd(line: Element) {
let right = Number.NEGATIVE_INFINITY;
const intersects = (a: DOMRect, b: DOMRect) =>
a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom;

/** True when the box covers a glyph of the line's code text, not counting its markers and popovers. */
function coversText(line: Element, box: DOMRect) {
const range = document.createRange();
const walker = document.createTreeWalker(line, NodeFilter.SHOW_TEXT);
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
const end = node.textContent?.trimEnd().length ?? 0;
if (!end || node.parentElement?.closest('.scb-annotation, .scb-float')) continue;
range.setStart(node, 0);
range.setEnd(node, end);
right = Math.max(right, range.getBoundingClientRect().right);
for (const r of range.getClientRects()) {
// A glyph box reaches past the ink of most glyphs, so the note may cover its top and bottom quarter.
if (intersects(new DOMRect(r.x, r.y + r.height / 4, r.width, r.height / 2), box)) return true;
}
}
return right;
return false;
}

const intersects = (a: DOMRect, b: DOMRect) =>
a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom;

/** True when the box, beside the marker, stays in the visible block and the viewport and covers no code or other marker. */
function fitsBeside(popover: HTMLElement, pre: HTMLElement) {
const box = popover.getBoundingClientRect();
Expand All @@ -36,10 +38,7 @@ function fitsBeside(popover: HTMLElement, pre: HTMLElement) {
) {
return false;
}
for (const line of pre.querySelectorAll('.ec-line')) {
const r = line.getBoundingClientRect();
if (r.bottom > box.top && r.top < box.bottom && textEnd(line) > box.left) return false;
}
for (const line of pre.querySelectorAll('.ec-line')) if (coversText(line, box)) return false;
for (const marker of pre.querySelectorAll('button.scb-annotation')) {
if (marker !== popover.previousElementSibling && intersects(marker.getBoundingClientRect(), box)) return false;
}
Expand Down
Loading