Skip to content

feat: add navigation menu for mobile #436

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 1, 2024
Merged
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
8 changes: 6 additions & 2 deletions frontend/components/Nav.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright 2024 the JSR authors. All rights reserved. MIT license.
import { ComponentChildren } from "preact";
import { NavMenu } from "../islands/NavMenu.tsx";

export interface NavProps {
children?: ComponentChildren;
Expand All @@ -11,9 +12,12 @@ export function Nav(props: NavProps) {
<nav
class={`${
props.noTopMargin ? "" : "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`}
} md:border-b border-jsr-cyan-300/30 max-w-full flex justify-between overflow-hidden items-end`}
>
{props.children}
<ul id="nav-items" class="flex flex-row">
{props.children}
</ul>
<NavMenu />
</nav>
);
}
Expand Down
2 changes: 2 additions & 0 deletions frontend/fresh.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import * as $GithubUserLink from "./islands/GithubUserLink.tsx";
import * as $GlobalSearch from "./islands/GlobalSearch.tsx";
import * as $HeaderLogo from "./islands/HeaderLogo.tsx";
import * as $HomepageHeroParticles from "./islands/HomepageHeroParticles.tsx";
import * as $NavMenu from "./islands/NavMenu.tsx";
import * as $PublishingTaskRequeue from "./islands/PublishingTaskRequeue.tsx";
import * as $UserManageScopeInvite from "./islands/UserManageScopeInvite.tsx";
import * as $UserMenu from "./islands/UserMenu.tsx";
Expand Down Expand Up @@ -127,6 +128,7 @@ const manifest = {
"./islands/GlobalSearch.tsx": $GlobalSearch,
"./islands/HeaderLogo.tsx": $HeaderLogo,
"./islands/HomepageHeroParticles.tsx": $HomepageHeroParticles,
"./islands/NavMenu.tsx": $NavMenu,
"./islands/PublishingTaskRequeue.tsx": $PublishingTaskRequeue,
"./islands/UserManageScopeInvite.tsx": $UserManageScopeInvite,
"./islands/UserMenu.tsx": $UserMenu,
Expand Down
81 changes: 81 additions & 0 deletions frontend/islands/NavMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright 2024 the JSR authors. All rights reserved. MIT license.
import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
import IconDots from "$tabler_icons/dots.tsx";

const useWindowWidth = () => {
const [windowWidth, setWindowWidth] = useState(0);
const handleWidth = () => {
setWindowWidth(globalThis.innerWidth);
};
useLayoutEffect(() => {
handleWidth();
globalThis.addEventListener("resize", handleWidth);

return () => globalThis.removeEventListener("resize", handleWidth);
}, []);
return windowWidth;
};

export function NavMenu() {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLButtonElement>(null);

useWindowWidth();
useEffect(() => {
function outsideClick(e: Event) {
if (ref.current && !ref.current.contains(e.target as Element)) {
setOpen(false);
}
}
document.addEventListener("click", outsideClick);
return () => document.removeEventListener("click", outsideClick);
}, []);

if (typeof document === "undefined") return null;

const navItems = document.getElementById("nav-items");
const nav = navItems?.parentElement;
let navWidth = 0;
if (nav) {
navWidth = nav.offsetWidth;
}

let sumWidth = 50;
let displayMenu = false;
const navMenuList = [];
if (navItems) {
for (let i = 0; i < navItems.children.length; i++) {
const child = navItems.children[i];
child.classList.remove("invisible");
sumWidth += child.clientWidth;

if (sumWidth > navWidth) {
displayMenu = true;
navMenuList.push(child.outerHTML);
child.classList.add("invisible");
}
}
}

return (
<button
id="nav-menu"
class={`group absolute right-4 md:right-10 rounded md:rounded-b-none border-1 md:border-b-0 border-jsr-cyan-100 hover:bg-jsr-cyan-50 hover:cursor-pointer ${
displayMenu ? "" : "hidden"
}`}
aria-expanded={open ? "true" : "false"}
onClick={() => setOpen((v) => !v)}
ref={ref}
>
<span class="flex p-1">
<IconDots />
</span>
{open && (
<div
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 [&>a]:rounded"
dangerouslySetInnerHTML={{ __html: navMenuList.join("") }}
/>
)}
</button>
);
}
Loading