Skip to content
Open
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
50 changes: 50 additions & 0 deletions ecosystem-explorer/src/components/layout/header.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter } from "react-router-dom";
import { describe, it, expect } from "vitest";
import { Header } from "./header";
Expand Down Expand Up @@ -67,4 +68,53 @@ describe("Header", () => {
const homeLink = screen.getByRole("link", { name: /otel explorer/i });
expect(homeLink).toHaveAttribute("href", "/");
});

it("shows hamburger button", () => {
render(
<MemoryRouter>
<Header />
</MemoryRouter>
);

expect(screen.getByRole("button", { name: /open menu/i })).toBeInTheDocument();
});

it("toggles mobile menu open and closed", async () => {
const user = userEvent.setup();
render(
<MemoryRouter>
<Header />
</MemoryRouter>
);

const toggleButton = screen.getByRole("button", { name: /open menu/i });
expect(toggleButton).toHaveAttribute("aria-expanded", "false");

await user.click(toggleButton);

expect(screen.getByRole("button", { name: /close menu/i })).toHaveAttribute(
"aria-expanded",
"true"
);
expect(screen.getByRole("navigation", { name: /mobile main/i })).toBeInTheDocument();
});

it("closes mobile menu when a nav link is clicked", async () => {
const user = userEvent.setup();
render(
<MemoryRouter>
<Header />
</MemoryRouter>
);

await user.click(screen.getByRole("button", { name: /open menu/i }));
expect(screen.getByRole("navigation", { name: /mobile main/i })).toBeInTheDocument();

const javaAgentLink = screen.getAllByRole("link", { name: /java agent/i }).find(
(el) => el.closest("nav[aria-label='Mobile main']")
);
await user.click(javaAgentLink!);

expect(screen.queryByRole("navigation", { name: /mobile main/i })).not.toBeInTheDocument();
});
});
68 changes: 54 additions & 14 deletions ecosystem-explorer/src/components/layout/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,72 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Link } from "react-router-dom";
import { useEffect, useState } from "react";
import { Menu, X } from "lucide-react";
import { Link, useLocation } from "react-router-dom";
import { OtelLogo } from "@/components/icons/otel-logo";

const NAV_ITEMS = [
{ to: "/java-agent", label: "Java Agent" },
{ to: "/collector", label: "Collector" },
] as const;

export function Header() {
const [menuOpen, setMenuOpen] = useState(false);
const location = useLocation();

useEffect(() => {
setMenuOpen(false);
}, [location.pathname]);

return (
<header className="border-border/30 bg-background/95 fixed top-0 right-0 left-0 z-50 h-16 border-b backdrop-blur-xl">
<div className="mx-auto flex h-full max-w-screen-2xl items-center justify-between px-6">
<Link to="/" className="flex items-center gap-3">
<OtelLogo className="text-primary h-6 w-6" />
<span className="text-foreground font-semibold">OTel Explorer</span>
</Link>
<nav className="hidden items-center gap-8 md:flex">
<Link
to="/java-agent"
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
>
Java Agent
</Link>
<Link
to="/collector"
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
>
Collector
</Link>
<nav aria-label="Main" className="hidden items-center gap-8 md:flex">
{NAV_ITEMS.map(({ to, label }) => (
<Link
key={to}
to={to}
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
>
{label}
</Link>
))}
</nav>
<button
type="button"
aria-label={menuOpen ? "Close menu" : "Open menu"}
aria-expanded={menuOpen}
aria-controls="mobile-nav"
className="text-muted-foreground hover:text-foreground md:hidden"
onClick={() => setMenuOpen((prev) => !prev)}
>
{menuOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button>
</div>
<nav
id="mobile-nav"
aria-label="Mobile main"
hidden={!menuOpen}
className="border-border/30 bg-background/95 border-b px-6 py-4 md:hidden"
>
<ul className="flex flex-col gap-4">
{NAV_ITEMS.map(({ to, label }) => (
<li key={to}>
<Link
to={to}
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
>
{label}
</Link>
</li>
))}
</ul>
</nav>
</header>
);
}
Loading