Skip to content

Commit 0f6fdca

Browse files
enhancement/issue 88 header navigation using content as data (#122)
1 parent b27f919 commit 0f6fdca

File tree

9 files changed

+2043
-664
lines changed

9 files changed

+2043
-664
lines changed

src/components/header/header.js

+33-19
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getContentByCollection } from "@greenwood/cli/src/data/client.js";
12
import discordIcon from "../../assets/discord.svg?type=raw";
23
import githubIcon from "../../assets/github.svg?type=raw";
34
import twitterIcon from "../../assets/twitter-logo.svg?type=raw";
@@ -6,7 +7,12 @@ import greenwoodLogo from "../../assets/greenwood-logo-full.svg?type=raw";
67
import styles from "./header.module.css";
78

89
export default class Header extends HTMLElement {
9-
connectedCallback() {
10+
async connectedCallback() {
11+
const currentRoute = this.getAttribute("current-route") ?? "";
12+
const navItems = (await getContentByCollection("nav")).sort((a, b) =>
13+
a.data.order > b.data.order ? 1 : -1,
14+
);
15+
1016
this.innerHTML = `
1117
<header class="${styles.container}">
1218
<a href="/" title="Greenwood Home Page" class="${styles.logoLink}">
@@ -16,15 +22,18 @@ export default class Header extends HTMLElement {
1622
<div class="${styles.navBar}">
1723
<nav role="navigation" aria-label="Main">
1824
<ul class="${styles.navBarMenu}">
19-
<li class="${styles.navBarMenuItem}">
20-
<a href="/docs/" title="Documentation">Docs</a>
21-
</li>
22-
<li class="${styles.navBarMenuItem}">
23-
<a href="/guides/" title="Guides">Guides</a>
24-
</li>
25-
<li class="${styles.navBarMenuItem}">
26-
<a href="/blog/" title="Blog">Blog</a>
27-
</li>
25+
${navItems
26+
.map((item) => {
27+
const { route, label } = item;
28+
const isActiveClass = currentRoute.startsWith(item.route) ? 'class="active"' : "";
29+
30+
return `
31+
<li class="${styles.navBarMenuItem}">
32+
<a href="${route}" ${isActiveClass} title="${label}">${label}</a>
33+
</li>
34+
`;
35+
})
36+
.join("")}
2837
</ul>
2938
</nav>
3039
@@ -64,15 +73,20 @@ export default class Header extends HTMLElement {
6473
6574
<nav role="navigation" aria-label="Mobile">
6675
<ul class="${styles.mobileMenuList}">
67-
<li class="${styles.mobileMenuListItem}">
68-
<a href="/docs/" title="Documentation">Docs</a>
69-
</li>
70-
<li class="${styles.mobileMenuListItem}">
71-
<a href="/guides/" title="Guides">Guides</a>
72-
</li>
73-
<li class="${styles.mobileMenuListItem}">
74-
<a href="/blog/" title="Blog">Blog</a>
75-
</li>
76+
${navItems
77+
.map((item) => {
78+
const { route, label } = item;
79+
const isActiveClass = currentRoute.startsWith(item.route)
80+
? 'class="active"'
81+
: "";
82+
83+
return `
84+
<li class="${styles.mobileMenuListItem}">
85+
<a href="${route}" ${isActiveClass} title="${label}">${label}</a>
86+
</li>
87+
`;
88+
})
89+
.join("")}
7690
</ul>
7791
</nav>
7892
</div>

src/components/header/header.module.css

+16-11
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@
4040
padding: 0;
4141
}
4242

43+
.mobileMenuList {
44+
text-align: left;
45+
margin: var(--size-4) 0 0;
46+
}
47+
4348
.navBarMenuItem a {
4449
text-decoration: none;
4550
color: var(--color-black);
@@ -50,11 +55,22 @@
5055
text-decoration: none;
5156
}
5257

58+
.navBarMenuItem a.active,
5359
.navBarMenuItem a:hover,
5460
.navBarMenuItem a:focus {
5561
text-decoration: underline;
5662
}
5763

64+
.mobileMenuListItem {
65+
list-style-type: none;
66+
margin: 10px 0;
67+
font-size: var(--font-size-5);
68+
}
69+
70+
.mobileMenuListItem a.active {
71+
text-decoration: underline;
72+
}
73+
5874
.socialTray {
5975
display: flex;
6076
gap: var(--size-3);
@@ -103,17 +119,6 @@
103119
color: var(--color-black);
104120
}
105121

106-
.mobileMenuList {
107-
text-align: left;
108-
margin: var(--size-4) 0 0;
109-
}
110-
111-
.mobileMenuListItem {
112-
list-style-type: none;
113-
margin: 10px 0;
114-
font-size: var(--font-size-5);
115-
}
116-
117122
@media screen and (min-width: 480px) {
118123
.container {
119124
justify-content: space-between;

src/components/header/header.spec.js

+47-37
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,36 @@
11
import { expect } from "@esm-bundle/chai";
22
import "./header.js";
3+
import pages from "../../stories/mocks/graph.json" with { type: "json" };
4+
5+
const CURRENT_ROUTE = "/guides/";
6+
const ICONS = [
7+
{
8+
link: "https://github.com/ProjectEvergreen/greenwood",
9+
title: "GitHub",
10+
},
11+
{
12+
link: "https://discord.gg/bsy9jvWh",
13+
title: "Discord",
14+
},
15+
{
16+
link: "https://twitter.com/PrjEvergreen",
17+
title: "Twitter",
18+
},
19+
];
20+
21+
window.fetch = function () {
22+
return new Promise((resolve) => {
23+
resolve(new Response(JSON.stringify(pages.filter((page) => page.data.collection === "nav"))));
24+
});
25+
};
326

427
describe("Components/Header", () => {
5-
const NAV = [
6-
{
7-
title: "Documentation",
8-
label: "Docs",
9-
},
10-
{
11-
title: "Guides",
12-
label: "Guides",
13-
},
14-
{
15-
title: "Blog",
16-
label: "Blog",
17-
},
18-
];
19-
const ICONS = [
20-
{
21-
link: "https://github.com/ProjectEvergreen/greenwood",
22-
title: "GitHub",
23-
},
24-
{
25-
link: "https://discord.gg/bsy9jvWh",
26-
title: "Discord",
27-
},
28-
{
29-
link: "https://twitter.com/PrjEvergreen",
30-
title: "Twitter",
31-
},
32-
];
33-
3428
let header;
3529

3630
before(async () => {
3731
header = document.createElement("app-header");
32+
header.setAttribute("current-route", CURRENT_ROUTE);
33+
3834
document.body.appendChild(header);
3935

4036
await header.updateComplete;
@@ -62,16 +58,23 @@ describe("Components/Header", () => {
6258

6359
it("should have the expected desktop navigation links", () => {
6460
const links = header.querySelectorAll("nav[aria-label='Main'] ul li a");
61+
let activeRoute = undefined;
6562

66-
Array.from(links).forEach((link) => {
67-
const navItem = NAV.find(
68-
(nav) => `/${nav.label.toLowerCase()}/` === link.getAttribute("href"),
69-
);
63+
Array.from(links).forEach((link, idx) => {
64+
const navItem = pages.find((nav) => nav.route === link.getAttribute("href"));
7065

7166
expect(navItem).to.not.equal(undefined);
67+
expect(navItem.data.order).to.equal((idx += 1));
7268
expect(link.textContent).to.equal(navItem.label);
7369
expect(link.getAttribute("title")).to.equal(navItem.title);
70+
71+
// current route should display as active
72+
if (navItem.route === CURRENT_ROUTE && link.getAttribute("class").includes("active")) {
73+
activeRoute = navItem;
74+
}
7475
});
76+
77+
expect(activeRoute.route).to.equal(CURRENT_ROUTE);
7578
});
7679

7780
it("should have the expected social link icons", () => {
@@ -121,16 +124,23 @@ describe("Components/Header", () => {
121124

122125
it("should have the expected navigation links", () => {
123126
const links = header.querySelectorAll("nav[aria-label='Mobile'] ul li a");
127+
let activeRoute = undefined;
124128

125-
Array.from(links).forEach((link) => {
126-
const navItem = NAV.find(
127-
(nav) => `/${nav.label.toLowerCase()}/` === link.getAttribute("href"),
128-
);
129+
Array.from(links).forEach((link, idx) => {
130+
const navItem = pages.find((nav) => nav.route === link.getAttribute("href"));
129131

130132
expect(navItem).to.not.equal(undefined);
133+
expect(navItem.data.order).to.equal((idx += 1));
131134
expect(link.textContent).to.equal(navItem.label);
132135
expect(link.getAttribute("title")).to.equal(navItem.title);
136+
137+
// current route should display as active
138+
if (navItem.route === CURRENT_ROUTE && link.getAttribute("class").includes("active")) {
139+
activeRoute = navItem;
140+
}
133141
});
142+
143+
expect(activeRoute.route).to.equal(CURRENT_ROUTE);
134144
});
135145
});
136146

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
11
import "./header.js";
2+
import pages from "../../stories/mocks/graph.json";
23

34
export default {
45
title: "Components/Header",
6+
parameters: {
7+
fetchMock: {
8+
mocks: [
9+
{
10+
matcher: {
11+
url: "http://localhost:1984/___graph.json",
12+
response: {
13+
body: pages.filter((page) => page.data.collection === "nav"),
14+
},
15+
},
16+
},
17+
],
18+
},
19+
},
520
};
621

7-
const Template = () => "<app-header></app-header>";
22+
const Template = () => "<app-header current-route='/guides/'></app-header>";
823

924
export const Primary = Template.bind({});

src/layouts/app.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
</head>
2929

3030
<body>
31-
<app-header></app-header>
31+
<app-header current-route="${globalThis.page.route}"></app-header>
3232

3333
<main class="page-content">
3434
<page-outlet></page-outlet>

src/pages/blog/index.html

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
---
2+
collection: nav
3+
order: 3
4+
---
5+
16
<html>
27
<head>
38
<title>Greenwood - ${globalThis.page.title}</title>

src/pages/docs/index.md

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
---
22
title: Docs
33
layout: docs
4+
collection: nav
5+
order: 1
46
---
57

68
<app-heading-box heading="Docs">

src/pages/guides/index.md

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
---
22
title: Guides
33
layout: guides
4+
collection: nav
5+
order: 2
46
---
57

68
<app-heading-box heading="Guides">

0 commit comments

Comments
 (0)