feat(web): stop filename squeeze on changes-panel row hover (#1114)

This commit is contained in:
Carlos Florêncio
2026-05-28 16:27:16 +01:00
committed by GitHub
parent a068c88b99
commit 8f065f1673
2 changed files with 52 additions and 4 deletions
@@ -109,6 +109,52 @@ describe("FileRow truncation (regression: path overlaps diff stats in narrow pan
});
});
describe("FileRow hover swap (stats <-> actions occupy same cell)", () => {
// Goal: hovering must not shift the file name. The stats group and the
// hover-actions group live in the same CSS grid cell — stats fade out and
// actions fade in, so the right cluster's width is stable.
it("stats container fades out on hover and never intercepts pointer events", () => {
const { container } = renderRow("file.go");
const li = container.querySelector("li") as HTMLElement;
const gridWrapper = li.children[1] as HTMLElement;
const statsLayer = gridWrapper.children[0] as HTMLElement;
expect(statsLayer.className).toContain("group-hover:opacity-0");
expect(statsLayer.className).toContain("transition-opacity");
// Defensive: stats has no interactive children today, but blocking pointer
// events on this always-non-interactive layer prevents a future regression
// if someone adds a clickable element here.
expect(statsLayer.className).toContain("pointer-events-none");
});
it("actions container fades in on hover and only accepts pointer/keyboard events while hovered", () => {
const { container } = renderRow("file.go");
const li = container.querySelector("li") as HTMLElement;
const gridWrapper = li.children[1] as HTMLElement;
const actionsLayer = gridWrapper.children[1] as HTMLElement;
expect(actionsLayer.className).toContain("opacity-0");
expect(actionsLayer.className).toContain("group-hover:opacity-100");
expect(actionsLayer.className).toContain("transition-opacity");
// Critical: the actions div is the top child in the same grid cell, so
// without pointer-events gating it would eat clicks aimed at the
// visually-shown stats. (Keyboard Tab focus is a separate concern — a
// pre-existing gap not closed by `pointer-events: none`, which only
// blocks mouse/touch.)
expect(actionsLayer.className).toContain("pointer-events-none");
expect(actionsLayer.className).toContain("group-hover:pointer-events-auto");
});
it("wrapper stacks both layers into one grid cell so width is max(stats, actions)", () => {
const { container } = renderRow("file.go");
const li = container.querySelector("li") as HTMLElement;
const gridWrapper = li.children[1] as HTMLElement;
expect(gridWrapper.className).toContain("grid");
// Arbitrary child selector pins every direct child to row 1 / col 1 so
// they overlap instead of laying out side-by-side.
expect(gridWrapper.className).toContain("col-start-1");
expect(gridWrapper.className).toContain("row-start-1");
});
});
describe("FileRow active-tab highlight", () => {
it("renders data-active='true' and bg-accent/60 when isActive", () => {
const { container } = render(
@@ -123,7 +123,11 @@ export function FileRow({
</p>
</button>
</div>
<div className="flex items-center gap-2 shrink-0">
<div className="grid items-center shrink-0 [&>*]:col-start-1 [&>*]:row-start-1">
<div className="flex items-center gap-2 justify-end transition-opacity group-hover:opacity-0 pointer-events-none">
<LineStat added={file.plus} removed={file.minus} />
<FileStatusIcon status={file.status} />
</div>
<FileRowActions
path={file.path}
repo={file.repositoryName}
@@ -142,8 +146,6 @@ export function FileRow({
) : null
}
/>
<LineStat added={file.plus} removed={file.minus} />
<FileStatusIcon status={file.status} />
</div>
</li>
);
@@ -217,7 +219,7 @@ function FileRowActions({
stageButton?: React.ReactNode;
}) {
return (
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100">
<div className="flex items-center gap-1 justify-end opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none group-hover:pointer-events-auto">
<Tooltip>
<TooltipTrigger asChild>
<button