Skip to content

Commit 31eaf2f

Browse files
committed
fix(ui): uses overlay link pattern for ItemRow
- replaces stretched pseudo-element with separate overlay <a> - fixes truncate clipping ::after (overflow:hidden conflict) - conditionally renders link only for valid GitHub URLs - adds aria-label with repo, number, and title context
1 parent d3a6cce commit 31eaf2f

File tree

2 files changed

+36
-22
lines changed

2 files changed

+36
-22
lines changed

src/app/components/dashboard/ItemRow.tsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,19 @@ export default function ItemRow(props: ItemRowProps) {
2828
transition-colors
2929
${isCompact() ? "px-4 py-2" : "px-4 py-3"}`}
3030
>
31+
{/* Overlay link — covers entire row; interactive children use relative z-10 */}
32+
<Show when={safeUrl()}>
33+
{(url) => (
34+
<a
35+
href={url()}
36+
target="_blank"
37+
rel="noopener noreferrer"
38+
class="absolute inset-0"
39+
aria-label={`${props.repo} #${props.number}: ${props.title}`}
40+
/>
41+
)}
42+
</Show>
43+
3144
{/* Repo badge */}
3245
<Show when={!props.hideRepo}>
3346
<span
@@ -46,14 +59,9 @@ export default function ItemRow(props: ItemRowProps) {
4659
<span class="text-base-content/60 shrink-0">
4760
#{props.number}
4861
</span>
49-
<a
50-
href={safeUrl()}
51-
target="_blank"
52-
rel="noopener noreferrer"
53-
class="font-medium text-base-content truncate after:absolute after:inset-0"
54-
>
62+
<span class="font-medium text-base-content truncate">
5563
{props.title}
56-
</a>
64+
</span>
5765
</div>
5866

5967
{/* Labels row */}
@@ -77,9 +85,9 @@ export default function ItemRow(props: ItemRowProps) {
7785
</div>
7886
</Show>
7987

80-
{/* Additional children slot */}
88+
{/* Additional children slot — z-10 to sit above stretched link */}
8189
<Show when={props.children !== undefined}>
82-
<div class={isCompact() ? "mt-0.5" : "mt-1"}>{props.children}</div>
90+
<div class={`relative z-10 ${isCompact() ? "mt-0.5" : "mt-1"}`}>{props.children}</div>
8391
</Show>
8492
</div>
8593

tests/components/ItemRow.test.tsx

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,23 +54,33 @@ describe("ItemRow", () => {
5454
screen.getByTestId("child-slot");
5555
});
5656

57+
it("children slot sits above overlay link (relative z-10)", () => {
58+
const { container } = render(() => (
59+
<ItemRow {...defaultProps}>
60+
<span data-testid="child-slot">extra content</span>
61+
</ItemRow>
62+
));
63+
const childWrapper = container.querySelector("[data-testid='child-slot']")!.parentElement!;
64+
expect(childWrapper.className).toContain("relative");
65+
expect(childWrapper.className).toContain("z-10");
66+
});
67+
5768
it("does not render children slot when not provided", () => {
5869
render(() => <ItemRow {...defaultProps} />);
5970
expect(screen.queryByTestId("child-slot")).toBeNull();
6071
});
6172

62-
it("title is a real link with correct href and target", () => {
73+
it("renders overlay link with correct href, target, rel, and aria-label", () => {
6374
render(() => <ItemRow {...defaultProps} />);
64-
const link = screen.getByText("Fix a bug").closest("a")!;
75+
const link = screen.getByRole("link", { name: /octocat\/Hello-World #42: Fix a bug/ });
6576
expect(link.getAttribute("href")).toBe(defaultProps.url);
6677
expect(link.getAttribute("target")).toBe("_blank");
6778
expect(link.getAttribute("rel")).toBe("noopener noreferrer");
6879
});
6980

70-
it("title link has no href for non-GitHub URLs", () => {
81+
it("does not render overlay link for non-GitHub URLs", () => {
7182
render(() => <ItemRow {...defaultProps} url="https://evil.com/bad" />);
72-
const link = screen.getByText("Fix a bug").closest("a")!;
73-
expect(link.getAttribute("href")).toBeNull();
83+
expect(screen.queryByRole("link")).toBeNull();
7484
});
7585

7686
it("calls onIgnore when ignore button is clicked", async () => {
@@ -84,15 +94,11 @@ describe("ItemRow", () => {
8494
expect(onIgnore).toHaveBeenCalledOnce();
8595
});
8696

87-
it("ignore button does not navigate (sits above stretched link)", async () => {
88-
const user = userEvent.setup();
89-
const onIgnore = vi.fn();
90-
render(() => <ItemRow {...defaultProps} onIgnore={onIgnore} />);
91-
97+
it("ignore button has relative z-10 to sit above overlay link", () => {
98+
render(() => <ItemRow {...defaultProps} />);
9299
const ignoreBtn = screen.getByLabelText(/Ignore #42/i);
93-
await user.click(ignoreBtn);
94-
95-
expect(onIgnore).toHaveBeenCalledOnce();
100+
expect(ignoreBtn.className).toContain("relative");
101+
expect(ignoreBtn.className).toContain("z-10");
96102
});
97103

98104
it("applies compact padding in compact density", () => {

0 commit comments

Comments
 (0)