From adec9c309cd1a88b26fb69d06c791d7816610d00 Mon Sep 17 00:00:00 2001 From: tdlxgpp Date: Mon, 24 Aug 2026 00:59:42 +0800 Subject: [PATCH] fix: keep top-level dashboard nav highlighted on nested routes (#220) --- .../dashboard/DashboardShell.test.tsx | 86 +++++++++++++++++++ src/components/dashboard/DashboardShell.tsx | 14 ++- 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 src/components/dashboard/DashboardShell.test.tsx diff --git a/src/components/dashboard/DashboardShell.test.tsx b/src/components/dashboard/DashboardShell.test.tsx new file mode 100644 index 0000000..fbf5fae --- /dev/null +++ b/src/components/dashboard/DashboardShell.test.tsx @@ -0,0 +1,86 @@ +/** + * Tests for DashboardShell active-nav highlighting (#220). + * + * Before the fix, `pathname === item.href` meant a nested route such as + * /issues/abc123 did not highlight "Bounty pipeline", so the sidebar looked + * like nothing was selected whenever the user drilled into a child page. + * + * The expected behavior: + * - Top-level sections (/issues, /milestones, /reputation/...) stay + * highlighted on their child routes. + * - Dashboard overview links (/dashboard/...) still use exact matching so a + * nested route under an unrelated dashboard section does not keep the + * overview link permanently highlighted. + */ + +import { render, screen } from "@testing-library/react"; +import { usePathname } from "next/navigation"; +import { DashboardShell } from "./DashboardShell"; + +jest.mock("next/navigation", () => ({ + usePathname: jest.fn(), +})); + +const mockedUsePathname = usePathname as jest.Mock; + +function renderMaintainerShell() { + return render( + +
content
+
, + ); +} + +function activeLink(name: string | RegExp) { + return screen.getByRole("link", { name }); +} + +describe("DashboardShell — active nav highlighting (#220)", () => { + it("highlights a top-level section on its exact route", () => { + mockedUsePathname.mockReturnValue("/issues"); + renderMaintainerShell(); + + expect(activeLink("Bounty pipeline")).toHaveClass("bg-indigo-50"); + }); + + it("keeps a top-level section highlighted on a nested child route", () => { + mockedUsePathname.mockReturnValue("/issues/abc123"); + renderMaintainerShell(); + + expect(activeLink("Bounty pipeline")).toHaveClass("bg-indigo-50"); + expect(activeLink("Bounty pipeline")).toHaveAttribute("href", "/issues"); + }); + + it("keeps Milestones highlighted on its nested child route", () => { + mockedUsePathname.mockReturnValue("/milestones/planning-2026"); + renderMaintainerShell(); + + expect(activeLink("Milestones")).toHaveClass("bg-indigo-50"); + }); + + it("does not highlight unrelated sections while on a nested child route", () => { + mockedUsePathname.mockReturnValue("/issues/abc123"); + renderMaintainerShell(); + + expect(activeLink("Milestones")).not.toHaveClass("bg-indigo-50"); + }); + + it("uses exact matching for dashboard overview links", () => { + const { unmount } = renderMaintainerShell(); + mockedUsePathname.mockReturnValue("/dashboard/maintainer/other"); + + // Neither the Overview nor Team link (both point at + // /dashboard/maintainer) should remain active on a different nested + // dashboard path. + expect(activeLink("Overview")).not.toHaveClass("bg-indigo-50"); + expect(activeLink("Team")).not.toHaveClass("bg-indigo-50"); + unmount(); + }); + + it("still highlights the dashboard overview link on its exact route", () => { + mockedUsePathname.mockReturnValue("/dashboard/maintainer"); + renderMaintainerShell(); + + expect(activeLink("Overview")).toHaveClass("bg-indigo-50"); + }); +}); diff --git a/src/components/dashboard/DashboardShell.tsx b/src/components/dashboard/DashboardShell.tsx index d47dbf1..bafeed4 100644 --- a/src/components/dashboard/DashboardShell.tsx +++ b/src/components/dashboard/DashboardShell.tsx @@ -48,6 +48,18 @@ const roleSwitcher: { role: Role; label: string; href: string }[] = [ { role: "sponsor", label: "Sponsor", href: "/dashboard/sponsor" }, ]; +function isNavItemActive(pathname: string, href: string): boolean { + // Dashboard overview links are single-page destinations: nested routes + // under a different dashboard section must not keep them highlighted. + if (href.startsWith("/dashboard/")) { + return pathname === href; + } + + // Top-level sections (e.g. /issues, /milestones) should stay highlighted + // while the user is drilled into a child page such as /issues/abc123. + return pathname === href || pathname.startsWith(`${href}/`); +} + export function DashboardShell({ role, title, @@ -75,7 +87,7 @@ export function DashboardShell({