Skip to content

Commit 2d6e39a

Browse files
committed
feat: add navigation menu for mobile
1 parent 01dc968 commit 2d6e39a

File tree

3 files changed

+88
-2
lines changed

3 files changed

+88
-2
lines changed

frontend/components/Nav.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
// Copyright 2024 the JSR authors. All rights reserved. MIT license.
22
import { ComponentChildren } from "preact";
3+
import { NavMenu } from "../islands/NavMenu.tsx";
34

45
export interface NavProps {
56
children?: ComponentChildren;
67
}
78

89
export function Nav(props: NavProps) {
910
return (
10-
<nav class="mt-3 md:border-b border-jsr-cyan-300/30 flex flex-wrap md:flex-nowrap flex-row max-w-full overflow-auto items-end">
11-
{props.children}
11+
<nav class="mt-3 md:border-b border-jsr-cyan-300/30 max-w-full flex justify-between overflow-hidden items-end">
12+
<ul id="nav-items" class="flex flex-row">
13+
{props.children}
14+
</ul>
15+
<NavMenu />
1216
</nav>
1317
);
1418
}

frontend/fresh.gen.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import * as $GitHubActionsLink from "./islands/GitHubActionsLink.tsx";
5353
import * as $GithubUserLink from "./islands/GithubUserLink.tsx";
5454
import * as $GlobalSearch from "./islands/GlobalSearch.tsx";
5555
import * as $HomepageHero from "./islands/HomepageHero.tsx";
56+
import * as $NavMenu from "./islands/NavMenu.tsx";
5657
import * as $PublishingTaskRequeue from "./islands/PublishingTaskRequeue.tsx";
5758
import * as $UserManageScopeInvite from "./islands/UserManageScopeInvite.tsx";
5859
import * as $UserMenu from "./islands/UserMenu.tsx";
@@ -123,6 +124,7 @@ const manifest = {
123124
"./islands/GithubUserLink.tsx": $GithubUserLink,
124125
"./islands/GlobalSearch.tsx": $GlobalSearch,
125126
"./islands/HomepageHero.tsx": $HomepageHero,
127+
"./islands/NavMenu.tsx": $NavMenu,
126128
"./islands/PublishingTaskRequeue.tsx": $PublishingTaskRequeue,
127129
"./islands/UserManageScopeInvite.tsx": $UserManageScopeInvite,
128130
"./islands/UserMenu.tsx": $UserMenu,

frontend/islands/NavMenu.tsx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
2+
import IconDots from "$tabler_icons/dots.tsx";
3+
4+
const useWindowWidth = () => {
5+
const [windowWidth, setWindowWidth] = useState(0);
6+
const handleWidth = () => {
7+
setWindowWidth(globalThis.innerWidth);
8+
};
9+
useLayoutEffect(() => {
10+
handleWidth();
11+
globalThis.addEventListener("resize", handleWidth);
12+
13+
return () => globalThis.removeEventListener("resize", handleWidth);
14+
}, []);
15+
return windowWidth;
16+
};
17+
18+
export function NavMenu() {
19+
const [open, setOpen] = useState(false);
20+
const ref = useRef<HTMLDivElement>(null);
21+
22+
useWindowWidth();
23+
useEffect(() => {
24+
function outsideClick(e: Event) {
25+
if (ref.current && !ref.current.contains(e.target as Element)) {
26+
setOpen(false);
27+
}
28+
}
29+
document.addEventListener("click", outsideClick);
30+
return () => document.removeEventListener("click", outsideClick);
31+
}, []);
32+
33+
if (typeof document === "undefined") return null;
34+
35+
const navItems = document.getElementById("nav-items");
36+
const nav = navItems?.parentElement;
37+
let navWidth = 0;
38+
if (nav) {
39+
navWidth = nav.offsetWidth;
40+
}
41+
42+
let sumWidth = 50;
43+
let displayMenu = false;
44+
const navMenuList = [];
45+
if (navItems) {
46+
for (let i = 0; i < navItems.children.length; i++) {
47+
const child = navItems.children[i];
48+
child.classList.remove("invisible");
49+
sumWidth += child.clientWidth;
50+
51+
if (sumWidth > navWidth) {
52+
displayMenu = true;
53+
navMenuList.push(child.outerHTML);
54+
child.classList.add("invisible");
55+
}
56+
}
57+
}
58+
59+
return (
60+
<div
61+
id="nav-menu"
62+
class={`group absolute right-4 md:right-10 rounded border-2 border-jsr-cyan-200 hover:bg-jsr-cyan-50 hover:cursor-pointer ${
63+
displayMenu ? "" : "hidden"
64+
}`}
65+
aria-expanded={open ? "true" : "false"}
66+
onClick={() => setOpen((v) => !v)}
67+
ref={ref}
68+
>
69+
<span class="flex p-1">
70+
<IconDots />
71+
</span>
72+
{open && (
73+
<div
74+
class="absolute top-[120%] -right-4 z-[70] px-1 py-2 rounded border-1.5 border-current bg-white w-56 shadow overflow-hidden opacity-100 translate-y-0 transition"
75+
dangerouslySetInnerHTML={{ __html: navMenuList.join("") }}
76+
/>
77+
)}
78+
</div>
79+
);
80+
}

0 commit comments

Comments
 (0)