Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
81 changes: 81 additions & 0 deletions ecosystem-explorer/src/v1/components/layout/sub-nav.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { describe, it, expect } from "vitest";
import { SubNav } from "./sub-nav";

function renderSubNav(props: Parameters<typeof SubNav>[0]) {
return render(
<MemoryRouter>
<SubNav {...props} />
</MemoryRouter>
);
}

describe("SubNav", () => {
it("renders nothing when both crumbs and actions are empty", () => {
const { container } = renderSubNav({ crumbs: [] });
expect(container.firstChild).toBeNull();
});

it("renders breadcrumb landmark with each crumb in order", () => {
renderSubNav({
crumbs: [
{ label: "Collector", href: "/collector" },
{ label: "Components", href: "/collector/components" },
{ label: "otlp" },
],
});

const nav = screen.getByRole("navigation", { name: /breadcrumb/i });
expect(nav).toBeInTheDocument();

expect(screen.getByRole("link", { name: "Collector" })).toHaveAttribute("href", "/collector");
expect(screen.getByRole("link", { name: "Components" })).toHaveAttribute(
"href",
"/collector/components"
);
expect(screen.getByText("otlp")).toHaveAttribute("aria-current", "page");
});

it("renders the last crumb as a non-link with aria-current=page", () => {
renderSubNav({
crumbs: [{ label: "Home", href: "/" }, { label: "Detail" }],
});

expect(screen.queryByRole("link", { name: "Detail" })).toBeNull();
expect(screen.getByText("Detail")).toHaveAttribute("aria-current", "page");
});

it("renders actions in the right-aligned slot", () => {
renderSubNav({
crumbs: [{ label: "Home", href: "/" }],
actions: <button type="button">Filter</button>,
});

expect(screen.getByRole("button", { name: "Filter" })).toBeInTheDocument();
});

it("renders actions even when there are no crumbs", () => {
renderSubNav({
crumbs: [],
actions: <button type="button">Edit</button>,
});

expect(screen.getByRole("button", { name: "Edit" })).toBeInTheDocument();
});
});
82 changes: 82 additions & 0 deletions ecosystem-explorer/src/v1/components/layout/sub-nav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/*
* SubNav — breadcrumb row that sits below the navbar on inner pages.
*
* Mirrors opentelemetry.io's `.breadcrumb` pattern (Bootstrap), but tightened
* to the explorer's chrome. The right-side `actions` slot is for page-level
* controls (filter toggles, "Edit on GitHub", etc.). Renders nothing if both
* `crumbs` and `actions` are empty so callers can mount it unconditionally.
*/
import { ChevronRight } from "lucide-react";
import { Link } from "react-router-dom";

export interface BreadcrumbItem {
label: string;
href?: string;
}

export interface SubNavProps {
crumbs: BreadcrumbItem[];
actions?: React.ReactNode;
className?: string;
}

export function SubNav({ crumbs, actions, className }: SubNavProps) {
if (crumbs.length === 0 && !actions) return null;

return (
<div className={className ? `td-subnav ${className}` : "td-subnav"}>
<div className="td-subnav__container">
{crumbs.length > 0 && (
<nav className="td-subnav__breadcrumb" aria-label="Breadcrumb">
<ol className="td-subnav__crumbs">
{crumbs.map((crumb, idx) => {
const isLast = idx === crumbs.length - 1;
const ariaCurrent = isLast ? "page" : undefined;
return (
<li key={idx} className="td-subnav__crumb">
{idx > 0 && (
<ChevronRight
className="td-subnav__separator"
aria-hidden
focusable="false"
/>
)}
{isLast || !crumb.href ? (
<span
className="td-subnav__crumb-label td-subnav__crumb-label--current"
aria-current={ariaCurrent}
>
{crumb.label}
</span>
) : (
<Link to={crumb.href} className="td-subnav__crumb-label">
{crumb.label}
</Link>
)}
</li>
);
})}
</ol>
</nav>
)}
{actions && <div className="td-subnav__actions">{actions}</div>}
</div>
</div>
);
}
4 changes: 3 additions & 1 deletion ecosystem-explorer/src/v1/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@
* - tokens.css — v1-only surface-token overrides, scoped via `.v1-app`
* - navbar.css — v1 navbar (`.td-navbar`) — mirrors opentelemetry.io
* - theme-toggle.css — v1 theme toggle dropdown — mirrors opentelemetry.io
* - sub-nav.css — v1 breadcrumb sub-nav row beneath the navbar
*
* Navbar and theme-toggle styles use class selectors (`.td-navbar`,
* All v1 styles use class selectors (`.td-navbar`, `.td-subnav`,
* `.td-light-dark-menu__*`) and only match elements rendered by `<V1App />`,
* so they're inert in the legacy bundle even when both branches ship.
*/
@import "./tokens.css";
@import "./navbar.css";
@import "./theme-toggle.css";
@import "./sub-nav.css";
93 changes: 93 additions & 0 deletions ecosystem-explorer/src/v1/styles/sub-nav.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/* Pixel-anchored at 16px-rem like the rest of v1 chrome. */

.td-subnav {
font-size: 16px;
background-color: hsl(var(--card-hsl));
border-bottom: 1px solid hsl(var(--border-hsl));
}

.td-subnav__container {
width: 100%;
max-width: 1320px;
margin: 0 auto;
padding: 0.5rem 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
min-height: 2.5rem;
flex-wrap: wrap;
}

.td-subnav__breadcrumb {
min-width: 0;
}

.td-subnav__crumbs {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.25rem;
font-size: 0.875rem;
line-height: 1.25;
}

.td-subnav__crumb {
display: inline-flex;
align-items: center;
gap: 0.25rem;
}

.td-subnav__separator {
width: 0.875rem;
height: 0.875rem;
color: hsl(var(--muted-foreground-hsl));
flex-shrink: 0;
}

.td-subnav__crumb-label {
color: hsl(var(--muted-foreground-hsl));
text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 24rem;
}

.td-subnav__crumb-label:hover,
.td-subnav__crumb-label:focus-visible {
color: hsl(var(--foreground-hsl));
text-decoration: underline;
text-underline-offset: 0.2em;
}

.td-subnav__crumb-label--current {
color: hsl(var(--foreground-hsl));
font-weight: 600;
}

.td-subnav__actions {
display: flex;
align-items: center;
gap: 0.5rem;
flex-shrink: 0;
}