diff --git a/apps/web/src/components/ui/middle-truncate.browser.test.tsx b/apps/web/src/components/ui/middle-truncate.browser.test.tsx
index 3fbf36523254..91de6b169f64 100644
--- a/apps/web/src/components/ui/middle-truncate.browser.test.tsx
+++ b/apps/web/src/components/ui/middle-truncate.browser.test.tsx
@@ -1,4 +1,4 @@
-import { describe, expect, it } from "vite-plus/test";
+import { describe, expect, it, vi } from "vite-plus/test";
import { render } from "../../../test/render";
import { MiddleTruncate } from "./middle-truncate";
@@ -24,6 +24,24 @@ describe("MiddleTruncate", () => {
expect(label()).toHaveStyle({ clipPath: "none" });
});
+ it("fits every head mounted in the same commit", async () => {
+ const values = ["t3/investigate-release-build-times", "t3/remove-stale-worktree-branches"];
+ await using screen = await render(
+ <>
+ {values.map((value) => (
+
{narrow(value)}
+ ))}
+ >,
+ );
+ const heads = () => [
+ ...screen.container.querySelectorAll("[title] > span:first-child"),
+ ];
+
+ await vi.waitFor(() =>
+ expect(heads().filter((head) => head.hasAttribute("data-overflow"))).toHaveLength(2),
+ );
+ });
+
it.each([
["Fix the sidebar", 7],
["report\u2003\u2003draft.txt", 9],
diff --git a/apps/web/src/components/ui/middle-truncate.tsx b/apps/web/src/components/ui/middle-truncate.tsx
index faf8ab6b3f39..a5213209f0ed 100644
--- a/apps/web/src/components/ui/middle-truncate.tsx
+++ b/apps/web/src/components/ui/middle-truncate.tsx
@@ -25,8 +25,8 @@ export function MiddleTruncate({
const element = headRef.current;
// A new head can keep the same box size, so refit here, not only on resize.
if (!element || head === undefined) return;
- fitHead(element);
- return observeResize(element, () => fitHead(element));
+ scheduleFit(element);
+ return observeResize(element, () => scheduleFit(element));
}, [head]);
return (
();
+
+function scheduleFit(head: HTMLElement) {
+ if (pendingFits.size === 0) queueMicrotask(fitPendingHeads);
+ pendingFits.add(head);
+}
+
// An overflowing head is cut after its last whole character: clip the partial glyph
// and slide the tail left over the slack. Neither changes layout, so the observed
// size stays the true available width.
+function fitPendingHeads() {
+ const heads = [...pendingFits].filter((head) => head.isConnected);
+ pendingFits.clear();
+ const overflowing = heads.map(
+ (head) => head.scrollWidth > head.clientWidth && head.firstChild instanceof Text,
+ );
+ heads.forEach((head, index) => head.toggleAttribute("data-overflow", overflowing[index]));
+ const slacks = heads.map((head, index) => (overflowing[index] ? measureSlack(head) : 0));
+ heads.forEach((head, index) => {
+ const slack = slacks[index]!;
+ head.style.clipPath = slack > 0 ? `inset(-0.25em ${slack}px -0.25em 0)` : "";
+ const tail = head.nextElementSibling;
+ if (tail instanceof HTMLElement) tail.style.translate = slack > 0 ? `-${slack}px 0` : "";
+ });
+}
-function fitHead(head: HTMLElement) {
- const text = head.firstChild;
- const overflowing = head.scrollWidth > head.clientWidth && text instanceof Text;
- head.toggleAttribute("data-overflow", overflowing);
- let slack = 0;
- if (overflowing) {
- const range = document.createRange();
- const left = head.getBoundingClientRect().left;
- const widthTo = (end: number) => {
- range.setStart(text, 0);
- range.setEnd(text, end);
- return range.getBoundingClientRect().right - left;
- };
- let fits = 0;
- let over = text.length;
- while (over - fits > 1) {
- const mid = (fits + over) >> 1;
- if (widthTo(mid) <= head.clientWidth) fits = mid;
- else over = mid;
- }
- if (/[\uD800-\uDBFF]/.test(text.data[fits - 1] ?? "")) fits -= 1;
- slack = Math.max(0, head.clientWidth - widthTo(fits));
+function measureSlack(head: HTMLElement) {
+ const text = head.firstChild as Text;
+ const range = document.createRange();
+ const left = head.getBoundingClientRect().left;
+ const width = head.clientWidth;
+ const widthTo = (end: number) => {
+ range.setStart(text, 0);
+ range.setEnd(text, end);
+ return range.getBoundingClientRect().right - left;
+ };
+ let fits = 0;
+ let over = text.length;
+ while (over - fits > 1) {
+ const mid = (fits + over) >> 1;
+ if (widthTo(mid) <= width) fits = mid;
+ else over = mid;
}
- head.style.clipPath = slack > 0 ? `inset(-0.25em ${slack}px -0.25em 0)` : "";
- const tail = head.nextElementSibling;
- if (tail instanceof HTMLElement) tail.style.translate = slack > 0 ? `-${slack}px 0` : "";
+ if (/[\uD800-\uDBFF]/.test(text.data[fits - 1] ?? "")) fits -= 1;
+ return Math.max(0, width - widthTo(fits));
}
// The segments render pre so the space at the cut survives, so collapse here what CSS